Früher wurden Wolken in Spielen mit gewöhnlichen 2D-Sprites gezeichnet, die immer in Richtung der Kamera gedreht werden. In den letzten Jahren können Sie mit neuen Grafikkartenmodellen physikalisch korrekte Wolken zeichnen, ohne dass Leistungseinbußen auftreten. Es wird angenommen, dass voluminöse Wolken im Spiel das Studio Guerrilla Games zusammen mit dem Spiel Horizon Zero Dawn brachten. Natürlich konnten solche Wolken früher gerendert werden, aber das Studio bildete so etwas wie einen Industriestandard für die Quellressourcen und die verwendeten Algorithmen, und jetzt entspricht jede Implementierung von volumetrischen Wolken irgendwie diesem Standard.

Der gesamte Prozess des Renderns von Clouds ist sehr gut in Phasen unterteilt, und es ist wichtig zu beachten, dass eine ungenaue Implementierung selbst bei einer von ihnen zu solchen Konsequenzen führen kann, dass nicht klar ist, wo der Fehler liegt und wie er behoben werden kann. Daher ist es ratsam, jedes Mal eine Kontrollschlussfolgerung über das Ergebnis zu ziehen.
Tonzuordnung, sRGB
Bevor Sie mit der Beleuchtung beginnen, müssen Sie zwei Dinge tun:
- Wenden Sie mindestens die einfachste Tonzuordnung an, bevor Sie das endgültige Bild auf dem Bildschirm anzeigen:
tunedColor=color/(1+color)
Dies ist notwendig, da die berechneten Farbwerte viel größer als Eins sind.
- Stellen Sie sicher, dass der endgültige Framebuffer, in den Sie zeichnen und der auf dem Bildschirm angezeigt wird, im sRGB-Format vorliegt. Wenn die Aktivierung des sRGB-Modus ein Problem darstellt, kann die Konvertierung manuell im Shader durchgeführt werden:
finalColor=pow(color, vec3(1.0/2.2))
Die Formel ist für die meisten Fälle geeignet, jedoch je nach Monitor nicht zu 100%. Es ist wichtig, dass die sRGB-Konvertierung immer zuletzt erfolgt.
Beleuchtungsmodell
Stellen Sie sich einen Raum vor, der mit teilweise transparenter Materie unterschiedlicher Dichte gefüllt ist. Wenn ein Lichtstrahl durch eine solche Substanz tritt, ist er vier Effekten ausgesetzt: Absorption, Streuung, Verstärkung der Streuung und Selbststrahlung. Letzteres tritt bei chemischen Prozessen in einem Stoff auf und ist hier nicht betroffen.
Angenommen, wir haben einen Lichtstrahl, der durch Materie von Punkt A nach Punkt B geht:
AbsorptionLicht, das durch eine Substanz fällt, wird von dieser Substanz absorbiert. Der nicht absorbierte Lichtanteil kann durch die Formel ermittelt werden:
wo

- das am Punkt nach der Absorption verbleibende Licht

.

- in einiger Entfernung auf das Segment AB zeigen

von A.
StreuungEin Teil des Lichts unter dem Einfluss von Materieteilchen ändert seine Richtung. Der Anteil des Lichts, der seine Richtung nicht geändert hat, kann durch die Formel ermittelt werden:
wo

- Lichtanteil, dessen Richtung sich nach der Streuung an einem Punkt nicht geändert hat

.
Absorption und Dispersion müssen kombiniert werden:
Funktion

Dämpfung oder Auslöschung genannt. Eine Funktion

- Übertragungsfunktion. Es zeigt, wie viel Licht beim Übergang von Punkt A nach Punkt B übrig bleibt.
Hinsichtlich

und

::

wobei C eine bestimmte Konstante ist, die für jeden Kanal in RGB einen anderen Wert haben kann,

Ist die Dichte des Mediums am Punkt

.
Lassen Sie uns nun die Aufgabe komplizieren. Licht bewegt sich von Punkt A nach Punkt B, es stirbt während der Bewegung aus. Am Punkt X wird ein Teil des Lichts in verschiedene Richtungen gestreut, eine der Richtungen entspricht dem Beobachter am Punkt O. Als nächstes bewegt sich ein Teil des gestreuten Lichts von Punkt X zu Punkt O und dämpft wieder. Der Weg des AXO-Lichts interessiert uns.
Den Lichtverlust beim Übergang von A nach X kennen wir:

