Combinando campos de distancia firmados en 2D

En el tutorial anterior, aprendimos cómo crear y mover formas simples usando funciones de distancia con signo. En este artículo, aprenderemos cómo combinar varias formas para crear campos de distancia más complejos. La mayoría de las técnicas descritas aquí las aprendí de la biblioteca de funciones de distancia con el signo glsl, que se puede encontrar aquí . También hay varias formas de combinar formas, que no discuto aquí.


Preparación


Para la visualización de campos de distancia con signo (campos de distancia con signo, SDF), utilizaremos una configuración simple y luego le aplicaremos los operadores. Para mostrar los campos de distancia, utilizará la visualización de las líneas de distancia del primer tutorial. En aras de la simplicidad, estableceremos todos los parámetros, excepto los parámetros de visualización en el código, pero puede reemplazar cualquier valor con una propiedad para que sea personalizable.

El sombreador principal con el que comenzaremos se ve así:

Shader "Tutorial/035_2D_SDF_Combinations/Champfer Union"{ 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) { const float PI = 3.14159; float2 squarePosition = position; squarePosition = translate(squarePosition, float2(1, 0)); squarePosition = rotate(squarePosition, .125); float squareShape = rectangle(squarePosition, float2(2, 2)); float2 circlePosition = position; circlePosition = translate(circlePosition, float2(-1.5, 0)); float circleShape = circle(circlePosition, 2.5); float combination = combination_function(circleShape, squareShape); return combination; } 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 } 

Y la función 2D_SDF.cginc en la misma carpeta con el sombreador, que expandiremos, al principio se ve así:

 #ifndef SDF_2D #define SDF_2D //transforms 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; } //shapes 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 

Combinaciones simples


Comenzaremos con algunas formas simples de combinar dos formas para crear una forma grande, conjugaciones, intersecciones y sustracciones, así como una forma de transformar una forma en otra.

Emparejamiento


El operador más simple es el emparejamiento. Con él, podemos juntar las dos figuras y obtener la distancia con el signo de la figura conectada. Cuando tenemos una distancia con el signo de dos figuras, podemos combinarlas tomando la menor de las dos usando la función min .

Debido a la elección del menor de los dos valores, la cifra final estará por debajo de 0 (visible) donde una de las dos figuras entrantes tiene una distancia al borde inferior a 0; Lo mismo se aplica a todos los demás valores de distancia, mostrando una combinación de dos figuras.

Aquí nombraré la función para crear la conjugación "fusionar", en parte porque las estamos fusionando, en parte porque la palabra clave de unión en hlsl está reservada, por lo que no se puede usar como el nombre de la función.

 //in 2D_SDF.cginc include file float merge(float shape1, float shape2){ return min(shape1, shape2); } 

 //in scene function in shader float combination = merge(circleShape, squareShape); 




Intersección


Otra forma común de conectar formas es usar áreas en las que se superponen dos formas. Para hacer esto, tomamos el valor máximo de las distancias de las dos figuras que queremos combinar. Cuando usamos el mayor de los dos valores, obtenemos un valor mayor que 0 (fuera de la figura), cuando cualquiera de las distancias a las dos figuras está fuera de la figura, y otras distancias también se alinean de manera similar.

 //in 2D_SDF.cginc include file float intersect(float shape1, float shape2){ return max(shape1, shape2); } 

 //in scene function in shader float combination = intersect(circleShape, squareShape); 


Resta


Sin embargo, a menudo no queremos procesar ambas formas de la misma manera, y necesitamos restar la otra de una forma. Esto es bastante fácil de hacer intersectando entre la forma que queremos cambiar y todo menos la forma que queremos restar. Obtenemos los valores opuestos para las partes interna y externa de la figura, invirtiendo la distancia con el signo. Lo que era 1 unidad fuera de la figura ahora es 1 unidad dentro.

 //in 2D_SDF.cginc include file float subtract(float base, float subtraction){ return intersect(base, -subtraction); } 

 //in scene function in shader float combination = subtract(squareShape, circleShape); 


Interpolación


