Campos de distância com sinal em 2D
Descreva uma forma pela distância até a borda, negativa por dentro, e tire preenchimento, contorno, brilho e bordas limpas de um único número.
Como funciona
O jeito rápido de desenhar um círculo é um teste: este pixel está dentro ou não? Isso dá um bit por pixel, o que basta
para um preenchimento de borda dura e nada mais. Um campo de distância com sinalUma 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 faz uma pergunta
melhor: a que distância este pixel está da borda da forma? A resposta é negativa dentro, zero na borda e positiva fora.
Desse único número você tira o preenchimento, o contorno, um brilho suave, cantos arredondados ou a mistura com outra
forma.
O círculo
A distância de um ponto até a borda de um círculo é a distância até o centro menos o raio:
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
p é a posição do pixel em relação ao centro da forma, então um círculo em outro lugar é sdCircle(p - center, r), a
translação das transformações 2D. Use coordenadas centralizadas e com a proporção corrigida (veja o fundamento de UV),
senão as distâncias ficam esticadas e o círculo vira uma elipse cuja “distância” está errada.
A caixa
Para uma caixa alinhada aos eixos com metade do tamanho b, dobre o plano num só quadrante com abs, já que a caixa é
simétrica, e meça o quanto o ponto passou do canto:
float sdBox(vec2 p, vec2 b) {
// por eixo: o quanto está fora de cada par de lados
vec2 d = abs(p) - b;
// fora: a distância até o lado ou canto mais próximo;
// dentro: a distância (negativa) até o lado mais próximo
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
Fora da caixa, max(d, 0.0) fica só com os eixos em que o ponto passou de um lado. Se passou de um lado, o comprimento
é a distância até esse lado; se passou dos dois, é a distância até o canto. Dentro, os dois componentes de d são
negativos, e o maior deles é a distância até o lado mais próximo. Os dois termos nunca contribuem ao mesmo tempo.
O segmento
Um segmento de reta de a até b projeta o ponto na reta, limita a projeção ao segmento e mede a distância até esse
ponto mais próximo:
float sdSegment(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a;
vec2 ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); // 0 em a, 1 em b
return length(pa - ba * h);
}
Um segmento não tem lado de dentro, então a distância dele nunca é negativa. Subtrair uma espessura dá uma cápsula,
uma linha com pontas redondas: sdSegment(p, a, b) - width.
Da distância aos pixels
Para desenhar uma forma, transforme a distância em cobertura. Smoothstep Um limiar suave. Vale 0 abaixo da primeira borda, 1 acima da segunda e segue uma curva em S com as pontas planas entre elas.step(0.0, -d) dá uma borda dura, serrilhada. Um
smoothstepUm limiar suave. Vale 0 abaixo da primeira borda, 1 acima da segunda e segue uma curva em S com as pontas planas entre elas.t = clamp((x − e0) / (e1 − e0), 0, 1); s = t² (3 − 2t)
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y; // y vai de -1 a 1
float px = 2.0 / uResolution.y; // um pixel nessas unidades
float d = sdBox(p, vec2(0.5, 0.3));
float fill = 1.0 - smoothstep(-px, px, d); // 1 dentro, 0 fora, suave por um pixel
A borda fica suave por um pixel em qualquer tamanho e em qualquer tela, porque a distância é medida nas mesmas
unidades de px. Quando as coordenadas são escaladas ou distorcidas, o tamanho do pixel muda pela tela; o fundamento
de derivadas de espaço de tela mostra o fwidth(d), que mede isso por pixel.
Outros visuais saem do mesmo d:
// um traço de 0.04 de largura centrado na borda
float outline = 1.0 - smoothstep(0.0, px, abs(d) - 0.02);
// some para fora a partir da borda
float glow = exp(-8.0 * max(d, 0.0));
// a mesma caixa com cantos arredondados de 0.1
float rounded = sdBox(p, vec2(0.5, 0.3) - 0.1) - 0.1;
abs(d) é a distância sem sinal até a borda pelos dois lados, então subtrair uma largura dá um traço. Subtrair uma
constante de qualquer distância faz a forma crescer para fora nessa medida e arredonda todo canto convexo, e é por isso
que a caixa arredondada encolhe a caixa primeiro.
Exata, limitante e escala
As funções acima são exatas: devolvem a distância euclidiana real em todo lugar. Muitas formas úteis só têm um limitante, um valor correto na borda e nunca maior que a distância real, mas que pode ser menor em outros pontos. Limitantes servem para preenchimentos. Eles deixam contornos e brilhos irregulares e, no raymarching 3D, custam passos extras.
Escalar as coordenadas escala a distância junto. Desenhe uma forma na escala s como sdShape(p / s) * s; sem o
* s final, a distância fica na unidade errada e a suavidade da borda muda com o tamanho. Uma escala não uniforme
(diferente em x e y) quebra a distância, então estique os parâmetros da forma (como o b da caixa) em vez do espaço.
Rotação e translação mantêm as distâncias exatas, então as operações do fundamento de transformações 2D funcionam
diretamente.