Più mani con JavaScript

La prima versione di Flight Simulator è stata distribuita nel 1980 per Apple II e, incredibilmente, era in 3D! E 'stato un risultato notevole. È ancora più sorprendente se si considera che tutto il 3D è stato fatto a mano, il risultato di calcoli meticolosi e comandi pixel di basso livello. Quando Bruce Atwick ha affrontato le prime versioni di Flight Simulator, non solo non c'erano framework 3D, ma non c'erano affatto framework! Quelle versioni del gioco erano per lo più scritte in assembly, a un solo passo da quelle e gli zeri che fluiscono attraverso una CPU.

Quando decidiamo di reimmaginare il Flight Simulator (o Flight Arcade come lo chiamiamo noi) per il Web e per dimostrare cosa è possibile nel nuovo motore Microsoft Edge e nel motore di rendering EdgeHTML, non abbiamo potuto fare a meno di pensare al contrasto della creazione 3D quindi e ora vecchio Flight Sim, nuovo Flight Sim, vecchio Internet Explorer, nuovo Microsoft Edge. La codifica moderna sembra quasi lussuosa mentre scolpiamo mondi 3D in WebGL con grandi framework come Babylon.js. Ci consente di concentrarci su problemi di altissimo livello. 

In questo articolo condividerò il nostro approccio a una di queste divertenti sfide: un modo semplice per creare un terreno su vasta scala dall'aspetto realistico.

Nota: il codice interattivo e gli esempi per questo articolo si trovano anche in Flight Arcade / Learn.

Modellazione e terreno 3D

La maggior parte degli oggetti 3D sono creati con strumenti di modellazione, e per una buona ragione. Creare oggetti complessi (come un aereo o anche un edificio) è difficile da fare in codice. Gli strumenti di modellazione hanno quasi sempre un senso, ma ci sono delle eccezioni! Uno di quelli potrebbe essere casi come le dolci colline dell'arcipelago di Flight Arcade. Abbiamo finito per utilizzare una tecnica che abbiamo trovato più semplice e forse anche più intuitiva: una heightmap.

Una heightmap è un modo per utilizzare un'immagine bidimensionale regolare per descrivere il rilievo in elevazione di una superficie come un'isola o un altro terreno. È un modo abbastanza comune per lavorare con i dati di elevazione, non solo nei giochi, ma anche nei sistemi di informazione geografica (GIS) usati dai cartografi e dai geologi.

Per aiutarti a farti un'idea di come funziona, controlla la heightmap in questa demo interattiva. Prova a disegnare nell'editor di immagini, quindi controlla il terreno risultante.

Il concetto dietro una heightmap è piuttosto semplice. In un'immagine come quella in alto, il nero puro è il "pavimento" e il bianco puro è il picco più alto. I colori della scala di grigi intermedi rappresentano le altezze corrispondenti. Questo ci dà 256 livelli di elevazione, che è un sacco di dettagli per il nostro gioco. Le applicazioni reali potrebbero utilizzare lo spettro dei colori completo per memorizzare molti più livelli di dettaglio (2564 = 4.294.967.296 livelli di dettaglio se includi un canale alfa).

Una heightmap presenta alcuni vantaggi rispetto a una mesh poligonale tradizionale:

Innanzitutto, le heightmap sono molto più compatte. Vengono memorizzati solo i dati più significativi (l'elevazione). Dovrà essere trasformato in un oggetto 3D a livello di codice, ma questo è lo scambio classico: si risparmia spazio ora e si paga in seguito con il calcolo. Memorizzando i dati come un'immagine, ottieni un altro vantaggio di spazio: puoi sfruttare le tecniche di compressione delle immagini standard e rendere i dati minuscoli (in confronto)!

In secondo luogo, heightmaps è un modo conveniente per generare, visualizzare e modificare il terreno. È piuttosto intuitivo quando ne vedi uno. Sembra un po 'come guardare una mappa. Ciò si è rivelato particolarmente utile per Flight Arcade. Abbiamo progettato e modificato la nostra isola proprio in Photoshop! Ciò ha reso molto semplice apportare piccole modifiche, se necessario. Quando, per esempio, volevamo essere sicuri che la pista fosse completamente piana, abbiamo semplicemente fatto in modo di dipingere su quell'area in un unico colore.

Puoi vedere la heightmap per Flight Arcade di seguito. Vedi se riesci a individuare le aree "piatte" che abbiamo creato per la pista e il villaggio.

Il heightmap per l'isola di Flight Arcade. È stato creato in Photoshop e si basa sulla "grande isola" in una famosa catena di isole dell'Oceano Pacifico. Qualche supposizione?Una trama che viene mappata sulla mesh 3D risultante dopo la decodifica della heightmap. Maggiori informazioni su quello sotto.

Decodifica della mappa altezza

Abbiamo creato Flight Arcade con Babylon.js e Babylon ci ha fornito un percorso piuttosto semplice da heightmap a 3D. Babylon fornisce un'API per generare una geometria mesh da un'immagine heightmap:

var ground = BABYLON.Mesh.CreateGroundFromHeightMap ('your-mesh-name', '/path/to/heightmap.png', 100, // larghezza della maglia rettificata (asse x) 100, // profondità della maglia rettificata (asse z) 40, // numero di suddivisioni 0, // altezza minima 50, // altezza massima scena, falso, // aggiornabile? null // richiamata quando la mesh è pronta);

La quantità di dettagli è determinata dalla proprietà di quella suddivisione. È importante notare che il parametro si riferisce al numero di suddivisioni su ciascun lato dell'immagine heightmap, non al numero totale di celle. Quindi aumentare leggermente questo numero può avere un grande effetto sul numero totale di vertici nella mesh.

  • 20 suddivisioni = 400 celle
  • 50 suddivisioni = 2.500 celle
  • 100 suddivisioni = 10.000 celle
  • 500 suddivisioni = 250.000 celle
  • 1.000 suddivisioni = 1.000.000 di celle

Nella prossima sezione impareremo come strutturare il terreno, ma quando si sperimenta la creazione di heightmap, è utile vedere il wireframe. Ecco il codice per applicare una semplice trama wireframe, quindi è facile vedere come i dati heightmap vengono convertiti nei vertici della nostra mesh:

// materiale wireframe semplice var material = new BABYLON.StandardMaterial ('ground-material', scene); material.wireframe = true; ground.material = material;

Creazione del dettaglio della trama

Una volta ottenuto un modello, la mappatura di una trama era relativamente semplice. Per Flight Arcade, abbiamo semplicemente creato un'immagine molto grande che corrisponde all'isola nella nostra heightmap. L'immagine viene tesa sui contorni del terreno, quindi la trama e la mappa dell'altezza rimangono correlate. Questo è stato davvero facile da visualizzare e, ancora una volta, tutto il lavoro di produzione è stato fatto in Photoshop.

L'immagine della texture originale è stata creata a 4096x4096. È abbastanza grande! (Alla fine abbiamo ridotto le dimensioni di un livello a 2048x2048 per mantenere il download ragionevole, ma tutto lo sviluppo è stato fatto con l'immagine a dimensione intera.) Ecco un campione full-pixel dalla texture originale. 

Un campione full-pixel della texture dell'isola originale. L'intera città è solo di circa 300 px quadrati.

Questi rettangoli rappresentano gli edifici della città sull'isola. Abbiamo subito notato una discrepanza nel livello dei dettagli di texturing che potremmo ottenere tra il terreno e gli altri modelli 3D. Anche con la nostra trama da isola gigante, la differenza appariva stranamente evidente!

Per risolvere il problema, abbiamo "miscelato" ulteriori dettagli nella struttura del terreno sotto forma di rumore casuale. Puoi vedere il prima e il dopo di seguito. Nota come il rumore aggiuntivo migliora l'aspetto dei dettagli nel terreno.

Abbiamo creato uno shader personalizzato per aggiungere il rumore. Gli shader ti danno un'incredibile quantità di controllo sul rendering di una scena WebGL 3D, e questo è un ottimo esempio di come uno shader può essere utile.

Uno shader WebGL consiste di due parti principali: gli shader dei vertici e dei frammenti. L'obiettivo principale del vertex shader è quello di mappare i vertici in una posizione nella cornice renderizzata. Lo shader del frammento (o pixel) controlla il colore risultante dei pixel.

Gli shader sono scritti in un linguaggio di alto livello chiamato GLSL (Graphics Library Shader Language), che assomiglia a C. Questo codice viene eseguito sulla GPU. Per uno sguardo approfondito su come funzionano gli shader, consulta questo tutorial su come creare il tuo shader personalizzato per Babylon.js, o consulta questa guida per principianti per la codifica degli shader grafici.

The Vertex Shader

Non stiamo cambiando il modo in cui la nostra trama viene mappata sulla mesh di base, quindi il nostro vertex shader è piuttosto semplice. Calcola semplicemente la mappatura standard e assegna la posizione di destinazione.

galleggiante mediocre di precisione; // Attributi attributo posizione vec3; attributo vec3 normale; attributo vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Variabile vec4 vPosition variabile; variando vec3 vNormal; vec2 vUV variabile; void main () vec4 p = vec4 (position, 1.0); vPosition = p; vNormal = normale; vUV = uv; gl_Position = worldViewProjection * p; 

The Fragment Shader

Il nostro shader di frammenti è un po 'più complicato. Combina due immagini diverse: la base e le immagini miste. L'immagine di base è mappata sull'intera rete di base. In Flight Arcade, questa è l'immagine a colori dell'isola. L'immagine di fusione è la piccola immagine di rumore usata per dare al suolo un po 'di consistenza e dettaglio a distanze ravvicinate. Lo shader combina i valori di ciascuna immagine per creare una trama combinata in tutta l'isola.

L'ultima lezione in Flight Arcade si svolge in una giornata di nebbia, quindi l'altro compito del nostro shader pixel è di regolare il colore per simulare la nebbia. La regolazione si basa sulla distanza del vertice dalla fotocamera, con i pixel distanti che sono più "oscurati" dalla nebbia. Vedrai questo calcolo della distanza nel calcFogFactor funzione sopra il codice principale dello shader.

#ifdef GL_ES precision highp float; #endif uniform world4; vec4 vPosition variabile; variando vec3 vNormal; vec2 vUV variabile; // Refs uniforme sampler2D baseSampler; sampler2D blendSampler uniforme; uniforme float blendScaleU; uniforme float blendScaleV; # define FOGMODE_NONE 0. #define FOGMODE_EXP 1. #define FOGMODE_EXP2 2. #define FOGMODE_LINEAR 3. #define E 2.71828 uniforme vec4 vFogInfos; uniforme vec3 vFogColor; float calcFogFactor () // ottiene la distanza dalla telecamera al vertice float 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);  morsetto di ritorno (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); // moltiplicare la modalità di fusione vec4 color = baseColor * blendColor; // fattore in nebbia color float fog = calcFogFactor (); color.rgb = fog * color.rgb + (1.0 - fog) * vFogColor; gl_FragColor = color; 

Il pezzo finale per il nostro shader Blend personalizzato è il codice JavaScript utilizzato da Babylon. Lo scopo principale di questo codice è di preparare i parametri passati ai nostri vertex e pixel shader.

funzione BlendMaterial (nome, scena, opzioni) this.name = nome; this.id = nome; this.options = opzioni; this.blendScaleU = options.blendScaleU || 1; this.blendScaleV = options.blendScaleV || 1; this._scene = scena; scene.materials.push (questo); var assets = options.assetManager; var textureTask = assets.addTextureTask ('blend-material-base-task', options.baseImage); textureTask.onSuccess = _.bind (function (task) this.baseTexture = task.texture; this.baseTexture.uScale = 1; this.baseTexture.vScale = 1; if (options.baseHasAlpha) this.baseTexture.hasAlpha = vero;, questo); textureTask = assets.addTextureTask ('blend-material-blend-task', options.blendImage); textureTask.onSuccess = _.bind (function (task) this.blendTexture = task.texture; this.blendTexture.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE; this.blendTexture.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE;, this);  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 = function (mesh) var engine = this._scene.getEngine (); // assicurati che le trame siano pronte se (! this.baseTexture ||! this.blendTexture) return false;  if (! this._effect) this._effect = engine.createEffect (// nome shader "blend", // attributi che descrivono la topologia dei vertici ["posizione", "normale", "uv"], // uniformi ( variabili esterne) definite dagli shader ["worldViewProjection", "world", "blendScaleU", "blendScaleV", "vFogInfos", "vFogColor"], // sampler (oggetti usati per leggere le trame) ["baseSampler", "blendSampler "], // facoltativo definisce la stringa" ");  if (! this._effect.isReady ()) return false;  return true; ; BlendMaterial.prototype.bind = function (world, mesh) 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 ("mondo", mondo); this._effect.setMatrix ("worldViewProjection", world.multiply (scene.getTransformMatrix ())); // Textures 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 semplifica la creazione di un materiale personalizzato basato su shader. Il nostro materiale Blend è relativamente semplice, ma ha davvero fatto una grande differenza nell'aspetto dell'isola quando l'aereo volava basso verso il basso. Gli shader portano la potenza della GPU al browser, espandendo i tipi di effetti creativi che puoi applicare alle tue scene 3D. Nel nostro caso, questo è stato il tocco finale!

Più mani con JavaScript

Microsoft ha un sacco di apprendimento gratuito su molti argomenti JavaScript open source, e siamo in missione per creare molto di più con Microsoft Edge. Ecco alcuni da verificare:

  • Microsoft Edge Web Summit 2015 (una serie completa di cosa aspettarsi con il nuovo browser, nuove funzionalità della piattaforma Web e relatori ospiti della comunità)
  • Best of // BUILD / e Windows 10 (incluso il nuovo motore JavaScript per siti e app)
  • Avanzare JavaScript senza rompere il Web (keynote recente di Christian Heilmann)
  • App Web ospitate e innovazioni della piattaforma Web (un'immersione profonda su argomenti come manifoldJS)
  • Suggerimenti pratici sulle prestazioni per rendere il tuo HTML / JavaScript più veloce (una serie in sette parti dal design reattivo ai giochi casuali all'ottimizzazione delle prestazioni)
  • La piattaforma web moderna JumpStart (i fondamenti di HTML, CSS e JavaScript)

E alcuni strumenti gratuiti per iniziare: Visual Studio Code, Azure Trial e strumenti di test cross-browser, tutti disponibili per Mac, Linux o Windows.

Questo articolo fa parte della serie di web dev tech di Microsoft. Siamo entusiasti di condividere Microsoft Edge e il nuovo Motore di rendering EdgeHTML con te. Ottieni macchine virtuali gratuite o test in remoto sul tuo dispositivo Mac, iOS, Android o Windows @ http://dev.modern.ie/.