, so wie wir den Lichtverlust von X nach O kennen - das

. Aber was ist mit dem Lichtanteil, der in Richtung des Betrachters gestreut wird?
AmplifikationsdispersionWenn bei gewöhnlicher Streuung die Lichtintensität abnimmt, nimmt sie bei verstärkter Streuung aufgrund der in benachbarten Bereichen aufgetretenen Lichtstreuung zu. Die Gesamtlichtmenge, die aus benachbarten Regionen kommt, kann durch die Formel ermittelt werden:
wo

bedeutet, das Integral über die Kugel zu nehmen,

- Phasenfunktion

- Licht aus der Richtung

.
Es ist ziemlich schwierig, Licht aus allen Richtungen zu berechnen. Wir wissen jedoch, dass der ursprüngliche Lichtanteil von unserem ursprünglichen AB-Strahl getragen wird. Die Formel kann stark vereinfacht werden:
wo

- der Winkel zwischen dem Lichtstrahl und dem Beobachterstrahl (d. H. Der Winkel AXO),

- der Anfangswert der Lichtintensität. Wenn wir alle oben genannten Punkte zusammenfassen, erhalten wir die Formel:
wo

- einfallendes Licht

- das Licht erreicht den Betrachter.
Wir erschweren die Aufgabe etwas mehr. Nehmen wir an, das Licht wird von einem gerichteten Licht emittiert, d.h. die Sonne:
Alles passiert genauso wie im vorherigen Fall, aber oft. Licht von Punkt A1 wird am Punkt X1 in Richtung des Beobachters am Punkt O gestreut, Licht von Punkt A2 wird am Punkt X2 in Richtung des Beobachters am Punkt O usw. gestreut. Wir sehen, dass das Licht, das den Betrachter erreicht, gleich der Summe ist:
Oder ein genauerer integraler Ausdruck:
Es ist wichtig, das hier zu verstehen

d.h. Das Segment ist in unendlich viele Abschnitte mit einer Länge von Null unterteilt.
Himmel
Mit einer leichten Vereinfachung wird ein Sonnenstrahl, der durch die Atmosphäre geht, nur gestreut, d.h.

.
Und nicht einmal eine Art von Streuung, sondern zwei: Rayleigh-Streuung und Mi-Streuung. Das erste wird durch Luftmoleküle verursacht, und das zweite wird durch ein Aerosol aus Wasser verursacht.
Die Gesamtdichte der Luft (oder des Aerosols), durch die ein Lichtstrahl von Punkt A nach Punkt B strömt:
wo

- Skalierungshöhe, h - aktuelle Höhe.
Eine einfache integrale Lösung wäre:
Dabei ist dh die Schrittgröße, mit der die Höhenprobe entnommen wird.
Schauen Sie sich nun die Abbildung an und verwenden Sie die im vorherigen Teil des „Beleuchtungsmodells“ abgeleitete Formel:
Der Beobachter schaut von O nach O '. Wir wollen alles Licht sammeln, das die Punkte X1, X2, ..., Xn erreicht, in ihnen gestreut wird und dann den Betrachter erreicht:
wo

die Intensität des von der Sonne emittierten Lichts,

- Höhe am Punkt

;; im Falle des Himmels die Konstante C, die in Funktion ist

bezeichnet als

.
Die Lösung des Integrals kann wie folgt sein:
Diese Formel gilt sowohl für Rayleigh-Streuung als auch für Mie-Streuung. Infolgedessen addieren sich die Lichtwerte für jede der Streuungen einfach:
Rayleigh-Dispersion

(enthält Werte für jeden RGB-Kanal)

Ergebnis:
Mi streuen

(Werte für alle RGB-Kanäle sind gleich)

Ergebnis:
Die Anzahl der Stichproben pro Segment

und auf dem Segment

