9.6 Iluminação na prática
Nesta seção, veremos mais um aprimoramento do visualizador de modelos 3D, como uma continuação do projeto viewer2 da seção anterior (seção 9.6).
Esta será a versão 3 do visualizador (viewer3), e terá shaders extras que implementam os seguintes modelos de reflexão e sombreamento:
- O modelo de Phong, usando sombreamento de Gouraud (shaders
gouraud.vertegouraud.frag); - O modelo de Phong, usando sombreamento de Phong (shaders
phong.vertephong.frag); - O modelo de Blinn–Phong, usando sombreamento de Phong (shaders
blinnphong.verteblinnphong.frag);
Há também algumas funcionalidades complementares, como a possibilidade de modificar em tempo real os parâmetros \(\kappa\), \(\iota\) e \(\alpha\) através de sliders do tipo ImGui::ColorEdit3, e a possibilidade de mudar a orientação da fonte de luz (uma fonte de luz direcional) usando o botão direito do mouse com o trackball virtual.
O resultado ficará como a seguir.
O código C++ é semelhante ao do projeto anterior. As poucas modificações feitas são relacionadas a conceitos que já vimos anteriormente: modificação da interface ImGui e uso do trackball virtual. A seguir, vamos nos concentrar nas modificações mais relevantes que são os novos shaders de iluminação.
Baixe o código completo deste link.
Phong com sombreamento de Gouraud
Iniciaremos com o sombreamento de Gouraud, que consiste em avaliar a equação do modelo de iluminação para cada vértice. Desse modo, praticamente todo o trabalho será feito no vertex shader.
Como o fragment shader é mais simples, começaremos por ele.
gouraud.frag
#version 300 es
precision mediump float;
in vec4 fragColor;
out vec4 outColor;
void main() {
if (gl_FrontFacing) {
outColor = fragColor;
} else {
float i = (fragColor.r + fragColor.g + fragColor.b) / 3.0;
outColor = vec4(i, 0, 0, 1.0);
}
}Este fragment shader é parecido com o que utilizamos nos últimos projetos. O shader recebe uma cor interpolada do rasterizador (fragColor) e copia essa cor para o atributo de saída (outColor).
Na linha 9, verificamos se o fragmento pertence a um triângulo visto de frente. Em caso positivo, a cor de saída é a própria cor de entrada. Caso contrário, calculamos a média entre as componentes RGB (linha 12), e fazemos com que a cor de saída seja um tom de vermelho usando esse valor médio (linha 13). Essa é só uma forma de distinguirmos visualmente o que é o lado da frente e o lado de trás de um triângulo, como fizemos em shaders anteriores.
Vamos ao que interessa, que é o shader gouraud.vert.
gouraud.vert
Para simplificar, implementaremos o modelo de reflexão de Phong para apenas uma fonte de luz direcional. Assim, a equação terá o formato mais simples
\[ \mathbf{I}=\kappa_a \iota_a + \kappa_d \iota_{d} (\hat{\mathbf{l}} \cdot \hat{\mathbf{n}}) + \kappa_s \iota_{s} (\hat{\mathbf{r}} \cdot \hat{\mathbf{v}})^\alpha. \]
O conteúdo completo do shader é listado a seguir. Vamos comentá-lo parte por parte.
#version 300 es
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projMatrix;
uniform mat3 normalMatrix;
// Light properties
uniform vec4 lightDirWorldSpace;
uniform vec4 Ia, Id, Is;
// Material properties
uniform vec4 Ka, Kd, Ks;
uniform float shininess;
out vec4 fragColor;
vec4 Phong(vec3 N, vec3 L, vec3 V) {
N = normalize(N);
L = normalize(L);
// Compute lambertian term
float lambertian = max(dot(N, L), 0.0);
// Compute specular term
float specular = 0.0;
if (lambertian > 0.0) {
// vec3 R = normalize(2.0 * dot(N, L) * N - L);
vec3 R = reflect(-L, N);
V = normalize(V);
float angle = max(dot(R, V), 0.0);
specular = pow(angle, shininess);
}
vec4 diffuseColor = Kd * Id * lambertian;
vec4 specularColor = Ks * Is * specular;
vec4 ambientColor = Ka * Ia;
return ambientColor + diffuseColor + specularColor;
}
void main() {
vec3 P = (viewMatrix * modelMatrix * vec4(inPosition, 1.0)).xyz;
vec3 N = normalMatrix * inNormal;
vec3 L = -(viewMatrix * lightDirWorldSpace).xyz;
vec3 V = -P;
fragColor = Phong(N, L, V);
gl_Position = projMatrix * vec4(P, 1.0);
}A entrada é um atributo de posição e um atributo de vetor normal unitário. Ambos estão em coordenadas do espaço do objeto (coordenadas do VBO):
A saída é um atributo de cor:
O trecho a seguir contém a definição das variáveis uniformes:
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projMatrix;
uniform mat3 normalMatrix;
// Light properties
uniform vec4 lightDirWorldSpace;
uniform vec4 Ia, Id, Is;
// Material properties
uniform vec4 Ka, Kd, Ks;
uniform float shininess;Entre as variáveis uniformes, temos as matrizes de transformação (modelMatrix até normalMatrix), as constantes de intensidade da fonte de luz (Ia,Id,Is), as constantes de reflexão do material (Ka,Kd,Ks) e o expoente de brilho especular (shininess) que é a constante \(\alpha\) do termo especular da equação.
Como a fonte de luz é direcional, a direção da luz é dada pelo vetor lightDirWorldSpace, que está em coordenadas do espaço do mundo (isto é, não precisamos multiplicar pela matriz modelMatrix).
Vamos analisar inicialmente o código de main:
void main() {
vec3 P = (viewMatrix * modelMatrix * vec4(inPosition, 1.0)).xyz;
vec3 N = normalMatrix * inNormal;
vec3 L = -(viewMatrix * lightDirWorldSpace).xyz;
vec3 V = -P;
fragColor = Phong(N, L, V);
gl_Position = projMatrix * vec4(P, 1.0);
}Na linha 46, calculamos P como inPosition transformado para o espaço da câmera. O sufixo .xyz significa que queremos apenas as coordenadas \(x\), \(y\) e \(z\) do vetor (no cálculo da iluminação, não utilizamos a coordenada homogênea). Esse P corresponde ao ponto \(P\) do modelo de Phong apresentado na seção 9.2.
Na linha 47, N é a normal de vértice inNormal convertida para o espaço da câmera. N corresponde ao vetor \(\mathbf{n}\) do modelo de Phong, mas sem estar normalizado.
Na linha 48, L é a direção oposta da direção da luz, convertida para o espaço da câmera. O vetor resultante é o vetor \(\mathbf{l}\) do modelo de Phong. Este vetor também não está normalizado (ainda).
Na linha 49, V é o vetor de direção até a câmera, e corresponde ao vetor
\[
\mathbf{v}=E-P
\]
do modelo de Phong, onde \(E\) é a posição da câmera, e \(P\) é a posição do ponto. Como \(P\) (P) está no espaço da câmera, então \(E=(0,0,0)\). Isso é assim porque, no espaço da câmera, a posição da câmera é a própria origem. Logo,
\[ \begin{align} \mathbf{v}&=\mathbf{0}-P\\ &=-P. \end{align} \]
Perceba que agora temos os vetores principais (\(\mathbf{n}\), \(\mathbf{l}\) e \(\mathbf{v}\)) necessários para avaliar a equação do modelo de reflexão de Phong. Só ficou faltando o vetor \(\mathbf{r}\) (vetor de reflexão ideal), mas este pode ser obtido a partir de \(\mathbf{n}\) e \(\mathbf{l}\).
Os vetores N, L, V, e também o ponto P, estão em um mesmo espaço, que neste caso é o espaço da câmera.
Poderíamos ter representado os pontos e vetores em outro espaço, como o espaço do objeto ou o espaço do mundo. Para o modelo de reflexão de Phong, isso não faz diferença. Entretanto, na avaliação da equação, todos os vetores da equação devem estar em um mesmo espaço.
Nossa escolha em usar o espaço da câmera é simplesmente uma conveniência. No espaço da câmera, \(E=(0,0,0)\) e assim não precisamos enviar a posição da câmera ao vertex shader como mais uma variável uniforme.
Após a definição dos vetores, chamamos uma função Phong que recebe N, L e V, e avalia a equação do modelo de reflexão O resultado é a cor do vértice que será enviada ao rasterizador:
Na linha 53, gl_Position é o ponto P transformado do espaço da câmera para o espaço de recorte através da matriz de projeção:
Vamos agora à definição da função Phong:
vec4 Phong(vec3 N, vec3 L, vec3 V) {
N = normalize(N);
L = normalize(L);
// Compute lambertian term
float lambertian = max(dot(N, L), 0.0);
// Compute specular term
float specular = 0.0;
if (lambertian > 0.0) {
// vec3 R = normalize(2.0 * dot(N, L) * N - L);
vec3 R = reflect(-L, N);
V = normalize(V);
float angle = max(dot(R, V), 0.0);
specular = pow(angle, shininess);
}
vec4 diffuseColor = Kd * Id * lambertian;
vec4 specularColor = Ks * Is * specular;
vec4 ambientColor = Ka * Ia;
return ambientColor + diffuseColor + specularColor;
}A função começa com a normalização de N e L para obter \(\hat{\mathbf{n}}\) e \(\hat{\mathbf{l}}\):
Em seguida é calculado o cosseno do ângulo entre \(\hat{\mathbf{n}}\) e \(\hat{\mathbf{l}}\) (\(\hat{\mathbf{n}} \cdot \hat{\mathbf{l}}\)), que aqui chamamos de termo lambertiano:
Só consideramos valores no intervalo \([0,1]\). O mínimo é fixado em \(0\) através da função max(..., 0.0); o máximo é \(1\) porque os vetores são unitários.
Após o cálculo do termo lambertiano, temos o cálculo do termo especular:
// Compute specular term
float specular = 0.0;
if (lambertian > 0.0) {
// vec3 R = normalize(2.0 * dot(N, L) * N - L);
vec3 R = reflect(-L, N);
V = normalize(V);
float angle = max(dot(R, V), 0.0);
specular = pow(angle, shininess);
}Primeiro, note que só calculamos o termo especular se o termo lambertiano for positivo (linha 30). Fazemos isso pois, na equação de renderização, não existe brilho especular caso a luz não incida sobre a superfície. Em outras palavras, não existe brilho especular na sombra.
O vetor \(\hat{\mathbf{r}}\) é calculado na linha 32 usando a função reflect do GLSL. Essa função faz o mesmo que está comentado na linha 31.
O vetor \(\hat{\mathbf{v}}\) é obtido na linha 33 através da normalização de \(\mathbf{v}\).
Na linha 34 é calculado o cosseno do ângulo entre \(\hat{\mathbf{r}}\) e \(\hat{\mathbf{v}}\) (isto é, \(\hat{\mathbf{r}} \cdot \hat{\mathbf{v}}\)), e novamente só são considerados valores no intervalo \([0,1]\).
Na linha 35, o valor é elevado à constante shininess para obter o valor final \((\hat{\mathbf{r}} \cdot \hat{\mathbf{v}})^\alpha\).
No restante do código, as reflexões difusa, especular e ambiente são calculadas usando as intensidades da fonte de luz e termos de reflexão do material. O resultado é somado para obter a cor final:
vec4 diffuseColor = Kd * Id * lambertian;
vec4 specularColor = Ks * Is * specular;
vec4 ambientColor = Ka * Ia;
return ambientColor + diffuseColor + specularColor;Isso conclui o modelo de reflexão de Phong com sombreamento de Gouraud.
Phong com sombreamento de Phong
phong.vert
Quando usamos sombreamento de Phong, precisamos calcular vetores \(\mathbf{v}\), \(\mathbf{n}\) e \(\mathbf{l}\) para cada fragmento, pois a função Phong que utilizamos em gouraud.vert agora será chamada no fragment shader.
No sombreamento de Phong, o vertex shader é responsável por calcular os vetores V, L e N e enviá-los ao fragment shader através de atributos de saída fragV, fragL e fragN. Assim, o atributo de saída não será mais uma cor fragColor.
O código completo é mostrado a seguir:
#version 300 es
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projMatrix;
uniform mat3 normalMatrix;
uniform vec4 lightDirWorldSpace;
out vec3 fragV;
out vec3 fragL;
out vec3 fragN;
void main() {
vec3 P = (viewMatrix * modelMatrix * vec4(inPosition, 1.0)).xyz;
vec3 N = normalMatrix * inNormal;
vec3 L = -(viewMatrix * lightDirWorldSpace).xyz;
fragL = L;
fragV = -P;
fragN = N;
gl_Position = projMatrix * vec4(P, 1.0);
}phong.frag
O código completo do fragment shader é listado abaixo. Vamos analisá-lo na sequência.
#version 300 es
precision mediump float;
in vec3 fragN;
in vec3 fragL;
in vec3 fragV;
// Light properties
uniform vec4 Ia, Id, Is;
// Material properties
uniform vec4 Ka, Kd, Ks;
uniform float shininess;
out vec4 outColor;
vec4 Phong(vec3 N, vec3 L, vec3 V) {
N = normalize(N);
L = normalize(L);
// Compute lambertian term
float lambertian = max(dot(N, L), 0.0);
// Compute specular term
float specular = 0.0;
if (lambertian > 0.0) {
// vec3 R = normalize(2.0 * dot(N, L) * N - L);
vec3 R = reflect(-L, N);
V = normalize(V);
float angle = max(dot(R, V), 0.0);
specular = pow(angle, shininess);
}
vec4 diffuseColor = Kd * Id * lambertian;
vec4 specularColor = Ks * Is * specular;
vec4 ambientColor = Ka * Ia;
return ambientColor + diffuseColor + specularColor;
}
void main() {
vec4 color = Phong(fragN, fragL, fragV);
if (gl_FrontFacing) {
outColor = color;
} else {
float i = (color.r + color.g + color.b) / 3.0;
outColor = vec4(i, 0, 0, 1.0);
}
}Observe que os atributos de entrada do fragment shader são os atributos de saída do vertex shader:
As constantes utilizadas na equação são definidas como variáveis uniformes:
// Light properties
uniform vec4 Ia, Id, Is;
// Material properties
uniform vec4 Ka, Kd, Ks;
uniform float shininess;A função Phong é exatamente a mesma que utilizamos em gouraud.vert. O resto do código é o código de main:
void main() {
vec4 color = Phong(fragN, fragL, fragV);
if (gl_FrontFacing) {
outColor = color;
} else {
float i = (color.r + color.g + color.b) / 3.0;
outColor = vec4(i, 0, 0, 1.0);
}
}Esta função main é praticamente a mesma de gouraud.frag. A diferença é que a cor do fragmento é calculada por Phong.
Blinn-Phong com sombreamento de Phong
O vertex shader do modelo de Blinn–Phong com sombreamento de Phong é exatamente o mesmo de phong.vert, pois os vetores da equação de iluminação são os mesmos.
O fragment shader também é praticamente idêntico. A única diferença é que utilizamos uma função BlinnPhong no lugar de Phong. A função é definida a seguir:
vec4 BlinnPhong(vec3 N, vec3 L, vec3 V) {
N = normalize(N);
L = normalize(L);
// Compute lambertian term
float lambertian = max(dot(N, L), 0.0);
// Compute specular term
float specular = 0.0;
if (lambertian > 0.0) {
V = normalize(V);
vec3 H = normalize(L + V);
float angle = max(dot(H, N), 0.0);
specular = pow(angle, shininess);
}
vec4 diffuseColor = Kd * Id * lambertian;
vec4 specularColor = Ks * Is * specular;
vec4 ambientColor = Ka * Ia;
return ambientColor + diffuseColor + specularColor;
}O código também é muito parecido com o da função Phong. A única mudança está na forma de calcular o termo especular.
Na linha 29, o vetor halfway \(\hat{\mathbf{h}}\) (H) é computado como a normalização da soma de \(\hat{\mathbf{l}}\) (L) e \(\hat{\mathbf{v}}\) (V).
Na linha 30, calcula-se o cosseno do ângulo entre \(\hat{\mathbf{h}}\) e \(\hat{\mathbf{n}}\) (isto é, \(\hat{\mathbf{h}} \cdot \hat{\mathbf{n}}\)), e novamente consideramos apenas os valores no intervalo \([0,1]\).
O restante do código é o mesmo de Phong.