Sombras 2D en campos de distancia firmados

Ahora que conocemos los conceptos básicos de combinar funciones de distancia firmadas, puede usarlas para crear cosas geniales. En este tutorial, los usaremos para representar sombras bidimensionales suaves. Si no ha leído mis tutoriales anteriores sobre campos de distancia firmados (SDF), le recomiendo que los estudie, comenzando con un tutorial sobre la creación de formas simples .


[GIF generó artefactos adicionales durante la recompresión].

Configuración básica


Creé una configuración simple con una sala, utiliza las técnicas descritas en tutoriales anteriores. Anteriormente, no mencioné que utilicé la función abs para el vector2 para reflejar la posición relativa a los ejes xey, y también que invertí la distancia de la figura para intercambiar las partes interna y externa.

Copiaremos el archivo 2D_SDF.cginc del tutorial anterior en una carpeta con el sombreador, que escribiremos en este tutorial.

 Shader "Tutorial/037_2D_SDF_Shadows"{ Properties{ } SubShader{ //the material is completely non-transparent and is rendered at the same time as the other opaque geometry Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { float bounds = -rectangle(position, 2); float2 quarterPos = abs(position); float corner = rectangle(translate(quarterPos, 1), 0.5); corner = subtract(corner, rectangle(position, 1.2)); float diamond = rectangle(rotate(position, 0.125), .5); float world = merge(bounds, corner); world = merge(world, diamond); return world; } fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); return dist; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

Si todavía utilizáramos la técnica de visualización del tutorial anterior, la figura se vería así:


Sombras simples


Para crear sombras nítidas, recorremos el espacio desde la posición de la muestra hasta la posición de la fuente de luz. Si encontramos un objeto en el camino, decidimos que el píxel debe estar sombreado, y si llegamos a la fuente sin obstáculos, decimos que no está sombreado.

Comenzamos calculando los parámetros básicos de la viga. Ya tenemos un punto de partida (la posición del píxel que estamos renderizando) y un punto de destino (la posición de la fuente de luz) para el haz. Necesitamos una longitud y una dirección normalizada. La dirección se puede obtener restando el principio del final y normalizando el resultado. La longitud se puede obtener restando las posiciones y pasando el valor al método de length .

 float traceShadow(float2 position, float2 lightPosition){ float direction = normalise(lightPosition - position); float distance = length(lightPosition - position); } 

Luego, iterativamente, damos la vuelta al rayo en el bucle. Estableceremos iteraciones del bucle en la declaración de definición, y esto nos permitirá configurar el número máximo de iteraciones más adelante, y también permitirá que el compilador optimice un poco el sombreador expandiendo el bucle.

En el bucle, necesitamos la posición en la que nos encontramos ahora, por lo que lo declaramos fuera del bucle con el valor inicial 0. En el bucle, podemos calcular la posición de la muestra agregando el avance del haz multiplicado por la dirección del haz con la posición base. Luego, muestreamos la función de distancia con signo en la posición recién calculada.

 // outside of function #define SAMPLES 32 // in shadow function float rayDistance = 0; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(pos + direction * rayDistance); //do other stuff and move the ray further } 

Luego verificamos si ya estamos en el punto donde podemos detener el ciclo. Si la distancia de la escena de la función de distancia con el signo es cercana a 1, entonces podemos suponer que el rayo está bloqueado por una figura y devolver 0. Si el rayo se extiende más allá de la distancia a la fuente de luz, podemos suponer que llegamos a la fuente sin colisiones y devolver el valor 1)

Si el retorno no es exitoso, entonces necesita calcular la siguiente posición de la muestra. Esto se hace agregando distancia en la escena de avance del haz. La razón de esto es que la distancia en la escena nos da la distancia a la figura más cercana, por lo que si agregamos este valor al haz, probablemente no podremos emitir el haz más allá de la figura más cercana, o incluso más allá, lo que conducirá al flujo de sombras.

En el caso de que no encontráramos nada y no alcanzáramos la fuente de luz en el momento en que se completó el stock de la muestra (el ciclo terminó), también debemos devolver el valor. Como esto ocurre principalmente al lado de las formas, poco antes de que el píxel todavía se considere sombreado, aquí usamos el valor de retorno de 0.

 #define SAMPLES 32 float traceShadows(float2 position, float2 lightPosition){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return 1; } rayProgress = rayProgress + sceneDist; } return 0; } 

Para usar esta función, la llamamos en una función de fragmento con la posición del píxel y la posición de la fuente de luz. Luego multiplicamos el resultado por cualquier color para mezclarlo con el color de las fuentes de luz.

También utilicé la técnica descrita en el primer tutorial sobre campos de distancia con un signo para visualizar la geometría. Luego acabo de agregar doblado y geometría. Aquí podemos simplemente usar la operación de suma, y ​​no realizar interpolación lineal o acciones similares, porque la forma es negra en todas partes donde no está la forma, y ​​la sombra es negra donde sea que esté.

fixed4 frag(v2f i) : SV_TARGET{ float2 position = i.worldPos.xz;

 float2 lightPos; sincos(_Time.y, lightPos.x /*sine of time*/, lightPos.y /*cosine of time*/); float shadows = traceShadows(position, lightPos); float3 light = shadows * float3(.6, .6, 1); float sceneDistance = scene(position); float distanceChange = fwidth(sceneDistance) * 0.5; float binaryScene = smoothstep(distanceChange, -distanceChange, sceneDistance); float3 geometry = binaryScene * float3(0, 0.3, 0.1); float3 col = geometry + light; return float4(col, 1); } 


Sombras suaves


Pasar de estas sombras duras a más suaves y más realistas es bastante fácil. Al mismo tiempo, el sombreador no se vuelve mucho más costoso computacionalmente.

Primero, simplemente obtenemos la distancia al objeto de escena más cercano para cada muestra que omitimos, y seleccionamos el más cercano. Entonces, donde solíamos devolver 1, será posible devolver la distancia a la figura más cercana. Para que el brillo de la sombra no sea demasiado alto y no conduzca a la creación de colores extraños, lo pasaremos por el método de saturate , que lo limita a un intervalo de 0 a 1. Obtenemos un mínimo entre la figura más cercana actual y la siguiente después de verificar si el haz de la fuente de luz ya ha alcanzado la distribución de lo contrario, podemos tomar muestras que van más allá de la fuente de luz y obtener artefactos extraños.

 float traceShadows(float2 position, float2 lightPosition){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; float nearest = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(nearest); } nearest = min(nearest, sceneDist); rayProgress = rayProgress + sceneDist; } return 0; } 


