Blending und Unity Terrain: So entfernen Sie Kreuzungen und lassen Ihre Augen nicht mehr weh tun

Um eine realistische Welt im Spiel zu erhalten, ist es notwendig, die Interaktion verschiedener Landformen untereinander und mit anderen Modellen zu berücksichtigen. Und wenn die sichtbaren Schnittlinien zwischen den 3D-Modellen die Integrität des Bildes beeinträchtigen, sollten Sie überlegen, wie Sie sie beseitigen können. Der häufigste Fall solcher Linien, der vielen bekannt sein mag, ist der Schnittpunkt von Werbetafeln aus Partikeln mit undurchsichtiger Geometrie.

Bild

Ein weiteres Beispiel ist die störende natürliche Zusammensetzung des Schnittpunkts von Felsen und Vegetation mit der Oberfläche der Landschaft in „Outdoor“ -Szenen.

Bild

Zusätzlich zu verschiedenen Glättungsmethoden (SSAA, MSAA, CSAA, FXAA, NFAA, CMAA, DLAA, TAA usw.), die zwar das trotzige Auftreten solcher Schnittlinien mildern, die Situation jedoch nicht vollständig korrigieren, gibt es effektivere Techniken. Wir werden sie berücksichtigen.

Tiefenmischung


Unity verfügt über eine integrierte Lösung, um sichtbare Schnittpunkte zwischen transparenten Partikeln und undurchsichtiger Geometrie zu vermeiden, die als weiche Partikel bezeichnet werden. Die Shader, die diesen Effekt unterstützen, verbessern die Transparenz der Partikel weiter, je nachdem, wie gering der Unterschied zwischen der Tiefe des Partikelfragments und der Tiefe der opaken Geometrie ist.

Bild
Das Funktionsprinzip weicher Partikel

Für den korrekten Betrieb weicher Partikel ist natürlich ein Tiefenpuffer erforderlich. Im Fall einer verzögerten Schattierung wird der Tiefenpuffer im Stadium des Renderns von Vollbildpuffern gebildet, und unter Berücksichtigung der MRT (Multiple Render Targets, nicht Magnetresonanztomographie) wird sein Vorhandensein nicht in zusätzlichen Berechnungskosten ausgedrückt.

Bei Vorwärtsschattierung und Verwendung der Unity Legacy-Pipeline war ein zusätzlicher Durchgang erforderlich, um die undurchsichtige Geometrie in den Tiefenpuffer zu rendern [1] . Dieser Durchlauf wird aktiviert, indem der Eigenschaft Camera.depthTextureMode der entsprechende Wert zugewiesen wird. Diese Eigenschaft ist im Inspektorfenster nicht verfügbar, jedoch in der API [2] .

Jetzt können Sie Ihre eigene Version der Scriptable Render Pipeline mit Forward Shading implementieren, die mit Hilfe von MRT gleichzeitig sowohl den Tiefenpuffer als auch den Farbpuffer rendern kann.


Eliminieren von Schnittlinien in Shadern, die weiche Partikel unterstützen

Im Allgemeinen gibt es keine technischen Hindernisse für die Verwendung der Tiefenmischmethode, um sichtbare Schnittpunkte von 3D-Modellen mit der Landschaft zu eliminieren:

Code anzeigen
// Blending with depth buffer

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

struct v2f
{
    // ...

    half4 projPos : TEXCOORD0;
};

v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);

    // ...

    o.projPos = ComputeScreenPos(o.pos);
    COMPUTE_EYEDEPTH(o.projPos.z);

    // ...

    return o;
}

fixed4 frag(v2f i) : COLOR
{     
    fixed4 result = 0;
      
    // ... 

    float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos));
    float sceneZ = LinearEyeDepth(depth);
    float partZ = i.projPos.z;
    float fade = saturate( sceneZ - partZ );
    result.a = smoothstep( BlendStart, BlendEnd, fade );

    // ... 
       
    return result; 
}


Dieser Ansatz hat jedoch mehrere Nachteile.

Der erste Nachteil hängt mit der Leistung zusammen. Das Tiefenmischen funktioniert in der Phase des Hardware-Pipe-Mischens, dh unmittelbar nach der Rasterung und Berechnung des Fragment-Shaders. In diesem Stadium wird das Ergebnis der Ausführung des Fragment-Shaders mit dem im Ausgabepuffer [3] [4] [5] aufgezeichneten Ergebnis gemäß der Formel gemischt , die durch die Aufrufe der API [6] [7] [8] [9] vordefiniert wurde .