Sie können 32 und höher nehmen. Der Erdradius beträgt 6371000 m, die Atmosphäre 100000 m.
Was tun mit all dem?
- In jedem Pixel des Bildschirms berechnen wir die Richtung des Beobachters V.
- Wir nehmen die Position des Beobachters O gleich {0, 6371000, 0} ein.
- Wir finden
als Ergebnis des Schnittpunkts des Strahls, der vom Punkt O ausgeht, und der Richtung von V und der Kugel, die am Punkt {0,0,0} und einem Radius von 6471000 zentriert ist - Liniensegment
in 32 gleich lange Abschnitte teilen - Für jeden Abschnitt berechnen wir die Rayleigh-Streuung und die Mie-Streuung und fügen alles hinzu. Darüber hinaus zu berechnen
Wir müssen auch das Segment aufteilen
Jeweils 32 gleiche Parzellen.
kann durch eine Variable gelesen werden, deren Wert mit jedem Schritt im Zyklus zunimmt.
Das Endergebnis:
Cloud-Modell
Wir werden verschiedene Arten von Rauschen in 3D benötigen. Das erste ist Perlins lauerndes fraktales Brown'sches Bewegungsgeräusch (fBm):
Ergebnis für ein 2D-Slice:
Das zweite ist Voronois Tarngeräusch.
Ergebnis für ein 2D-Slice:
Um Vorleys Tarn-fBm-Rauschen zu erhalten, müssen Sie Voronojs Tarn-fBm-Rauschen invertieren. Ich habe die Wertebereiche jedoch nach meinem Ermessen geringfügig geändert:
float fbmTiledWorley3(...) { return clamp((1.0-fbmTiledVoronoi3(...))*1.5-0.25, 0.0, 1.0); }
Das Ergebnis ähnelt sofort Wolkenstrukturen:
Für Wolken benötigen Sie zwei spezielle Texturen. Der erste hat eine Größe von 128 x 128 x 128 und ist für niederfrequentes Rauschen verantwortlich, der zweite hat eine Größe von 32 x 32 x 32 und ist für hochfrequentes Rauschen verantwortlich. Jede Textur verwendet nur einen Kanal im R8-Format. In einigen Beispielen werden 4 Kanäle von R8G8B8A8 für die erste Textur und drei Kanäle von R8G8B8 für die zweite Textur verwendet, und dann werden die Kanäle in einem Shader gemischt. Ich verstehe den Punkt nicht, da das Mischen im Voraus erfolgen kann, wodurch die Cache-Kohärenz stärker beeinträchtigt wird.
Zum Mischen und an einigen Stellen wird die Funktion remap () verwendet, mit der die Werte von einem Bereich zum anderen skaliert werden:
float remap(float value, float minValue, float maxValue, float newMinValue, float newMaxValue) { return newMinValue+(value-minValue)/(maxValue-minValue)*(newMaxValue-newMinValue); }
Beginnen wir mit der Vorbereitung der Textur mit niederfrequentem Rauschen:
R-Kanal - Perlins fBm-Rauschen
G-Kanal - gekacheltes fBm Vorley-Rauschen
B-Kanal - kleineres fBm Worley-Rauschen mit kleinerem Maßstab
A-Kanal - Varleys taylable fBm-Rauschen mit noch kleinerem Maßstab
Das Mischen erfolgt folgendermaßen:
finalValue=remap(noise.x, (noise.y * 0.625 + noise.z*0.25 + noise.w * 0.125)-1, 1, 0, 1)
Ergebnis für ein 2D-Slice:
Bereiten Sie nun die Textur mit hochfrequentem Rauschen vor:
R-Kanal - gekacheltes fBm Vorley-Rauschen
G-Kanal - kleiner skaliertes fBm Vorley-Rauschen
B-Kanal - Varley taylivaya fBm Rauschen mit noch kleinerem Maßstab
finalValue=noise.x * 0.625 + noise.y*0.25 + noise.z * 0.125;
Ergebnis für ein 2D-Slice:
Wir benötigen auch eine 2D-Textur-Wetterkarte, die das Vorhandensein, die Dichte und die Form von Wolken in Abhängigkeit von den Koordinaten des Raums bestimmt. Es wurde von Künstlern gemalt, um die Wolkendecke zu optimieren. Die Interpretation der Farbkanäle der Wetterkarte kann in der von mir geliehenen Version unterschiedlich sein:
R-Kanal - Wolkendecke in geringer Höhe
G-Kanal - Wolkendecke in großer Höhe
B-Kanal - maximale Wolkenhöhe
A-Kanal - Wolkendichte
Jetzt können wir eine Funktion erstellen, die die Dichte der Wolken in Abhängigkeit von den Koordinaten des 3D-Raums zurückgibt.
Am Eingang ein Punkt im Raum mit Koordinaten in km
vec3 position
Fügen Sie den Versatz sofort dem Wind hinzu
position.xz+=vec2(0.2f)*ufmParams.time;
Holen Sie sich die Wetterkartenwerte
vec4 weather=textureLod(ufmWeatherMap, position.xz/4096.0f, 0);
Wir erhalten den Prozentsatz der Höhe (von 0 bis 1)
float height=cloudGetHeight(position);
Fügen Sie eine kleine Abrundung der Wolken unten hinzu:
float SRb=clamp(remap(height, 0, 0.07, 0, 1), 0, 1);
Wir nehmen eine lineare Abnahme der Dichte auf 0 mit zunehmender Höhe gemäß dem B-Kanal der Wetterkarte vor:
float SRt=clamp(remap(height, weather.b*0.2, weather.b, 1, 0), 0, 1);
Kombinieren Sie das Ergebnis:
float SA=SRb*SRt;
Fügen Sie noch einmal die Rundung der Wolken unten hinzu:
float DRb=height*clamp(remap(height, 0, 0.15, 0, 1), 0, 1);
Fügen Sie auch die Rundung der Wolken oben hinzu:
float DRt=height*clamp(remap(height, 0.9, 1, 1, 0), 0, 1);
Wir kombinieren das Ergebnis, hier addieren wir den Einfluss der Dichte aus der Wetterkarte und den Einfluss der Dichte, der über gui eingestellt wird:
float DA=DRb*DRt*weather.a*2*ufmProperties.density;
Kombinieren Sie niederfrequentes und hochfrequentes Rauschen aus unseren Texturen:
float SNsample=textureLod(ufmLowFreqNoiseTexture, position/48.0f, 0).x*0.85f+textureLod(ufmHighFreqNoiseTexture, position/4.8f, 0).x*0.15f;
In allen Dokumenten, die ich lese, erfolgt die Zusammenführung auf andere Weise, aber diese Option hat mir gefallen.
Wir bestimmen den Umfang der Abdeckung (% des Himmels, der von Wolken besetzt ist), der über GUI eingestellt wird. Die R- und G-Kanäle der Wetterkarte werden ebenfalls verwendet:
float WMc=max(weather.r, clamp(ufmProperties.coverage-0.5, 0, 1)*weather.g*2);
Berechnen Sie die endgültige Dichte:
float d=clamp(remap(SNsample*SA, 1-ufmProperties.coverage*WMc, 1, 0, 1), 0, 1)*DA;
Ganze Funktion:
float cloudSampleDensity(vec3 position) { position.xz+=vec2(0.2f)*ufmParams.time; vec4 weather=textureLod(ufmWeatherMap, position.xz/4096.0f+vec2(0.2, 0.1), 0); float height=cloudGetHeight(position); float SRb=clamp(remap(height, 0, 0.07, 0, 1), 0, 1); float SRt=clamp(remap(height, weather.b*0.2, weather.b, 1, 0), 0, 1); float SA=SRb*SRt; float DRb=height*clamp(remap(height, 0, 0.15, 0, 1), 0, 1); float DRt=height*clamp(remap(height, 0.9, 1, 1, 0), 0, 1); float DA=DRb*DRt*weather.a*2*ufmProperties.density; float SNsample=textureLod(ufmLowFreqNoiseTexture, position/48.0f, 0).x*0.85f+textureLod(ufmHighFreqNoiseTexture, position/4.8f, 0).x*0.15f; float WMc=max(weather.r, clamp(ufmProperties.coverage-0.5, 0, 1)*weather.g*2); float d=clamp(remap(SNsample*SA, 1-ufmProperties.coverage*WMc, 1, 0, 1), 0, 1)*DA; return d; }
Was genau diese Funktion sein sollte, ist eine offene Frage, denn wenn Sie die Gesetze ignorieren, denen die Wolken beim Einstellen von Parametern folgen, können Sie ein sehr ungewöhnliches und schönes Ergebnis erzielen. Es hängt alles von der Anwendung ab.
Integration
Die Erdatmosphäre ist in zwei Schichten unterteilt: innere und äußere, zwischen denen sich Wolken befinden können. Diese Schichten können durch Kugeln, aber auch durch Ebenen dargestellt werden. Ich ließ mich auf den Kugeln nieder. Für die erste Schicht habe ich den Kugelradius von 6415 km genommen, für die zweite Schicht den Radius von 6435 km. Der Radius der Erde ist auf 6400 km gerundet. Einige Parameter hängen von der bedingten Dicke des „wolkigen“ Teils der Atmosphäre (20 km) ab.
Im Gegensatz zum Himmel sind Wolken undurchsichtig, und für die Integration muss nicht nur die Farbe, sondern auch der Wert für den Alphakanal ermittelt werden. Zuerst benötigen Sie eine Funktion, die die Gesamtdichte der Wolke zurückgibt, durch die ein Lichtstrahl der Sonne fällt.
Niemand macht darauf aufmerksam, aber die Praxis hat gezeigt, dass es überhaupt nicht notwendig ist, den gesamten Weg des Strahls zu berücksichtigen, sondern nur die extremste Lücke. Wir gehen davon aus, dass Wolken über einem abgeschnittenen Segment überhaupt nicht existieren.
Darüber hinaus ist die Anzahl der Dichteproben, die ohne Beeinträchtigung der Leistung durchgeführt werden können, sehr begrenzt. Guerilla-Spiele machen 6. Darüber hinaus sagte der Entwickler in einer der Präsentationen, dass sie diese Proben innerhalb des Kegels streuen, und die letzte Probe wurde speziell sehr weit vom Rest entfernt hergestellt, um so viel Platz wie möglich abzudecken. Die resultierenden Ungenauigkeiten und Rauschen werden immer noch vor dem Hintergrund benachbarter Proben geglättet, was zu einer erhöhten Genauigkeit führt.
Am Ende habe ich mich für 4 Proben entschieden, die auf derselben Linie liegen, aber letztere wird mit einem um das 6-fache erhöhten Schritt genommen. Die Schrittweite beträgt 20 km * 0,01, was 200 m entspricht.
Die Funktion ist ziemlich einfach:
float cloudSampleDirectDensity(vec3 position, vec3 sunDir) {
Jetzt können Sie zum schwierigeren Teil übergehen. Wir bestimmen den Beobachter auf der Erdoberfläche am Punkt {0, 6400,0} und finden den Schnittpunkt des Beobachtungsstrahls mit einer Kugel mit einem Radius von 6415 km und einem Mittelpunkt {0,0,0} - wir erhalten den Startpunkt S.
Unten ist die Basisversion der Funktion:
vec4 mainMarching(vec3 viewDir, vec3 sunDir) { vec3 position; crossRaySphereOutFar(vec3(0.0, 6400.0, 0.0), viewDir, vec3(0.0), 6415.0, position); float avrStep=(6435.0-6415.0)/64.0; for(int i=0;i<128;i++) { position+=viewDir*step; if(length(position)>6435.0) break; } return vec4(0.0); }
Die Schrittgröße ist definiert als 20 km / 64. d.h. Bei der streng vertikalen Richtung des Beobachterstrahls werden 64 Proben erstellt. Wenn diese Richtung jedoch horizontaler ist, sind die Abtastwerte etwas größer, sodass der Zyklus nicht 64 Schritte umfasst, sondern 128 mit einem Rand.
Zu Beginn nehmen wir an, dass die endgültige Farbe Schwarz und die Transparenz Einheit ist. Mit jedem Schritt erhöhen wir den Farbwert und verringern den Transparenzwert. Wenn die Transparenz nahe bei 0 liegt, können Sie die Schleife vorab beenden:
vec3 color=vec3(0.0); float transmittance=1.0; …
ufmProperties.attenuation - Es gibt nur C in

und ufmProperties.attenuation2 ist C in

. ufmProperties.sunIntensity - die Strahlungsintensität der Sonne. sunColor - die Farbe der Sonne.
Ergebnis:
Ein Fehler ist sofort erkennbar - starke Schattierung. Aber jetzt werden wir den Mangel an verstärkter Beleuchtung in der Nähe der Sonne korrigieren. Es ist passiert, weil wir keine Phasenfunktion hinzugefügt haben. Um die Streuung des durch die Wolken hindurchtretenden Lichts zu berechnen, wird die Phasenfunktion von Hengy-Greenstein verwendet, die es 1941 für ähnliche Berechnungen in Gasclustern im Weltraum öffnete:

Hier sollte ein Exkurs gemacht werden. Nach dem kanonischen Beleuchtungsmodell sollte die Phasenfunktion eins sein. In der Realität ist das erzielte Ergebnis jedoch für niemanden geeignet, und jeder verwendet zwei Phasenfunktionen und kombiniert sogar seine Werte auf besondere Weise. Ich habe mich auch auf zwei Phasenfunktionen konzentriert, aber ich addiere einfach ihre Werte. Die erste Phasenfunktion hat g nahe 1 und ermöglicht es Ihnen, helles Licht in der Nähe der Sonne zu erzeugen. Die Funktion der zweiten Phase hat g nahe 0,5 und ermöglicht es Ihnen, die Beleuchtung in der gesamten Himmelskugel allmählich zu verringern.
Aktualisierter Code:
ufmProperties.eccentrisy, ufmProperties.eccentrisy2 sind g-Werte
Ergebnis:
Jetzt können Sie den Kampf mit zu viel Schattierung beginnen. Es ist vorhanden, weil wir das Licht der umgebenden Wolken und des Himmels, das im wirklichen Leben ist, nicht berücksichtigt haben.
Ich habe dieses Problem folgendermaßen gelöst:
return vec4(color+ambientColor*ufmProperties.ambient, 1.0-transmittance);
Während ambientColor die Farbe des Himmels in Richtung des Beobachtungsstrahls ist, ist ufmProperties.ambient der Abstimmungsparameter.
Ergebnis:
Es bleibt das letzte Problem zu lösen. Im wirklichen Leben sehen wir einen bestimmten Nebel oder Dunst, der es uns nicht erlaubt, sehr weit entfernte Objekte zu sehen, je horizontaler die Sicht gehalten wird. Dies muss sich auch im Code widerspiegeln. Ich nahm den üblichen Kosinus des Blickwinkels und der Exponentialfunktion. Darauf basierend wird ein bestimmter Mischungskoeffizient berechnet, der eine lineare Interpolation zwischen der resultierenden Farbe und der Hintergrundfarbe ermöglicht.
float blending=1.0-exp(-max(0.0, dot(viewDir, vec3(0.0,1.0,0.0)))*ufmProperties.fog); blending=blending*blending*blending; return vec4(mix(ambientColor, color+ambientColor*ufmProperties.ambient, blending), 1.0-transmittance);
ufmProperties.fog - zur manuellen Konfiguration.
Zusammenfassungsfunktion:
vec4 mainMarching(vec3 viewDir, vec3 sunDir, vec3 sunColor, vec3 ambientColor) { vec3 position; crossRaySphereOutFar(vec3(0.0, 6400.0, 0.0), viewDir, vec3(0.0), 6415.0, position); float avrStep=(6435.0-6415.0)/64.0; vec3 color=vec3(0.0); float transmittance=1.0; for(int i=0;i<128;i++) { float density=cloudSampleDensity(position)*avrStep; if(density>0.0) { float sunDensity=cloudSampleDirectDensity(position, sunDir); float mu=max(0.0, dot(viewDir, sunDir)); float m11=ufmProperties.phaseInfluence*cloudPhaseFunction(mu, ufmProperties.eccentrisy); float m12=ufmProperties.phaseInfluence2*cloudPhaseFunction(mu, ufmProperties.eccentrisy2); float m2=exp(-ufmProperties.attenuation*sunDensity); float m3=ufmProperties.attenuation2*density; float light=ufmProperties.sunIntensity*(m11+m12)*m2*m3; color+=sunColor*light*transmittance; transmittance*=exp(-ufmProperties.attenuation*density); } position+=viewDir*avrStep; if(transmittance<0.05 || length(position)>6435.0) break; } float blending=1.0-exp(-max(0.0, dot(viewDir, vec3(0.0,1.0,0.0)))*ufmProperties.fog); blending=blending*blending*blending; return vec4(mix(ambientColor, color+ambientColor*ufmProperties.ambient, blending), 1.0-transmittance); }
Demo-Video:
Optimierung und mögliche Verbesserungen
Nach der Implementierung des grundlegenden Rendering-Algorithmus besteht das nächste Problem darin, dass er zu langsam arbeitet. Meine Version produzierte 25 fps in voller HD auf der Radeon RX 480. Die beiden folgenden Ansätze zur Lösung des Problems wurden von Guerilla Games selbst vorgeschlagen.
Wir zeichnen, was wirklich sichtbar istDer Bildschirm ist in Kacheln mit einer Größe von 16 x 16 Pixel unterteilt. Zunächst wird die übliche 3D-Umgebung gezeichnet. Es stellt sich heraus, dass der größte Teil des Himmels von Bergen oder großen Objekten bedeckt ist. Dementsprechend müssen Sie die Berechnung nur in den Kacheln durchführen, in denen die Wolken durch nichts blockiert sind.
ReprojektionWenn die Kamera stationär ist, stellt sich heraus, dass die Wolken im Allgemeinen nicht aktualisiert werden können. Wenn sich die Kamera jedoch bewegt hat, bedeutet dies nicht, dass wir den gesamten Bildschirm aktualisieren müssen. Alles ist bereits gezeichnet, Sie müssen nur das Bild gemäß den neuen Koordinaten neu erstellen. Das Finden alter Koordinaten auf neuen durch die Projektions- und Ansichtsmatrizen des aktuellen und des vorherigen Frames wird als Projektion bezeichnet. Bei einer Kameraverschiebung übertragen wir also einfach die Farben gemäß den neuen Koordinaten. In Fällen, in denen diese Koordinaten außerhalb des Bildschirms angezeigt werden, müssen die Wolken ehrlich neu gezeichnet werden.
TeilaktualisierungDie Idee der Neuprojektion gefällt mir nicht, da sich bei einer scharfen Drehung der Kamera möglicherweise herausstellt, dass die Wolken für ein Drittel des Bildschirms gerendert werden müssen, was zu Verzögerungen führen kann. Ich weiß nicht, wie Guerilla-Spiele damit umgegangen sind, aber zumindest in Horizon Zero Dawn bewegt sich die Kamera bei der Steuerung des Joysticks reibungslos und es gibt keine Probleme mit scharfen Sprüngen. Daher habe ich als Experiment meinen eigenen Ansatz entwickelt. Wolken werden in einer kubischen Karte in 5 Gesichtern gezeichnet, weil Der Boden interessiert uns nicht. , ⅔ . 88. 64 . , .. , . radeon rx 480 500 fps full hd 330 fps opengl. Radeon hd 5700 series 109 fps full hd opengl (vulkan ).
mip-mip- , , mip-.
- - Guerrilla Games 3D , , 2D .
Curl-curl-. , .
, , . , . .
.
, , . , . Frostbite.
Nützliche Links
Guerrilla Gamesd1z4o56rleaq4j.cloudfront.net/downloads/assets/Nubis-Authoring-Realtime-Volumetric-Cloudscapes-with-the-Decima-Engine-Final.pdf?mtime=20170807141817killzone.dl.playstation.net/killzone/horizonzerodawn/presentations/Siggraph15_Schneider_Real-Time_Volumetric_Cloudscapes_of_Horizon_Zero_Dawn.pdfwww.youtube.com/watch?v=-d8qT5-1LOIGPU Pro 7vk.com/doc179245989_437393482?hash=a9af5f665eda4edf58&dl=806d4dbdac0f7a761cwww.scratchapixel.com/lessons/procedural-generation-virtual-worlds/simulating-sky/simulating-colors-of-the-skyFrostbitemedia.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016-pbs-frostbite-sky-clouds-new.pdfwww.shadertoy.com/view/XlBSRz