Conceptos básicos de campo de distancia firmados en 2D

Aunque las mallas son la forma más simple y versátil de renderizar, existen otras opciones para representar formas en 2d y 3d. Un método comúnmente utilizado son los campos de distancia con signo (SDF). Los campos de distancia firmados proporcionan un trazado de rayos menos costoso, permiten que diferentes formas fluyan suavemente entre sí y ahorran texturas de baja resolución para imágenes de alta calidad.

Comenzaremos generando el signo de los campos de distancia usando funciones en dos dimensiones, pero luego continuaremos generándolos en 3D. Usaré las coordenadas del espacio mundial para que tengamos la menor dependencia posible de la escala y las coordenadas UV, por lo que si no comprende cómo funciona, estudie este tutorial en una superposición plana , que explica lo que está sucediendo.


Preparación de la base


Eliminaremos temporalmente las propiedades del sombreador de superposición plana base, porque por ahora nos ocuparemos de la base técnica. Luego, escribimos la posición del vértice en el mundo directamente en la estructura del fragmento, y no la convertiremos primero a UV. En la última etapa de preparación, escribiremos una nueva función que calcula la escena y devuelve la distancia a la superficie más cercana. Luego llamamos a las funciones y usamos el resultado como un color.

Shader "Tutorial/034_2D_SDF_Basics"{ SubShader{ //           Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.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; //         o.position = UnityObjectToClipPos(v.vertex); //     o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { //      return 0; } fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = fixed4(dist, dist, dist, 1); return col; } ENDCG } } FallBack "Standard" //fallback   ,       } 

Escribiré todas las funciones para los campos de distancia firmados en un archivo separado para que podamos usarlos repetidamente. Para hacer esto, crearé un nuevo archivo. No le agregaremos ningún mal, luego lo configuramos y completamos la protección de inclusión condicional, verificando primero si la variable del preprocesador está configurada. Si aún no está definido, lo definimos y completamos la construcción condicional if después de las funciones que queremos incluir. La ventaja de esto es que si agregamos el archivo dos veces (por ejemplo, si agregamos dos archivos diferentes, cada uno de los cuales tiene las funciones que necesitamos, y ambos agregan el mismo archivo), esto romperá el sombreador. Si está seguro de que esto nunca sucederá, entonces no puede realizar esta verificación.

 // in include file // include guards that keep the functions from being included more than once #ifndef SDF_2D #define SDF_2D // functions #endif 

Si el archivo de inclusión se encuentra en la misma carpeta que el sombreador principal, simplemente podemos incluirlo usando la construcción pragma.

 // in main shader #include "2D_SDF.cginc" 

Por lo tanto, solo veremos una superficie negra en la superficie renderizada, lista para mostrar la distancia con un letrero.


Círculo


La función más simple del campo de distancia con signo es la función de círculo. La función recibirá solo la posición de la muestra y el radio del círculo. Comenzamos obteniendo la longitud del vector de posición de la muestra. Entonces obtenemos un punto en la posición (0, 0), que es similar a un círculo con un radio de 0.

 float circle(float2 samplePosition, float radius){ return length(samplePosition); } 

Luego puede llamar a la función de círculo en la función de escena y devolver la distancia que devuelve.

 float scene(float2 position) { float sceneDistance = circle(position, 2); return sceneDistance; } 


Luego agregamos el radio a los cálculos. Un aspecto importante de las funciones de distancia con signo es que cuando estamos dentro del objeto, obtenemos una distancia negativa a la superficie (esto es lo que significa la palabra con signo en el campo de distancia con signo de expresión). Para aumentar el círculo a un radio, simplemente restamos el radio de la longitud. Así, la superficie, que está en todas partes donde la función devuelve 0, se mueve hacia afuera. Lo que está en dos unidades de la distancia desde la superficie para un círculo con un tamaño de 0, es solo una unidad de un círculo con un radio de 1, y una unidad dentro del círculo (el valor es -1) para un círculo con un radio de 3;

 float circle(float2 samplePosition, float radius){ return length(samplePosition) - radius; } 


Ahora lo único que no podemos hacer es mover el círculo desde el centro. Para solucionar esto, puede agregar un nuevo argumento a la función de círculo para calcular la distancia entre la posición de muestra y el centro del círculo, y restar el radio de este valor para definir un círculo. O bien, puede redefinir el origen moviendo el espacio del punto de muestra y luego obtener un círculo en ese espacio. La segunda opción parece mucho más complicada, pero dado que mover objetos es una operación que queremos usar para todas las figuras, es mucho más universal y, por lo tanto, lo explicaré.

En movimiento


"Transformación del espacio de un punto" - suena mucho peor de lo que realmente es. Esto significa que pasamos el punto a la función, y la función lo cambia para que podamos usarlo en el futuro. En el caso de una transferencia, simplemente restamos el desplazamiento del punto. La posición se resta cuando queremos mover las formas en la dirección positiva, porque las formas que representamos en el espacio se mueven en la dirección opuesta a mover el espacio.

Por ejemplo, si queremos dibujar una esfera en la posición (3, 4) , entonces necesitamos cambiar el espacio para que (3, 4) convierta en (0, 0) , y para esto necesitamos restar (3, 4) . Ahora, si dibujamos una esfera alrededor de un nuevo punto de origen, será un punto antiguo (3, 4) .

 // in sdf functions include file float2 translate(float2 samplePosition, float2 offset){ return samplePosition - offset; } 

 float scene(float2 position) { float2 circlePosition = translate(position, float2(3, 2)); float sceneDistance = circle(circlePosition, 2); return sceneDistance; } 


Rectángulo


Otra forma simple es un rectángulo. Para empezar, consideramos los componentes por separado. Primero obtenemos la distancia desde el centro, tomando el valor absoluto. Luego, de manera similar a un círculo, restamos la mitad del tamaño (que esencialmente se asemeja al radio de un rectángulo). Para mostrar cómo se verán los resultados, por ahora solo devolveremos un componente.

 float rectangle(float2 samplePosition, float2 halfSize){ float2 componentWiseEdgeDistance = abs(samplePosition) - halfSize; return componentWiseEdgeDistance.x; } 


Ahora podemos obtener una versión barata del rectángulo simplemente devolviendo el componente más grande 2. Esto funciona en muchos casos, pero no correctamente, porque no muestra la distancia correcta alrededor de las esquinas.


Los valores correctos para el rectángulo fuera de la figura se pueden obtener primero tomando el máximo entre las distancias a los bordes y 0, y luego tomando su longitud.

Si no limitamos la distancia desde abajo a 0, simplemente calculamos la distancia a las esquinas (donde edgeDistances son (0, 0) ), pero las coordenadas entre las esquinas no caerán por debajo de 0, por lo que se usará todo el borde. La desventaja de esto es que 0 se usa como la distancia desde el borde para todo el interior de la figura.

Para corregir la distancia 0 para toda la parte interna, debe generar la distancia interna, simplemente usando la fórmula barata del rectángulo (tomando el valor máximo del componente xey), y luego garantizando que nunca excederá 0, llevando el valor mínimo de este a 0. Luego agregamos la distancia externa, que nunca es inferior a 0, y la distancia interna, que nunca excede de 0, y obtenemos la función de distancia final.

 float rectangle(float2 samplePosition, float2 halfSize){ float2 componentWiseEdgeDistance = abs(samplePosition) - halfSize; float outsideDistance = length(max(componentWiseEdgeDistance, 0)); float insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0); return outsideDistance + insideDistance; } 

Como previamente grabamos la función de transferencia en una forma universal, ahora también podemos usarla para mover su centro a cualquier lugar.

 float scene(float2 position) { float2 circlePosition = translate(position, float2(1, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } 


Girar


Girar formas es similar a mover. Antes de calcular la distancia a la figura, rotamos las coordenadas en la dirección opuesta. Para simplificar la comprensión de las rotaciones tanto como sea posible, multiplicamos la rotación por 2 * pi para obtener el ángulo en radianes. Por lo tanto, pasamos una rotación a la función, donde 0.25 es un cuarto de vuelta, 0.5 es media vuelta y 1 es una vuelta completa (puede realizar conversiones de manera diferente si le parece más natural). También invertimos la rotación, porque necesitamos rotar la posición en la dirección opuesta a la rotación de la figura por la misma razón que cuando se mueve.

Para calcular las coordenadas rotadas, primero calculamos el seno y el coseno en función del ángulo. Hlsl tiene una función sincos que calcula ambos valores más rápido que cuando se calcula por separado.

Al construir un nuevo vector para el componente x, tomamos el componente original x multiplicado por coseno y el componente y multiplicado por seno. Esto se puede recordar fácilmente si recuerda que el coseno de 0 es 1, y cuando se gira por 0, queremos que la componente x del nuevo vector sea exactamente la misma que antes (es decir, multiplique por 1). El componente y, que anteriormente apuntaba hacia arriba, no contribuyó al componente x, gira hacia la derecha y sus valores comienzan en 0, al principio cada vez más grandes, es decir, su movimiento se describe completamente por un seno.

Para la componente y del nuevo vector, multiplicamos el coseno por la componente y del viejo vector y restamos el seno multiplicado por la vieja componente x. Para comprender por qué restamos, en lugar de sumar el seno, multiplicado por el componente x, es mejor imaginar cómo cambia el vector (1, 0) cuando se gira en sentido horario. El componente y del resultado comienza en 0 y luego se convierte en menos de 0. Esto es lo contrario de cómo se comporta el seno, por lo que cambiamos el signo.

 float2 rotate(float2 samplePosition, float rotation){ const float PI = 3.14159; float angle = rotation * PI * 2 * -1; float sine, cosine; sincos(angle, sine, cosine); return float2(cosine * samplePosition.x + sine * samplePosition.y, cosine * samplePosition.y - sine * samplePosition.x); } 

Ahora que hemos escrito el método de rotación, podemos usarlo en combinación con la transferencia para mover y rotar la figura.

 float scene(float2 position) { float2 circlePosition = position; circlePosition = rotate(circlePosition, _Time.y); circlePosition = translate(circlePosition, float2(2, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } 


En este caso, primero rotamos el objeto alrededor del centro de toda la escena, de modo que la rotación también afecta la transferencia. Para rotar una figura en relación con su propio centro, primero debe moverla y luego rotarla. Debido a este cambio de orden en el momento de la rotación, el centro de la figura se convertirá en el centro del sistema de coordenadas.

 float scene(float2 position) { float2 circlePosition = position; circlePosition = translate(circlePosition, float2(2, 0)); circlePosition = rotate(circlePosition, _Time.y); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } 


Escalamiento


La escala funciona de manera similar a otras formas de transformar formas. Dividimos las coordenadas por escala, representando la figura en el espacio con una escala reducida, y en el sistema de coordenadas base se hacen más grandes.

 float2 scale(float2 samplePosition, float scale){ return samplePosition / scale; } 

 float scene(float2 position) { float2 circlePosition = position; circlePosition = translate(circlePosition, float2(0, 0)); circlePosition = rotate(circlePosition, .125); float pulseScale = 1 + 0.5*sin(_Time.y * 3.14); circlePosition = scale(circlePosition, pulseScale); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } 


Aunque esto realiza la escala correctamente, la distancia también se escala. La principal ventaja del campo de distancia con signo es que siempre sabemos la distancia a la superficie más cercana, pero el alejamiento destruye por completo esta propiedad. Esto se puede solucionar fácilmente multiplicando el campo de distancia obtenido de la función de distancia del signo (en nuestro caso, el rectangle ) por la escala. Por la misma razón, no podemos escalar fácilmente de manera desigual (con diferentes escalas para los ejes x e y).

 float scene(float2 position) { float2 circlePosition = position; circlePosition = translate(circlePosition, float2(0, 0)); circlePosition = rotate(circlePosition, .125); float pulseScale = 1 + 0.5*sin(_Time.y * 3.14); circlePosition = scale(circlePosition, pulseScale); float sceneDistance = rectangle(circlePosition, float2(1, 2)) * pulseScale; return sceneDistance; } 


Visualización


Los campos de distancia firmados se pueden usar para una variedad de cosas, como crear sombras, renderizar escenas 3D, física y renderizar texto. Pero aún no queremos profundizar en la complejidad, por lo tanto, explicaré solo dos técnicas de su visualización. La primera es una forma clara con antialiasing, la segunda es la representación de líneas dependiendo de la distancia.

Forma clara


Este método es similar al que se usa a menudo al representar texto, crea una forma clara. Si queremos generar un campo de distancia no desde una función, sino leerlo desde una textura, esto nos permite usar texturas con una resolución mucho más baja de lo habitual y obtener buenos resultados. TextMesh Pro utiliza esta técnica para representar texto.

Para aplicar esta técnica, aprovechamos el hecho de que los datos en los campos de distancia están firmados y conocemos el punto de corte. Comenzamos calculando qué tan lejos cambia el campo de distancia al siguiente píxel. Este debería ser el mismo valor que la longitud del cambio de coordenadas, pero es más fácil y más confiable calcular la distancia con un signo.

Habiendo recibido el cambio de distancia, podemos hacer un paso suave desde la mitad del cambio de distancia a menos / más la mitad del cambio de distancia. Esto realizará un recorte simple alrededor de 0, pero con suavizado. Luego puede usar este valor suavizado para cualquier valor binario que necesitemos. En este ejemplo, cambiaré el sombreador a un sombreador de transparencia y lo usaré para el canal alfa. Doy un paso suave de un valor positivo a uno negativo porque queremos que el valor negativo del campo de distancia sea visible. Si no comprende bien cómo funciona el procesamiento de transparencias aquí, le recomiendo leer mi tutorial sobre procesamiento de transparencias.

 //properties Properties{ _Color("Color", Color) = (1,1,1,1) } 

 //in subshader outside of pass Tags{ "RenderType"="Transparent" "Queue"="Transparent"} Blend SrcAlpha OneMinusSrcAlpha ZWrite Off 

 fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); float distanceChange = fwidth(dist) * 0.5; float antialiasedCutoff = smoothstep(distanceChange, -distanceChange, dist); fixed4 col = fixed4(_Color, antialiasedCutoff); return col; } 


Lineas de elevacion


Otra técnica común para visualizar campos de distancia es mostrar distancias como líneas. En nuestra implementación, agregaré algunas líneas gruesas y algunas líneas finas entre ellas. También pintaré el interior y el exterior de la figura en diferentes colores para que pueda ver dónde está el objeto.

Comenzaremos mostrando la diferencia entre el interior y el exterior de la figura. Los colores se pueden personalizar en el material, por lo que agregaremos nuevas propiedades, así como variables de sombreado para los colores internos y externos de la figura.

 Properties{ _InsideColor("Inside Color", Color) = (.5, 0, 0, 1) _OutsideColor("Outside Color", Color) = (0, .5, 0, 1) } 

 //global shader variables float4 _InsideColor; float4 _OutsideColor; 

Luego, en el sombreador de fragmentos, verificamos dónde se encuentra el píxel, que representamos comparando la distancia con el signo con 0 usando la función de step . Utilizamos esta variable para interpolar de color interno a externo y representarla en la pantalla.

 fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist)); return col; } 


Para renderizar líneas, primero debemos especificar con qué frecuencia renderizaremos líneas y qué tan gruesas serán, configurando las propiedades y las variables de sombreado correspondientes.

 //Properties _LineDistance("Mayor Line Distance", Range(0, 2)) = 1 _LineThickness("Mayor Line Thickness", Range(0, 0.1)) = 0.05 

 //shader variables float _LineDistance; float _LineThickness; 

Luego, para representar las líneas, comenzaremos calculando el cambio en la distancia para poder usarlo más tarde para suavizar. También ya lo dividimos entre 2, porque luego sumamos la mitad y restamos la mitad para cubrir la distancia de cambio de 1 píxel.

 float distanceChange = fwidth(dist) * 0.5; 

Luego tomamos la distancia y la transformamos para que tenga el mismo comportamiento en los puntos repetidos. Para hacer esto, primero lo dividimos por la distancia entre las líneas, mientras que no obtendremos números completos en cada primer paso, sino números completos solo en función de la distancia que establezcamos.

Luego sumamos 0.5 al número, tomamos la parte fraccional y restamos 0.5 nuevamente. La parte fraccionaria y la resta son necesarias aquí para que la línea pase por cero en el patrón repetitivo. Agregamos 0.5 para obtener la parte fraccionaria para neutralizar una substracción adicional de 0.5; el desplazamiento conducirá al hecho de que los valores en los que el gráfico es 0 están en 0, 1, 2, etc., y no en 0.5, 1.5, etc.

Los últimos pasos para convertir el valor: tomamos el valor absoluto y nuevamente lo multiplicamos por la distancia entre las líneas. El valor absoluto hace que las áreas antes y después de los puntos de la línea permanezcan iguales, lo que facilita la creación de recorte para las líneas. La última operación, en la que nuevamente multiplicamos el valor por la distancia entre las líneas, es necesaria para neutralizar la división al comienzo de la ecuación, gracias a esto, el cambio en el valor es nuevamente el mismo que al principio, y el cambio calculado previamente en la distancia sigue siendo correcto.


 float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance; 

Ahora que hemos calculado la distancia a las líneas en función de la distancia a la figura, podemos dibujar las líneas. Hacemos un paso suave desde el grosor de línea menos la mitad del cambio en la distancia hasta el grosor de línea más la mitad del cambio en la distancia y usamos la distancia de la línea recién calculada como un valor para la comparación. Después de calcular este valor, lo multiplicamos por color para crear líneas negras (también puede cambiar a un color diferente si necesita líneas multicolores).

 fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist)); float distanceChange = fwidth(dist) * 0.5; float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance; float majorLines = smoothstep(_LineThickness - distanceChange, _LineThickness + distanceChange, majorLineDistance); return col * majorLines; } 


Implementamos líneas delgadas entre gruesas de la misma manera: agregamos una propiedad que determina cuántas líneas delgadas deben estar entre las gruesas, y luego hacemos lo que hicimos con las gruesas, pero debido a la distancia entre líneas delgadas, dividimos la distancia entre las gruesas por el número de líneas delgadas entre ellos. También haremos el número de líneas finas IntRange , gracias a esto solo podemos asignar valores enteros y no obtener líneas finas que no IntRange gruesas. Después de calcular líneas finas, las multiplicamos por color de la misma manera que las gruesas.

 //properties [IntRange]_SubLines("Lines between major lines", Range(1, 10)) = 4 _SubLineThickness("Thickness of inbetween lines", Range(0, 0.05)) = 0.01 

 //shader variables float _SubLines; float _SubLineThickness; 

 fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist)); float distanceChange = fwidth(dist) * 0.5; float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance; float majorLines = smoothstep(_LineThickness - distanceChange, _LineThickness + distanceChange, majorLineDistance); float distanceBetweenSubLines = _LineDistance / _SubLines; float subLineDistance = abs(frac(dist / distanceBetweenSubLines + 0.5) - 0.5) * distanceBetweenSubLines; float subLines = smoothstep(_SubLineThickness - distanceChange, _SubLineThickness + distanceChange, subLineDistance); return col * majorLines * subLines; } 