Dies ist der am wenigsten fortschrittliche Teil einer Hardware-Pipeline in dem Sinne, dass sie genau so funktioniert wie ihr Vorgänger vor zwanzig Jahren. Die GPU liest den Wert aus dem Speicher, mischt ihn mit dem Wert des Fragment-Shaders und schreibt ihn zurück in den Speicher.

Es gibt auch einen Unterschied, ob die Tiefenmischung für vollständig transparente oder teilweise transparente 3D-Modelle verwendet werden soll. Transparent - zum Beispiel Partikelwerbetafeln - ist der gesamte Render transparent, auch ohne in die Tiefe zu mischen. Bei undurchsichtigen 3D-Modellen wird die reale, greifbare, sichtbare Transparenz beim Mischen in der Tiefe nur mit einer sehr geringen Anzahl von Fragmenten versehen, während die überwiegende Mehrheit von ihnen undurchsichtig bleibt. Letzteres bedeutet jedoch keineswegs, dass das Mischen nicht für das Rendern verwendet wird - es funktioniert einfach im Leerlauf.

Der zweite Nachteil hängt damit zusammen, wie die Farbe zum Mischen ausgewählt wird. Kurz gesagt, alle Fragmente, die in einem bestimmten Bildschirmpixel gemischt werden, liegen auf einem Strahl, der von der Weltposition der Kamera ausgeht und durch die Weltposition dieses Bildschirmpixels geht. Dies bedeutet wiederum, dass bei jeder Änderung der Kameraposition oder -ausrichtung eine Parallaxe beobachtet wird: Fragmente des 3D-Modells, die sich näher an der Kamera befinden, bewegen sich schneller als Fragmente der Landschaft, die sich weiter von der Kamera entfernt befinden [10] [11] . Dies macht sich insbesondere aus der Nähe bei konstanter seitlicher Verschiebung der Kamera bemerkbar.


Seitliche Parallaxe beim Bewegen der Kamera: Fragmente des 3D-Modells werden im Vergleich zu Fragmenten der Landschaft um eine größere Entfernung verschoben


Seitliche Parallaxe beim Bewegen der Kamera: Wenn Sie die Kamera auf einem Fragment der Landschaft fixieren, wird deutlich, wie schnell sich die Fragmente des Modells bewegen.

Wenn die Kamera gedreht wird, wird die Parallaxe sofort entlang zweier Achsen der Bildschirmkoordinaten beobachtet. In der Dynamik ist dies jedoch weniger offensichtlich als die laterale Parallaxe.


Azimutale Parallaxe bei verschobener Kamera: Für das Gehirn ist es schwieriger, das Parallaxenmuster zu erkennen, wenn die Fragmente entlang zweier

Achsen verschoben werden. Am auffälligsten ist jedoch, dass sich das Erscheinungsbild der Tiefenmischung in Abhängigkeit von dem Winkel ändert, in dem der Betrachter auf die Oberfläche der Landschaft schaut. Die Mischzone wird fast unsichtbar, wenn die Blickrichtung senkrecht zur Normalen zur Landschaftsoberfläche verläuft. Die Größe dieser Zone nimmt jedoch schnell zu, wenn Sie die Kamera nach unten neigen.


Ändern der Breite der Mischzone beim Neigen der Kamera

in die Tiefe Das Mischen ist möglicherweise eine gute Option, um die Schnittlinien von 3D-Modellen mit der Landschaft zu entfernen, wenn nicht die Vielzahl der damit verbundenen Artefakte. Diese Methode eignet sich besser für Partikeleffekte, die nicht statisch sind und in der Regel keine sehr detaillierten Texturen enthalten. Daher werden in ihrem Fall keine Parallaxeneffekte beobachtet.


Höhenkartenmischung


Eine weitere Option zum Implementieren der Landschaftsüberblendung ist die Verwendung einer Höhenkarte, auf die Unity über die TerrainData-API zugreifen kann [12] .

Wenn Sie die Position des Terrain-Objekts und die Abmessungen des in TerrainData angegebenen Terrains kennen und eine "Höhenkarte" zur Hand haben, können Sie die Höhe des Terrains an jedem in Weltkoordinaten angegebenen Punkt berechnen.


Geländeparameter, die zum Abtasten der Höhenkarte erforderlich sind