Una forma no obvia de combinar dos figuras es interpolar entre ellas. También es posible hasta cierto punto para mallas poligonales con formas de mezcla, pero es mucho más limitado que lo que podemos hacer con campos de distancia con signo. Por simple interpolación entre las distancias de dos figuras, logramos un flujo suave de una a la otra. Para la interpolación, simplemente puede usar el método lerp .

 //in 2D_SDF.cginc include file float interpolate(float shape1, float shape2, float amount){ return lerp(shape1, shape2, amount); } 

 //in scene function in shader float pulse = sin(_Time.y) * 0.5 + 0.5; float combination = interpolate(circleShape, pulse); 


Otros compuestos


Habiendo recibido conexiones simples, ya tenemos todo lo necesario para una combinación simple de figuras, pero la sorprendente propiedad de los campos de signos de distancia es que no podemos limitarnos a esto, hay muchas formas diferentes de combinar figuras y realizar acciones interesantes en los lugares de su conexión. Aquí explicaré solo algunas de estas técnicas nuevamente, pero puede encontrar muchas otras en la biblioteca http://mercury.sexy/hg_sdf (escríbame si conoce otras bibliotecas SDF útiles).

Redondeo


Podemos interpretar la superficie de dos figuras combinadas como el eje xy el eje y de la posición en el sistema de coordenadas, y luego calcular la distancia al origen de coordenadas de esta posición. Si hacemos esto, obtendremos una figura muy extraña, pero si limitamos el eje a valores inferiores a 0, obtendremos algo que se asemeja a la conjugación suave de las distancias internas de dos figuras.

 float round_merge(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1, shape2); intersectionSpace = min(intersectionSpace, 0); return length(intersectionSpace); } 


Esto es hermoso, pero no podemos usar esto para cambiar la línea donde la distancia es 0, por lo que esta operación no es más valiosa que el emparejamiento ordinario. Pero antes de conectar las dos figuras, podemos aumentarlas un poco. De la misma manera que creamos un círculo, para agrandar una figura, la restamos de su distancia para empujar una línea más hacia afuera, en la cual la distancia con un signo es 0.

 float radius = max(sin(_Time.y * 5) * 0.5 + 0.4, 0); float combination = round_intersect(squareShape, circleShape, radius); 

 float round_merge(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 - radius, shape2 - radius); intersectionSpace = min(intersectionSpace, 0); return length(intersectionSpace); } 


Simplemente amplía la figura y garantiza transiciones suaves en el interior, pero no queremos aumentar las figuras, solo necesitamos una transición suave. La solución es restar el radio nuevamente después de calcular la longitud. La mayoría de las partes se verán igual que antes, excepto por la transición entre las figuras, que se suaviza maravillosamente de acuerdo con el radio. Ignoraremos la parte exterior de la figura por ahora.

 float round_merge(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 - radius, shape2 - radius); intersectionSpace = min(intersectionSpace, 0); return length(intersectionSpace) - radius; } 


La última etapa es la corrección de la parte externa de la figura. Además, mientras que el interior de la figura es verde, y usamos este color para el exterior. El primer paso es intercambiar las partes externas e internas, simplemente invirtiendo su distancia con un signo. Luego reemplazamos la parte donde se resta el radio. Primero lo cambiamos de la resta a la suma. Esto es necesario, porque antes de combinar con el radio, dibujamos la distancia del vector, por lo tanto, de acuerdo con esto, debemos invertir la operación matemática utilizada. Luego, reemplazaremos el radio con el mate habitual, lo que nos dará los valores correctos fuera de la figura, pero no cerca de los bordes y dentro de la figura. Para evitar esto, tomamos un máximo entre el valor y el radio, obteniendo así un valor positivo de los valores correctos fuera de la figura, así como la suma del radio que necesitamos dentro de la figura.

 float round_merge(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 - radius, shape2 - radius); intersectionSpace = min(intersectionSpace, 0); float insideDistance = -length(intersectionSpace); float simpleUnion = merge(shape1, shape2); float outsideDistance = max(simpleUnion, radius); return insideDistance + outsideDistance; } 


