Verwendung eines SubViewports als Textur
Einführung
In diesem Tutorial lernen Sie die Verwendung von SubViewport als Textur kennen, die auf 3D-Objekte angewendet werden kann. Zu diesem Zweck erstellen wir einen prozeduralen Planeten wie dem folgenden hier:
Bemerkung
Dieses Tutorial behandelt nicht, wie man eine dynamische Atmosphäre, wie die dieses Planeten, programmiert.
Dieses Tutorial setzt voraus, dass Sie mit der Einrichtung einer grundlegenden Szene vertraut sind, einschließlich: einer Camera3D, einer Lichtquelle, einer MeshInstance3D mit einem Primitive Mesh, und der Anwendung eines StandardMaterial3D auf das Mesh. Der Schwerpunkt liegt auf der Verwendung des SubViewport zur dynamischen Erstellung von Texturen, die auf das Mesh angewendet werden können.
In diesem Tutorial werden wir die folgenden Themen behandeln:
Verwendung von SubViewport als Rendertextur
Abbilden einer Textur auf eine Kugel mit gleichwinkliger Zuordnung
Fragment-Shader-Techniken für prozedurale Planeten
Festlegen einer Rauheitskarte aus einer Viewport-Textur
Die Szene einrichten
Erstellen Sie eine neue Szene und fügen Sie die folgenden Nodes genau wie unten gezeigt hinzu.
Gehen Sie in die MeshInstance3D und machen Sie das Mesh zu einem SphereMesh
Einrichten des SubViewports
Klicken Sie auf den Node SubViewport und setzen Sie seine Größe auf (1024, 512). Der SubViewport kann eigentlich jede Größe haben, solange die Breite doppelt so groß ist wie die Höhe. Die Breite muss doppelt so groß sein wie die Höhe, damit das Bild genau auf die Kugel abgebildet werden kann, da wir eine gleichwinklige Projektion verwenden werden, aber dazu später mehr.
Als nächstes deaktivieren Sie 3D. Wir werden ein ColorRect zum Rendern der Oberfläche verwenden, also brauchen wir auch kein 3D.
Wählen Sie den ColorRect und setzen Sie im Inspektor die Anker-Vorgabe auf Full Rect. Dadurch wird sichergestellt, dass der ColorRect den gesamten SubViewport einnimmt.
Als nächstes fügen wir ein Shader Material zum ColorRect hinzu (ColorRect > CanvasItem > Material > Material > Neu: ShaderMaterial).
Bemerkung
Grundlegende Vertrautheit mit Shading wird für dieses Tutorial empfohlen. Aber auch wenn Sie keine Erfahrung mit Shadern haben, wird der gesamte Code zur Verfügung gestellt, so dass Sie keine Probleme haben sollten, diesem Tutorial zu folgen.
Klicken Sie auf den Button tfläche des Dropdown-Menüs für das Shader-Material und klicken Sie auf / Edit. Von hier aus gehen Sie zu Shader > Neuer Shader. Geben Sie ihm einen Namen und klicken Sie auf "Erstellen". Klicken Sie auf den Shader im Inspektor, um den Shader-Editor zu öffnen. Löschen Sie den Default-Code und fügen Sie den folgenden hinzu:
shader_type canvas_item;
void fragment() {
COLOR = vec4(UV.x, UV.y, 0.5, 1.0);
}
Wenn Sie den Shader-Code speichern, werden Sie im Inspektor sehen, dass der obige Code einen Farbverlauf wie unten gerendert hat.
Jetzt haben wir die Grundlagen für einen SubViewport, den wir rendern, und wir haben ein eindeutiges Bild, das wir auf die Kugel anwenden können.
Anwenden der Textur
Jetzt gehen Sie in die MeshInstance3D und fügen ein StandardMaterial3D dazu. Es besteht keine Notwendigkeit für ein spezielles Shader Material (obwohl das eine gute Idee für fortgeschrittene Effekte wäre, wie die Atmosphäre im obigen Beispiel).
MeshInstance3D > GeometryInstance > Geometrie > Material-Überschreibung > Neu: StandardMaterial3D
Klicken Sie dann auf das Dropdown-Menü für das StandardMaterial3D und klicken Sie auf "Bearbeiten"
Gehen Sie zum Abschnitt "Resource" und setzen Sie ein Häkchen bei Local to scene. Gehen Sie dann zum Abschnitt "Albedo" und klicken Sie neben der Property "Textur", um eine Albedo-Textur hinzuzufügen. Hier werden wir die von uns erstellte Textur anwenden. Wählen Sie "Neu: ViewportTexture"
Klicken Sie im Inspektor auf die gerade erstellte ViewportTexture und dann auf "Zuweisen". Wählen Sie dann aus dem Menü, das sich öffnet, den Viewport aus, in den wir zuvor gerendert haben.
Ihre Kugel sollte jetzt mit den Farben eingefärbt sein, die wir im Viewport gerendert haben.
Haben Sie die hässliche Naht bemerkt, die sich an der Stelle bildet, an der die Textur um die Textur gewickelt wird? Das liegt daran, dass wir eine Farbe auf der Grundlage von UV-Koordinaten auswählen, und UV-Koordinaten lassen sich nicht um die Textur herumführen. Dies ist ein klassisches Problem bei der 2D-Kartenprojektion. Spieleentwickler haben oft eine 2-dimensionale Karte, die sie auf eine Kugel projizieren wollen, aber wenn sie sich um die Kugel wickelt, entstehen große Nähte. Es gibt eine elegante Lösung für dieses Problem, die wir im nächsten Abschnitt erläutern werden.
Erzeugen der Planetentextur
Wenn wir nun auf unser SubViewport rendern, erscheint es wie von Zauberhand auf der Kugel. Aber es gibt eine hässliche Naht, die durch unsere Texturkoordinaten entsteht. Wie bekommen wir also einen Bereich von Koordinaten, der die Kugel auf schöne Weise umhüllt? Eine Lösung besteht darin, eine Funktion zu verwenden, die sich auf dem Bereich unserer Textur wiederholt. sin und cos sind zwei solcher Funktionen. Wenden wir sie auf die Textur an und sehen wir, was passiert. Wenden wir sie auf die Textur an und sehen wir, was passiert. Ersetzen Sie den bestehenden Farbcode im Shader durch den folgenden:
COLOR.xyz = vec3(sin(UV.x * 3.14159 * 4.0) * cos(UV.y * 3.14159 * 4.0) * 0.5 + 0.5);
Gar nicht so schlecht. Wenn Sie sich umsehen, können Sie sehen, dass die Naht jetzt verschwunden ist, aber an ihrer Stelle haben wir eine Quetschung an den Polen. Das liegt an der Art und Weise, wie Godot in seinem StandardMaterial3D Texturen auf Kugeln abbildet. Es verwendet eine Projektionstechnik namens gleichwinklige Projektion, die eine sphärische Karte auf eine 2D-Ebene überträgt.
Bemerkung
Wenn Sie sich für ein paar zusätzliche Informationen über die Methode interessieren, werden wir von sphärischen Koordinaten in kartesische Koordinaten umrechnen. Sphärische Koordinaten bilden die Längen- und Breitengrade der Kugel ab, während kartesische Koordinaten im Grunde ein Vektor vom Mittelpunkt der Kugel zum aktuellen Punkt sind.
Für jedes Pixel wird seine 3D-Position auf der Kugel berechnet. Daraus ermitteln wir mit Hilfe von 3D-Rauschen einen Farbwert. Indem wir das Rauschen in 3D berechnen, lösen wir das Problem des Quetschens an den Polen. Um zu verstehen, warum das so ist, stellen Sie sich vor, dass das Rauschen über die Oberfläche der Kugel und nicht über die 2D-Ebene berechnet wird. Wenn man über die Oberfläche der Kugel rechnet, stößt man nie auf eine Kante und erzeugt somit auch keine Naht oder einen Quetschungspunkt am Pol. Der folgende Code wandelt die UVs in kartesische Koordinaten um.
float theta = UV.y * 3.14159;
float phi = UV.x * 3.14159 * 2.0;
vec3 unit = vec3(0.0, 0.0, 0.0);
unit.x = sin(phi) * sin(theta);
unit.y = cos(theta) * -1.0;
unit.z = cos(phi) * sin(theta);
unit = normalize(unit);
Und wenn wir unit als Ausgabe-COLOR-Wert verwenden, erhalten wir:
Da wir nun die 3D-Position der Kugeloberfläche berechnen können, können wir 3D-Rauschen verwenden, um den Planeten zu erstellen. Wir werden diese Rauschfunktion direkt aus einem Shadertoy verwenden:
vec3 hash(vec3 p) {
p = vec3(dot(p, vec3(127.1, 311.7, 74.7)),
dot(p, vec3(269.5, 183.3, 246.1)),
dot(p, vec3(113.5, 271.9, 124.6)));
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
return mix(mix(mix(dot(hash(i + vec3(0.0, 0.0, 0.0)), f - vec3(0.0, 0.0, 0.0)),
dot(hash(i + vec3(1.0, 0.0, 0.0)), f - vec3(1.0, 0.0, 0.0)), u.x),
mix(dot(hash(i + vec3(0.0, 1.0, 0.0)), f - vec3(0.0, 1.0, 0.0)),
dot(hash(i + vec3(1.0, 1.0, 0.0)), f - vec3(1.0, 1.0, 0.0)), u.x), u.y),
mix(mix(dot(hash(i + vec3(0.0, 0.0, 1.0)), f - vec3(0.0, 0.0, 1.0)),
dot(hash(i + vec3(1.0, 0.0, 1.0)), f - vec3(1.0, 0.0, 1.0)), u.x),
mix(dot(hash(i + vec3(0.0, 1.0, 1.0)), f - vec3(0.0, 1.0, 1.0)),
dot(hash(i + vec3(1.0, 1.0, 1.0)), f - vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z );
}
Bemerkung
Alle Anerkennung gebührt dem Autor Inigo Quilez. Es wird unter der MIT-Lizenz veröffentlicht.
Um nun Rauschen zu verwenden, fügen Sie der Funktion Fragment folgendes hinzu:
float n = noise(unit * 5.0);
COLOR.xyz = vec3(n * 0.5 + 0.5);
Bemerkung
Um die Textur hervorzuheben, setzen wir das Material auf unschattiert.
Sie können jetzt sehen, dass das Rauschen tatsächlich nahtlos um die Kugel herumgeht. Allerdings sieht das nicht aus wie der Planet, der Ihnen versprochen wurde. Gehen wir also zu etwas Bunterem über.
Einfärben des Planeten
Nun zum Einfärben des Planeten. Es gibt zwar viele Möglichkeiten, dies zu tun, aber für den Moment werden wir bei einem Farbverlauf zwischen Wasser und Land bleiben.
Um einen Gradienten in GLSL zu erzeugen, benutzen wir die Funktion mix. Die Funktion mix nimmt zwei Werte, zwischen denen interpoliert werden soll, und ein drittes Argument, um festzulegen, wie stark zwischen ihnen interpoliert werden soll; im Wesentlichen mischt sie die beiden Werte zusammen. In anderen APIs wird diese Funktion oft lerp genannt. Allerdings ist lerp typischerweise für das Mischen von zwei Float-Zahlen reserviert; mix kann beliebige Werte annehmen, seien es Float-Zahlen oder Vektortypen.
COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), n * 0.5 + 0.5);
Die erste Farbe ist blau für den Ozean. Die zweite Farbe ist eine Art rötliche Farbe (weil alle außerirdischen Planeten rotes Terrain brauchen). Und schließlich werden sie durch n * 0,5 + 0,5 zusammengemischt. n variiert gleichmäßig zwischen -1 und 1. Also übertragen wir es auf den Bereich 0-1, den mix erwartet. Jetzt kann man sehen, dass die Farben zwischen blau und rot wechseln.
Das ist ein bisschen unschärfer, als wir wollen. Planeten haben normalerweise eine relativ klare Trennung zwischen Land und Meer. Um das zu erreichen, ändern wir den letzten Term in smoothstep(-0.1, 0.0, n). Und so wird die ganze Zeile zu:
COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), smoothstep(-0.1, 0.0, n));
Was smoothstep macht, ist 0 zurückzugeben, wenn das dritte Argument kleiner als das erste ist und 1, wenn das dritte Argument größer als das zweite ist. Es gibt einen gleitenden Wert zwischen 0 und 1 zurück, wenn die dritte Zahl zwischen dem ersten und dem zweiten liegt. In dieser Zeile liefert smoothstep also 0, wenn n kleiner als -0.1 ist und 1, wenn n größer als 0 ist.
Noch eine Sache, um das Ganze ein wenig planetarer zu machen. Das Land sollte nicht so klumpig sein; wir sollten die Kanten ein wenig rauer gestalten. Ein Trick, der oft in Shadern verwendet wird, um rau aussehendes Terrain mit Rauschen zu erzeugen, ist das Übereinanderschichten von Rauschebenen mit verschiedenen Frequenzen. Wir verwenden eine Ebene, um die allgemeine Klecksstruktur der Kontinente zu erzeugen. Eine weitere Ebene bricht die Kanten ein wenig auf, dann eine weitere und so weiter. Wir werden n mit vier Zeilen Shader-Code berechnen, anstatt nur mit einer. Aus n wird:
float n = noise(unit * 5.0) * 0.5;
n += noise(unit * 10.0) * 0.25;
n += noise(unit * 20.0) * 0.125;
n += noise(unit * 40.0) * 0.0625;
Nun sieht der Planet so aus:
Erzeugen eines Ozeans
Eine letzte Sache, damit es mehr wie ein Planet aussieht. Der Ozean und das Land reflektieren das Licht unterschiedlich. Wir wollen also, dass der Ozean ein wenig mehr leuchtet als das Land. Wir können dies erreichen, indem wir einen vierten Wert in den Alpha-Kanal unserer Ausgabe COLOR eingeben und ihn als Rauheitskarte verwenden.
COLOR.a = 0.3 + 0.7 * smoothstep(-0.1, 0.0, n);
Diese Zeile liefert 0.3 für Wasser und 1.0 für Land. Das bedeutet, dass das Land ziemlich rau sein wird, während das Wasser ziemlich glatt sein wird.
Dann stellen Sie im Material unter dem Abschnitt "Metallic" sicher, daß Metallic auf 0 und Specular auf 1 gesetzt ist. Der Grund dafür ist, daß das Wasser das Licht sehr gut reflektiert, aber nicht metallisch ist. Diese Werte sind nicht physikalisch genau, aber für diese Demo sind sie gut genug.
Als nächstes setzen Sie unter dem Abschnitt "Rauheit" die Rauheitstextur auf eine Viewport Texture, die auf unsere Planetentextur SubViewport zeigt. Schließlich setzen wir den Texturkanal auf Alpha. Dies weist den Renderer an, den Alpha-Kanal unserer Ausgabe COLOR als Rauheits-Wert zu verwenden.
Sie werden feststellen, dass sich nur sehr wenig ändert, außer dass der Planet den Himmel nicht mehr spiegelt. Dies geschieht, weil standardmäßig, wenn etwas mit einem Alpha-Wert gerendert wird, es als transparentes Objekt über den Hintergrund gezeichnet wird. Und da der Default-Hintergrund des SubViewport undurchsichtig ist, hat der Alpha-Kanal der Viewport Texture den Wert 1, was dazu führt, dass die Planetentextur überall mit etwas blasseren Farben und einem Rauheits-Wert von 1 gezeichnet wird. Um dies zu korrigieren, gehen wir in den SubViewport und aktivieren die "Transparent Bg"-Property. Da wir nun ein transparentes Objekt über ein anderes legen, müssen wir blend_premul_alpha aktivieren:
render_mode blend_premul_alpha;
Dadurch werden die Farben mit dem Alpha-Wert vormultipliziert und dann korrekt zusammengemischt. Wenn man eine transparente Farbe über eine andere mischt, selbst wenn der Hintergrund einen Alpha-Wert von 0 hat (wie in diesem Fall), kommt es normalerweise zu seltsamen Farbverläufen. Die Einstellung blend_premul_alpha behebt das.
Jetzt sollte der Planet so aussehen, als ob er das Licht auf dem Ozean, aber nicht auf dem Land reflektiert. Bewegen Sie das OmniLight3D in der Szene, damit Sie den Effekt der Reflexionen auf dem Ozean sehen können.
Und da haben Sie es. Ein prozeduraler Planet, der unter Verwendung eines SubViewport generiert wurde.