// Setting up a heightmap and uniforms to use with shaders... 

Shader.SetGlobalTexture(Uniforms.TerrainHeightmap, terrain.terrainData.heightmapTexture);
Shader.SetGlobalVector(Uniforms.HeightmapScale, terrain.terrainData.heightmapScale);
Shader.SetGlobalVector(Uniforms.TerrainSize, terrain.terrainData.size);
Shader.SetGlobalVector(Uniforms.TerrainPos, terrain.transform.position);

Nun, nachdem Sie die Höhe der Landschaft berechnet haben, können Sie auch die UV-Koordinaten im Shader berechnen, um die Karte der Höhen der Landschaft in Weltkoordinaten abzutasten.

// Computes UV for sampling terrain heightmap... 

float2 TerrainUV(float3 worldPos)
{
    return (worldPos.xz - TerrainPos.xz) / TerrainSize.xz;
}

Um denselben Code in Fragment- und Vertex-Shadern verwenden zu können, wird die tex2Dlod-Funktion zum Abtasten verwendet. Darüber hinaus hat die Höhenkarte keine Mip-Pegel, sodass das Abtasten mit der tex2D-Funktion, die den Mip-Pegel automatisch berechnet, grundsätzlich bedeutungslos ist.

// Returns the height of terrain at a given position in world space... 

float TerrainHeight(float2 terrainUV)
{
    float heightmapSample = tex2Dlod(TerrainHeightmap, float4(terrainUV,0,0));
    return TerrainPos.y + UnpackHeightmap(heightmapSample) * HeightmapScale.y * 2;
}

Sie können versuchen, die Beseitigung von Schnittpunkten durch Transparenz zu reproduzieren, ohne einen Tiefenpuffer zu verwenden. Dies löst keine anderen mit dieser Methode verbundenen Probleme, ermöglicht jedoch die Überprüfung der Funktionsfähigkeit des Mischens mithilfe einer Höhenkarte.

Code anzeigen
// Blending with terrain heightmap

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;

sampler2D_float TerrainHeightmap; 
float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;        

struct v2f
{
   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;

   // ...
};


v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);

    // ...

    return o;
}

fixed4 frag(v2f i) : COLOR
{
    fixed4 result = 0;

    // ... 

    half height = TerrainHeight(i.heightMapUV);
    half deltaHeight = i.worldPos.y - height;
    result.a = smoothstep( BlendStart, BlendEnd, deltaHeight );

    // ... 
       
    return result; 
}





Tiefenmischung und Höhenmischung. Die Breite der Mischzone unterscheidet sich bei gleichen Shader-Parametern.

In den Abbildungen werden für beide Methoden identische Mischparameter verwendet. Die Breite der Mischzonen ist optisch unterschiedlich, da das Mischen mit einer Höhenkarte nicht vom Winkel zwischen dem Blick des Betrachters und der Normalen zur Landschaft abhängt.

Das Überblenden mit einer Höhenkarte ist zumindest in einer Hinsicht besser als das Überblenden in der Tiefe: Es korrigiert die Abhängigkeit des Überblendens, die mit bloßem Auge sichtbar ist, von dem Winkel, in dem die Kamera die Landschaft betrachtet. Leider wird der Parallaxeeffekt immer noch beobachtet.


Vermischung der Landschaftsgestaltung


Um die Parallaxe zu beseitigen, müssen Sie ein Fragment des 3D-Modells mit einem Fragment der Landschaft mischen, das sich vertikal darunter befindet (die Farbauswahl zum Mischen hängt in diesem Fall nicht von der Position und Ausrichtung der Kamera ab).


So beheben Sie die Parallaxe: Auswählen eines Landschaftsfragments zum Mischen

Natürlich sprechen wir hier mehr über ein virtuelles Landschaftsfragment. Abhängig von der Position der Kamera ist eine Situation möglich, in der ein Fragment der Landschaft, mit dem ein Fragment eines 3D-Modells gemischt werden muss, nicht einmal in das Sichtfeld der Kamera fällt. Ein ähnliches Problem besteht beim Rendern lokaler Reflexionen im Bildschirmbereich (SSLR). Es besteht darin, dass es unmöglich ist, die Reflexion eines Fragments zu rendern, das sich nicht auf dem Bildschirm befindet [13] .

