Sombras ======= Nas aulas anteriores vimos como aumentar o realismo das imagens geradas por computador com alguns efeitos de iluminação e textura. Um outro efeito que contribui bastante para a sensação de realismo são as sombras. A maneira pela qual os objetos projetam sombras no solo e sobre outras superfícies nos fornece importantes pistas visuais sobre as relações espaciais entre esses objetos. Como exemplo disso, imagine que você está olhando para baixo (digamos, em um ângulo de :math:`45^o`) para uma bola sobre uma mesa lisa. Suponha que (1) a bola seja movida verticalmente para cima a uma pequena altura, ou (2) a bola seja movida horizontalmente diretamente para longe de você por uma curta distância. Em ambos os casos, a impressão no quadro visual é essencialmente a mesma. Ou seja, a bola se move para cima no quadro da imagem (veja a :numref:`f21-sombras`). .. figura 60 .. figure:: ./figuras/a21/a21-sombras.png :alt: As sombras indicam relações espaciais entre objetos. :name: f21-sombras :width: 60.0% :align: center As sombras indicam relações espaciais entre objetos. Fonte: `Notas de aula do Prof. Dave Mount `__. Se a sombra da bola fosse desenhada, no entanto, a diferença seria bastante perceptível. No caso (1), de movimento vertical (:numref:`f21-sombras` b), a sombra permanece em posição fixa na mesa enquanto a bola se afasta. No caso (2), de movimento horizontal, a bola e sua sombra se movem juntas (:numref:`f21-sombras` c). Sombras fortes e suaves ----------------------- Na vida real, com poucas exceções, experimentamos sombras como objetos “disformes”. A razão é que a maioria das fontes de luz ocupam uma área e não podem ser consideradas fontes pontuais. Uma exceção é o Sol em um dia sem nuvens. Quando uma fonte de luz cobre alguma área, a sombra varia desde regiões que estão completamente fora da sombra, até uma região, chamada de **penumbra**, onde a luz é parcialmente visível, até uma região chamada de **umbra**, onde a luz fica totalmente oculta. A região da umbra está completamente sombreada é consideramos como sombra **forte** (*hard shadow*). A penumbra, em contraste, tende a variar suavemente de sombra para não-sombra à medida que mais e mais da fonte de luz é visível na superfície. Renderizar efeitos de penumbra é um processo computacionalmente intenso pois é necessário estimar a fração da área da fonte de luz que é visível para cada ponto da superfície. Métodos de renderização estáticos, como rastreamento de raios, podem modelar esses efeitos. Em contraste, os sistemas em tempo real quase sempre renderizam sombras fortes ou empregam alguns truques de imagem (por exemplo, desfocando as imagens) para criar a ilusão de sombras suaves. Nos exemplos a seguir vamos supor que a fonte de luz é um ponto e consideraremos apenas a renderização de sombras fortes. Polígono de sombra ------------------ É provável que a maneira mais simples de renderizar sombras seja “pintá-las” nas superfícies onde elas são projetadas. Por exemplo, suponha que uma sombra esteja sendo projetada sobre uma mesa plana por algum objeto P . Primeiro, calculamos P', a forma da sombra de P no tampo da mesa e, em seguida, renderizamos um polígono na forma de P' diretamente na mesa. Se P for um polígono e a sombra estiver sendo projetada em uma superfície plana, então a forma da sombra P' também será um polígono (*shadow polygon*). Portanto, precisamos apenas determinar a transformação que mapeia cada vértice :math:`v` de P para o vértice :math:`v'` correspondente na sombra. Este processo é ilustrado na :numref:`f21-projecao`. Considere um exemplo simples disso. Suponha que o espaço seja modelado usando um sistema de coordenadas onde o eixo :math:`z` aponta para cima e o plano :math:`xy` é a superfície do solo, sobre a qual a sombra será projetada. Suponha ainda que a fonte de luz seja um ponto no infinito, dado em coordenadas homogêneas (projetivas) como :math:`(v_x, v_y, v_z, 0)^T`. Isso significa que a direção da fonte de luz é dada pelo vetor :math:`v = (v_x, v_y, v_z)^T`. .. figura 61 .. figure:: ./figuras/a21/a21-projecao.png :alt: O polígono de sombra pode ser gerado projetando os vértices sobre um plano. :name: f21-projecao :width: 30.0% :align: center O polígono de sombra pode ser gerado projetando os vértices sobre um plano. Fonte: `Notas de aula do Prof. Dave Mount `__. Para especificar o solo, observamos que, geralmente, um plano no espaço tridimensional pode ser especificado por uma equação da forma :math:`a x + b y + c z + d = 0`. No nosso caso, o solo satisfaz a equação :math:`z = 0` (ou seja, :math:`a = b = d = 0` e :math:`c = 1`). Dado um vértice :math:`p` de P, queremos imaginar um raio projetado da fonte de luz através de :math:`v` até atingir o solo. Tal raio tem o vetor direcional :math:`-v` (uma vez que é direcionado para longe da luz) e passa por :math:`p`, e assim um ponto arbitrário neste raio pode ser representado como :math:`r(\alpha) = p - \alpha v`, onde :math:`\alpha \ge 0` é qualquer escalar não negativo. Esta é uma equação vetorial, e assim temos :math:`r(\alpha)_x = p_x -\alpha\;v_x\;\;`, :math:`\;\;r(\alpha)_y = p_y -\alpha\;v_y\;\;` e :math:`\;\;r(\alpha)_z = p_z - \alpha\;v_z`. Sabemos que a sombra atinge o solo em :math:`z = 0`, e assim temos :math:`0 = r(\alpha)_z = p_z - \alpha \; v_z`, de onde inferimos que :math:`\alpha^* = p_z/v_z`. Podemos derivar as coordenadas :math:`x` e :math:`y` do ponto de sombra como :math:`p'_x = r(\alpha^*)_x = p_x - \cfrac{p_z}{v_z}v_x\;\;` e :math:`\;\;p'_y = r(\alpha^*)_y = p_y - \cfrac{p_z}{v_z}v_y`. Assim, o ponto de sombra desejado pode ser expresso como :math:`p' = ( p_x - \cfrac{v_x}{v_z}p_z, \; p_y - \cfrac{v_y}{v_z}p_z, \; 0)^T`. A matriz de projeção de sombra ------------------------------ É interessante observar que essa transformação é uma transformação afim de :math:`p`. Em particular, podemos expressar isso em forma matricial, chamada matriz de projeção de sombra, como :math:`\begin{pmatrix}\;p'_x\;\\ p'_y \\ p'_z \\ 1 \end{pmatrix} = \begin{pmatrix}\; 1 & 0 & -v_x/v_z & 0 \; \\ 0 & 1 & -v_y / v_z & 0 \\ 0 & 0 & 0 & 0\\ 0 & 0 & 0 & 1 \end{pmatrix} \begin{pmatrix}\;p_x\;\\ p_y \\ p_z \\ 1\end{pmatrix}` Isso é bom pois fornece um mecanismo particularmente elegante para renderizar o polígono de sombra. O processo é descrito no trecho de código a seguir. O primeiro desenho desenha uma projeção de P no chão na cor da sombra, e o segundo desenha o próprio P. Observe que isso pressupõe que P é construído a partir de polígonos, mas essa suposição vale para todos os desenhos no WebGL. .. code:: JavaScript 1. Crie dois polígono, um original e outro de sombra. Os vértices do polígono de sombra devem receber a cor preta. 3. Calcule a matriz model-view, carregue-a no uniforme correspondete e desenhe o polígo original. 2. Calcule a matriz de projeção da sombra, carregue-a no uniforme correspondente à sombra e desenhe o polígono de sombra. Observe que esses dois polígonos podem ser desenhados em qualquer ordem visto que o buffer de profundidade resolve a oclusão. Essa matriz de projeção da sombra funciona para **fontes de luz no infinito**. O que fazemos então quando a fonte de luz não está no infinito? Nesse caso, o problema é que a transformação da projeção da sombra não é mais uma transformação afim. Em particular, os raios de luz não são paralelos entre si, eles **convergem** para a fonte de luz. Refletindo um pouco mais sobre isso, é fácil perceber que esse é exatamente o problema de **projeção perspectiva**. Portanto, a projeção da sombra é apenas um exemplo de transformação projetiva, onde podemos considerar que a fonte de luz é o centro de projeção da câmera e o solo é o plano da imagem. Considerando então que a transformação de projeção de sombra é uma transformação projetiva, a transformação acima precisa ser aplicada à matriz de projeção perspectiva. Dada uma fonte de luz localizada na posição :math:`l = (l_x,l_y,l_z)^T`, a matriz de projeção de sombra para projetar sombras no plano :math:`z = 0` é dada por (lembrando que aplicamos a normalização de perspectiva após a transformação) . :math:`\begin{pmatrix}\;p'_x\;\\ p'_y \\ p'_z \\ 1 \end{pmatrix} = \begin{pmatrix}\; l_z & 0 & -l_x & 0 \; \\ 0 & lz & -l_y & 0 \\ 0 & 0 & 0 & 0\\ 0 & 0 & -1 & l_z \end{pmatrix} \begin{pmatrix}\;p_x\;\\ p_y \\ p_z \\ 1\end{pmatrix} = \begin{pmatrix}\;l_zp_x - l_xp_z\;\\ l_zp_y - l_yp_z \\ 0 \\ l_z - p_z\end{pmatrix} \equiv \begin{pmatrix}\;(l_zp_x - l_xp_z)/(l_z-p_z)\;\\ (l_zp_y - l_yp_z)/(l_z-p_z) \\ 0 \\ 1\end{pmatrix}` Deixamos a derivação desta matriz como exercício. Sua aplicação segue o mesmo processo usado acima, mas lembre-se que a normalização perspectiva será aplicada após a aplicação desta matriz. Polígono de sombra no WegGL --------------------------- A aplicação de uma matriz de projeção de sombra usando WebGL é relativamente simples. Podemos utilizar o mesmo shader para desenhar o mesmo objeto, duas vezes. Nesse exemplo, vamos considerar o plano :math:`xz` como chão e uma quadrado vermelho de lado unitário com altura :math:`y = 0.5`. A sombra portanto deve estar no plano :math:`y=0`. O programa a seguir ilustra o efeito de sombras geradas por uma fonte de luz rodando em volta do eixo :math:`y`. O código completo desse exemplo está disponível no `JSitor `__ e pode ser visto logo a seguir. .. raw:: html O shader e demais funções são simples relativamente simples. Por isso vamos considerar com atenção apenas a função de desenho ``render()``, como mostrada abaixo: .. code:: JavaScript :number-lines: function render () { // atualiza fonte de luz LUZ.theta += VEL_ROTACAO; if (LUZ.theta > 2 * Math.PI) LUZ.theta -= 2 * Math.PI; // limpa tela gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // desenhando um quadrado vermelho let modelView = lookAt(CAM.pos, CAM.at, CAM.up); gl.uniformMatrix4fv(gShader.uModelView, false, flatten(modelView)) gl.uniform4fv(gShader.uColor, RED); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); // desenhar a sombra usando preto LUZ.pos[0] = Math.sin(LUZ.theta); LUZ.pos[2] = Math.cos(LUZ.theta); modelView = mult(modelView, translate(LUZ.pos[0], LUZ.pos[1], LUZ.pos[2])); modelView = mult(modelView, gShader.matSombra); modelView = mult(modelView, translate(-LUZ.pos[0], -LUZ.pos[1], -LUZ.pos[2])); gl.uniformMatrix4fv(gShader.uModelView, false, flatten(modelView)) gl.uniform4fv(gShader.uColor, BLACK); gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); requestAnimationFrame(render); }; As primeiras linhas atualizam a rotação da fonte de luz e limpam o buffer para receber um novo desenho. As linhas 9 a 13 desenham o quadrado vermelho, sempre na mesma posição, visto que a matriz ``modelView`` só depende da câmera ``CAM``. As linhas 15 a 21 modificam a matriz ``modelView`` para projetar a sombra desse polígono. Nesse exemplo, a matriz ``gShader.matSombra`` é simplesmente: .. code:: JavaScript // matriz de projeção da sombra var m = mat4(); m[3][1] = -1/LUZ.pos[1]; m[3][3] = 0; gShader.matSombra = m; como calculada ao final da função ``initShaders()``. Observe que, como ``LUZ.pos[1]`` não é alterada, a matriz ``gShader.matSombra`` é constante, apesar da posição da LUZ ser alterada em :math:`xz` para criar o efeito de rotação na sombra. .. Na prática, para implementar sombras usando WebGL, vamos precisar criar um programa que desenha as sombras em um framebuffer de "rascunho" (ou seja, não no canvas), usando a fonte de luz como "câmera" para projetar as sombras em uma vista da câmera verdadeira (o observador). Vamos deixar essa discussão para a próxima aula. .. Infelizmente, existe uma dificuldade em aplicar isso diretamente no WebGL. O problema é que, por se tratar de uma transformação de projeção, ela pode interferir na projeção da cena na câmera. No entanto, as coordenadas fornecidas a esta matriz já foram transformadas no quadro de visão da câmera. Antes de aplicar esta transformação, a fonte de luz e as equações do plano devem primeiro ser convertidas em coordenadas do quadro de visualização. Omitiremos a discussão deste assunto. Esse processo simples funciona bem para projetar sombras sobre uma superfície plana, mas torna difícil sua aplicação sobre objetos genéricos, com múltiplas faces. Para tratar esses casos, vamos discutir a técnica de mapa de sombras (*shadow maps*), que é baseada na ideia de projeção de texturas. Mapa de sombras --------------- Se usarmos o pipeline da GPU para renderizar a cena "vista" pela fonte de luz pontual, o buffer de profundidades será carregado com as distâncias entre a fonte de luz e o primeiro fragmento iluminado. Essas distâncias (ou profundidades) podem ser armazenadas em uma "textura" chamada de **mapa de profundidades** ou **mapa de sombras**, como mostra a Figura :numref:`f21-shadow-map`. .. figure:: ./figuras/a21/depth-map-generation.png :alt: O mapa de sombras armazena a profundidade (na forma de uma textura) dos fragmentos quando vistos por uma câmera na mesma posição da fonte de luz. :name: f21-shadow-map :width: 50.0% :align: center O mapa de sombras armazena a profundidade (na forma de uma textura) dos fragmentos quando vistos por uma câmera na mesma posição da fonte de luz. Fonte: `WebGL2 Shadows `__. Observe que essa técnica utiliza apenas o buffer de profundidade (não usa o buffer de cores, por exemplo). Você saberia dizer o que acontece com as sombras quando geramos uma imagem de uma câmera na mesma posição da fonte de luz? Essa imagem resultante **não possui sombras**! Como usar o mapa de sombras? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Uma vez calculado o mapa de sombras, a cena é renderizada novamente, mas agora como se vista pela câmera. Para saber se um fragmento é iluminado ou não (ou seja, está em uma sombra), devemos comparar as distâncias de cada fragmento até a fonte de luz com a distância correspondente armazenada no **mapa de sombras**, como ilustrado na Figura :numref:`f21-comparacao`. Quando a distância no mapa é menor, significa que a fonte de luz ilumina um ponto diferente e portanto o ponto deve ser renderizado como sombra. Caso contrário, o fragmento recebe sua cor natural, talvez proveniente de um modelo de iluminação. .. figure:: ./figuras/a21/a21-comparacao.png :alt: Para um ponto P, a profundidade armazenada no mapa de sombras é comparada com sua distância à fonte de luz. :name: f21-comparacao :width: 60.0% :align: center Para um ponto P, a profundidade armazenada no mapa de sombras é comparada com sua distância à fonte de luz. Fonte: `WebGL2 Shadows `__. .. Conflitos de profundidade: cor x sombra ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Um aspecto importante que precisamos tomar cuidado para evitar alguns artefatos visuais é a comparação entre as duas distâncias (comparação entre dois floats). Devido a problemas de quantização, quando essas distâncias forem muito próximas, o processo pode confundir uma sombra com um fragmento visível e vice-versa. Para minimizar esse problema, podemos considerar uma pequena constante positiva :math:`\delta > 0` ao fazermos a comparação entre as distâncias. .. Assim, se a sombra for desenhada exatamente em :math:`z = 0`, haverá competição no buffer de profundidade entre o solo e a sombra. Uma solução rápida para isso é afastar um pouco a sombra do chão. Por exemplo, armazene uma pequena constante positiva :math:`\delta > 0` na terceira linha, última coluna da matriz acima. Isso forçará a coordenada :math:`z` de :math:`p'` ser :math:`\delta`, resultando em um ponto ligeiramente acima do solo. A escolha de :math:`\delta` é um pouco delicada e depende da escala geral da sua cena e da distância da câmera. .. O OpenGL fornece uma função que alcançará o mesmo tipo de perturbação nos valores de profundidade para evitar conflitos no buffer de profundidade. Não vou discutir isso, mas se você estiver interessado, confira o comando OpenGL glPolygonOffset. Se você decidir usar isso, também precisará habilitar GL POLYGON OFFSET FILL e desativá-lo quando terminar. .. Estampas de sombras -------------------- Um segundo problema é o desenho das sombras no plano :math:`z = 0`. Mas ao chegarmos à borda da mesa, vamos continuar desenhando a sombra, mesmo que ela caia para fora da borda ( ver Fig. 62(a)). .. Para evitar esse problema, podemos fazer uso de uma técnica chamada de estampa (*stencil*). A idéia é primeiro estampar a sombra em um framebuffer de rascunho (veja a Fig. 62(b)). Este buffer forma uma espécie de “máscara” que pode ser aplicada para limitar o desenho futuro. Em seguida, desenhamos a sombra, mas habilitamos a estampa, o que significa que os pixels que estejam fora da região da estampa não são desenhados (veja a Fig. 62(c)). Finalmente, o resto da cena é desenhado. (Como a sombra é desenhada um pouco mais alta do que o tampo da mesa, ela sobreviverá no buffer de profundidade.) .. figura 62 Onde estamos e para onde vamos? ------------------------------- Nessa aula apresentamos algumas formas para desenhar sombras fortes geradas por fontes de luz pontuais sobre objetos genéricos. .. As sombras são efeitos de iluminação global pois consideram a relação dos objetos entre si e a fonte de luz, enquanto que em modelos de iluminação local, cada ponto considera apenas sua posição relativa a cada luz. Na próxima aula vamos discutir como renderizar sombras no WebGL usando um mapa de sombras. Para isso, veremos como projetar a cena sobre uma textura, para calcular as profundidades vistas pela fonte de luz. .. e, logo em seguida, introduzir a técnica de ray tracing que, por ser baseada em um modelo de iluminação global, é capaz de reproduzir sombras fortes e suaves. Para saber mais --------------- * Capítulo 5 e 8 do livro "Interactive Computer Graphics, 8th edition" de Angel e Shreiner. * `Notas de aula do Prof. Dave Mount `__. * `WebGL2 Shadows `__.