Lo primero que notamos después de esto son los extraños "dientes" en las sombras. Surgen porque la distancia desde la escena a la fuente de luz es menor que 1. Intenté contrarrestar esto de varias maneras, pero no pude encontrar una solución. En cambio, podemos implementar la nitidez de la sombra. La nitidez será otro parámetro en la función de sombra. En el bucle, multiplicamos la distancia en la escena por la nitidez, y luego, con la nitidez 2, la parte suave y gris de la sombra se convierte en la mitad. Cuando se usa la nitidez, la fuente de luz puede ser de la figura a una distancia de al menos 1 dividida por la nitidez, de lo contrario aparecerán artefactos. Por lo tanto, si usa una nitidez de 20, la distancia debe ser de al menos 0.05 unidades.

 float traceShadows(float2 position, float2 lightPosition, float hardness){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; float nearest = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(nearest); } nearest = min(nearest, hardness * sceneDist); rayProgress = rayProgress + sceneDist; } return 0; } 

 //in fragment function float shadows = traceShadows(position, lightPos, 20); 


Al minimizar este problema, notamos lo siguiente: incluso en áreas que no deben estar sombreadas, el debilitamiento aún es visible cerca de las paredes. Además, la suavidad de la sombra parece ser la misma para toda la sombra, y no es nítida al lado de la figura y es más suave cuando se aleja del objeto que emite la sombra.