Im Fall der Landschaft kann die Farbe des virtuellen Fragments mit Hilfe der von der Unity-API bereitgestellten Hilfstexturen mit hoher Genauigkeit rekonstruiert werden: normale Karte [14] , helle Karte [15] , gewichtete Texturen zum Mischen von Ebenen [16] und enthaltene Texturen Zusammensetzung der Schichten [17] .


Rekonstruktion eines Fragmentes der Landschaft

Alle Texturen, aus denen sich die Landschaft zusammensetzt, werden mit dem gleichen UV-Wert wie die Höhenkarte abgetastet. Bei Ebenen werden die Koordinaten für die Abtastung durch die für eine bestimmte Ebene angegebenen Kachelparameter angepasst [18] [19] .

Code anzeigen
// Blending with reconstructed terrain fragments

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;

sampler2D_float TerrainHeightmapTexture;
sampler2D_float TerrainNormalTexture;
sampler2D TerrainAlphaMap;

float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;
Float4 TerrainLightmap_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap0);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap0);
half4 TerrainSplatMap0_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap1);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap1);
half4 TerrainSplatMap1_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap2);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap2);
half4 TerrainSplatMap2_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap3);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap3);
half4 TerrainSplatMap3_ST;

struct v2f
{
   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;
#if defined(LIGHTMAP_ON)
   float2 modelLightMapUV : TEXCOORD2;
   float2 terrainLightMapUV : TEXCOORD3;
#endif

   // ...
};


v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);

#if defined(LIGHTMAP_ON)
    o.modelLightMapUV = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
    o.terrainLightMapUV = o.heightMapUV * TerrainLightmap_ST.xy + TerrainLightmap_ST.zw;
#endif

    // ...

    return o;
}
half3 TerrainNormal(float2 terrainUV)
{
    return tex2Dlod( TerrainNormalTexture, float4(terrainUV,0,0) ).xyz * 2.0 - 1.0;
}

half4 TerrainSplatMap(float2 uv0, float2 uv1, float2 uv2, float2 uv3, half4 control)
{
    half4 splat0 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap0, TerrainSplatMap0, uv0);
    half4 splat1 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap1, TerrainSplatMap1, uv1);
    half4 splat2 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap2, TerrainSplatMap2, uv2);
    half4 splat3 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap3, TerrainSplatMap3, uv3);         
    half4 result = splat0 * control.r + 
                   splat1 * control.g + 
                   splat2 * control.b + 
                   splat3 * control.a;
    return result;
}

half3 TerrainNormalMap(float2 uv0, float2 uv1, float2 uv2, float2 uv3, half4 control)
{
    half4 n0 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap0, TerrainSplatMap0, uv0);
    half4 n1 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap1, TerrainSplatMap1, uv1);
    half4 n2 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap2, TerrainSplatMap2, uv2);
    half4 n3 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap3, TerrainSplatMap3, uv3);
    half3 result = UnpackNormalWithScale(n0, 1.0) * control.r +
                   UnpackNormalWithScale(n1, 1.0) * control.g +
                   UnpackNormalWithScale(n2, 1.0) * control.b +
                   UnpackNormalWithScale(n3, 1.0) * control.a;
    result.z += 1e-5;
    return result;
}

half3 TerrainLightmap(float2 uv, half3 normal)
{
#if defined(LIGHTMAP_ON)
#if defined(DIRLIGHTMAP_COMBINED)
    half4 lm = UNITY_SAMPLE_TEX2D(unity_Lightmap, uv);
    half4 lmd = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, uv);
    half3 result = DecodeLightmapRGBM(lm, unity_Lightmap_HDR);
    result = DecodeDirectionalLightmap(result, lmd, normal);
#else
    half4 lm = UNITY_SAMPLE_TEX2D(unity_Lightmap, uv);
    half3 result = DecodeLightmapRGBM(lm, unity_Lightmap_HDR);
#endif                
#else
    half3 result = UNITY_LIGHTMODEL_AMBIENT.rgb;
#endif
    return result;
}

