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!
vec3 viewDirection = normalize(worldPosition - viewPosition);vec3 reflectedDirection = reflect(viewDirection, normal);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:
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).
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.
//P is rayOrigin + (rayDirection * t)//where t is the return value//returns -1.0 if the ray is outside the boxfloat intersectRayInsideBox(vec3 rayOrigin, vec3 rayDirection, mat4 worldToLocal) {vec3 localOrigin = (worldToLocal * vec4(rayOrigin, 1.0)).xyz;vec3 aabbCheck = abs(localOrigin);if (max(aabbCheck.x, max(aabbCheck.y, aabbCheck.z)) > 1.0) {return -1.0;}vec3 localDirection = mat3(worldToLocal) * rayDirection;vec3 firstPlane = (vec3(-1.0) - localOrigin) / localDirection;vec3 secondPlane = (vec3(1.0) - localOrigin) / localDirection;vec3 furthestPlane = max(firstPlane, secondPlane);return min(furthestPlane.x, min(furthestPlane.y, furthestPlane.z));}
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.
Vector3f position = new Vector3f(0f, 2f, 0f);Quaternionf rotation = new Quaternionf(0f, 0f, 0f, 1f);Vector3f halfExtents = new Vector3f(5f, 2f, 5f);Matrix4f worldToLocal = new Matrix4f().translate(position).rotate(rotation).scale(halfExtents).invert();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.
uniform mat4 cubemapWorldToLocal;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.
vec3 viewDirection = normalize(worldPosition - viewPosition);vec3 reflectedDirection = reflect(viewDirection, normal);vec3 reflection = vec3(0.0);float t = intersectRayInsideBox(worldPosition, reflectedDirection, cubemapWorldToLocal);if (t >= 0.0) {vec3 P = worldPosition + (reflectedDirection * t);vec3 correctedDirection = normalize(P - cubemapPosition);reflection = texture(reflectionCubemap, correctedDirection).rgb;}
5 Resultado Final
E esse é o resultado final: