Den första versionen av Flight Simulator skickades 1980 för Apple II och det var fantastiskt i 3D! Det var en anmärkningsvärd prestation. Det är ännu mer fantastiskt när du överväger att allt 3D var gjort för hand, resultatet av noggranna beräkningar och pixelkommandon med låg nivå. När Bruce Atwick tacklade de tidiga versionerna av Flight Simulator var det inte bara några 3D-ramar, men det fanns inga ramar alls! Dessa versioner av spelet var mestadels skrivna i montering, bara ett steg bort från dem och nollor som flyter genom en CPU.
När vi bestämmer oss för att omformulera Flight Simulator (eller Flight Arcade som vi kallar det) för webben och visa vad som är möjligt i den nya Microsoft Edge-webbläsaren och EdgeHTML-återgivningsmotorn, kunde vi inte låta bli att tänka på kontrast att skapa 3D då och nu gammal Flight Sim, ny Flight Sim, gammal Internet Explorer, ny Microsoft Edge. Modern kodning verkar nästan lyxig som vi skulpterar 3D-världar i WebGL med stora ramar som Babylon.js. Det låter oss fokusera på väldigt höga problem.
I den här artikeln delar jag vår inställning till en av dessa roliga utmaningar: ett enkelt sätt att skapa realistiskt storskalig terräng.
Obs! Interaktiv kod och exempel för den här artikeln finns också på Flight Arcade / Learn.
De flesta 3D-objekt skapas med modelleringsverktyg och av goda skäl. Att skapa komplexa objekt (som ett flygplan eller till och med en byggnad) är svårt att göra i kod. Modelleringsverktyg ger nästan alltid mening, men det finns undantag! En av dessa kan vara fall som Flight Arcade-öarnas rullande kullar. Vi slutade använda en teknik som vi visade sig vara enklare och möjligen ännu mer intuitiv: en heightmap.
En höjdkarta är ett sätt att använda en vanlig tvådimensionell bild för att beskriva höjdavlastningen på en yta som en ö eller annan terräng. Det är ett ganska vanligt sätt att arbeta med höjddata, inte bara i spel utan även i geografiska informationssystem (GIS) som används av kartografer och geologer.
För att hjälpa dig att få en uppfattning om hur det här fungerar, kolla in höjdkartan i denna interaktiva demo. Försök att dra i bildredigeraren och kolla sedan på den resulterande terrängen.
Konceptet bakom en heightmap är ganska enkelt. I en bild som den ovanstående är ren svart "golvet" och rent vit är den högsta toppen. Grayscale-färgerna däremellan representerar motsvarande höjder. Detta ger oss 256 nivåer av höjd, vilket är gott om detaljer för vårt spel. Verkliga applikationer kan använda fullfärgsspektret för att lagra signifikant mer detaljnivåer (2564 = 4 294 967 296 detaljnivåer om du inkluderar en alfakanal).
En höjdkarta har några fördelar över ett traditionellt polygonalt nät:
Först är heightmaps mycket kompaktare. Endast de viktigaste uppgifterna (höjden) lagras. Det måste programmeras om till ett 3D-objekt, men det här är den klassiska handeln: du sparar utrymme nu och betalar senare med beräkning. Genom att lagra data som en bild får du ytterligare en fördel i rummet: du kan utnyttja standardbildskomprimeringstekniker och göra datan liten (jämfört)!
För det andra är heightmaps ett bekvämt sätt att generera, visualisera och redigera terräng. Det är ganska intuitivt när man ser en. Det känns lite som att titta på en karta. Detta visade sig vara särskilt användbart för Flight Arcade. Vi har utformat och redigerat vår ö rätt i Photoshop! Det gjorde det väldigt enkelt att göra små justeringar efter behov. När vi till exempel ville försäkra oss om att banan var helt platt, så försökte vi bara måla över det området i en enda färg.
Du kan se längdkarta för flygbågen nedan. Se om du kan upptäcka de "platta" områdena vi skapade för landningsbanan och byn.
Höjdkarta för Flight Arcade Island. Det skapades i Photoshop och det är baserat på den "stora ön" i en känd östkedja i Stillahavsområdet. Några gissningar?En textur som kartläggs på det resulterande 3D-nätet efter höjdkartan är avkodad. Mer om det nedan.Vi byggde Flight Arcade med Babylon.js, och Babylon gav oss en ganska enkel väg från heightmap till 3D. Babylon tillhandahåller ett API för att skapa en mask geometri från en heightmap-bild:
var marken = BABYLON.Mesh.CreateGroundFromHeightMap ('ditt-mask-namn', '/path/to/heightmap.png', 100, // grundmaskens bredd (x-axel) 100, // djupet av marknätet (z-axel) 40, // antal indelningar 0, // min höjd 50, // maxhöjdscen, falsk, // uppdaterbar? null // återuppringning när nätet är klart);
Mängden detaljer bestäms av den delavdelningens egendom. Det är viktigt att notera att parametern hänvisar till antalet underavdelningar på varje sida av heightmap-bilden, inte det totala antalet celler. Så att öka detta antal kan något ha stor effekt på det totala antalet hörn i nätet.
I nästa avsnitt lär vi oss hur man textar marken, men när man experimenterar med hur man skapar heightmap är det användbart att se wireframe. Här är koden för att applicera en enkel wireframe-textur så det är lätt att se hur heightmap-data konverteras till snittet på vårt nät:
// enkelt wireframe material var material = ny BABYLON.StandardMaterial ("ground-material", scen); material.wireframe = true; ground.material = material;
När vi en gång hade en modell var kartläggning en textur relativt enkel. För Flight Arcade skapade vi helt enkelt en mycket stor bild som matchade ön i vår heightmap. Bilden sträcker sig över terrängens konturer, så texturen och höjdkartan förblir korrelerade. Det var väldigt lätt att visualisera, och ännu en gång gjordes allt produktionsarbete i Photoshop.
Den ursprungliga texturbilden skapades vid 4096x4096. Det är ganska stort! (Vi sänkte så småningom storleken med en nivå till 2048x2048 för att hålla nedladdningen rimlig, men hela utvecklingen gjordes med fullstorleksbilden.) Här är ett fullpixelprov från den ursprungliga texturen.
Ett fullpixelprov av den ursprungliga ötexturen. Hela staden är bara cirka 300 px kvadrat.Dessa rektanglar representerar byggnaderna i staden på ön. Vi märkte snabbt en skillnad i nivån på textureringsdetaljer som vi kunde uppnå mellan terrängen och de andra 3D-modellerna. Även med vår gigantiska ötextur var skillnaden störande!
För att fixa detta, "blandade" vi ytterligare detaljer i terrängtexturen i form av slumpmässigt brus. Du kan se före och efter nedan. Lägg märke till hur det extra ljudet förbättrar utseendet på detaljer i terrängen.
Vi skapade en anpassad shader för att lägga till ljudet. Shaders ger dig en otrolig mängd kontroll över rendering av en WebGL 3D-scen, och detta är ett utmärkt exempel på hur en shader kan vara användbar.
En WebGL-skuggare består av två stora bitar: vertex och fragment shaders. Huvudmålet för vertex-shader är att kartlägga vertikaler till en position i den gjorda ramen. Fragmentet (eller pixel) -skärmen styr den resulterande färgen på pixlarna.
Shaders är skrivna på ett språk på hög nivå som heter GLSL (Graphics Library Shader Language), som liknar C. Denna kod utförs på GPU. För en djupgående titt på hur shaders fungerar, se den här handledningen om hur du skapar en egen anpassad shader för Babylon.js, eller se den här nybörjarens guide till kodningsgrafikskuggare.
Vi ändrar inte hur vår textur kartläggs på marknätet, så vår toppskärmshuggare är ganska enkel. Det beräknar bara standardkartläggningen och tilldelar målplatsen.
precision mediump float; // Egenskaper attribut vec3 position; attribut vec3 normal; attribut vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varierande varierande vec4 vPosition; varierande vec3 vNormal; varierande vec2 vUV; void main () vec4 p = vec4 (position, 1.0); vPosition = p; vNormal = normal; vUV = uv; gl_Position = worldViewProjection * p;
Vår fragmentskärm är lite mer komplicerad. Den kombinerar två olika bilder: basen och blandbilden. Basbilden kartläggs över hela marknätet. I Flight Arcade är detta färgbilden på ön. Blandbilden är den lilla ljudbilden som används för att ge marken lite textur och detalj på avstånd. Skärmen kombinerar värdena från varje bild för att skapa en sammanlagd textur över ön.
Den sista lektionen i Flight Arcade äger rum på en dimmig dag, så den andra uppgiften vår pixel shader har är att justera färgen för att simulera dimma. Justeringen är baserad på hur långt vertexet är från kameran, med avlägsna pixlar blir tyngre "dunkla" av dimma. Du ser denna distansberäkning i calcFogFactor
funktion över huvudskärmskoden.
#ifdef GL_ES precision highp float; #endif enhetlig mat4 worldView; varierande vec4 vPosition; varierande vec3 vNormal; varierande vec2 vUV; // Refs uniform sampler2D baseSampler; enhetlig sampler2D blendSampler; enhetlig float blendScaleU; enhetlig float blendScaleV; #define FOGMODE_NONE 0. #define FOGMODE_EXP 1. #define FOGMODE_EXP2 2. #define FOGMODE_LINEAR 3. #define E 2.71828 enhetlig vec4 vFogInfos; enhetlig vec3 vFogColor; float calcFogFactor () // får avstånd från kamera till vertex float fogDistance = gl_FragCoord.z / gl_FragCoord.w; float fogCoeff = 1,0; float fogStart = vFogInfos.y; float fogEnd = vFogInfos.z; float fogDensity = vFogInfos.w; om (FOGMODE_LINEAR == vFogInfos.x) fogCoeff = (fogEnd - fogDistance) / (fogEnd - fogStart); annars om (FOGMODE_EXP == vFogInfos.x) fogCoeff = 1.0 / pow (E, fogDistance * fogDensity); annars om (FOGMODE_EXP2 == vFogInfos.x) fogCoeff = 1.0 / pow (E, fogDistance * fogDistance * fogDensity * fogDensity); returklämma (fogCoeff, 0,0, 1,0); void main (void) vec4 baseColor = texture2D (basSampler, vUV); vec2 blendUV = vec2 (vUV.x * blendScaleU, vUV.y * blendScaleV); vec4 blendColor = texture2D (blendSampler, blendUV); // multiplicera typ blandningsläge vec4 color = baseColor * blendColor; // faktor i dimma färg float dimma = calcFogFactor (); color.rgb = dimma * color.rgb + (1.0 - dimma) * vFogColor; gl_FragColor = färg;
Slutstycket för vår anpassade Blend shader är JavaScript-koden som används av Babylon. Huvudsyftet med denna kod är att förbereda parametrarna som skickas till våra vertex och pixel shaders.
funktion BlendMaterial (namn, scen, alternativ) this.name = name; this.id = namn; this.options = options; this.blendScaleU = options.blendScaleU || 1; this.blendScaleV = options.blendScaleV || 1; this._scene = scene; scene.materials.push (detta); var assets = options.assetManager; var textureTask = assets.addTextureTask ('mix-material-bas-task', options.baseImage); textureTask.onSuccess = _.bind (funktion (uppgift) this.baseTexture = task.texture; this.baseTexture.uScale = 1; this.baseTexture.vScale = 1; om (options.baseHasAlpha) this.baseTexture.hasAlpha = sant;, detta); textureTask = assets.addTextureTask ('mix-material-blend-task', options.blendImage); textureTask.onSuccess = _.bind (funktion (uppgift) this.blendTexture = task.texture; this.blendTexture.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE; this.blendTexture.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE;, detta); 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 (mesh) var motor = this._scene.getEngine (); // Se till att texturer är klara om (! this.baseTexture ||! this.blendTexture) return false; om (! this._effect) this._effect = engine.createEffect (// shader name "blanda", // attribut som beskriver toppologi av vertices ["position", "normal", "uv"], // uniformer externa variabler) definierade av shaders ["worldViewProjection", "world", "blendScaleU", "blendScaleV", "vFogInfos", "vFogColor"], // samplers (objekt som används för att läsa texturer) ["baseSampler", "blendSampler "], // valfri definiera sträng" "); om (! this._effect.isReady ()) return false; returnera sant; ; BlendMaterial.prototype.bind = funktion (värld, nät) 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 ("world", world); this._effect.setMatrix ("worldViewProjection", world.multiply (scene.getTransformMatrix ())); // Texturer 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 (); om (this.blendTexture) this.blendTexture.dispose (); this.baseDispose (); ;
Babylon.js gör det enkelt att skapa ett anpassat shader-baserat material. Vårt blandningsmaterial är relativt enkelt, men det gjorde verkligen stor skillnad i utseendet på ön när planet flög lågt till marken. Shaders ger GPU: ns kraft till webbläsaren, och utvidgar de typer av kreativa effekter du kan tillämpa på dina 3D-scener. I vårt fall var det sista handen!
Microsoft har en massa gratis lärande på många JavaScript-ämnen med öppen källkod, och vi har ett uppdrag att skapa mycket mer med Microsoft Edge. Här är några att kolla in:
Och några gratis verktyg för att komma igång: Visual Studio Code, Azure Trial och testverktyg för cross-browser - alla tillgängliga för Mac, Linux eller Windows.
Den här artikeln är en del av web dev-tekniken från Microsoft. Vi är glada att dela Microsoft Edge och den nya EdgeHTML-återgivningsmotor med dig. Få gratis virtuella maskiner eller testa fjärran på din Mac, iOS, Android eller Windows-enheten @ http://dev.modern.ie/.