Parallax Corrected Cubemaps

Como cubemaps normais mas melhor

0005

2024/11/13 02:16

EN-US | PT-BR

1 Introdução

Esse artigo foi inspirado por Image-based Lighting approaches and parallax-corrected cubemap

Esse artigo assume que você já sabe como carregar e gerar cubemaps.

2 Reflexo Clássico de Cubemap

Então, qual é o jeito padrão de fazer reflexos de cubemap? bom, é simples: calcule a direção de view da posição da câmera para a posição do fragmento, reflita usando a função "reflect" com a normal do fragmento e pronto!

  1. vec3 viewDirection = normalize(worldPosition - viewPosition);
  2. vec3 reflectedDirection = reflect(viewDirection, normal);
  3. vec3 reflection = texture(reflectionCubemap, reflectedDirection).rgb;

Como pode ver, o reflexo parece estar se movendo conosco, como se estivesse bem longe como o céu, que seria bom se isso fosse um cubemap de céu mas isso é um cubemap local gerado a partir de um local específico.

Dica: Quando estiver gerando cubemaps desative todos os reflexos de specular de luzes e outros cubemaps pois estes dependem da direção da view e poderia ficar muito esquisito olhando de outros ângulos, cubemaps geralmente só devem conter luz difusa.

Se desenharmos isto no paint.net, é isso que temos:

Cubemap Reflection

Como estamos fazendo sampling do cubemap usando apenas a direção refletida "R" sem levar a posição do cubemap em conta (denotada pela bola), o cubemap acaba fazendo sampling da posição incorreta denotada por "P?" ao invés da posição correta denotada por "P" (que não temos) e é por isso que vemos grande parte do teto ao invés das paredes no vídeo.

3 Parallax Corrected Cubemap

Um Parallax Corrected Cubemap resolve esse problema encontrado o P correto utilizando raytracing e então fazendo sampling do cubemap usando a direção da posição do cubemap para P (a direção C).

Parallax Corrected Cubemap

4 Raytracing

Não deixe a palavra "raytracing" te assustar, mesmo se pudéssemos gerar um BVH de nossa cena e usar ele para encontrar P, seria muito lento e complicado para se usar, ao invés nós vamos simplificar nossa cena para um cubo definido pelo usuário e então fazer a intersecção de um raio por dentro dele.

4.1 Em Código

Só precisamos de uma pequena função para fazer a intersecção de um raio com uma caixa por dentro.

Usando o código no artigo mencionado na introdução, eu fiz uma versão mais genérica dele e também adicionei uma checagem de aabb para evitar que o raio faça intersecção por fora da caixa pois percebi que causa vários problemas gráficos em alguns cenários.

  1. //P is rayOrigin + (rayDirection * t)
  2. //where t is the return value
  3. //returns -1.0 if the ray is outside the box
  4. float intersectRayInsideBox(vec3 rayOrigin, vec3 rayDirection, mat4 worldToLocal) {
  5. vec3 localOrigin = (worldToLocal * vec4(rayOrigin, 1.0)).xyz;
  6. vec3 aabbCheck = abs(localOrigin);
  7. if (max(aabbCheck.x, max(aabbCheck.y, aabbCheck.z)) > 1.0) {
  8. return -1.0;
  9. }
  10. vec3 localDirection = mat3(worldToLocal) * rayDirection;
  11. vec3 firstPlane = (vec3(-1.0) - localOrigin) / localDirection;
  12. vec3 secondPlane = (vec3(1.0) - localOrigin) / localDirection;
  13. vec3 furthestPlane = max(firstPlane, secondPlane);
  14. return min(furthestPlane.x, min(furthestPlane.y, furthestPlane.z));
  15. }

Mesmo se a função foi feita para um aabb, ela na verdade suporta qualquer caixa rotacionada ou escalada, já que aplicamos o inverso da matriz da caixa ao raio (a matriz worldToLocal) ao invés da caixa que é sempre um AABB 2x2x2 na perspectiva da função.

No lado da CPU, só precisamos calcular a matriz worldToLocal e enviá-la para o shader junto com a posição do cubemap.

  1. Vector3f position = new Vector3f(0f, 2f, 0f);
  2. Quaternionf rotation = new Quaternionf(0f, 0f, 0f, 1f);
  3. Vector3f halfExtents = new Vector3f(5f, 2f, 5f);
  4. Matrix4f worldToLocal = new Matrix4f()
  5. .translate(position)
  6. .rotate(rotation)
  7. .scale(halfExtents)
  8. .invert()
  9. ;
  10. Vector3f cubemapPosition = new Vector3f(0f, 1.65f, 0f);

"position" é a posição da caixa, "rotation" é a rotação da caixa definida por um quaternion, "halfExtents" é a metade das extensões da caixa (uma caixa de comprimento 10,0 tem uma meia extensão de 5.0 no eixo X) e "cubemapPosition" é a posição do cubemap de onde ele foi gerado.

  1. uniform mat4 cubemapWorldToLocal;
  2. uniform vec3 cubemapPosition;

Como a intersecção só funciona se o fragmento estiver dentro da caixa, você deve fazer a caixa um pouco maior do que a área que ela representa.

E então, só precisamos fazer a intersecção do raio da posição do fragmento usando a direção refletida para encontrar P e então fazer sample do cubemap usando a direção corrigida.

  1. vec3 viewDirection = normalize(worldPosition - viewPosition);
  2. vec3 reflectedDirection = reflect(viewDirection, normal);
  3. vec3 reflection = vec3(0.0);
  4. float t = intersectRayInsideBox(worldPosition, reflectedDirection, cubemapWorldToLocal);
  5. if (t >= 0.0) {
  6. vec3 P = worldPosition + (reflectedDirection * t);
  7. vec3 correctedDirection = normalize(P - cubemapPosition);
  8. reflection = texture(reflectionCubemap, correctedDirection).rgb;
  9. }

5 Resultado Final

E esse é o resultado final: