Como uso derivadas do espaço da tela para antialias uma forma paramétrica em um pixel shader?

8

No artigo da Alpha Tested Magnification da Valve , ele menciona o uso de "derivados de espaço na tela por pixel" para fazer a suavização de serrilhado. Meu entendimento é que essas são as funções intrínsecas ddxe ddyintrínsecas no HLSL?

Estou tentando desenhar formas paramétricas (por exemplo, um círculo: x² + y² <1 ) em um sombreador e não sei como usar essa técnica para suavizar corretamente os pixels de borda da minha forma. Alguém pode dar um exemplo?

Para completar, eis um exemplo do tipo de pixel shader que estou criando:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    float dist = input.TexCoord.x * input.TexCoord.x
               + input.TexCoord.y * input.TexCoord.y;
    if(dist < 1)
        return float4(0, 0, 0, 1);
    else
        return float4(1, 1, 1, 1);
}
Andrew Russell
fonte

Respostas:

10

Tomando o seu exemplo, você tem uma função passo da distância, que produz uma aresta perfeitamente rígida (alias). Uma maneira simples de suavizar o círculo seria transformá-lo em um limiar suave, como:

float distFromEdge = 1.0 - dist;  // positive when inside the circle
float thresholdWidth = 0.01;  // a constant you'd tune to get the right level of softness
float antialiasedCircle = saturate((distFromEdge / thresholdWidth) + 0.5);
return lerp(outsideColor, insideColor, antialiasedCircle);

Aqui eu usei uma rampa linear fixa para uma função de limite suave, mas você também pode usar smoothstepou outra coisa. O + 0.5é centralizar a rampa na localização matemática da aresta. De qualquer forma, o importante é que essa função mude suavemente de outsideColorpara insideColoralgumas distâncias; portanto, se você escolher thresholdWidthadequadamente, obterá uma vantagem de aparência antialias.

Mas como você deve escolher thresholdWidth? Se for muito pequeno, você receberá o alias novamente e, se for muito grande, a borda ficará muito embaçada. Além disso, geralmente dependerá da posição da câmera: se distfor medido em unidades do espaço mundial ou do espaço da textura, um thresholdWidthque funcione para uma posição da câmera estará errado para outra.

Aqui é onde os derivados do espaço da tela entram (sim, eles são ddxe ddyfuncionam como você adivinhou). Ao calcular o comprimento do gradiente, distvocê pode ter uma idéia da rapidez com que está mudando no espaço da tela e usá-lo para estimar thresholdWidth, como:

float derivX = ddx(distFromEdge);
float derivY = ddy(distFromEdge);
float gradientLength = length(float2(derivX, derivY));
float thresholdWidth = 2.0 * gradientLength;  // the 2.0 is a constant you can tune

Você ainda tem um valor que pode ajustar para obter o nível desejado de suavidade, mas agora deve obter resultados consistentes, independentemente da posição da câmera.

Nathan Reed
fonte
Boa resposta - o código funciona bem. +1 e aceito. Mas eu poderia incomodá-lo a expandir sua resposta e explicar por que isso funciona? Estou um pouco confuso sobre o que derivXe derivYrealmente representam.
Andrew Russell
@AndrewRussell Você está familiarizado com derivativos, como no cálculo? derivXe derivYsão apenas (aproximações) as derivadas parciais de qualquer expressão na qual você passa ddxe ddy, com relação ao espaço na tela x e y. Se você não estudou cálculo, esse é um tópico maior do que posso explicar aqui. :)
Nathan Reed
Meu cálculo está um pouco enferrujado - tive que aprender novamente o que é uma derivada parcial . Mas a partir daí, acho que já entendi. Obrigado :)
Andrew Russell
O OpenGL não suporta ddx / ddy, portanto, nesses casos, você pode precisar deste http.developer.nvidia.com/GPUGems/gpugems_ch25.html, que basicamente usa uma textura pré-computada para identificar o nível do mipmap usado, para aproximar o ddx / ddy.
Runonthespot 9/09/13
2
@Runonthespot OpenGL tem derivados; eles são chamados de outra coisa: dFdx/ emdFdy vez de ddx/ ddy.
Nathan Reed