21. 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 \(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 Fig. 21.1).

As sombras indicam relações espaciais entre objetos.

Fig. 21.1 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 (Fig. 21.1 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 (Fig. 21.1 c).

21.1. 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.

21.2. 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 \(v\) de P para o vértice \(v'\) correspondente na sombra. Este processo é ilustrado na Fig. 21.2.

Considere um exemplo simples disso. Suponha que o espaço seja modelado usando um sistema de coordenadas onde o eixo \(z\) aponta para cima e o plano \(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 \((v_x, v_y, v_z, 0)^T\). Isso significa que a direção da fonte de luz é dada pelo vetor \(v = (v_x, v_y, v_z)^T\).

O polígono de sombra pode ser gerado projetando os vértices sobre um plano.

Fig. 21.2 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

\(a x + b y + c z + d = 0\).

No nosso caso, o solo satisfaz a equação \(z = 0\) (ou seja, \(a = b = d = 0\) e \(c = 1\)).

Dado um vértice \(p\) de P, queremos imaginar um raio projetado da fonte de luz através de \(v\) até atingir o solo. Tal raio tem o vetor direcional \(-v\) (uma vez que é direcionado para longe da luz) e passa por \(p\), e assim um ponto arbitrário neste raio pode ser representado como

\(r(\alpha) = p - \alpha v\),

onde \(\alpha \ge 0\) é qualquer escalar não negativo. Esta é uma equação vetorial, e assim temos

\(r(\alpha)_x = p_x -\alpha\;v_x\;\;\), \(\;\;r(\alpha)_y = p_y -\alpha\;v_y\;\;\) e \(\;\;r(\alpha)_z = p_z - \alpha\;v_z\).

Sabemos que a sombra atinge o solo em \(z = 0\), e assim temos

\(0 = r(\alpha)_z = p_z - \alpha \; v_z\),

de onde inferimos que \(\alpha^* = p_z/v_z\). Podemos derivar as coordenadas \(x\) e \(y\) do ponto de sombra como

\(p'_x = r(\alpha^*)_x = p_x - \cfrac{p_z}{v_z}v_x\;\;\) e \(\;\;p'_y = r(\alpha^*)_y = p_y - \cfrac{p_z}{v_z}v_y\).

Assim, o ponto de sombra desejado pode ser expresso como

\(p' = ( p_x - \cfrac{v_x}{v_z}p_z, \; p_y - \cfrac{v_y}{v_z}p_z, \; 0)^T\).

21.3. A matriz de projeção de sombra

É interessante observar que essa transformação é uma transformação afim de \(p\). Em particular, podemos expressar isso em forma matricial, chamada matriz de projeção de sombra, como

\(\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.

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 \(l = (l_x,l_y,l_z)^T\), a matriz de projeção de sombra para projetar sombras no plano \(z = 0\) é dada por (lembrando que aplicamos a normalização de perspectiva após a transformação) .

\(\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.

21.4. 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 \(xz\) como chão e uma quadrado vermelho de lado unitário com altura \(y = 0.5\). A sombra portanto deve estar no plano \(y=0\).

O programa a seguir ilustra o efeito de sombras geradas por uma fonte de luz rodando em volta do eixo \(y\). O código completo desse exemplo está disponível no JSitor e pode ser visto logo a seguir.

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:

 1function render () {
 2    // atualiza fonte de luz
 3    LUZ.theta += VEL_ROTACAO;
 4    if (LUZ.theta > 2 * Math.PI) LUZ.theta -= 2 * Math.PI;
 5
 6    // limpa tela
 7    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
 8
 9    // desenhando um quadrado vermelho
10    let modelView = lookAt(CAM.pos, CAM.at, CAM.up);
11    gl.uniformMatrix4fv(gShader.uModelView, false, flatten(modelView))
12    gl.uniform4fv(gShader.uColor, RED);
13    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
14
15    // desenhar a sombra usando preto
16    LUZ.pos[0] = Math.sin(LUZ.theta);
17    LUZ.pos[2] = Math.cos(LUZ.theta);
18
19    modelView = mult(modelView, translate(LUZ.pos[0], LUZ.pos[1], LUZ.pos[2]));
20    modelView = mult(modelView, gShader.matSombra);
21    modelView = mult(modelView, translate(-LUZ.pos[0], -LUZ.pos[1], -LUZ.pos[2]));
22
23    gl.uniformMatrix4fv(gShader.uModelView, false, flatten(modelView))
24    gl.uniform4fv(gShader.uColor, BLACK);
25    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
26
27    requestAnimationFrame(render);
28};

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:

// 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 \(xz\) para criar o efeito de rotação na sombra.

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.

21.5. 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 Fig. 21.3.

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.

Fig. 21.3 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!

21.5.1. 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 Fig. 21.4.

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.

Para um ponto P, a profundidade armazenada no mapa de sombras é comparada com sua distância à fonte de luz.

Fig. 21.4 Para um ponto P, a profundidade armazenada no mapa de sombras é comparada com sua distância à fonte de luz. Fonte: WebGL2 Shadows.

21.6. 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.

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.

21.7. Para saber mais