2D-Schatten auf signierten Distanzfeldern

Jetzt, da wir die Grundlagen der Kombination von vorzeichenbehafteten Distanzfunktionen kennen, können Sie sie verwenden, um coole Dinge zu erstellen. In diesem Tutorial werden wir sie verwenden, um weiche zweidimensionale Schatten zu rendern. Wenn Sie meine vorherigen Tutorials zu signierten Distanzfeldern (SDF) nicht gelesen haben, empfehle ich Ihnen dringend, sie zu studieren, beginnend mit einem Tutorial zum Erstellen einfacher Formen .


[GIF erzeugte während der Rekomprimierung zusätzliche Artefakte.]

Grundkonfiguration


Ich habe eine einfache Konfiguration mit einem Raum erstellt, die die in früheren Tutorials beschriebenen Techniken verwendet. Zuvor habe ich nicht erwähnt, dass ich die abs Funktion für vector2 verwendet habe, um die Position relativ zur x- und y-Achse zu spiegeln, und dass ich den Abstand der Figur umgekehrt habe, um den inneren und den äußeren Teil zu vertauschen.

Wir werden die Datei 2D_SDF.cginc aus dem vorherigen Tutorial in einen Ordner mit dem Shader kopieren, den wir in diesem Tutorial schreiben werden.

 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 } 

Wenn wir immer noch die Visualisierungstechnik aus dem vorherigen Tutorial verwenden würden, würde die Abbildung folgendermaßen aussehen:


Einfache Schatten


Um scharfe Schatten zu erzeugen, gehen wir um den Raum von der Position der Probe zur Position der Lichtquelle. Wenn wir unterwegs ein Objekt finden, entscheiden wir, dass das Pixel schattiert werden soll, und wenn wir ungehindert zur Quelle gelangen, sagen wir, dass es nicht schattiert ist.

Wir beginnen mit der Berechnung der Grundparameter des Strahls. Wir haben bereits einen Startpunkt (die Position des Pixels, das wir rendern) und einen Zielpunkt (die Position der Lichtquelle) für den Strahl. Wir brauchen eine Länge und eine normalisierte Richtung. Die Richtung kann erhalten werden, indem der Anfang vom Ende subtrahiert und das Ergebnis normalisiert wird. Die Länge kann erhalten werden, indem die Positionen subtrahiert und der Wert an die length wird.

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

Dann gehen wir iterativ um den Strahl in der Schleife herum. Wir werden Iterationen der Schleife in der define-Deklaration festlegen. Auf diese Weise können wir die maximale Anzahl von Iterationen später konfigurieren und dem Compiler ermöglichen, den Shader durch Erweitern der Schleife ein wenig zu optimieren.

In der Schleife benötigen wir die Position, an der wir uns gerade befinden, also deklarieren wir sie außerhalb der Schleife mit dem Anfangswert 0. In der Schleife können wir die Position der Probe berechnen, indem wir den Strahlvorschub multipliziert mit der Richtung des Strahls mit der Basisposition addieren. Dann probieren wir die vorzeichenbehaftete Distanzfunktion an der gerade berechneten Position aus.

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

Dann prüfen wir, ob wir bereits an dem Punkt sind, an dem wir den Zyklus stoppen können. Wenn der Abstand der Szene der Entfernungsfunktion mit dem Vorzeichen nahe 1 liegt, können wir annehmen, dass der Strahl durch eine Zahl blockiert ist und 0 zurückgibt. Wenn sich der Strahl weiter als der Abstand zur Lichtquelle ausbreitet, können wir annehmen, dass wir die Quelle ohne Kollisionen erreicht haben und den Wert zurückgeben 1.

Wenn die Rückgabe nicht erfolgreich ist, müssen Sie die nächste Position der Probe berechnen. Dies erfolgt durch Hinzufügen eines Abstands in der Strahlvorschubszene. Der Grund dafür ist, dass die Entfernung in der Szene die Entfernung zur nächsten Figur angibt. Wenn wir also diesen Wert zum Strahl hinzufügen, können wir den Strahl wahrscheinlich nicht weiter als bis zur nächsten Figur oder sogar darüber hinaus emittieren, was zum Schattenfluss führt.

Für den Fall, dass wir zum Zeitpunkt der Fertigstellung des Probenbestands (Ende des Zyklus) auf nichts gestoßen sind und die Lichtquelle nicht erreicht haben, müssen wir auch den Wert zurückgeben. Da dies hauptsächlich neben den Formen geschieht, kurz bevor das Pixel noch als schattiert betrachtet wird, verwenden wir hier den Rückgabewert 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; } 

