Combinaison de champs de distance signés en 2D

Dans le didacticiel précédent, nous avons appris à créer et à déplacer des formes simples à l'aide des fonctions de distance signée. Dans cet article, nous allons apprendre à combiner plusieurs formes pour créer des champs de distance plus complexes. La plupart des techniques décrites ici, j'ai appris de la bibliothèque de fonctions de distance avec le signe glsl, qui peut être trouvée ici . Il existe également plusieurs façons de combiner des formes, dont je ne parlerai pas ici.


La préparation


Pour la visualisation des champs de distance signés (champs de distance signés, SDF), nous utiliserons une configuration simple, puis nous y appliquerons les opérateurs. Pour afficher les champs de distance, il utilisera la visualisation des lignes de distance du premier tutoriel. Par souci de simplicité, nous allons définir tous les paramètres à l'exception des paramètres de visualisation dans le code, mais vous pouvez remplacer n'importe quelle valeur par une propriété pour la rendre personnalisable.

Le shader principal avec lequel nous allons commencer ressemble à ceci:

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 } 

Et la fonction 2D_SDF.cginc dans le même dossier avec le shader, que nous allons développer, ressemble d'abord à ceci:

 #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 

Combinaisons simples


Nous commencerons par quelques façons simples de combiner deux formes pour créer une grande forme, des conjugaisons, des intersections et des soustractions, ainsi qu'un moyen de transformer une forme en une autre.

Jumelage


L'opérateur le plus simple est l'appariement. Avec lui, nous pouvons assembler les deux figures et obtenir la distance avec le signe de la figure connectée. Lorsque nous avons une distance avec le signe de deux chiffres, nous pouvons les combiner en prenant le plus petit des deux en utilisant la fonction min .

En raison du choix de la plus petite des deux valeurs, le chiffre final sera inférieur à 0 (visible) où l'un des deux chiffres entrants a une distance du bord inférieure à 0; la même chose s'applique à toutes les autres valeurs de distance, montrant une combinaison de deux chiffres.

Ici, je nommerai la fonction pour créer la conjugaison «fusion», en partie parce que nous les fusionnons, en partie parce que le mot-clé union dans hlsl est réservé, donc il ne peut pas être utilisé comme nom de la fonction.

 //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); 




Intersection


Une autre façon courante de connecter des formes consiste à utiliser des zones dans lesquelles deux formes se chevauchent. Pour ce faire, nous prenons la valeur maximale des distances des deux figures que nous voulons combiner. Lorsque vous utilisez la plus grande des deux valeurs, nous obtenons une valeur supérieure à 0 (en dehors de la figure), lorsque l'une des distances à deux figures est en dehors de la figure et que d'autres distances sont également alignées de la même manière.

 //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); 


Soustraction


Cependant, souvent, nous ne voulons pas traiter les deux formes de la même manière, et nous devons soustraire l'autre d'une forme. C'est assez facile à faire en croisant la forme que nous voulons changer et tout sauf la forme que nous voulons soustraire. Nous obtenons les valeurs opposées pour les parties intérieure et extérieure de la figure, en inversant la distance avec le signe. Ce qui était 1 unité en dehors du chiffre est maintenant 1 unité à l'intérieur.

 //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); 


Interpolation


Une manière non évidente de combiner deux figures est d'interpoler entre elles. Il est également possible dans une certaine mesure pour les maillages de polygones avec des formes de fusion, mais est beaucoup plus limité que ce que nous pouvons faire avec les champs de distance signés. Par simple interpolation entre les distances de deux figures, on obtient un écoulement fluide de l'une dans l'autre. Pour l'interpolation, vous pouvez simplement utiliser la méthode 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); 


Autres composés


Ayant reçu des connexions simples, nous avons déjà tout le nécessaire pour une simple combinaison de chiffres, mais la propriété étonnante des champs de signe de distance est que nous ne pouvons pas nous limiter à cela, il existe de nombreuses façons différentes de combiner des figures et d'effectuer des actions intéressantes aux endroits de leur connexion. Ici, je vais expliquer à nouveau seulement certaines de ces techniques, mais vous pouvez en trouver beaucoup d'autres dans la bibliothèque http://mercury.sexy/hg_sdf (écrivez-moi si vous connaissez d'autres bibliothèques SDF utiles).

Arrondi


Nous pouvons interpréter la surface de deux figures combinées comme l'axe x et l'axe y de la position dans le système de coordonnées, puis calculer la distance jusqu'à l'origine des coordonnées de cette position. Si nous faisons cela, nous obtiendrons un chiffre très étrange, mais si nous limitons l'axe à des valeurs inférieures à 0, nous obtenons quelque chose qui ressemble à la conjugaison lisse des distances internes de deux chiffres.

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


C'est beau, mais nous ne pouvons pas l'utiliser pour changer la ligne où la distance est 0, donc cette opération n'a pas plus de valeur qu'un appariement ordinaire. Mais avant de connecter les deux chiffres, nous pouvons les augmenter un peu. De la même manière que nous avons créé un cercle, pour agrandir une figure, nous le soustrayons de sa distance afin de pousser la ligne plus loin, dans laquelle la distance avec un signe est 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); } 


