Die erste Version des Flugsimulators wurde 1980 für den Apple II ausgeliefert und war erstaunlicherweise in 3D! Das war eine bemerkenswerte Leistung. Umso erstaunlicher ist es, wenn man bedenkt, dass alles 3D von Hand gemacht wurde, das Ergebnis akribischer Berechnungen und Low-Level-Pixelbefehle. Als Bruce Atwick die frühen Versionen von Flight Simulator in Angriff nahm, gab es nicht nur keine 3D-Frameworks, sondern überhaupt keine Frameworks! Diese Versionen des Spiels wurden größtenteils in Assembler geschrieben, nur einen Schritt von Einsen und Nullen entfernt, die durch eine CPU fließen.
Als wir uns vorgenommen haben, Flight Simulator (oder Flight Arcade, wie wir es nennen), für das Web zu reimagieren und zu zeigen, was im neuen Microsoft Edge-Browser und EdgeHTML-Rendering-Modul möglich ist, müssen wir uns über den Kontrast der 3D-Erstellung Gedanken machen und jetzt alter Flight Sim, neuer Flight Sim, alter Internet Explorer, neuer Microsoft Edge. Moderne Codierung scheint fast luxuriös zu sein, da wir 3D-Welten in WebGL mit großartigen Frameworks wie Babylon.js modellieren. Dadurch können wir uns auf sehr wichtige Probleme konzentrieren.
In diesem Artikel werde ich unsere Herangehensweise an eine dieser lustigen Herausforderungen erläutern: eine einfache Möglichkeit, realistisch aussehendes großräumiges Gelände zu schaffen.
Hinweis: Interaktiver Code und Beispiele für diesen Artikel finden Sie auch in Flight Arcade / Learn.
Die meisten 3D-Objekte werden aus guten Gründen mit Modellierungswerkzeugen erstellt. Das Erstellen komplexer Objekte (wie ein Flugzeug oder sogar ein Gebäude) ist im Code schwer zu erreichen. Modellierwerkzeuge sind fast immer sinnvoll, aber es gibt Ausnahmen! Einer dieser Fälle könnte die sanften Hügel der Insel Flight Arcade sein. Am Ende benutzten wir eine Technik, die wir als einfacher und möglicherweise noch intuitiver empfanden: eine Höhenkarte.
Eine Höhenkarte ist eine Möglichkeit, ein reguläres zweidimensionales Bild zu verwenden, um das Höhenrelief einer Oberfläche wie einer Insel oder einem anderen Gelände zu beschreiben. Es ist ein ziemlich üblicher Weg, mit Höhendaten zu arbeiten, nicht nur in Spielen, sondern auch in geographischen Informationssystemen (GIS), die von Kartographen und Geologen verwendet werden.
Um Ihnen eine Vorstellung davon zu vermitteln, wie dies funktioniert, lesen Sie die Höhenkarte in dieser interaktiven Demo. Versuchen Sie, im Bildeditor zu zeichnen, und überprüfen Sie das resultierende Terrain.
Das Konzept hinter einer Höhenkarte ist ziemlich einfach. In einem Bild wie dem obigen ist reines Schwarz der „Boden“ und reines Weiß der höchste Gipfel. Die Graustufenfarben dazwischen repräsentieren entsprechende Höhen. Das gibt uns 256 Höhenlagen, was für unser Spiel sehr detailliert ist. In realen Anwendungen kann das gesamte Farbspektrum verwendet werden, um deutlich mehr Detailebenen zu speichern (256)4 = 4.294.967.296 Detailstufen, wenn Sie einen Alphakanal einschließen).
Eine Höhenkarte hat einige Vorteile gegenüber einem herkömmlichen polygonalen Netz:
Erstens sind Heightmaps viel kompakter. Es werden nur die wichtigsten Daten (die Höhe) gespeichert. Es muss programmgesteuert in ein 3D-Objekt umgewandelt werden, aber dies ist der klassische Handel: Sie sparen jetzt Platz und bezahlen später mit der Berechnung. Durch das Speichern der Daten als Bild erhalten Sie einen weiteren Platzvorteil: Sie können standardmäßige Bildkomprimierungstechniken nutzen und die Daten klein machen (zum Vergleich).!
Zweitens sind Heightmaps eine bequeme Möglichkeit, Terrain zu generieren, zu visualisieren und zu bearbeiten. Es ist ziemlich intuitiv, wenn Sie eine sehen. Es fühlt sich ein bisschen wie eine Karte an. Dies erwies sich für Flight Arcade als besonders nützlich. Wir haben unsere Insel in Photoshop entworfen und bearbeitet! Dies machte es sehr einfach, nach Bedarf kleine Anpassungen vorzunehmen. Wenn wir zum Beispiel sicherstellen wollten, dass die Landebahn völlig flach ist, haben wir nur darauf geachtet, diesen Bereich in einer einzigen Farbe zu übermalen.
Die Höhenkarte für Flight Arcade sehen Sie unten. Sehen Sie, ob Sie die „flachen“ Bereiche sehen können, die wir für die Landebahn und das Dorf geschaffen haben.
Die Höhenkarte für die Flight Arcade-Insel. Es wurde in Photoshop erstellt und basiert auf der "großen Insel" in einer berühmten Inselkette des Pazifischen Ozeans. Irgendwelche Ideen?Eine Textur, die nach der Höhenkarte auf das resultierende 3D-Netz abgebildet wird, wird dekodiert. Mehr dazu unten.Wir haben Flight Arcade mit Babylon.js erstellt, und Babylon gab uns einen recht einfachen Weg von der Höhenkarte zu 3D. Babylon bietet eine API zum Generieren einer Netzgeometrie aus einem Heightmap-Bild:
var ground = BABYLON.Mesh.CreateGroundFromHeightMap ('your-mesh-name', '/path/to/heightmap.png'), 100, // Breite des Bodennetzes (x-Achse) 100, // Tiefe des Bodennetzes (z-Achse) 40, // Anzahl der Unterteilungen 0, // min Höhe 50, // max Höhe Szene, falsch, // aktualisierbar? null // Rückruf, wenn das Netz bereit ist);
Die Menge an Details wird durch das Eigentum dieser Unterteilung bestimmt. Beachten Sie, dass sich der Parameter auf die Anzahl der Unterteilungen auf jeder Seite des Höhenkartenbilds bezieht, nicht auf die Gesamtanzahl der Zellen. Eine geringfügige Erhöhung dieser Anzahl kann einen großen Einfluss auf die Gesamtanzahl der Scheitelpunkte in Ihrem Netz haben.
Im nächsten Abschnitt erfahren Sie, wie Sie den Boden strukturieren. Wenn Sie jedoch mit der Erstellung von Höhenkarten experimentieren, ist es hilfreich, das Drahtmodell zu sehen. Hier ist der Code zum Anwenden einer einfachen Wireframe-Textur, sodass Sie leicht sehen können, wie die Heightmap-Daten in die Scheitelpunkte unseres Netzes konvertiert werden:
// einfaches Drahtgittermaterial var material = new BABYLON.StandardMaterial ('Bodenmaterial', Szene); material.wireframe = true; Grundmaterial = Material;
Sobald wir ein Modell hatten, war das Mapping einer Textur relativ unkompliziert. Für Flight Arcade haben wir einfach ein sehr großes Bild erstellt, das der Insel in unserer Höhenkarte entspricht. Das Bild wird über die Konturen des Geländes gespannt, sodass die Textur und die Höhenkarte miteinander korrelieren. Dies war wirklich leicht zu visualisieren und die gesamte Produktionsarbeit wurde erneut in Photoshop ausgeführt.
Das ursprüngliche Texturbild wurde bei 4096 x 4096 erstellt. Das ist ziemlich groß! (Wir haben die Größe letztendlich um 2048 x 2048 reduziert, um den Download angemessen zu halten, aber die gesamte Entwicklung wurde mit einem Bild in voller Größe durchgeführt.) Hier ist ein Vollpixel-Beispiel aus der ursprünglichen Textur.
Ein Vollpixel-Beispiel der ursprünglichen Inselstruktur. Die gesamte Stadt ist nur rund 300 px groß.Diese Rechtecke repräsentieren die Gebäude in der Stadt auf der Insel. Wir haben schnell eine Diskrepanz in Bezug auf die Texturierungseinzelheiten festgestellt, die zwischen dem Terrain und den anderen 3D-Modellen erzielt werden konnte. Selbst bei unserer riesigen Inselstruktur war der Unterschied ablenkend!
Um dies zu korrigieren, haben wir zusätzliche Details in Form von zufälligem Rauschen in die Geländetextur eingefügt. Das Vorher und Nachher sehen Sie unten. Beachten Sie, wie das zusätzliche Geräusch das Erscheinungsbild von Details im Gelände verbessert.
Wir haben einen benutzerdefinierten Shader erstellt, um das Rauschen hinzuzufügen. Shader geben Ihnen eine unglaubliche Kontrolle über das Rendern einer WebGL 3D-Szene. Dies ist ein hervorragendes Beispiel dafür, wie ein Shader nützlich sein kann.
Ein WebGL-Shader besteht aus zwei Hauptteilen: den Scheitelpunkten für Scheitelpunkte und Fragmente. Das Hauptziel des Vertex-Shaders ist das Zuordnen von Scheitelpunkten zu einer Position im gerenderten Bild. Der Fragment- (oder Pixel-) Shader steuert die resultierende Farbe der Pixel.
Shader werden in einer Hochsprache namens GLSL (Graphics Library Shader Language) geschrieben, die C ähnelt. Dieser Code wird auf der GPU ausgeführt. In diesem Tutorial erfahren Sie, wie Sie einen eigenen benutzerdefinierten Shader für Babylon.js erstellen, oder erfahren Sie in dieser Anleitung für Anfänger, wie Sie Grafik-Shader codieren.
Wir ändern nicht, wie unsere Textur auf das Bodennetz abgebildet wird, daher ist unser Scheitelpunkt-Shader ziemlich einfach. Es berechnet lediglich die Standardzuordnung und weist den Zielort zu.
Präzisions-Mediumschwimmer; // Attribute attribut vec3 position; Attribut vec3 normal; Attribut vec2 uv; // Uniformen uniform mat4 worldViewProjection; // variierende variierende vec4 vPosition; variierendes vec3 vNormal; variierendes vec2 vUV; void main () vec4 p = vec4 (Position 1,0); vPosition = p; vNormal = normal; vUV = UV; gl_Position = worldViewProjection * p;
Unser Fragment-Shader ist etwas komplizierter. Es kombiniert zwei verschiedene Bilder: das Grundbild und das Mischbild. Das Basisbild wird über das gesamte Bodennetz abgebildet. In Flight Arcade ist dies das Farbbild der Insel. Das Mischbild ist das kleine Rauschbild, das verwendet wird, um dem Boden in geringer Entfernung Textur und Details zu verleihen. Der Shader kombiniert die Werte aus jedem Bild, um eine kombinierte Textur auf der gesamten Insel zu erzeugen.
Die letzte Lektion in Flight Arcade findet an einem nebligen Tag statt. Daher besteht die andere Aufgabe unseres Pixel-Shatters darin, die Farbe anzupassen, um den Nebel zu simulieren. Die Einstellung basiert darauf, wie weit der Scheitelpunkt von der Kamera entfernt ist, wobei entfernte Pixel durch den Nebel stärker "verdeckt" werden. Diese Entfernungsberechnung sehen Sie im calcFogFactor
Funktion oberhalb des Haupt-Shader-Codes.
#ifdef GL_ES precision highp float; #endif uniform mat4 worldView; variierende vec4 vPosition; variierendes vec3 vNormal; variierendes vec2 vUV; // referiert einheitlich sampler2D baseSampler; einheitlicher sampler2D blendSampler; Uniform Float BlendScaleU; Uniform Float BlendScaleV; #define FOGMODE_NONE 0. #define FOGMODE_EXP 1. #define FOGMODE_EXP2 2. #define FOGMODE_LINEAR 3. #define E 2.71828 einheitlich vec4 vFogInfos; einheitliche vec3 vFogColor; float calcFogFactor () // erhält Abstand von der Kamera zum Scheitelpunkt floD fogDistance = gl_FragCoord.z / gl_FragCoord.w; float fogCoeff = 1,0; float fogStart = vFogInfos.y; float fogEnd = vFogInfos.z; float fogDensity = vFogInfos.w; if (FOGMODE_LINEAR == vFogInfos.x) fogCoeff = (fogEnd - fogDistance) / (fogEnd - fogStart); else if (FOGMODE_EXP == vFogInfos.x) fogCoeff = 1,0 / pow (E, fogDistance * fogDensity); else if (FOGMODE_EXP2 == vFogInfos.x) fogCoeff = 1.0 / Pow (E, fogDistance * fogDistance * fogDensity * fogDensity); Rücklaufklemme (fogCoeff, 0.0, 1.0); void main (void) vec4 baseColor = texture2D (baseSampler, vUV); vec2 blendUV = vec2 (vUV.x * blendScaleU, vUV.y * blendScaleV); vec4 blendColor = texture2D (blendSampler, blendUV); // Typ des Mischmodus multiplizieren vec4 color = baseColor * blendColor; // Faktor in der Nebelfarbe Floatnebel = calcFogFactor (); color.rgb = fog * color.rgb + (1,0 - fog) * vFogColor; gl_FragColor = Farbe;
Das letzte Stück für unseren benutzerdefinierten Blend-Shader ist der von Babylon verwendete JavaScript-Code. Der Hauptzweck dieses Codes besteht darin, die an unsere Scheitelpunkte und Pixel-Shader übergebenen Parameter vorzubereiten.
Funktion BlendMaterial (Name, Szene, Optionen) this.name = name; this.id = name; this.options = Optionen; this.blendScaleU = options.blendScaleU || 1; this.blendScaleV = options.blendScaleV || 1; this._scene = Szene; scene.materials.push (this); var assets = options.assetManager; var textureTask = assets.addTextureTask ('blend-material-base-task', options.baseImage); textureTask.onSuccess = _.bind (Funktion (Task) this.baseTexture = task.texture; this.baseTexture.uScale = 1; this.baseTexture.vScale = 1; if (options.baseHasAlpha) this.baseTexture.hasAlpha = wahr;, dies); textureTask = assets.addTextureTask ('blend-material-blend-task', options.blendImage); textureTask.onSuccess = _.bind (Funktion (Task) this.blendTexture = task.texture; this.blendTexture.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE; this.blendTexture.wrapV = BABYYON.Texture.MIRROR_ADURNATURN). BlendMaterial.prototype = Object.create (BABYLON.Material.prototype); BlendMaterial.prototype.needAlphaBlending = function () return (this.options.baseHasAlpha === true); ; BlendMaterial.prototype.needAlphaTesting = function () return false; ; BlendMaterial.prototype.isReady = Funktion (Netz) var engine = this._scene.getEngine (); // sicherstellen, dass die Texturen bereit sind if (! this.baseTexture ||! this.blendTexture) return false; if (! this._effect) this._effect = engine.createEffect (// Shader-Name "blend", // Attribute, die die Topologie von Scheitelpunkten beschreiben ["position", "normal", "uv"]), // Uniform externe Variablen) definiert durch die Shader ["worldViewProjection", "world", "blendScaleU", "blendScaleV", "vFogInfos", "vFogColor"], // Sampler (Objekte zum Lesen von Texturen) ["baseSampler", "blendSampler "], // optional definierter String" "); if (! this._effect.isReady ()) return false; return true; ; BlendMaterial.prototype.bind = Funktion (Welt, Netz) var scene = this._scene; this._effect.setFloat4 ("vFogInfos", scene.fogMode, scene.fogStart, scene.fogEnd, scene.fogDensity); this._effect.setColor3 ("vFogColor", scene.fogColor); this._effect.setMatrix ("Welt", Welt); this._effect.setMatrix ("worldViewProjection", world.multiply (scene.getTransformMatrix ())); // Texturen this._effect.setTexture ("baseSampler", this.baseTexture); this._effect.setTexture ("blendSampler", this.blendTexture); this._effect.setFloat ("blendScaleU", this.blendScaleU); this._effect.setFloat ("blendScaleV", this.blendScaleV); ; BlendMaterial.prototype.dispose = function () if (this.baseTexture) this.baseTexture.dispose (); if (this.blendTexture) this.blendTexture.dispose (); this.baseDispose (); ;
Babylon.js vereinfacht das Erstellen von benutzerdefinierten Shader-basierten Materialien. Unser Blend-Material ist relativ einfach, aber es machte einen großen Unterschied im Aussehen der Insel, als das Flugzeug tief auf den Boden flog. Shader bringen die Kraft der GPU in den Browser und erweitern die Arten von kreativen Effekten, die Sie auf Ihre 3D-Szenen anwenden können. In unserem Fall war das der letzte Schliff!
Microsoft bietet viele kostenlose Lernmöglichkeiten zu vielen Open-Source-JavaScript-Themen. Mit Microsoft Edge möchten wir noch viel mehr schaffen. Hier sind einige zu überprüfen:
Einige kostenlose Tools für den Einstieg: Visual Studio Code, Azure Trial und Cross-Browser-Testtools - alles verfügbar für Mac, Linux oder Windows.
Dieser Artikel ist Teil der Web-Dev-Tech-Serie von Microsoft. Wir freuen uns zu teilen Microsoft Edge und das Neue EdgeHTML-Rendering-Engine mit dir. Erhalten Sie kostenlose virtuelle Maschinen oder testen Sie remote auf Ihrem Mac, iOS, Android oder Windows-Gerät. http://dev.modern.ie/.