Arreglaremos esto dividiendo la distancia en la escena por la propagación del haz. Gracias a esto, dividiremos la distancia en números muy pequeños al comienzo del haz, es decir, aún obtendremos valores altos y una hermosa sombra clara. Cuando encontramos el punto más cercano al rayo en los puntos posteriores del rayo, el punto más cercano se divide por un número mayor, lo que hace que la sombra sea más suave. Como esto no está completamente relacionado con la distancia más corta, cambiaremos el nombre de la variable a shadow .

También haremos un cambio menor más: dado que estamos dividiendo por rayProgress, no debe comenzar con 0 (dividir por cero es casi siempre una mala idea de dividir). Para empezar, puede elegir cualquier número muy pequeño.

 float traceShadows(float2 position, float2 lightPosition, float hardness){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0.0001; float shadow = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(shadow); } shadow = min(shadow, hardness * sceneDist / rayProgress); rayProgress = rayProgress + sceneDist; } return 0; } 


Múltiples fuentes de iluminación


En esta implementación simple de un solo núcleo, la forma más fácil de obtener múltiples fuentes de luz es calcularlas individualmente y agregar los resultados.

 fixed4 frag(v2f i) : SV_TARGET{ float2 position = i.worldPos.xz; float2 lightPos1 = float2(sin(_Time.y), -1); float shadows1 = traceShadows(position, lightPos1, 20); float3 light1 = shadows1 * float3(.6, .6, 1); float2 lightPos2 = float2(-sin(_Time.y) * 1.75, 1.75); float shadows2 = traceShadows(position, lightPos2, 10); float3 light2 = shadows2 * float3(1, .6, .6); float sceneDistance = scene(position); float distanceChange = fwidth(sceneDistance) * 0.5; float binaryScene = smoothstep(distanceChange, -distanceChange, sceneDistance); float3 geometry = binaryScene * float3(0, 0.3, 0.1); float3 col = geometry + light1 + light2; return float4(col, 1); } 


Código fuente


Biblioteca SDF bidimensional (no ha cambiado, pero se usa aquí)



Sombras suaves bidimensionales



 Shader "Tutorial/037_2D_SDF_Shadows"{ Properties{ } SubShader{ //the material is completely non-transparent and is rendered at the same time as the other opaque geometry Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { float bounds = -rectangle(position, 2); float2 quarterPos = abs(position); float corner = rectangle(translate(quarterPos, 1), 0.5); corner = subtract(corner, rectangle(position, 1.2)); float diamond = rectangle(rotate(position, 0.125), .5); float world = merge(bounds, corner); world = merge(world, diamond); return world; } #define STARTDISTANCE 0.00001 #define MINSTEPDIST 0.02 #define SAMPLES 32 float traceShadows(float2 position, float2 lightPosition, float hardness){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float lightSceneDistance = scene(lightPosition) * 0.8; float rayProgress = 0.0001; float shadow = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(shadow); } shadow = min(shadow, hardness * sceneDist / rayProgress); rayProgress = rayProgress + max(sceneDist, 0.02); } return 0; } fixed4 frag(v2f i) : SV_TARGET{ float2 position = i.worldPos.xz; float2 lightPos1 = float2(sin(_Time.y), -1); float shadows1 = traceShadows(position, lightPos1, 20); float3 light1 = shadows1 * float3(.6, .6, 1); float2 lightPos2 = float2(-sin(_Time.y) * 1.75, 1.75); float shadows2 = traceShadows(position, lightPos2, 10); float3 light2 = shadows2 * float3(1, .6, .6); float sceneDistance = scene(position); float distanceChange = fwidth(sceneDistance) * 0.5; float binaryScene = smoothstep(distanceChange, -distanceChange, sceneDistance); float3 geometry = binaryScene * float3(0, 0.3, 0.1); float3 col = geometry + light1 + light2; return float4(col, 1); } ENDCG } } FallBack "Standard" } 

Este es solo uno de los muchos ejemplos de uso de campos de distancia con signo. Hasta ahora son bastante engorrosos, porque todas las formas deben registrarse en el sombreador o pasar a través de las propiedades del sombreador, pero tengo algunas ideas sobre cómo hacerlas más convenientes para futuros tutoriales.

Source: https://habr.com/ru/post/438942/


All Articles