Pular para o conteúdo
Renderização · Métodos de renderizaçãoIntermediário

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.

Demo ao vivo em breve

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 fragmento pinta esses pixels. O raymarching inverte isso. Ele desenha um retângulo que cobre a tela, e o shader de fragmento faz todo o trabalho 3D sozinho. Para cada pixel, ele lança um raio da câmera para dentro da cena e procura ao longo dele a primeira superfície.

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 normal da superfície. Uma malha guarda a normal por vértice; uma SDF a entrega de graça por outro caminho. A distância cresce mais rápido na direção que se afasta reto da superfície, então a direção em que 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 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 escalar sombreia o acerto como em qualquer malha:

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.

Leitura complementar