como mencionado no capítulo do triângulo Hello, shaders são pequenos programas que repousam na GPU. Estes programas são executados para cada seção específica do pipeline gráfico. Em um sentido básico, shaders não são nada mais do que programas transformando entradas em saídas. Shaders também são programas muito isolados em que eles não estão autorizados a se comunicar uns com os outros; a única comunicação que eles têm é através de suas entradas e Saídas.no capítulo anterior, tocámos brevemente na superfície dos sombreados e como os utilizar correctamente. Vamos agora explicar os shaders, e especificamente a linguagem de sombreamento OpenGL, de uma forma mais geral.
GLSL
Shaders são escritos na linguagem semelhante a C GLSL. O GLSL é adaptado para uso com gráficos e contém características úteis especificamente direcionadas para a manipulação de vetores e matrizes.
Shaders sempre começam com uma declaração de versão, seguido por uma lista de variáveis de entrada e saída, uniformes e sua função principal. Cada ponto de entrada de shader está em sua função principal onde processamos quaisquer variáveis de entrada e produzimos os resultados em suas variáveis de saída. Não te preocupes, se não sabes o que são uniformes, chegaremos a eles em breve.
uma máquina de barbear tipicamente tem a seguinte estrutura:
#version version_numberin type in_variable_name;in type in_variable_name;out type out_variable_name; uniform type uniform_name; void main(){ // process input(s) and do some weird graphics stuff ... // output processed stuff to output variable out_variable_name = weird_stuff_we_processed;}
quando estamos a falar especificamente sobre o sombreador de vértices cada variável de entrada é também conhecida como um atributo de vértices. Há um número máximo de atributos de vértices que podemos declarar limitados pelo hardware. O OpenGL garante que há sempre pelo menos 16 atributos de vértices de 4 componentes disponíveis, mas algum ‘hardware’ pode permitir mais que você pode obter ao questionar GL_MAX_VERTEX_ ATTRIBS:
int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
isto frequentemente devolve o mínimo de que deve ser mais do que suficiente para a maioria dos fins.
tipos
GLSL tem, como qualquer outra linguagem de programação, tipos de dados para especificar com que tipo de variável queremos trabalhar. GLSL tem mais do padrão tipos básicos sabemos que a partir de linguagens como C: int
float
double
uint
e bool
. GLSL também apresenta dois tipos de contêineres que vamos usar muito, nomeadamente vectors
e matrices
. Discutiremos matrizes num capítulo posterior.
vetores
um vetor em GLSL é um container de componente de 1,2,3 ou 4 para qualquer um dos tipos básicos mencionados. Eles podem assumir a seguinte forma (n
representa o número de componentes):
-
vecn
: o padrão de vetor den
carros alegóricos. -
bvecn
: um vector den
booleanos. -
ivecn
: um vetor den
inteiros. -
uvecn
: um vector den
inteiros sem sinal. -
dvecn
: um vector den
componentes duplos.
na maioria das vezes estaremos usando o básico vecn
uma vez que os flutuadores são suficientes para a maioria de nossos propósitos.
componentes de um vetor podem ser acessados via vec.x
ondex
é o primeiro componente do vetor. Você pode usar .x
.y
.z
e .w
para acessar o seu primeiro, segundo, terceiro e quarto componente, respectivamente. GLSL também permite que você use rgba
para cores ou stpq
para coordenadas de textura, acessando os mesmos componentes.
o tipo de dados vetoriais permite uma seleção de componentes interessante e flexível chamada “swizzling”. O Swizzling permite-nos usar uma sintaxe como esta.:
vec2 someVec;vec4 differentVec = someVec.xyxx;vec3 anotherVec = differentVec.zyw;vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
Você pode usar qualquer combinação de até 4 letras para criar um novo vetor (do mesmo tipo) enquanto o vetor tem esses componentes; não é permitido o acesso a .z
componente de uma vec2
por exemplo. Podemos também passar vetores como argumentos para diferentes vetor construtor chama, reduzindo o número de argumentos necessários:
vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);
Vetores são, portanto, flexível, tipo de dados que podemos usar para todos os tipos de entrada e saída. Ao longo do livro você verá muitos exemplos de como podemos gerenciar vetores criativamente.
Ins and outs
Shaders são pequenos programas simpáticos por conta própria, mas eles são parte de um todo e por essa razão queremos ter Entradas e Saídas nos shaders individuais para que possamos mover as coisas ao redor. GLSL defined the in
and out
keywords specifically for that purpose. Cada sombreador pode especificar Entradas e Saídas usando essas palavras-chave e onde quer que uma variável de saída combine com uma variável de entrada da próxima etapa de sombreamento eles são passados ao longo. O vértice e o fragmento shader diferem um pouco.
o sombreador de vértices deve receber alguma forma de entrada, caso contrário seria bastante ineficaz. O sombreador de vértices difere na sua entrada, na medida em que recebe a sua entrada diretamente a partir dos dados de vértices. Para definir como os dados de vértices são organizados, especificamos as variáveis de entrada com metadados de localização para que possamos configurar os atributos de vértices na CPU. Nós vimos isso no capítulo anterior como layout (location = 0)
. O sombreador de vértices, portanto, requer uma especificação de layout extra para suas entradas para que possamos ligá-lo com os dados de vértices.
também é possível omitir olayout (location = 0)
especificador e consulta para o atributo locais em seu código de OpenGL via glGetAttribLocation, mas eu prefiro defini-las no shader de vértice. É mais fácil de entender e poupa-lhe (e OpenGL) algum trabalho.
a outra exceção é que o fragmento shader requer umvec4
variável de saída de cores, uma vez que o fragmento shaders precisa gerar uma cor de saída final. Se não indicar uma cor de saída no seu shader de fragmentos, o resultado do buffer de cores para esses fragmentos será indefinido (o que normalmente significa que o OpenGL os irá desenhar a preto ou branco).
Por isso, se quisermos enviar dados de um sombreado para o outro, teremos de declarar uma saída no sombreado de envio e uma entrada semelhante no sombreado de recepção. Quando os tipos e os nomes são iguais em ambos os lados, o OpenGL irá ligar essas variáveis em conjunto e então é possível enviar dados entre shaders (isto é feito ao ligar um objeto de programa). Para mostrar como isso funciona na prática, nós vamos alterar os shaders do capítulo anterior para deixar o vértice shader decidir a cor para o fragmento shader.
Vertex shader
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0 out vec4 vertexColor; // specify a color output to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color}
Fragment shader
#version 330 coreout vec4 FragColor; in vec4 vertexColor; // the input variable from the vertex shader (same name and same type) void main(){ FragColor = vertexColor;}
Você pode ver que nós declarada vertexColor variável como uma vec4
saída que nós colocamos no shader de vértice e declaramos um semelhante vertexColor entrada no shader de fragmento. Uma vez que ambos têm o mesmo tipo e nome, o vértice-cor no fragmento shader é ligado ao vértice-cor no vértice shader. Como definimos a cor para uma cor Vermelho-escuro no sombreado vértice, os fragmentos resultantes devem ser Vermelho-escuro também. A seguinte imagem mostra o resultado:
lá vamos nós! Acabámos de enviar um valor do sombreador de vértices para o shader de fragmentos. Vamos apimentar um pouco e ver se podemos enviar uma cor da nossa aplicação para o shader de fragmentos! uniformes uniformes
uniformes são outra maneira de passar dados de nossa aplicação na CPU para os shaders na GPU. Uniformes são, no entanto, ligeiramente diferentes em comparação com atributos de vértices. Em primeiro lugar, os uniformes são globais. Global, significando que uma variável uniforme é única por objeto do programa shader, e pode ser acessada a partir de qualquer shader em qualquer fase do programa shader. Segundo, seja qual for o valor uniforme, os uniformes manterão os seus valores até serem reinicializados ou actualizados.
para declarar um uniforme no GLSL nós simplesmente adicionamos o uniform
palavra-chave a um shader com um tipo e um nome. A partir daí podemos usar o uniforme recém-declarado no sombreado. Vamos ver se desta vez podemos definir a cor do triângulo através de um uniforme.:
#version 330 coreout vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code.void main(){ FragColor = ourColor;}
declaramos um uniformevec4
ourColor no shader do fragmento e ajustamos a cor de saída do fragmento ao conteúdo deste valor uniforme. Uma vez que os uniformes são variáveis globais, podemos defini-los em qualquer fase sombreada que gostaríamos, de modo que não há necessidade de passar pelo vértice sombreado novamente para obter algo para o fragmento shader. Não vamos usar este uniforme no sombreador de vértices, por isso não há necessidade de o definir lá.
Se você declarar um uniforme que não é usado em qualquer lugar em seu código GLSL, o compilador irá silenciosamente remover a variável da versão compilada, que é a causa de vários erros frustrantes; tenha isso em mente!
O Uniforme está atualmente vazio; ainda não adicionamos nenhum dado ao uniforme, então vamos tentar isso. Primeiro precisamos encontrar o índice / localização do atributo uniforme em nosso shader. Uma vez que tenhamos o índice/localização do uniforme, podemos atualizar seus valores. Em vez de passar uma única cor para o shader de fragmentos, vamos apimentar as coisas gradualmente mudando de cor ao longo do tempo:
float timeValue = glfwGetTime();float greenValue = (sin(timeValue) / 2.0f) + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUseProgram(shaderProgram);glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
primeiro, recuperamos o tempo de execução em segundos via glfwGetTime(). Em seguida, variamos a cor no intervalo de 0.0
1.0
usando a função sin e armazenar o resultado em greenValue.
então nós pesquisamos para a localização do uniforme ourColor usando glGetUniformLocation. Nós fornecemos o programa shader e o nome do uniforme (que queremos recuperar o local) para a função de consulta. Se o glGetUniformLocation devolve -1
, não conseguiu encontrar a localização. Finalmente podemos definir o valor uniforme usando a função glUniform4f. Note que encontrar a localização uniforme não requer que você use o programa shader primeiro, mas atualizar um uniforme requer que você primeiro use o programa (chamando glUseProgram), porque ele define o uniforme no programa shader atualmente ativo.
porque OpenGL está em seu núcleo uma biblioteca C não tem suporte nativo para sobrecarga de funções, então onde uma função pode ser chamada com diferentes tipos OpenGL define novas funções para cada tipo requerido; gluniforme é um exemplo perfeito disso. A função requer um postfix específico para o tipo de uniforme que você deseja definir. Alguns dos postfixes possíveis são:
-
f
: a função espera umfloat
como seu valor. -
i
: a função espera umint
como seu valor. -
ui
: a função espera umunsigned int
como seu valor. -
3f
: a função espera 3float
s como seu valor. -
fv
: a função espera umfloat
vector / array como seu valor.
sempre que quiser configurar uma opção do OpenGL, basta escolher a função sobrecarregada que corresponde ao seu tipo. No nosso caso, queremos definir 4 flutuadores do uniforme individualmente para que possamos passar os nossos dados através do glUniform4f (note que também poderíamos ter usado ofv
versão). agora que sabemos como definir os valores de variáveis uniformes, podemos usá-los para renderização. Se queremos que a cor mude gradualmente, queremos atualizar este uniforme a cada moldura, caso contrário o triângulo manteria uma única cor sólida se nós só defini-lo uma vez. Então nós calculamos o valor verde e atualizamos o uniforme cada iteração de renderização:
while(!glfwWindowShouldClose(window)){ // input processInput(window); // render // clear the colorbuffer glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // be sure to activate the shader glUseProgram(shaderProgram); // update the uniform color float timeValue = glfwGetTime(); float greenValue = sin(timeValue) / 2.0f + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // now render the triangle glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); // swap buffers and poll IO events glfwSwapBuffers(window); glfwPollEvents();}
o código é uma adaptação relativamente simples do código anterior. Desta vez, atualizamos um valor uniforme em cada moldura antes de desenhar o triângulo. Se você atualizar o uniforme corretamente, você deve ver a cor do seu triângulo gradualmente mudar de verde para preto e de volta para verde.
confira o código-fonte aqui se estiver preso.
Como pode ver, os uniformes são uma ferramenta útil para definir atributos que podem mudar cada moldura, ou para trocar dados entre a sua aplicação e seus shaders, mas e se quisermos definir uma cor para cada vértice? Nesse caso, teríamos de declarar todos os uniformes que temos vértices. Uma solução melhor seria incluir mais dados nos atributos de vértices que é o que vamos fazer agora.
mais atributos!
vimos no capítulo anterior como podemos preencher um VBO, configurar os ponteiros dos atributos de vértices e armazená-lo em um VAO. Desta vez, nós também queremos adicionar dados de cor para os dados de vértices. Nós vamos adicionar dados de cor como 3 float
s para a matriz de vértices. Podemos atribuir um vermelho, verde e azul para cada um dos cantos do nosso triângulo, respectivamente:
float vertices = { // positions // colors 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top };
Já que agora temos mais dados para enviar para o shader de vértice, é necessário ajustar o shader de vértice para também receber o nosso valor de cor como um vértice atributo de entrada. Note que podemos definir a localização de aColor atributo a 1 com o layout do especificador:
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0layout (location = 1) in vec3 aColor; // the color variable has attribute position 1 out vec3 ourColor; // output a color to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); ourColor = aColor; // set ourColor to the input color we got from the vertex data}
Desde que não usamos mais de um uniforme para o fragmento da cor, mas agora usar o ourColor variável de saída de nós vai ter que alterar o fragment shader assim:
#version 330 coreout vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0);}
Porque adicionamos outro vértice atributo e atualizado, o VBO da memória temos que re-configurar o vértice atributo de ponteiros. Os dados atualizados na memória do VBO agora se parecem um pouco com este:
o fato de Saber que o layout atual podemos atualizar o vértice formato com glVertexAttribPointer:
// position attributeglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// color attributeglVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));glEnableVertexAttribArray(1);
Os primeiros argumentos de glVertexAttribPointer são relativamente simples. Desta vez estamos configurando o atributo vertex na localização do atributo 1
. Os valores de cor têm um tamanho de 3
float
s e não normalizamos os valores.
Uma vez que agora temos dois atributos de vértices, temos de calcular novamente o valor stride. Para obter o próximo valor do atributo (por exemplo, o seguinte x
componente do vetor posição) na matriz de dados temos que mover 6
float
s para a direita, três para os valores de posição e de três para os valores de cor. Isto nos dá um valor de stride de 6 vezes o tamanho de umfloat
em bytes (=24
bytes).
Também, desta vez temos que especificar um offset. Para cada vértice, o atributo “posição vértice” é o primeiro a declarar um deslocamento de 0
. O atributo de cor começa após os dados de posição, de modo que o deslocamento é 3 * sizeof(float)
em bytes (= 12
bytes).
a execução da aplicação deverá resultar na seguinte imagem:
confira o código-fonte aqui se estiver preso.
A imagem pode não ser exatamente o que você esperaria, uma vez que só fornecemos 3 cores, não a enorme paleta de cores que estamos vendo agora. Isto é tudo o resultado de algo chamado interpolação de fragmentos no fragmento shader. Ao renderizar um triângulo, o estágio de rasterização geralmente resulta em muito mais fragmentos do que os vértices originalmente especificados. O rasterizador, em seguida, determina as posições de cada um desses fragmentos com base em onde eles residem na forma do triângulo.baseado nestas posições, ele interpola todas as variáveis de entrada do fragmento shader. Digamos, por exemplo, que temos uma linha onde o ponto superior tem uma cor verde e o ponto inferior uma cor Azul. Se o fragment shader é executado em um fragmento que reside em torno de uma posição no 70%
da linha, a sua cor resultante atributo de entrada, em seguida, seria uma combinação linear de verde e azul; para ser mais preciso: 30%
azul e 70%
verde. isto é exatamente o que aconteceu no triângulo. Temos 3 vértices e, portanto, 3 cores, e a julgar pelos pixels do triângulo, ele provavelmente contém cerca de 50000 fragmentos, onde o fragmento shader interpolou as cores entre esses pixels. Se você der uma boa olhada nas cores você verá que tudo faz sentido: vermelho a Azul primeiro chega a roxo e depois a azul. Interpolação de fragmentos é aplicada a todos os atributos de entrada do fragmento shader.
nossa própria classe shader
escrever, compilar e gerenciar shaders pode ser bastante complicado. Como um toque final sobre o assunto shader vamos tornar a nossa vida um pouco mais fácil, construindo uma classe shader que lê shaders do disco, compila e os Liga, verifica por erros e é fácil de usar. Isso também lhe dá um pouco de uma idéia de como podemos encapsular algum do conhecimento que aprendemos até agora em objetos abstratos úteis.
vamos criar a classe shader inteiramente em um arquivo de cabeçalho, principalmente para fins de aprendizagem e portabilidade. Vamos começar adicionando as inclusões necessárias e definindo a estrutura da classe:
#ifndef SHADER_H#define SHADER_H#include <glad/glad.h> // include glad to get all the required OpenGL headers #include <string>#include <fstream>#include <sstream>#include <iostream> class Shader{public: // the program ID unsigned int ID; // constructor reads and builds the shader Shader(const char* vertexPath, const char* fragmentPath); // use/activate the shader void use(); // utility uniform functions void setBool(const std::string &name, bool value) const; void setInt(const std::string &name, int value) const; void setFloat(const std::string &name, float value) const;}; #endif
usámos várias directivas de pré-processador no topo do ficheiro de cabeçalho. Usando estas pequenas linhas de código informa o seu compilador para apenas incluir e compilar este ficheiro header se ainda não tiver sido incluído, mesmo que vários ficheiros incluam o cabeçalho shader. Isto evita a ligação de conflitos.
A classe shader possui o ID do programa shader. Seu construtor requer os caminhos de arquivos do código fonte do vértice e do fragmento shader, respectivamente, que podemos armazenar em disco como arquivos de texto simples. Para adicionar um pouco extra, também adicionamos várias funções utilitárias para facilitar um pouco as nossas vidas: o uso ativa o programa shader, e tudo o conjunto… funções consultar um local uniforme e definir o seu valor.
a leitura do ficheiro
estamos a usar ficheiros C++ para ler o conteúdo do ficheiro em vários string
objectos:
Shader(const char* vertexPath, const char* fragmentPath){ // 1. retrieve the vertex/fragment source code from filePath std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // ensure ifstream objects can throw exceptions: vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); try { // open files vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; // read file's buffer contents into streams vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // close file handlers vShaderFile.close(); fShaderFile.close(); // convert stream into string vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch(std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; } const char* vShaderCode = vertexCode.c_str(); const char* fShaderCode = fragmentCode.c_str();
a seguir precisamos de compilar e ligar os shaders. Note que também estamos revisando se a compilação/ligação falhou e, em caso afirmativo, imprimir os erros de tempo de compilação. Isto é extremamente útil ao depurar (você vai precisar desses logs de erro eventualmente):
// 2. compile shadersunsigned int vertex, fragment;int success;char infoLog; // vertex Shadervertex = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);// print compile errors if anyglGetShaderiv(vertex, GL_COMPILE_STATUS, &success);if(!success){ glGetShaderInfoLog(vertex, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}; // similiar for Fragment Shader // shader ProgramID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);// print linking errors if anyglGetProgramiv(ID, GL_LINK_STATUS, &success);if(!success){ glGetProgramInfoLog(ID, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;} // delete the shaders as they're linked into our program now and no longer necessaryglDeleteShader(vertex);glDeleteShader(fragment);
a função de uso é simples:
void use() { glUseProgram(ID);}
Similarly for any of the uniform setter functions:
void setBool(const std::string &name, bool value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); }void setInt(const std::string &name, int value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), value); }void setFloat(const std::string &name, float value) const{ glUniform1f(glGetUniformLocation(ID, name.c_str()), value); }
and there we have it, a completed shader class. Usando o shader de classe é bastante fácil criar um objeto shader uma vez e a partir desse ponto, basta começar a usá-lo:
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");while(...){ ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff();}
Aqui temos armazenado o vértice e shader de fragmento de código fonte em dois arquivos chamados shader.vs
e shader.fs
. Você é livre para nomear seus arquivos shader como quiser; Pessoalmente, considero as extensões .vs
e.fs
bastante intuitivas.
pode encontrar aqui o código-fonte, usando a nossa classe shader recentemente criada. Lembre-se que pode carregar nos caminhos dos ficheiros shader para encontrar o código-fonte dos shaders.
exercícios
- Ajuste o sombreador de vértices de modo que o triângulo esteja invertido: solução.
- Indique um deslocamento horizontal através de um uniforme e mova o triângulo para o lado direito do ecrã no sombreador de vértices usando este valor de deslocamento: solução.
- dá a posição de vértice para o módulo de fragmentos usando a palavra-chave
out
e define a cor do fragmento igual a esta posição de vértice (veja como mesmo os valores de posição de vértice são interpolados através do triângulo). Uma vez que você conseguiu fazer isso, tente responder à seguinte pergunta: por que o lado inferior-esquerdo do nosso triângulo é preto?: solucao.