Código fuente


Características 2D SDF



 #ifndef SDF_2D #define SDF_2D float2 rotate(float2 samplePosition, float rotation){ const float PI = 3.14159; float angle = rotation * PI * 2 * -1; float sine, cosine; sincos(angle, sine, cosine); return float2(cosine * samplePosition.x + sine * samplePosition.y, cosine * samplePosition.y - sine * samplePosition.x); } float2 translate(float2 samplePosition, float2 offset){ //move samplepoint in the opposite direction that we want to move shapes in return samplePosition - offset; } float2 scale(float2 samplePosition, float scale){ return samplePosition / scale; } float circle(float2 samplePosition, float radius){ //get distance from center and grow it according to radius return length(samplePosition) - radius; } float rectangle(float2 samplePosition, float2 halfSize){ float2 componentWiseEdgeDistance = abs(samplePosition) - halfSize; float outsideDistance = length(max(componentWiseEdgeDistance, 0)); float insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0); return outsideDistance + insideDistance; } #endif 

Ejemplo de círculo



 Shader "Tutorial/034_2D_SDF_Basics/Rectangle"{ 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) { float2 circlePosition = position; circlePosition = rotate(circlePosition, _Time.y * 0.5); circlePosition = translate(circlePosition, float2(2, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = fixed4(dist, dist, dist, 1); return col; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

Ejemplo de rectángulo



 Shader "Tutorial/034_2D_SDF_Basics/Rectangle"{ 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) { float2 circlePosition = position; circlePosition = rotate(circlePosition, _Time.y * 0.5); circlePosition = translate(circlePosition, float2(2, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = fixed4(dist, dist, dist, 1); return col; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

Corte



 Shader "Tutorial/034_2D_SDF_Basics/Cutoff"{ Properties{ _Color("Color", Color) = (1,1,1,1) } SubShader{ Tags{ "RenderType"="Transparent" "Queue"="Transparent"} Blend SrcAlpha OneMinusSrcAlpha ZWrite Off 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; }; fixed3 _Color; 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) { float2 circlePosition = position; circlePosition = rotate(circlePosition, _Time.y * 0.5); circlePosition = translate(circlePosition, float2(2, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); float distanceChange = fwidth(dist) * 0.5; float antialiasedCutoff = smoothstep(distanceChange, -distanceChange, dist); fixed4 col = fixed4(_Color, antialiasedCutoff); return col; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

Líneas de distancia



 Shader "Tutorial/034_2D_SDF_Basics/DistanceLines"{ Properties{ _InsideColor("Inside Color", Color) = (.5, 0, 0, 1) _OutsideColor("Outside Color", Color) = (0, .5, 0, 1) _LineDistance("Mayor Line Distance", Range(0, 2)) = 1 _LineThickness("Mayor Line Thickness", Range(0, 0.1)) = 0.05 [IntRange]_SubLines("Lines between major lines", Range(1, 10)) = 4 _SubLineThickness("Thickness of inbetween lines", Range(0, 0.05)) = 0.01 } 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) { float2 circlePosition = position; circlePosition = rotate(circlePosition, _Time.y * 0.2); circlePosition = translate(circlePosition, float2(2, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } float4 _InsideColor; float4 _OutsideColor; float _LineDistance; float _LineThickness; float _SubLines; float _SubLineThickness; fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist)); float distanceChange = fwidth(dist) * 0.5; float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance; float majorLines = smoothstep(_LineThickness - distanceChange, _LineThickness + distanceChange, majorLineDistance); float distanceBetweenSubLines = _LineDistance / _SubLines; float subLineDistance = abs(frac(dist / distanceBetweenSubLines + 0.5) - 0.5) * distanceBetweenSubLines; float subLines = smoothstep(_SubLineThickness - distanceChange, _SubLineThickness + distanceChange, subLineDistance); return col * majorLines * subLines; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

Espero haber logrado explicar los conceptos básicos de los campos de distancia con un signo, y ya está esperando algunos tutoriales nuevos en los que hablaré sobre otras formas de usarlos.

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


All Articles