Den første versjonen av Flight Simulator sendt i 1980 til Apple II, og utrolig, det var i 3D! Det var en bemerkelsesverdig prestasjon. Det er enda mer fantastisk når du vurderer at alt 3D ble gjort for hånd, resultatet av grundige beregninger og lavt nivå pikselkommandoer. Når Bruce Atwick taklet de tidlige versjonene av Flight Simulator, var det ikke bare 3D-rammer, men det var ikke noen rammeverk i det hele tatt! Disse versjonene av spillet var for det meste skrevet i samling, bare et skritt unna dem og nuller som strømmer gjennom en CPU.
Når vi tar sikte på å reimagine Flight Simulator (eller Flight Arcade som vi kaller det) på nettet, og for å demonstrere hva som er mulig i den nye Microsoft Edge-nettleseren og EdgeHTML-gjengemotoren, kan vi ikke unngå å tenke på kontrasten for å lage 3D så og nåværende Flight Sim, ny Flight Sim, gammel Internet Explorer, ny Microsoft Edge. Moderne koding virker nesten luksuriøs som vi skulpturer 3D-verdener i WebGL med flotte rammer som Babylon.js. Det lar oss fokusere på svært høyt nivå problemer.
I denne artikkelen deler jeg vår tilnærming til en av disse morsomme utfordringene: En enkel måte å skape realistisk utseende på stort terreng.
Merk: Interaktiv kode og eksempler for denne artikkelen finnes også på Flight Arcade / Learn.
De fleste 3D-objekter er laget med modelleringsverktøy, og med god grunn. Å lage komplekse gjenstander (som et fly eller til og med en bygning) er vanskelig å gjøre i kode. Modelleringsverktøy er nesten alltid fornuftig, men det er unntak! En av disse kan være tilfeller som de rullende åsene på Flight Arcade-øya. Vi endte opp med å bruke en teknikk som vi syntes å være enklere og muligens enda mer intuitiv: en høydeplan.
En høydeplan er en måte å bruke et vanlig todimensjonalt bilde på for å beskrive høydeavlastningen til en overflate som en øy eller et annet terreng. Det er en ganske vanlig måte å jobbe med høyde data, ikke bare i spill, men også i geografiske informasjonssystemer (GIS) brukt av kartografer og geologer.
For å hjelpe deg med å få en ide for hvordan dette virker, sjekk ut høydeplanen i denne interaktive demoen. Prøv å tegne i bildeeditoren, og sjekk ut det resulterende terrenget.
Konseptet bak en høydekart er ganske grei. På et bilde som det ovenfor er ren svart "gulvet" og ren hvit er den høyeste toppen. Grayscale-fargene i mellom representerer tilsvarende høyder. Dette gir oss 256 nivåer av høyde, som er rikelig med detaljer for spillet vårt. Virkelige programmer kan bruke fargespektret til å lagre betydelig mer detaljnivå (2564 = 4 294 967 296 detaljnivåer hvis du inkluderer en alfakanal).
En høydekart har noen fordeler i forhold til et tradisjonelt polygonalt nett:
Først er heightmaps mye mer kompakte. Bare de viktigste dataene (høyden) blir lagret. Det må bli forvandlet til et 3D-objekt programmatisk, men dette er den klassiske handel: du sparer plass nå og betaler senere med beregning. Ved å lagre dataene som et bilde, får du en annen plassfordel: Du kan utnytte standard bildekompresjonsteknikker og gjøre dataene små (til sammenligning)!
For det andre er heightmaps en praktisk måte å generere, visualisere og redigere terreng. Det er ganske intuitivt når du ser en. Det føles litt som å se på et kart. Dette viste seg å være spesielt nyttig for Flight Arcade. Vi har designet og redigert vår øy rett i Photoshop! Dette gjorde det veldig enkelt å foreta små justeringer etter behov. Når vi for eksempel ønsket å sikre at rullebanen var helt flat, så var det bare å sørge for å male over det området i en enkelt farge.
Du kan se høydekart for Flight Arcade nedenfor. Se om du kan se de "flate" områdene vi opprettet for rullebanen og landsbyen.
Høyden for Flight Arcade-øya. Den ble opprettet i Photoshop, og den er basert på den "store øya" i en berømt øyer i Stillehavet. Eventuelle gjetninger?En tekstur som blir kartlagt på det resulterende 3D-nettverket etter høydekartet dekodes. Mer om det nedenfor.Vi bygde Flight Arcade med Babylon.js, og Babylon ga oss en ganske enkel vei fra høydeplan til 3D. Babylon gir en API for å generere en mesh geometri fra et høydebildebilde:
var jordet = BABYLON.Mesh.CreateGroundFromHeightMap ('ditt-mask-navn', '/path/to/heightmap.png', 100, // bredde av bakkenettet (x-akse) 100, // dybde av bakkenettet (z akse) 40, // antall underavdelinger 0, // min høyde 50, // maks høyde scene, falsk, // oppdaterbar? null // tilbakering når masken er klar);
Mengden detaljer er bestemt av den delenes underverdi. Det er viktig å merke seg at parameteren refererer til antall underavdelinger på hver side av høydemapbildet, ikke totalt antall celler. Slik øker dette tallet litt, kan ha stor effekt på det totale antallet krysser i nettverket ditt.
I neste avsnitt lærer vi hvordan du skal tekstur på bakken, men når du eksperimenterer med høydeopprettingsskaping, er det nyttig å se wireframe. Her er koden for å bruke en enkel wireframe tekstur slik at det er lett å se hvordan høydemappedataene omdannes til kryssene på nettverket vårt:
// enkelt wireframe materiale var material = ny BABYLON.StandardMaterial ('bunnmateriale', scene); material.wireframe = true; ground.material = materiale;
Når vi hadde en modell, var kartlegging en tekstur relativt enkel. For Flight Arcade skapte vi ganske enkelt et veldig stort bilde som matchet øya i vår høydeplan. Bildet blir strukket over terrengets konturer, slik at tekstur og høydekart forblir korrelert. Dette var veldig enkelt å visualisere, og enda en gang ble alt produksjonsarbeidet gjort i Photoshop.
Det opprinnelige teksturbildet ble opprettet på 4096x4096. Det er ganske stort! (Vi reduserte til slutt størrelsen med et nivå til 2048x2048 for å holde nedlastningen rimelig, men all utvikling ble gjort med bildet i full størrelse.) Her er en fullpikselprøve fra den opprinnelige tekstur.
En fullpikselprøve av den originale øya tekstur. Hele byen er bare rundt 300 px kvadrat.Disse rektangler representerer bygningene i byen på øya. Vi la merke til en uoverensstemmelse i nivået på teksturdetaljer som vi kunne oppnå mellom terrenget og de andre 3D-modellene. Selv med den gigantiske øya tekstur var forskjellen distraherende åpenbar!
For å fikse dette, "blandet" vi ytterligere detaljer inn i terrengeteksturen i form av tilfeldig støy. Du kan se før og etter nedenfor. Legg merke til hvordan tilleggsstøyen øker utseendet på detaljer i terrenget.
Vi opprettet en tilpasset shader for å legge til støyen. Shaders gir deg en utrolig mengde kontroll over rendering av en WebGL 3D-scene, og dette er et godt eksempel på hvordan en shader kan være nyttig.
En WebGL-skygge består av to store brikker: toppunktet og fragmentet shaders. Hovedpunktet for toppskygger er å kartlegge hjørner til en posisjon i den gjengitte rammen. Fragmentet (eller piksel) -skyggeren kontrollerer den resulterende fargen på pikslene.
Shaders er skrevet på et høyt språk som kalles GLSL (Graphics Library Shader Language), som ligner C. Denne koden utføres på GPU. For en grundig titt på hvordan shaders fungerer, se denne veiledningen om hvordan du lager din egen tilpassede shader for Babylon.js, eller se denne nybegynners guide til kodende grafikkskyggere.
Vi endrer ikke hvordan tekstur vår er kartlagt på bakkenettverket, så vår vertex shader er ganske enkel. Den beregner bare standard kartlegging og tilordner målplasseringen.
presisjon mediump float; // Attributter attributt vec3 posisjon; attributt vec3 normal; attributt vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varierende varierende vec4 vPosition; varierende vec3 vNormal; varierende vec2 vUV; void main () vec4 p = vec4 (posisjon, 1.0); vPosition = p; vNormal = normal; vUV = uv; gl_Position = worldViewProjection * p;
Vår fragment shader er litt mer komplisert. Den kombinerer to forskjellige bilder: basen og blendbildene. Basisbildet er kartlagt over hele bakkenettet. I Flight Arcade er dette fargebildet på øya. Blendbildet er det lille støybildet som brukes til å gi bakken litt konsistens og detaljer på tette avstander. Skyggeren kombinerer verdiene fra hvert bilde for å skape en kombinert tekstur over hele øya.
Den siste leksjonen i Flight Arcade finner sted på en tåkete dag, så den andre oppgaven vår pixel shader har, er å justere fargen for å simulere tåke. Justeringen er basert på hvor langt vertexet er fra kameraet, med fjerne piksler blir tyngre "skjult" av tåken. Du ser denne avstandsberegningen i calcFogFactor
funksjon over hovedskyggekoden.
#ifdef GL_ES presisjon highp float; #endif uniform mat4 worldView; varierende vec4 vPosition; varierende vec3 vNormal; varierende vec2 vUV; // Refs uniform sampler2D baseSampler; uniform sampler2D blendSampler; uniform float blendScaleU; enhetlig flyteblendScaleV; #define FOGMODE_NONE 0. #define FOGMODE_EXP 1. #define FOGMODE_EXP2 2. #define FOGMODE_LINEAR 3. #define E 2.71828 uniform vec4 vFogInfos; uniform vec3 vFogColor; flyte calcFogFactor () // får avstand fra kamera til 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; hvis (FOGMODE_LINEAR == vFogInfos.x) fogCoeff = (fogEnd - fogDistance) / (fogEnd - fogStart); ellers hvis (FOGMODE_EXP == vFogInfos.x) fogCoeff = 1.0 / pow (E, fogDistance * fogDensity); annet hvis (FOGMODE_EXP2 == vFogInfos.x) fogCoeff = 1.0 / pow (E, fogDistance * fogDistance * fogDensity * fogDensity); returklemme (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); // multipliser type blanding modus vec4 color = baseColor * blendColor; // faktor i tåkefarge flyte tåke = calcFogFactor (); color.rgb = tåke * color.rgb + (1.0 - tåke) * vFogColor; gl_FragColor = farge;
Det endelige stykket for vår tilpassede Blend shader er JavaScript-koden som brukes av Babylon. Hovedformålet med denne koden er å forberede parametrene som sendes til våre vertex og pixel shaders.
funksjon BlendMaterial (navn, scene, alternativer) this.name = name; this.id = navn; this.options = alternativer; this.blendScaleU = options.blendScaleU || 1; this.blendScaleV = options.blendScaleV || 1; this._scene = scene; scene.materials.push (this); var assets = options.assetManager; var textureTask = assets.addTextureTask ('blend-material-base-oppgave', options.baseImage); textureTask.onSuccess = _.bind (funksjon (oppgave) this.baseTexture = task.texture; this.baseTexture.uScale = 1; this.baseTexture.vScale = 1; hvis (options.baseHasAlpha) this.baseTexture.hasAlpha = sant;, dette); textureTask = assets.addTextureTask ('blend-material-blend-task', options.blendImage); textureTask.onSuccess = _.bind (funksjon (oppgave) this.blendTexture = task.texture; this.blendTexture.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE; this.blendTexture.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE;, dette); 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 = funksjon (mesh) var motor = this._scene.getEngine (); // sørg for at teksturer er klare hvis (! this.baseTexture ||! this.blendTexture) return false; hvis (! this._effect) this._effect = engine.createEffect (// shadernavn "blend", // attributter som beskriver topologi av poengvinkler ["posisjon", "normal", "uv"], // uniformer eksterne variabler) definert av shaders ["worldViewProjection", "world", "blendScaleU", "blendScaleV", "vFogInfos", "vFogColor"], // samplers (objekter som brukes til å lese teksturer) ["baseSampler", "blendSampler "], // valgfri definere streng" "); hvis (! this._effect.isReady ()) return false; returnere sann; ; BlendMaterial.prototype.bind = funksjon (verden, nett) 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 ("verden", verden); this._effect.setMatrix ("worldViewProjection", world.multiply (scene.getTransformMatrix ())); // Teksturer 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 (); hvis (this.blendTexture) this.blendTexture.dispose (); this.baseDispose (); ;
Babylon.js gjør det enkelt å lage et skreddersydd materiale. Blendmaterialet vårt er relativt enkelt, men det gjorde virkelig stor forskjell i øyas utseende da flyet fløy lavt til bakken. Shaders gir kraften til GPU til nettleseren, og utvider de typer kreative effekter du kan søke på 3D-scenene dine. I vårt tilfelle var det sluttoppdraget!
Microsoft har en mengde gratis læring på mange JavaScript-temaer med åpen kildekode, og vi har et oppdrag å skape mye mer med Microsoft Edge. Her er noen å sjekke ut:
Og noen gratis verktøy for å komme i gang: Visual Studio Code, Azure Trial og testverktøy for kryss-leser-alt tilgjengelig for Mac, Linux eller Windows.
Denne artikkelen er en del av web dev-teknologiserien fra Microsoft. Vi er glade for å dele Microsoft Edge og den nye EdgeHTML rendering motor med deg. Få gratis virtuelle maskiner eller test eksternt på Mac, IOS, Android eller Windows-enheten @ http://dev.modern.ie/.