Campos de distância com sinal em 3D
Esfera, caixa, toro e cápsula como funções de distância de um ponto 3D, e a normal da superfície a partir do gradiente do campo.
Como funciona
Tudo dos campos de distânciaUma função que devolve a distância de um ponto até a borda de uma forma, negativa dentro e positiva fora. O zero é o próprio contorno. Campo de distância com sinal (SDF) Uma função que devolve a distância de um ponto até a borda de uma forma, negativa dentro e positiva fora. O zero é o próprio contorno.circle(p) = |p| − r em 2D vale em 3D: uma função recebe um ponto, agora um vec3, e
devolve a distância até a superfície mais próxima, negativa por dentro. As booleanas, o mínimo suave, a repetição e o
arredondamento funcionam sem mudança. O que o 3D acrescenta é um jeito de ver o campo, o raymarching (avançar ao longo
de um raio saindo da câmera pela distância que o campo informa), e a necessidade de uma normal da superfície para
iluminá-lo. Este fundamento trata das formas e da normal; o raymarching em si é um conceito de renderização mais adiante
na ordem.
As formas básicas
A maioria estende diretamente a versão 2D:
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
// b: metade do tamanho em cada eixo
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, max(d.y, d.z)), 0.0);
}
// um segmento de a até b, engrossado por r
float sdCapsule(vec3 p, vec3 a, vec3 b, float r) {
vec3 pa = p - a;
vec3 ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h) - r;
}
// n: normal unitária; o plano fica à distância h ao longo dela
float sdPlane(vec3 p, vec3 n, float h) {
return dot(p, n) - h;
}
A esfera, a caixa e a cápsula são o círculo, a caixa e o segmento com um componente a mais. O plano é um
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). 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).dot(a, b) = |a| |b| cos θ
Montando formas a partir das 2D
Algumas formas 3D são uma distância 2D aplicada num plano. Um toro é um círculo de raio big no plano xz, varrido por
um tubo de raio small. No semiplano que contém o eixo y e o ponto, o ponto fica em (length(p.xz) - big, p.y) em
relação à linha central do tubo, e o toro ali é um círculo 2D:
float sdTorus(vec3 p, float big, float small) {
vec2 q = vec2(length(p.xz) - big, p.y);
return length(q) - small;
}
O mesmo movimento, vec2(length(p.xz), p.y) passado a qualquer função de distância 2D, gira uma forma 2D em torno do
eixo y: uma caixa 2D dá um cilindro, uma caixa 2D arredondada dá um cilindro arredondado. A extrusão faz o caminho
contrário: avalie uma distância 2D em p.xy e faça a interseção com uma placa de meia espessura t ao longo de z, o
que transforma uma forma plana numa moeda ou numa letra com profundidade.
A normal a partir do gradiente
Para iluminar uma superfície, você precisa da normal dela. Um campo de distância a entrega de graça: o campo cresce mais
rápido no sentido que se afasta da superfície em linha reta, então o gradiente dele aponta ao longo 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.
O gradiente pode ser calculado avaliando o campo em pontos vizinhos, um par por eixo:
float scene(vec3 p) {
return min(sdSphere(p - vec3(-0.6, 0.0, 0.0), 0.5),
sdTorus(p - vec3(0.6, 0.0, 0.0), 0.4, 0.12));
}
vec3 calcNormal(vec3 p) {
const vec2 e = vec2(0.001, 0.0); // um passo pequeno, em unidades da cena
return normalize(vec3(
scene(p + e.xyy) - scene(p - e.xyy),
scene(p + e.yxy) - scene(p - e.yxy),
scene(p + e.yyx) - scene(p - e.yyx)));
}
Os swizzlesLer ou escrever os componentes de um vetor pelo nome, em qualquer ordem, como em v.xy, color.bgr ou p.yx. Na leitura, repetir é permitido. Swizzle Ler ou escrever os componentes de um vetor pelo nome, em qualquer ordem, como em v.xy, color.bgr ou p.yx. Na leitura, repetir é permitido.e.xyy, e.yxy e e.yyx são passos pequenos ao longo de x, y e z. Cada linha é
uma diferença central: o quanto a distância muda num passo minúsculo naquele eixo. O tamanho do passo é um meio-termo:
grande demais e os detalhes pequenos borram, pequeno demais e o erro de ponto flutuante deixa a normal ruidosa. 0.001
serve para cenas com cerca de uma unidade de largura; ajuste com a escala da cena, ou com a distância até a câmera.
Isso custa seis avaliações da cena inteira por pixel. Uma otimização comum amostra quatro pontos nos vértices de um tetraedro, o que dá a mesma direção com duas avaliações a menos; o artigo sobre normais nas fontes faz a dedução.
Vendo um campo 3D sem raymarching
Antes de escrever um raymarcher, você pode inspecionar um campo 3D fatiando-o: avalie-o num plano e desenhe o resultado
2D. Com uma coordenada de pixel 2D p:
float d = scene(vec3(p, sin(time) * 0.5)); // uma fatia que se move ao longo de z
vec3 color = d < 0.0 ? vec3(0.9, 0.5, 0.2) : vec3(0.2, 0.5, 0.9);
color *= 0.8 + 0.2 * cos(60.0 * d); // curvas de nível a cada ~0.1 unidade
// a própria superfície
color = mix(color, vec3(1.0), 1.0 - smoothstep(0.0, 0.01, abs(d)));
Curvas de nível igualmente espaçadas indicam que o campo é uma distância real. Onde elas se afastam, o campo subestima a distância, e o raymarcher vai precisar de mais passos ali; onde elas se apertam, ele superestima, e o raymarcher vai atravessar a superfície.