fixed4 frag(v2f i) : COLOR
{
    fixed4 result = 0;

    // ...

    // compute model color and put it to the result

    // ... 

    // reconstruction of terrain fragment

    float2 splatUV0 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap0);
    float2 splatUV1 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap1);
    float2 splatUV2 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap2);
    float2 splatUV3 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap3);

    half4 control = tex2D(_TerrainAlphaMap, i.heightMapUV);
    half4 terrainColor = TerrainSplatMap(splatUV0, splatUV1, splatUV2, splatUV3, control);

    half3 terrainSurfaceNormal = TerrainNormal(i.heightMapUV);
    half3 terrainSurfaceTangent = cross(terrainSurfaceNormal, float3(0,0,1));
    half3 terrainSurfaceBitangent = cross(terrainSurfaceTangent, terrainSurfaceNormal);

    half3 terrainNormal = TerrainNormalMap(splatUV0, splatUV1, splatUV2, splatUV3, control);
    terrainNormal = terrainNormal.x * terrainSurfaceTangent + 
                    terrainNormal.y * terrainSurfaceBitangent + 
                    terrainNormal.z * terrainSurfaceNormal;
    
    half3 terrainLightmapColor = TerrainLightmap(i.heightMapUV, terrainNormal);
    terrainColor *= terrainLightmapColor;

    // blend model color & terrain color

    half height = TerrainHeight(i.heightMapUV);
    half deltaHeight = i.worldPos.y - height;
    half blendingWeight = smoothstep(BlendStart, BlendEnd, deltaHeight);

    result.rgb = lerp(result.rgb, terrainColor, blendingFactor);
       
    return result; 
}


Das Mischen mit der Rekonstruktion von Landschaftsfragmenten behebt also alle Probleme, die für das Tiefenmischen und das Mischen mit einer Höhenkarte, einschließlich Parallaxe, typisch sind.



Vermischung der Landschaftsgestaltung


Rekonstruktionsleistung von Geländefragmenten


An diesem Punkt ist es Zeit zu fragen, was diese Art von Kompromiss wert ist. Auf den ersten Blick übersteigt die Ressourcenintensität der Rekonstruktion von Landschaftsfragmenten die Ressourcenintensität der Alpha-Mischung bei weitem. Für die Rekonstruktion ist es notwendig, mit einem Dutzend zusätzlicher Leseoperationen aus dem Speicher durchzuführen. Für das Alpha-Blending benötigen Sie nur eine Leseoperation aus dem Speicher und eine Schreiboperation in den Speicher.

In Wirklichkeit hängt alles von den Funktionen der Hardwareplattform ab. Die Fragmentrekonstruktion wird durch Texturkomprimierung, Mip-Mapping, GPU-Kernverarbeitungsleistung und spezifische Hardware-Tiefenpipeline-Optimierungen (frühe Tiefenunterdrückung) begünstigt. Und gegen die Alpha-Mischung wird die oben bereits erwähnte Tatsache spielen, dass es der am wenigsten progressive Teil einer GPU ist.

Es gibt jedoch immer Raum für Optimierungen. Bei der Rekonstruktion der Farbe der Landschaft ist diese Rekonstruktion beispielsweise nur für einen schmalen Streifen von Fragmenten des 3D-Modells erforderlich, der sich nicht höher als eine bestimmte Höhe über der Oberfläche der Landschaft befindet.

Dynamische Verzweigungen in Shadern können zu schlecht vorhersehbaren Leistungsergebnissen führen. Es gibt jedoch zwei Punkte, die berücksichtigt werden sollten:

  1. Das Überspringen unnötiger Berechnungen beim Verzweigen durch eine Bedingung sollte durchgeführt werden, wenn diese Bedingung in einem wesentlichen Teil der Fälle nicht erfüllt ist.
  2. . , ( , ), GPU. ― (branch granularity), , , , , . , , . , GPU , , . , GPU, , 1 (PowerVR SGX).


Visualisierung unterschiedlicher Kohärenzgrade

Bei der Rekonstruktion von Fragmenten werden diese beiden Punkte berücksichtigt: Die Verzweigungsbedingung ermöglicht es in den meisten Fällen, die Implementierung ressourcenintensiver Operationen zur Rekonstruktion der Farbe der Landschaft abzuschneiden, und diese Bedingung ist kohärent, mit Ausnahme einer sehr kleinen Anzahl von Fragmenten (in der Abbildung sind dies Fragmente, die liegen an der Grenze zwischen den Zonen „rot“ und „grün“).


Kohärenz der Rekonstruktion von Landschaftsfragmenten Es