Um diese Funktion zu verwenden, rufen wir sie in einer Fragmentfunktion mit der Position des Pixels und der Position der Lichtquelle auf. Dann multiplizieren wir das Ergebnis mit einer beliebigen Farbe, um es mit der Farbe der Lichtquellen zu mischen.

Ich habe auch die im ersten Tutorial beschriebene Technik über Distanzfelder mit einem Zeichen verwendet , um die Geometrie zu visualisieren. Dann habe ich nur gefaltet und Geometrie hinzugefügt. Hier können wir einfach die Additionsoperation verwenden und keine lineare Interpolation oder ähnliche Aktionen ausführen, da die Form überall dort schwarz ist, wo die Form nicht ist, und der Schatten überall dort schwarz ist, wo die Form ist.

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


Weiche Schatten


Es ist einfach genug, von diesen harten Schatten zu weicheren und realistischeren zu gelangen. In diesem Fall wird der Shader nicht viel rechenintensiver.

Zuerst ermitteln wir einfach die Entfernung zum nächsten Szenenobjekt für jedes Sample, das wir umgehen, und wählen das nächstgelegene aus. Wenn wir dann 1 zurückgegeben haben, ist es möglich, die Entfernung zur nächsten Zahl zurückzugeben. Damit die Helligkeit des Schattens nicht zu hoch ist und nicht zur Erzeugung seltsamer Farben führt, werden wir ihn durch die saturate , die ihn auf ein Intervall von 0 bis 1 begrenzt. Nachdem wir überprüft haben, ob der Strahl der Lichtquelle bereits die Verteilung erreicht hat, erhalten wir ein Minimum zwischen der aktuellsten und der nächsten Zahl Andernfalls können wir Proben nehmen, die über die Lichtquelle hinausgehen und seltsame Artefakte erhalten.

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


Das erste, was wir danach bemerken, sind die seltsamen "Zähne" im Schatten. Sie entstehen, weil der Abstand von der Szene zur Lichtquelle weniger als 1 beträgt. Ich habe versucht, dem auf verschiedene Weise entgegenzuwirken, konnte aber keine Lösung finden. Stattdessen können wir die Schärfe des Schattens implementieren. Die Schärfe ist ein weiterer Parameter in der Schattenfunktion. In der Schleife multiplizieren wir den Abstand in der Szene mit der Schärfe, und mit einer Schärfe von 2 wird der weiche, graue Teil des Schattens halb so groß. Bei Verwendung der Schärfe kann die Lichtquelle in einem Abstand von mindestens 1 geteilt durch die Schärfe von der Figur entfernt sein, da sonst Artefakte auftreten. Wenn Sie daher eine Schärfe von 20 verwenden, sollte der Abstand mindestens 0,05 Einheiten betragen.

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


Durch die Minimierung dieses Problems stellen wir Folgendes fest: Selbst in Bereichen, die nicht beschattet werden sollten, ist in der Nähe der Wände noch eine Schwächung sichtbar. Darüber hinaus scheint die Weichheit des Schattens für den gesamten Schatten gleich zu sein und neben der Figur nicht scharf und weicher zu sein, wenn Sie sich von dem Objekt entfernen, das den Schatten aussendet.

Wir werden dies beheben, indem wir den Abstand in der Szene durch die Strahlausbreitung teilen. Dank dessen teilen wir den Abstand am Anfang des Strahls in sehr kleine Zahlen, dh wir erhalten immer noch hohe Werte und einen schönen klaren Schatten. Wenn wir den Punkt finden, der dem Strahl am nächsten liegt, wird der nächste Punkt durch eine größere Zahl geteilt, wodurch der Schatten weicher wird. Da dies nicht vollständig mit der kürzesten Entfernung zusammenhängt, werden wir die Variable in shadow umbenennen.

Wir werden noch eine kleine Änderung vornehmen: Da wir durch rayProgress teilen, sollten Sie nicht mit 0 beginnen (das Teilen durch Null ist fast immer eine schlechte Idee des Teilens). Zu Beginn können Sie eine sehr kleine Zahl auswählen.

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


Mehrere Lichtquellen


In dieser einfachen Single-Core-Implementierung besteht der einfachste Weg, mehrere Lichtquellen zu erhalten, darin, sie einzeln zu berechnen und die Ergebnisse hinzuzufügen.

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


Quellcode


Zweidimensionale SDF-Bibliothek (hat sich nicht geändert, wird aber hier verwendet)



Zweidimensionale weiche Schatten



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

Dies ist nur eines von vielen Beispielen für die Verwendung von vorzeichenbehafteten Entfernungsfeldern. Bisher sind sie ziemlich umständlich, da alle Formen im Shader registriert oder durch die Eigenschaften des Shaders geleitet werden müssen, aber ich habe einige Ideen, wie sie für zukünftige Tutorials bequemer gestaltet werden können.

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


All Articles