Pular para o conteúdo
Fundamento · Campos de distânciaIntermediário

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 sinal 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. step(0.0, -d) dá uma borda dura, serrilhada. Um smoothstep ao longo de mais ou menos um pixel dá uma borda limpa e suavizada:

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.

Leitura complementar