Raymarching
Desenhe uma cena 3D sem triângulos. Lance um raio por pixel e avance por ele pela distância até a superfície mais próxima.
Como funciona
O pipeline de costume parte de triângulos: o shader de vértice os posiciona, o rasterizador descobre os pixels que eles
cobrem e o shader de fragmentoO programa da GPU que roda uma vez para cada pixel coberto por um triângulo e decide a cor dele. Shader de fragmento O programa da GPU que roda uma vez para cada pixel coberto por um triângulo e decide a cor dele.
A cena não é uma malha. É uma função.
Cenas como funções de distância
Uma função de distância com sinal (SDF) recebe um ponto no espaço e devolve a distância dele até a superfície mais
próxima: positiva do lado de fora, negativa do lado de dentro, zero exatamente sobre ela. Uma esfera de raio r na
origem cabe numa linha:
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
Caixas, toros e cápsulas também têm fórmulas curtas, e elas se combinam: o min de duas distâncias são as duas formas
juntas, o max é onde elas se sobrepõem. A cena inteira vira uma função só, em geral chamada map(p).
Sphere tracing
É a distância que torna a busca rápida. Se map(p) devolve 2.0, nada na cena está a menos de 2.0 de p, em direção
nenhuma. Então o raio pode saltar 2.0 para a frente sem atravessar nada. No ponto novo, pergunte de novo e salte de
novo. Longe de tudo, os passos são longos; conforme o raio se aproxima de uma superfície, eles encolhem, e quando a
distância cai abaixo de um limite pequeno, o raio acertou.
#version 300 es
precision highp float;
uniform vec2 uResolution;
out vec4 fragColor;
float map(vec3 p) {
return length(p) - 1.0; // uma esfera de raio 1 na origem
}
void main() {
// uma câmera em z = 3 olhando para -z, um raio por pixel
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec3 origin = vec3(0.0, 0.0, 3.0);
vec3 dir = normalize(vec3(p, -1.5)); // -1.5 define o campo de visão
float t = 0.0; // distância percorrida ao longo do raio
bool hit = false;
for (int n = 0; n < 96; n++) {
float d = map(origin + dir * t);
if (d < 0.001) { hit = true; break; }
t += d;
if (t > 20.0) break; // saiu da cena
}
vec3 color = vec3(0.05);
if (hit) color = vec3(1.0);
fragColor = vec4(color, 1.0);
}
Isso desenha um disco branco sobre um fundo escuro: a esfera, encontrada, mas ainda sem sombreamento.
Normais a partir do gradiente
A iluminação precisa da normalUm vetor unitário perpendicular a uma superfície, apontando para onde ela está virada. Iluminação e efeitos Fresnel o comparam com outras direções. Normal Um vetor unitário perpendicular a uma superfície, apontando para onde ela está virada. Iluminação e efeitos Fresnel o comparam com outras direções.map aumenta, o gradiente dela, é a normal. Amostrar map um passinho para cada lado em cada eixo dá
uma estimativa:
vec3 normalAt(vec3 p) {
const vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
Isso custa mais seis chamadas a Produto escalar Multiplica dois vetores componente a componente e soma os resultados. Para vetores unitários é o cosseno do ângulo entre eles, de 1 (mesma direção) a -1 (opostos).map. Uma versão mais barata amostra os quatro vértices de um tetraedro. Com uma normal
e uma direção de luz, o produto escalarMultiplica dois vetores componente a componente e soma os resultados. Para vetores unitários é o cosseno do ângulo entre eles, de 1 (mesma direção) a -1 (opostos).dot(a, b) = |a| |b| cos θ
vec3 n = normalAt(origin + dir * t);
float diffuse = max(dot(n, normalize(vec3(0.6, 0.8, 0.4))), 0.0);
Quanto custa
Todo pixel roda o laço, e todo passo avalia a cena inteira. O custo é pixels × passos × o preço de map. Os raios que
passam raspando numa superfície sem tocá-la são os caros: a distância continua pequena, então os passos continuam
pequenos. As alavancas de costume são um limite de passos, um limite de acerto que cresce com a distância, um
comprimento máximo de raio e uma resolução de renderização menor.
O método também depende de map dizer a verdade. Uma função que superestima a distância (comum depois de torcer ou
dobrar o espaço) faz o raio saltar por cima de superfícies finas, que então aparecem com buracos. Multiplicar cada passo
por 0.5 a 0.9 troca velocidade por segurança.
O que ele oferece em troca é igualmente real. Sombras suaves e oclusão ambiente saem de mais algumas marchas rumo à luz
ou ao longo da normal. Repetição infinita é um mod no ponto de entrada. Formas se fundem umas nas outras com um min
suave. Nada disso é fácil com triângulos.
Encontrando uma cena de malhas
Sozinha, uma forma feita com raymarching não sabe das malhas em volta. Para elas poderem esconder umas às outras, o
shader escreve a profundidade do acerto (gl_FragDepth no GLSL, DEPTH no Godot) para o teste de profundidade
compará-la com o resto da cena. Sem isso, o raymarching serve para demos de tela cheia, fundos e efeitos dentro de uma
caixa ou quad.
No Godot
Qualquer shader pode ter laços, então o mesmo código funciona num shader canvas_item sobre um ColorRect (2D, tela
cheia) ou num shader spatial sobre um cubo, em que o raio parte da câmera e passa por cada pixel coberto. No caso
spatial, escrever DEPTH deixa o resultado dentro de uma cena 3D.
Neste site
O card Sphere–box smooth blend é um raymarcher de tela cheia. Suas entradas são UV e tempo; ele também lê
i.resolution para a proporção da tela, e monta o raio da câmera do mesmo jeito, partindo de i.uv em vez de
gl_FragCoord.