bleiben noch einige Anmerkungen zu dieser Mischmethode hinzuzufügen:

  1. Unity bietet nur dann alle erforderlichen Texturen, wenn für die Landschaft der Modus "Instanz zeichnen" aktiviert ist [20] . Andernfalls ist die normale Karte nicht verfügbar, sodass Sie die Landschaftsbeleuchtung für die Überblendung nicht korrekt rekonstruieren können.
  2. Unity API , (base map) . - .
  3. , API (, Metal 16 ).
  4. 3D- , Terrain, SRP.
  5. 3D- , 3D- .
  6. , «» , «» . , «» , . «» .






Bei der Gestaltung von 3D-Modellen kann die Vielfalt der Geländereliefs, mit denen diese Modelle verwendet werden sollen, nicht berücksichtigt werden. Oft müssen 3D-Modelle tief in der Landschaft „versenkt“ oder gedreht werden, um hervorstehende Teile zu verbergen, oder umgekehrt, um versteckte Teile anzuzeigen, die sichtbar sein sollten. Erwärmungsmodelle schränken ihre Anwendbarkeit ein. Wenn 3D-Modelle früher als die Landschaft gerendert werden, führt dies auch zu einem Überzeichnungseffekt. Die Kurve wiederum ist auch bei weitem nicht für alle 3D-Modelle geeignet (z. B. nicht für Häuser und Bäume).


Um die hervorstehenden Elemente des 3D-Modells auszublenden, muss es in der Landschaft „ertrinken“

Snapping ist ein Begriff, der Benutzern von Grafikeditoren bekannt ist. Dies ist eine Funktion, mit der Kontrollpunkte an den Knoten des räumlichen Gitters und in 3D-Editoren an den Flächen und Oberflächen anderer Objekte „haften“ können. Das Aufrufen der Karte der Höhen der Landschaft im Vertex-Shader kann die Gestaltung von Szenen erheblich vereinfachen.


3D-Modell ohne Einrasten. 3D-Modell mit Vertex-Snapping. 3D-Modell mit Scheitelpunktfang und -überblendung. 3D-Modell mit Vertex-Snapping, Blending und statischer Beleuchtung

Die Hauptschwierigkeit bei der Implementierung des Fangens besteht darin, dass Sie herausfinden müssen, welche Eckpunkte des 3D-Modells Sie an der Höhenkarte fangen müssen und welche nicht wert sind. Scheitelpunkte enthalten nur Informationen über die lokale Natur der Oberfläche (was nicht ausreicht) und enthalten keine Informationen über ihre Topologie (was erforderlich ist).

Wie in anderen Anwendungsfällen ist dieses Problem in der Modellierungsphase am einfachsten zu lösen, indem die erforderlichen Parameter direkt in den Scheitelpunkten implementiert werden. Als solchen Parameter sollten Sie ein intuitives Attribut auswählen - zum Beispiel den Gewichtungsfaktor für das Einrasten (und nicht den Abstand zum Rand einer offenen Oberfläche, wie wir es aus Gründen der Flexibilität wünschen).


Gewichtungscodierung für das Einrasten

Code anzeigen
// Per-vertex snapping with terrain heightmap

#include "UnityCG.cginc"

sampler2D_float TerrainHeightmapTexture;

float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;

struct v2f
{

   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;

   // ...

};

v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);
    float snappingWeight = v.color.r;                
    half height = TerrainHeight( o.heightMapUV );                
    o.worldPos.y = lerp( o.worldPos.y, height, snappingWeight );
    o.pos = UnityWorldToClipPos( half4( o.worldPos, 1 ) );

    // ...

    return o;
}


Die Anwendbarkeit des Vertex-Schnappens ist durch die allgemeine Entsprechung zwischen dem Gelände und der Oberfläche des 3D-Modells begrenzt. Um ihre signifikanten Unterschiede auszugleichen, müssen andere, ressourcenintensivere Methoden verwendet werden - beispielsweise 3D-Modelle mit Skinning.


Fazit


Die Hauptidee, die aus dem Artikel herausgenommen werden sollte: Jeder ausreichend komplexe und potenziell skalierbare Shader benötigt Basisdaten. Die Aufgabe des Entwicklers besteht darin, zu verstehen, wie das Grafiksystem bedient werden kann: Welche Daten werden bereitgestellt, wie können sie miteinander kombiniert werden und wie werden sie in Shadern verwendet.

Im allgemeinen Fall können wir den Schluss ziehen, dass die einzige Möglichkeit, den Rahmen zu überwinden, durch den die Möglichkeiten von Grafikeffekten begrenzt sind, darin besteht, die Ergebnisse verschiedener Shader zu kombinieren.


Verweise



All Articles