Para crear una intersección, necesitamos hacer lo contrario: reducir las figuras por el radio, asegurarnos de que todos los componentes del vector sean mayores que 0, tomar la longitud y no cambiar su signo. Entonces crearemos la parte exterior de la figura. Luego, para crear la parte interna, tomamos la intersección habitual y nos aseguramos de que no sea menor que menos el radio. Luego, como antes, agregamos los valores internos y externos.

 float round_intersect(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 + radius, shape2 + radius); intersectionSpace = max(intersectionSpace, 0); float outsideDistance = length(intersectionSpace); float simpleIntersection = intersect(shape1, shape2); float insideDistance = min(simpleIntersection, -radius); return outsideDistance + insideDistance; } 


Y como último punto, la resta se puede describir nuevamente como la intersección entre la figura base y todo excepto la figura que estamos restando.

 float round_subtract(float base, float subtraction, float radius){ round_intersect(base, -subtraction, radius); } 


Aquí, y especialmente al restar, puede ver artefactos derivados de la suposición de que podemos usar dos figuras como coordenadas, pero para la mayoría de las aplicaciones, los campos de distancia siguen siendo lo suficientemente buenos.

Bisel


También podemos cortar la transición para darle un ángulo como un chaflán. Para lograr este efecto, primero creamos una nueva forma agregando las dos existentes. Si asumimos nuevamente que el punto en el que se encuentran las dos figuras es ortogonal, entonces esta operación nos dará una línea diagonal que pasa por el punto de encuentro de las dos superficies.


Como simplemente agregamos los dos componentes, la distancia con el signo de esta nueva línea tiene una escala incorrecta, pero podemos corregirla dividiéndola por la diagonal de una unidad cuadrada, es decir, la raíz cuadrada de 2. La división por la raíz de 2 es igual a multiplicando por la raíz cuadrada de 0.5, y simplemente podemos escribir este valor en el código para no calcular la misma raíz cada vez.

Ahora que tenemos una forma que tiene la forma de un bisel deseado, la expandiremos para que el bisel se extienda más allá de los límites de la figura. De la misma manera que antes, restamos el valor que necesitamos para aumentar la cifra. Luego combinamos la forma de bisel con la salida de la fusión habitual, lo que resulta en una transición biselada.

 float champferSize = sin(_Time.y * 5) * 0.3 + 0.3; float combination = champfer_merge(circleShape, squareShape, champferSize); 

 float champfer_merge(float shape1, float shape2, float champferSize){ const float SQRT_05 = 0.70710678118; float simpleMerge = merge(shape1, shape2); float champfer = (shape1 + shape2) * SQRT_05; champfer = champfer - champferSize; return merge(simpleMerge, champfer); } 


Para obtener un bisel cruzado, nosotros, como antes, agregamos dos figuras, pero luego reducimos la figura agregando el bisel e intersectamos con la figura cruzada habitual.

 float champfer_intersect(float shape1, float shape2, float champferSize){ const float SQRT_05 = 0.70710678118; float simpleIntersect = intersect(shape1, shape2); float champfer = (shape1 + shape2) * SQRT_05; champfer = champfer + champferSize; return intersect(simpleIntersect, champfer); } 


Y de manera similar a las restas anteriores, también podemos realizar la intersección con la segunda figura invertida aquí.

 float champfer_subtract(float base, float subtraction, float champferSize){ return champfer_intersect(base, -subtraction, champferSize); } 


Intersección redondeada


Hasta ahora, hemos utilizado solo operadores booleanos (excepto la interpolación). Pero podemos combinar las formas de otras formas, por ejemplo, creando nuevas formas redondeadas en los lugares donde se superponen los bordes de las dos formas.

Para hacer esto, nuevamente necesitamos interpretar las dos figuras como el eje xy el eje y del punto. Luego simplemente calculamos la distancia de este punto al origen. Cuando los límites de las dos figuras se superponen, la distancia a ambas figuras será 0, lo que nos da una distancia de 0 al punto de origen de nuestro sistema de coordenadas imaginario. Luego, si tenemos una distancia al origen, podemos realizar las mismas operaciones con él que para los círculos y restar el radio.

 float round_border(float shape1, float shape2, float radius){ float2 position = float2(shape1, shape2); float distanceFromBorderIntersection = length(position); return distanceFromBorderIntersection - radius; } 


Muesca de borde


