Até agora, em nossa série de artigos sobre OpenGL, sempre enviamos dados de vértices para o processador gráfico e produzimos apenas pixels para serem desenhados em framebuffers. E se quisermos recuperar os vértices depois de serem passados pelo vertex e geometry shaders? Nesse artigo, veremos uma forma de fazer isso, conhecida com feedback de transformações.
Até agora, usamos VBOs (Vertex Buffer Objects) para armazenar vértices que foram usados para operações de desenho. A extensão feedback de transformações permite que os shaders escrevam vértices de volta nesses objetos. Você poderia, por exemplo, criar um vertex shader que simulasse a gravidade e escrevesse posições de vértices atualizados de volta ao buffer. Isso permitira que você não precisasse ter que ficar transferindo esses dados entre a memória gráfica e a memória principal. Além disso, você teria como se beneficiar do vasto poder de processamento paralelo das GPUs modernas.
Feedback básico
Começaremos do início de forma que o programa final irá demonstrar claramente quão simples o feedback de transformações é. Infelizmente não haverá nada para visualizar dessa vez, pois não iremos desenhar nada na tela nesse artigo. Apesar desse recursos poder ser usado para simplificar efeitos com a simulação de partículas, a explicação desses tópicos estão um pouco fora do escopo desse artigo. Depois que você entender o básico sobre o feedback de transformações, será capaz de encontrar e entender vários artigos na web sobre esse assunto.
Vamos começar com um vertex shader simples.
const GLchar* vertexShaderSrc = GLSL(
in float inValue;
out float outValue;
void main() {
outValue = sqrt(inValue);
}
);
Esse vertex shader não aparenta fazer muito sentido. Ele não define um gl_Position e usa como entrada apenas um único float aleatório. Por sorte,podemos usar feedback de transformações para capturar o resultado, como veremos em alguns instantes.
GLuint shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(shader, 1, &vertexShaderSrc, nullptr);
glCompileShader(shader);
GLuint program = glCreateProgram();
glAttachShader(program, shader);
Compile o shader, crie um programa e anexe o shader, mas não chame glLinkProgram ainda! Antes de conectar seu programa, temos que informar ao OpenGL quais atributos da saída queremos capturas em um buffer.
const GLchar* feedbackVaryings[] = { "outValue" };
glTransformFeedbackVaryings(program, 1, feedbackVaryings, GL_INTERLEAVED_ATTRIBS);
O primeiro parâmetro é auto-explicatório, o segundo e o terceiro especificam o comprimento do array da saída e o próprio array, e o último parâmetro especifica como os dados devem ser escritos.
Os seguintes formatos estão disponíveis:
- GL_INTERLEAVED_ATTRIBS: Escreve todos os atributos em um único objeto de buffer.
- GL_SEPARATE_ATTRIBS: Escreve atributos em múltiplos objetos de buffer ou em diferentes posições de um buffer.
Algumas vezes é útil ter buffers separados para cada atributo, mas vamos manter as coisas simples nesse exemplo. Agora que especificamos as variáveis de saída, você pode conectar e ativar o programa. Isso se deve ao fato de processo de conexão (linking) depender do conhecimento sobre as saídas.
glLinkProgram(program);
glUseProgram(program);
Depois disso, crie e conecte o VAO:
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
Agora, crie um buffer com alguns dados de entrada para o vertex shader:
GLfloat data[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW);
Os números em data são os números que queremos que o shader calcule a raiz quadrada e o feedback de transformações nos ajudará a ler de volta os rsultados.
Com relação aos ponteiros dos vértices, você sabe o procedimento:
GLint inputAttrib = glGetAttribLocation(program, "inValue");
glEnableVertexAttribArray(inputAttrib);
glVertexAttribPointer(inputAttrib, 1, GL_FLOAT, GL_FALSE, 0, 0);
O feedback de transformações irá retornar os valores de outValue, mas primeiro precisamos criar um VBO para armazenar eles, como fizemos para os vértices da entrada:
GLuint tbo;
glGenBuffers(1, &tbo);
glBindBuffer(GL_ARRAY_BUFFER, tbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(data), nullptr, GL_STATIC_READ);
Note que agora passados um nullptr para cria um buffer grande o suficiente para armazenar os resultados, mas sem especificar nenhum dado inicial. O tipo de uso apropriado é GL_STATIC_READ agora, que indica que pretendemos usar o OpenGL para escrever nesse buffer e nosso aplicativo deve ler dele.
Agora fizemos todos os preparativos para o processo de cálculo (ou de renderização). Como não pretendemos desenhar nada, o rasterizador deve ser desativado.
glEnable(GL_RASTERIZER_DISCARD);
Para definir de fato o buffer que criamos acima como um buffer do feedback de transformações, temos que usar uma nova função chamada glBindBufferBase.
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, tbo);
O primeiro parâmetro é nesse momento necessário que seja GL_TRANSFORM_FEDDBACK_BUFFER para permitir futuras extensões. O segundo parâmetro é o índice da variável de saída, que é simplesmente 0 pois temos apenas uma. O parâmetro final especifica o objeto de buffer a ser usado.
Antes de chamar a função de desenho, você precisa entrar no modo de feedback de transformações.
glBeginTransformFeedback(GL_POINTS);
Isso certamente traz de volta memórias dos dias antigos fo glBegin! Assim como no caso do geometry shader no artigo anterior, os valores possível para o modo são um pouco limitados.
GL_POINTS
—GL_POINTS
GL_LINES
—GL_LINES, GL_LINE_LOOP, GL_LINE_STRIP, GL_LINES_ADJACENCY, GL_LINE_STRIP_ADJACENCY
GL_TRIANGLES
—GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES_ADJACENCY, GL_TRIANGLE_STRIP_ADJACENCY
Se você possuir apenas um vertex shader, como no nosso caso, a primitiva precisa coincidir com o que está sendo desenhado.
glDrawArrays(GL_POINTS, 0, 5);
Mesmo que agora estarmos trabalhando com dados, os números individuais ainda podem visto como “pontos” separados, de forma que usaremos essa primitiva.
Finalize o modo de feedback de transformações:
glEndTransformFeedback();
Normalmente, no final da operação de desenho, nós limpamos os buffers para exibir o resultado na rela. Ainda queremos nos certificar que a operação de renderização tenha sido finalizada antes de tentar acessar os resultados, assim nós esvaziamos o buffer de comandos do OpenGL:
glFlush();
Obter os resultados de volta é tão fácil quanto copiar os dados do buffer de volta para um array:
GLfloat feedback[5];
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(feedback), feedback);
Se você imprimir agora os valor do array, deve ver as raízes quadradas da entrada no terminal:
printf("%f %f %f %f %f\n", feedback[0], feedback[1], feedback[2], feedback[3], feedback[4]);
Parabéns, você agora sabe como fazer sua GPU executar tarefas de proposito geral com os vertex shaders! Naturalmente, um framework de GPGPU real como o OpenCL é geralmente melhor nessa tarefa, mas a vantagem do feedback de transformações é que você pode dar um novo proposito aos dados em operações de desenho, por exemplo usando o buffer do feedback de transformações como array e executando chamadas normais de desenho.
Se você possuir uma placa gráfica e o driver suportar, pode também usar os compute shaders do OpenGL 4.3, que são projetados para tarefas que não são normalmente relacionadas à desenho.
Você pode encontrar o código completo do exemplo aqui.
Feedback de transformações e geometry shaders
Quando você inclui um geometry shader, a operação de feedback de transformações irá capturas as saídas desse shader ao invés do vertex shader Por exemplo:
// Vertex shader
const GLchar* vertexShaderSrc = GLSL(
in float inValue;
out float geoValue;
void main() {
geoValue = sqrt(inValue);
}
);
// Geometry shader
const GLchar* geoShaderSrc = GLSL(
layout(points) in;
layout(triangle_strip, max_vertices = 3) out;
in float[] geoValue;
out float outValue;
void main() {
for (int i = 0; i < 3; i++) {
outValue = geoValue[0] + i;
EmitVertex();
}
EndPrimitive();
}
);
O geometru shader pega um ponto processado pelo vertex shader e gera 2 outros pontos para forma um triângulo onde cada ponto tem 1 unidade de valor a mais.
GLuint geoShader = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geoShader, 1, &geoShaderSrc, nullptr);
glCompileShader(geoShader);
...
glAttachShader(program, geoShader);
Compile a anexe o geometry shader ao programa para começar a usa-lo.
const GLchar* feedbackVaryings[] = { "outValue" };
glTransformFeedbackVaryings(program, 1, feedbackVaryings, GL_INTERLEAVED_ATTRIBS);
Apesar da saída está vindo agora do geometry shader, não mudaremos o nome, assim o código restante permanece inalterado.
Como cada vértice da entrada irá gerar 3 vértices com o saída, o buffer do feedback de transformações irá ser 3 vezes maior do que o buffer de entrada:
glBufferData(GL_ARRAY_BUFFER, sizeof(data) * 3, nullptr, GL_STATIC_READ);
Ao usar um geometry shader, a primitiva especificada em glBeginTransformFeedback deve coincidir com o tipo de saída do geometry shader:
glBeginTransformFeedback(GL_TRIANGLES);
Recuperar a saída funcionará da mesma forma:
// Fetch and print results
GLfloat feedback[15];
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(feedback), feedback);
for (int i = 0; i < 15; i++) {
printf("%f\n", feedback[i]);
}
além de você ter que prestar atenção ao tipo de primitiva do feedback e do tamanho de seus buffers, adicionar um geometry shader à equação não altera muito o processo além do shader responsável pela saída.
O código completo pode ser encontrado aqui.
Feedback variável
Como vimos no artigo anterior, geometry shaders possuem a propriedade única de gerar uma quantidade de dados variável. Por sorte, existem maneiras de manter um registro de quantas primitivas foram escritas pelo uso de query objects.
Assim como no caso de todos os outros objetos do OpenGL, teremos que criar um primeiro:
GLuint query;
glGenQueries(1, &query);
Então, logo antes de chamar glBeginTransformFeedback, você precisa informar ao OpenGL para manter o registro do número de primitivas escrito:
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, query);
Depois de glEndTransformFeedback, você pode parar de “registrar” esses dados:
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
Ler o resultado é feito dessa forma:
GLuint primitives;
glGetQueryObjectuiv(query, GL_QUERY_RESULT, &primitives);
Você pode então imprimir os valores junto com os demais dados:
printf("%u primitives written!\n\n", primitives);
Note que é retornado o número de primitivas, não o número de vértices. Como temos 15 vértices, cada triângulo tendo 3, temos 5 primitivas.
Query objects podem também ser usados para registrar coisas como GL_PRIMITIVES_GENERATED quando estiver lidando apenas com geometry shaders e GL_TIME_ELAPSED para medir o tempo gasto no servidor (placa gráfica) para executar a tarefa.
Veja o código completo se tiver alguma dúvida referente a esse tópico.
Conclusão
Você sabe agora o suficiente sobre geometry shaders e feedback de transformações para fazer com sua placa gráfica trabalhos muito interessantes além de desenhos! Você pode mesmo combinar feedback de transformações e rasterização para atualizar os vértices e desenha-los ao mesmo tempo!
Fonte: open.gl/feedback