Il agrandit simplement la figure et assure des transitions en douceur à l'intérieur, mais nous ne voulons pas augmenter les figures, nous avons seulement besoin d'une transition en douceur. La solution consiste à soustraire à nouveau le rayon après avoir calculé la longueur. La plupart des pièces auront la même apparence qu'avant, à l'exception de la transition entre les figures, qui est magnifiquement lissée en fonction du rayon. Nous ignorerons pour l'instant la partie extérieure de la figure.

 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 dernière étape est la correction de la partie extérieure de la figure. De plus, tandis que l'intérieur de la figure est vert, nous utilisons cette couleur pour l'extérieur. La première étape consiste à échanger les parties externes et internes, simplement en inversant leur distance avec un signe. Ensuite, nous remplaçons la partie où le rayon est soustrait. D'abord, nous le changeons de la soustraction à l'addition. Cela est nécessaire, car avant de combiner avec le rayon, nous avons tracé la distance du vecteur, par conséquent, conformément à cela, nous devons inverser l'opération mathématique utilisée. Ensuite, nous remplacerons le rayon par la contrainte habituelle, ce qui nous donnera les valeurs correctes en dehors de la figure, mais pas près des bords et à l'intérieur de la figure. Pour éviter cela, nous prenons un maximum entre la valeur et le rayon, obtenant ainsi une valeur positive des valeurs correctes en dehors de la figure, ainsi que l'ajout du rayon dont nous avons besoin à l'intérieur de la figure.

 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; } 


Pour créer une intersection, nous devons faire l'inverse - réduire les chiffres du rayon, s'assurer que toutes les composantes du vecteur sont supérieures à 0, prendre la longueur et ne pas changer son signe. Nous allons donc créer la partie extérieure de la figure. Ensuite, pour créer la partie intérieure, nous prenons l'intersection habituelle et nous assurons qu'elle n'est pas inférieure à moins le rayon. Ensuite, comme précédemment, nous ajoutons les valeurs internes et externes.

 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; } 


Et comme dernier point, la soustraction peut à nouveau être décrite comme l'intersection entre le chiffre de base et tout sauf le chiffre que nous soustrayons.

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


Ici, et en particulier lors de la soustraction, vous pouvez voir des artefacts résultant de l'hypothèse que nous pouvons utiliser deux chiffres comme coordonnées, mais pour la plupart des applications, les champs de distance sont encore assez bons.

Biseau


Nous pouvons également tondre la transition pour lui donner un angle comme un chanfrein. Pour obtenir cet effet, nous créons d'abord une nouvelle forme en ajoutant les deux existantes. Si nous supposons à nouveau que le point de rencontre des deux figures est orthogonal, alors cette opération nous donnera une ligne diagonale passant par le point de rencontre des deux surfaces.


Puisque nous avons simplement ajouté les deux composants, la distance avec le signe de cette nouvelle ligne a la mauvaise échelle, mais nous pouvons la corriger en la divisant par la diagonale d'un carré unitaire, c'est-à-dire la racine carrée de 2. La division par la racine de 2 est la même que multipliant par la racine carrée de 0,5, et nous pouvons simplement écrire cette valeur dans le code afin de ne pas calculer la même racine à chaque fois.

Maintenant que nous avons une forme qui a la forme d'un biseau souhaité, nous allons l'agrandir pour que le biseau dépasse les limites de la figure. De la même manière que précédemment, nous soustrayons la valeur dont nous avons besoin pour augmenter le chiffre. Ensuite, nous combinons la forme biseautée avec la sortie de la fusion habituelle, résultant en une transition biseautée.

 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); } 


Pour obtenir un biseau croisé, on ajoute comme précédemment deux chiffres, mais ensuite on réduit le chiffre en ajoutant la valeur du biseau et on coupe avec un chiffre croisé régulier.

 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); } 


Et de manière similaire aux soustractions précédentes, nous pouvons également effectuer l'intersection avec la deuxième figure inversée ici.

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


Intersection arrondie


Jusqu'à présent, nous n'avons utilisé que des opérateurs booléens (sauf pour l'interpolation). Mais nous pouvons combiner les formes d'autres manières, par exemple, en créant de nouvelles formes arrondies aux endroits où les bordures des deux formes se chevauchent.

Pour ce faire, nous devons à nouveau interpréter les deux figures comme l'axe x et l'axe y du point. Ensuite, nous calculons simplement la distance de ce point à l'origine. Lorsque les limites des deux figures se chevauchent, la distance aux deux figures sera de 0, ce qui nous donne une distance de 0 au point d'origine de notre système de coordonnées imaginaire. Ensuite, si nous avons une distance par rapport à l'origine, nous pouvons effectuer les mêmes opérations avec lui que pour les cercles et soustraire le rayon.

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


Encoche de bordure


La dernière chose que j'expliquerai est la façon de créer une encoche dans une forme à la position de bordure d'une autre forme.

Nous commençons par calculer la forme de la frontière du cercle. Cela peut être fait en obtenant la valeur absolue de la distance de la première figure, tandis que les parties intérieure et extérieure seront considérées comme la partie intérieure de la figure, mais la bordure a toujours la valeur 0. Si nous augmentons ce chiffre en soustrayant la largeur de l'encoche, nous obtiendrons la figure le long de la frontière de la figure précédente .

 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; } 


Maintenant, nous avons besoin de la frontière du cercle pour aller plus loin que par la valeur que nous spécifions. Pour ce faire, nous en soustrayons une version réduite du chiffre de base. La quantité de réduction de la forme de base est la profondeur de l'encoche.

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


La dernière étape consiste à soustraire l'encoche de la forme de base et à renvoyer le résultat.

 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); } 


Code source


La bibliothèque



 #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 shader



 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/fr438954/


All Articles