Lo último que explicaré es la forma de crear una muesca en una forma en la posición del borde de otra forma.

Comenzamos calculando la forma del límite del círculo. Esto se puede hacer obteniendo el valor absoluto de la distancia de la primera figura, mientras que las partes interna y externa se considerarán la parte interna de la figura, pero el borde aún tiene el valor 0. Si aumentamos esta figura restando el ancho de la muesca, obtendremos la figura a lo largo del borde de la figura anterior. .

 float depth = max(sin(_Time.y * 5) * 0.5 + 0.4, 0); float combination = groove_border(squareShape, circleShape, .3, depth); 

 float groove_border(float base, float groove, float width, float depth){ float circleBorder = abs(groove) - width; return circleBorder; } 


Ahora necesitamos que el borde del círculo sea más profundo solo por el valor que especificamos. Para hacer esto, restamos una versión reducida de la figura base. La cantidad de reducción en la forma de la base es la profundidad de la muesca.

 float groove_border(float base, float groove, float width, float depth){ float circleBorder = abs(groove) - width; float grooveShape = subtract(circleBorder, base + depth); return grooveShape; } 


El último paso es restar la muesca de la forma de la base y devolver el resultado.

 float groove_border(float base, float groove, float width, float depth){ float circleBorder = abs(groove) - width; float grooveShape = subtract(circleBorder, base + depth); return subtract(base, grooveShape); } 


Código fuente


La biblioteca



 #ifndef SDF_2D #define SDF_2D //transforms 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; } //combinations ///basic float merge(float shape1, float shape2){ return min(shape1, shape2); } float intersect(float shape1, float shape2){ return max(shape1, shape2); } float subtract(float base, float subtraction){ return intersect(base, -subtraction); } float interpolate(float shape1, float shape2, float amount){ return lerp(shape1, shape2, amount); } /// round float round_merge(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 - radius, shape2 - radius); intersectionSpace = min(intersectionSpace, 0); float insideDistance = -length(intersectionSpace); float simpleUnion = merge(shape1, shape2); float outsideDistance = max(simpleUnion, radius); return insideDistance + outsideDistance; } float round_intersect(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 + radius, shape2 + radius); intersectionSpace = max(intersectionSpace, 0); float outsideDistance = length(intersectionSpace); float simpleIntersection = intersect(shape1, shape2); float insideDistance = min(simpleIntersection, -radius); return outsideDistance + insideDistance; } float round_subtract(float base, float subtraction, float radius){ return round_intersect(base, -subtraction, radius); } ///champfer float champfer_merge(float shape1, float shape2, float champferSize){ const float SQRT_05 = 0.70710678118; float simpleMerge = merge(shape1, shape2); float champfer = (shape1 + shape2) * SQRT_05; champfer = champfer - champferSize; return merge(simpleMerge, champfer); } float champfer_intersect(float shape1, float shape2, float champferSize){ const float SQRT_05 = 0.70710678118; float simpleIntersect = intersect(shape1, shape2); float champfer = (shape1 + shape2) * SQRT_05; champfer = champfer + champferSize; return intersect(simpleIntersect, champfer); } float champfer_subtract(float base, float subtraction, float champferSize){ return champfer_intersect(base, -subtraction, champferSize); } /// round border intersection float round_border(float shape1, float shape2, float radius){ float2 position = float2(shape1, shape2); float distanceFromBorderIntersection = length(position); return distanceFromBorderIntersection - radius; } float groove_border(float base, float groove, float width, float depth){ float circleBorder = abs(groove) - width; float grooveShape = subtract(circleBorder, base + depth); return subtract(base, grooveShape); } //shapes 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 

Base de sombreador



 Shader "Tutorial/035_2D_SDF_Combinations/Round"{ 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) { const float PI = 3.14159; float2 squarePosition = position; squarePosition = translate(squarePosition, float2(1, 0)); squarePosition = rotate(squarePosition, .125); float squareShape = rectangle(squarePosition, float2(2, 2)); float2 circlePosition = position; circlePosition = translate(circlePosition, float2(-1.5, 0)); float circleShape = circle(circlePosition, 2.5); float combination = /* combination calculation here */; return combination; } 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 } 

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


All Articles