Iluminação e sombreamento no WebGL ================================== Agora vamos aplicar o modelo de Phong para gerar cenas com os efeitos de iluminação que discutimos nas aulas anteriores. Mas vamos começar com algo mais simples, fazendo os shaders calcularem apenas a componente de reflexão de difusão para uma fonte de luz pontual no infinito. Iluminação com luz direcional ----------------------------- Imagine uma cena iluminada pelo Sol ao meio dia. Como o Sol está muito distante da Terra, os raios de luz que chegam aqui podem ser considerados paralelos e, por serem paralelos, podemos simplificar a equação de iluminação. Para representar uma fonte de luz no infinito vamos considerar apenas a direção da luz por um vetor ``LUZ_DIR`` como: .. code:: JavaScript const LUZ_DIR = vec4(1.0, 1.0, 1.0, 0.0); const MAT_DIF = vec4(0.5, 1.0, 0.0, 1.0); Vamos aproveitar e considerar um cubo de lado unitário, centrado na origem, com todas as suas faces com a mesma cor definida por ``MAT_DIF``, ou seja, vamos assumir que essa já seja a cor refletida pela componente de difusão da luz depois de interagir com o material que compõe a superfície que é muito semelhante ao que a gente fazia nos exemplos vistos nas aulas anteriores. O efeito da componente de difusão da iluminação depende da normal em cada ponto, que deve apontar para **fora** do objeto. Devemos então incluir em nosso modelo, além da posição de cada vértice, a direção da normal que pode ser feito pelo trecho de código abaixo. .. code:: JavaScript const CUBO_CANTOS = [ vec4(-0.5, -0.5, 0.5, 1.0), vec4(-0.5, 0.5, 0.5, 1.0), vec4( 0.5, 0.5, 0.5, 1.0), vec4( 0.5, -0.5, 0.5, 1.0), vec4(-0.5, -0.5, -0.5, 1.0), vec4(-0.5, 0.5, -0.5, 1.0), vec4( 0.5, 0.5, -0.5, 1.0), vec4( 0.5, -0.5, -0.5, 1.0) ]; /** ................................................................ * Objeto Cubo de lado 1 centrado na origem. * * usa função auxiliar quad(pos, nor, vert, a, b, c, d) */ function Cubo() { this.np = 36; // número de posições (vértices) this.pos = []; // vetor de posições this.nor = []; // vetor de normais this.axis = EIXO_X_IND; // usado na animação da rotação this.theta = vec3(0, 0, 0); // rotação em cada eixo this.rodando = false; // pausa a animação this.init = function () { // carrega os buffers quad(this.pos, this.nor, CUBO_CANTOS, 1, 0, 3, 2); quad(this.pos, this.nor, CUBO_CANTOS, 2, 3, 7, 6); quad(this.pos, this.nor, CUBO_CANTOS, 3, 0, 4, 7); quad(this.pos, this.nor, CUBO_CANTOS, 6, 5, 1, 2); quad(this.pos, this.nor, CUBO_CANTOS, 4, 5, 6, 7); quad(this.pos, this.nor, CUBO_CANTOS, 5, 4, 0, 1); }; }; /** ................................................................ * cria triângulos de um quad e os carrega nos arrays * pos (posições) e nor (normais). * @param {*} pos : array de posições a ser carregado * @param {*} nor : array de normais a ser carregado * @param {*} vert : array com vértices do quad * @param {*} a : indices de vertices * @param {*} b : em ordem anti-horária * @param {*} c : * @param {*} d : */ function quad (pos, nor, vert, a, b, c, d) { var t1 = subtract(vert[b], vert[a]); var t2 = subtract(vert[c], vert[b]); var normal = cross(t1, t2); normal = vec3(normal); pos.push(vert[a]); nor.push(normal); pos.push(vert[b]); nor.push(normal); pos.push(vert[c]); nor.push(normal); pos.push(vert[a]); nor.push(normal); pos.push(vert[c]); nor.push(normal); pos.push(vert[d]); nor.push(normal); }; Cada uma das 6 faces do cubo é definida como uma sequência de 4 cantos do array ``CUBOS_CANTOS``. Para que as normais apontem para fora do cubo, as sequências devem ser definidas em ordem **anti-horária**. A função ``quad()`` é chamada para criar 2 triângulo para cada face e carregar os arrays de posições e normais do cubo. A função ``Cubo()`` é usada para `criar um objeto Cubo `__ que, nesse caso, ainda precisa ser inicializado por ``init()`` antes de ser utilizado, por exemplo, por meio do seguinte trecho de código: .. code:: JavaScript var gCubo = new Cubo(); gCubo.init(); Observe que poderíamos ainda definir cores diferentes para cada vértice mas, para realçar o efeito de iluminação, vamos usar uma única cor ``MAT_DIF``, uniforme para todas as faces. Vertex shader ~~~~~~~~~~~~~ Nesse primeiro exemplo vamos fazer o vertex shader apenas transformar os vetores de luz e normal para a posição da câmera e os passe para o fragment shader para calcular a cor final. .. code:: JavaScript var gVertexShaderSrc = `#version 300 es in vec4 aPosition; in vec3 aNormal; uniform mat4 uModel; uniform mat4 uView; uniform mat4 uPerspective; uniform vec4 uLuzDir; out vec3 vNormal; out vec3 vLuzDir; void main() { mat4 modelView = uView * uModel; gl_Position = uPerspective * modelView * aPosition; // orienta as normais como vistas pela câmera vNormal = mat3(modelView) * aNormal; vLuzDir = mat3(uView) * uLuzDir.xyz; } `; Como o vetor normal é uma propriedade de cada vértice, a normal é transformada pela matriz ``modelView``, que combina as transformações ``uModel`` e ``uView``. Assim, se o objeto estiver rodando com relação à câmera, esses vetores também estarão rodando e vão *ajudar* a calcular a iluminação de acordo com essa rotação. No entanto, observe que a direção da luz é transformada **apenas** segundo a matriz ``uView`` (que controla a vista da câmera). Assim, a orientação relativa entre a câmera e a direção da luz não se altera com a movimentação dos objetos (como deve ser, certo?). A cor dos vértices poderia ser calculada no vertex shader. Mas como elas são interpoladas, o efeito da iluminação pode ficar diluído. Para obter um resultado melhor, vamos passar esses dois vetores ao fragment shader, **sem normalizar**. Assim evitamos também os efeitos de interpolação sobre esses vetores. Fragment shader ~~~~~~~~~~~~~~~ Lembre-se que a reflexão de difusão é proporcional ao cosseno do ângulo entre o vetor normal e a direção da luz. Como a luz é direcional, essa direção é a mesma para todos os pontos. Podemos então calcular o coeficiente de reflexão de difusão ``kdd`` como o produto escalar dos vetores normal e direção da luz, após serem normalizados. Quando o coeficiente for negativo, sabemos também que o ponto não está sendo iluminado (ou está iluminado por trás e não pela frente). Nesse exemplo, esses pontos estão sendo pintados de vermelho, para que você possa verificar esse efeito também. Na prática, essas faces vermelhas receberiam a cor da componente ambiente, como veremos mais tarde. .. code:: JavaScript var gFragmentShaderSrc = `#version 300 es precision highp float; in vec3 vNormal; in vec3 vLuzDir; out vec4 corSaida; uniform vec4 uMatDif; // cor de difusao do material void main() { vec3 normal = normalize(vNormal); vec3 nvl = normalize(vLuzDir); float kdd = dot(normal, nvl); corSaida = vec4(1, 0, 0, 1); // parte não iluminada if (kdd > 0.0) { // parte iluminada corSaida = kdd * uMatDif; } corSaida.a = 1.0; } `; Exemplo completo ~~~~~~~~~~~~~~~~ O código completo desse exemplo está disponível no `JSitor `__ e também pode ser visto logo abaixo. Modifique os valores para colocar a câmera em posições distintas como ``eye=(2, 0, 0)``, ``eye=(2,2,0)`` e ``eye=(0,0,2)`` para ver o que acontece com a iluminação. Varie também a direção da luz e as cores. .. raw:: html Correção dos vetores normais ---------------------------- Um problema que pode ocorrer quando aplicamos a matriz ``modelView`` diretamente para transformar as normais é ilustrado na Figura :numref:`f18-transInversa`. .. figure:: figuras/a18/transInversa.gif :alt: Cubo em 3D iluminado usando o modelo de iluminação local de Phong. :name: f18-transInversa :width: 65.0% :align: center Observe que as normais podem ser deformadas pela transformação modelView, como ilustrado no lado esquerdo. Essa deformação pode ser corrigida usando a transposta da inversa da matriz modelView, como mostrado na animação do lado direito. Fonte: `webgl2fundamentals.org `__. Nessa animação, a esfera mostrada ao centro sofre uma transformação de escala não linear pela matriz ``modelView``, mostrada do lado esquerdo como ``world``. Observe que os vetores normais, ao serem transformados, deixam de ser perpendiculares à superfície. Ao invés de aplicar a matriz ``modelView``, para evitar esse problema devemos usar a matriz transposta da inversa da ``modelView``, como mostrado na animação do lado direito da figura como ``worldinverseTranspose``. Por que isso funciona? ~~~~~~~~~~~~~~~~~~~~~~ Uma explicação pode ser derivada da definição de vetor normal. Considere um vetor normal :math:`\vec{n}` e seja :math:`\vec{t}` o vetor tangente ao ponto :math:`P` que estamos desenhando. Como esses vetores são perpendiculares temos que :math:`\vec{n} \cdot \vec{t} = 0`. Vamos considerar uma superfície plana ao redor do ponto (que pode ser tão pequena quanto você queira). Seja o ponto :math:`Q` contido nesse plano, de forma que podemos calcular a tangente como :math:`\vec{t} = Q - P`. Vamos agora aplicar uma transformação :math:`M` sobre a superfície. O novo vetor tangente pode então ser calculado como: :math:`\vec{t}' = MQ - MP = M(Q-P) = M\vec{t}`. Considere agora a transformação :math:`N` que deve ser aplicada à normal :math:`\vec{n}` tal que :math:`(N \vec{n}) \cdot (M \vec{t}) = 0`. Vamos escrever essa equação na forma de um produto entre um vetor linha e um vetor coluna tal que :math:`transpose(N \vec{n) \; (M \vec{t}) = = transpose(\vec{n})\; transpose(N)\; M \; \vec{t} = 0`. Observe agora que, caso :math:`tranpose(N) \; M = I` (onde :math:`I` é a matriz identidade) temos que :math:`transpose(\vec{n}) \; \vec{t} = 0`. Agora, como exercício, mostre que quando aplicamos a transformação :math:`N = transpose(inverse(M))` sobre a normal, a normal transformada é perpendicular ao vetor :math:`M \vec{t}`. Corrigindo as normais no WebGL ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ O código completo desse exemplo está disponível no `JSitor `__ e também pode ser visto abaixo. Em particular, preste atenção na matriz ``uInverseTranspose`` (passada como uniforme) no vertex shader. Essa matriz é calculada na função ``render()``. .. raw:: html Modelo de Phong com fonte de luz pontual ---------------------------------------- Vamos agora implementar um programa gráfico que utilizada todas as componentes de reflexão do modelo de Phong usando uma fonte de luz pontual, ao invés de uma fonte direcional (no infinito) como usada nos exemplos anteriores. Para aplicar o modelo completo, precisamos definir as propriedades da luz e do material da superfície, para cada componente do modelo de reflexão. Nesse exemplo, vamos considerar dois objetos, ``LUZ`` e ``MAT`` com os seguintes atributos: .. code:: JavaScript const LUZ = { pos : vec4(0.0, 3.0, 0.0, 1.0), // posição amb : vec4(0.2, 0.2, 0.2, 1.0), // ambiente dif : vec4(1.0, 1.0, 1.0, 1.0), // difusão esp : vec4(1.0, 1.0, 1.0, 1.0), // especular }; const MAT = { amb : vec4(0.8, 0.8, 0.8, 1.0), dif : vec4(1.0, 0.0, 1.0, 1.0), alfa : 50.0, // brilho ou shininess }; O modelo do cubo, constituído por arrays de vértices e normais é exatamente o mesmo usado nos exemplos anteriores. Observe que cada vértice poderia receber ainda materiais diferentes. Nesse exemplo, novamente consideramos apenas um único material, para realçar os efeitos de iluminação apenas. É por essa razão também (realçar cada componente) que as cores de cada componente de reflexão do material foi escolhida para ser diferente das demais. Você pode alterar esses valores no exemplo do JSitor mais abaixo para ver o resultado. Vertex shader ~~~~~~~~~~~~~~ Vamos primeiro discutir as mudanças que precisamos fazer no código do vertex shader, fornecido abaixo. Como a fonte de luz não está no infinito, não podemos mais considerar que todos os raios de luz são paralelos. Por isso precisamos calcular o vetor ``vLight`` que aponta para a fonte de luz a partir de cada ponto da superfície, como vistos pela câmera (mas antes da deformação perspectiva!). Novamente, observe que cada posição ``pos`` é transformada pela matriz ``modelView``, enquanto a posição da fonte de luz vista pela câmera sofre apenas a transformação ``uView``. O vetor ``vLight`` é calculado pela diferença entre esses dois pontos. Devido à transformação da câmera, o vetor ``vView`` (que aponta para o observador) é simplesmente ``-pos``, já que o observador agora é o novo "centro de coordenadas". O vetor normal é corrigido pela matriz ``uInverseTranspose`` e todos esses vetores são passados para o fragment shader. .. code:: JavaScript var gVertexShaderSrc = `#version 300 es in vec4 aPosition; in vec3 aNormal; uniform mat4 uModel; uniform mat4 uView; uniform mat4 uPerspective; uniform mat4 uInverseTranspose; uniform vec4 uLuzPos; out vec3 vNormal; out vec3 vLight; out vec3 vView; void main() { mat4 modelView = uView * uModel; gl_Position = uPerspective * modelView * aPosition; // orienta as normais como vistas pela câmera vNormal = mat3(uInverseTranspose) * aNormal; vec4 pos = modelView * aPosition; vLight = (uView * uLuzPos - pos).xyz; vView = -(pos.xyz); } `; Fragment shader ~~~~~~~~~~~~~~~ No fragment shader, exibido abaixo, os vetores calculados pelo vertex shader são normalizados. O vetor *halfway* é calculado em função dos vetores ``vLight`` e ``vView`` como vimos em aulas passadas. .. code:: JavaScript var gFragmentShaderSrc = `#version 300 es precision highp float; in vec3 vNormal; in vec3 vLight; in vec3 vView; out vec4 corSaida; // cor = produto luz * material uniform vec4 uCorAmbiente; uniform vec4 uCorDifusao; uniform vec4 uCorEspecular; uniform float uAlfaEsp; void main() { vec3 normalV = normalize(vNormal); vec3 lightV = normalize(vLight); vec3 viewV = normalize(vView); vec3 halfV = normalize(lightV + viewV); // difusao float kd = max(0.0, dot(normalV, lightV) ); vec4 difusao = kd * uCorDifusao; // especular float ks = pow( max(0.0, dot(normalV, halfV)), uAlfaEsp); vec4 especular = vec4(1, 0, 0, 1); // parte não iluminada if (kd > 0.0) { // parte iluminada especular = ks * uCorEspecular; } corSaida = difusao + especular + uCorAmbiente; corSaida.a = 1.0; } `; Ao invés de passar as componentes de luz e material, as cores resultantes do produto ``LUZ`` * ``MAT`` são passadas como uniformes ao fragment shader. Assim, a ``uCorAmbiente`` recebe o produto (elemento a elemento) da ``LUZ.amb`` por ``MAT.amb`` e, da mesma forma, a CPU calcula e passa para a GPU a ``uCorDifusao``. A ``uCorEspecular`` só depende da fonte de luz. O fragment shader recebe ainda o parâmetro de brilho especular (``uAlfaEsp``) e calcula as componentes de difusão e especular aplicando as equações de iluminação do modelo de Phong. Essas componentes são somadas à ``uCorAmbiente`` para compor a cor final. Exemplo completo ~~~~~~~~~~~~~~~~ O código completo desse exemplo está disponível no `JSitor `__ e também pode ser visto abaixo. .. raw:: html A interface desse exemplo permite modificar o parâmetro de brilho especular. Para valores grandes, observe que é possível observar uma "bola branca" (região brilhante) que diminui de tamanho com o aumento do valor de ``alfaEsp``. Para valores menores, esse brilho fica cada vez mais ``esparramado`` pela superfície. Note também que o brilho depende muito da orientação relativa da superfície, observador e fonte de luz. Nesse exemplo, as faces não diretamente iluminadas pela fonte de luz ainda são pintadas de vermelho. Modifique esse exemplo para que calcular as cores corretamente segundo o modelo de Phong. Onde estamos e para onde vamos? ------------------------------- O processo para geração de imagens tridimensionais usados pelo WebGL é baseado no modelo de **câmera sintética**. Vimos como posicionar uma câmera virtual usando a função ``lookAt(eye, at, up)`` e como configurar suas propriedades de projeção perspectiva com profundidade usando a função ``perspective(fovy, aspect, near, far)``. A aplicação dessas funções sobre um modelo de objeto constituído por vértices em 3D resulta em vértices como vistos pela câmera, dentro de um volume canônico de visualização. A adoção desse volume permite que o recorte das partes não visíveis seja feito de forma bastante eficaz. A profundidade dos fragmentos restantes, dentro do volume, é testada para renderizar apenas os pontos mais próximos ao observador. Depois disso, para aumentar o realismo das imagens geradas por essa câmera sintética, consideramos os efeitos que uma fonte de luz pontual pode criar na imagem aplicando o modelo de reflexão de Phong. Esse é um **modelo de iluminação local**, que considera apenas as interações da luz (de uma fonte pontual) diretamente com cada elemento da superfície de um objeto (ou seja, não testa oclusão), por meio de 4 componentes: emissão, ambiente, difusa e especular. Vimos nessa aula que o modelo de Phong é computacionalmente eficiente e, embora o modelo não necessariamente retrate o comportamento real da luz, ele permite a renderização de imagens com efeitos de iluminação bem realistas em tempo real usando WebGL. Além de não tratar sombras (por se tratar de um modelo local), outra limitação dessa técnica é que ela considera superfícies lisas, que modelam bem superfícies metálicas por exemplo. A partir da próxima aula, veremos como tratar de superfícies com texturas. Para saber mais --------------- * Capítulo 6 do livro "Interactive Computer Graphics, 8th edition" de Angel e Shreiner. * `WebGL2 3D - Directional Lighting `__. * `Normal Transformation `__. .. from Stack https://stackoverflow.com/questions/13654401/why-transform-normals-with-the-transpose-of-the-inverse-of-the-modelview-matrix#:~:text=Simply%20divide%20the%20normal%20by,matrix%20you%20are%20using%20instead.&text=For%20any%20orthonormal%20transformation%20M,%5E(%2DT)%20%3D%20M. It flows from the definition of a normal. Suppose you have the normal, N, and a vector, V, a tangent vector at the same position on the object as the normal. Then by definition N·V = 0. Tangent vectors run in the same direction as the surface of an object. So if your surface is planar then the tangent is the difference between two identifiable points on the object. So if V = Q - R where Q and R are points on the surface then if you transform the object by B: V' = BQ - BR = B(Q - R) = BV The same logic applies for non-planar surfaces by considering limits. In this case suppose you intend to transform the model by the matrix B. So B will be applied to the geometry. Then to figure out what to do to the normals you need to solve for the matrix, A so that: (AN)·(BV) = 0 Turning that into a row versus column thing to eliminate the explicit dot product: [tranpose(AN)](BV) = 0 Pull the transpose outside, eliminate the brackets: transpose(N)*transpose(A)*B*V = 0 So that's "the transpose of the normal" [product with] "the transpose of the known transformation matrix" [product with] "the transformation we're solving for" [product with] "the vector on the surface of the model" = 0 But we started by stating that transpose(N)*V = 0, since that's the same as saying that N·V = 0. So to satisfy our constraints we need the middle part of the expression — transpose(A)*B — to go away. Hence we can conclude that: transpose(A)*B = identity => transpose(A) = identity*inverse(B) => transpose(A) = inverse(B) => A = transpose(inverse(B))