La primera versión de Flight Simulator se distribuyó en 1980 para Apple II y, sorprendentemente, ¡fue en 3D! Ese fue un logro notable. Es aún más sorprendente cuando se considera que todo el 3D se hizo a mano, el resultado de cálculos meticulosos y comandos de píxeles de bajo nivel. Cuando Bruce Atwick abordó las primeras versiones de Flight Simulator, no solo no había marcos 3D, sino que ¡no había marcos en absoluto! Esas versiones del juego se escribieron en su mayoría en ensamblaje, a solo un paso de los ceros y ceros que fluyen a través de una CPU.
Cuando decidimos reinventar Flight Simulator (o Flight Arcade como lo llamamos) para la web y demostrar lo que es posible en el nuevo navegador Microsoft Edge y el motor de renderizado EdgeHTML, no pudimos evitar pensar en el contraste de la creación de 3D. y el antiguo Flight Sim, el nuevo Flight Sim, el antiguo Internet Explorer, el nuevo Microsoft Edge. La codificación moderna parece casi lujosa a medida que esculpimos mundos 3D en WebGL con grandes marcos como Babylon.js. Nos permite enfocarnos en problemas de muy alto nivel..
En este artículo, compartiré nuestro enfoque de uno de estos divertidos desafíos: una forma sencilla de crear un terreno a gran escala de apariencia realista..
Nota: el código interactivo y los ejemplos de este artículo también se encuentran en Flight Arcade / Learn.
La mayoría de los objetos 3D se crean con herramientas de modelado, y por una buena razón. Crear objetos complejos (como un avión o incluso un edificio) es difícil de hacer en el código. Las herramientas de modelado casi siempre tienen sentido, ¡pero hay excepciones! Uno de esos casos podría ser el de las colinas onduladas de la isla de Flight Arcade. Terminamos usando una técnica que nos pareció más simple y posiblemente aún más intuitiva: un mapa de altura..
Un mapa de altura es una forma de usar una imagen bidimensional regular para describir el relieve de elevación de una superficie como una isla u otro terreno. Es una forma bastante común de trabajar con datos de elevación, no solo en juegos sino también en sistemas de información geográfica (GIS) utilizados por cartógrafos y geólogos..
Para ayudarlo a tener una idea de cómo funciona esto, consulte el mapa de altura en esta demostración interactiva. Intente dibujar en el editor de imágenes y luego eche un vistazo al terreno resultante.
El concepto detrás de un mapa de altura es bastante sencillo. En una imagen como la de arriba, el negro puro es el "piso" y el blanco puro es el pico más alto. Los colores en escala de grises en el medio representan elevaciones correspondientes. Esto nos da 256 niveles de elevación, que es un montón de detalles para nuestro juego. Las aplicaciones de la vida real pueden usar todo el espectro de colores para almacenar significativamente más niveles de detalle (2564 = 4,294,967,296 niveles de detalle si incluye un canal alfa).
Un mapa de altura tiene algunas ventajas sobre una malla poligonal tradicional:
Primero, los mapas de altura son mucho más compactos. Sólo se almacenan los datos más significativos (la elevación). Será necesario convertirlo en un objeto 3D mediante programación, pero este es el comercio clásico: ahorras espacio ahora y pagas más tarde con el cálculo. Al almacenar los datos como una imagen, obtiene otra ventaja de espacio: puede aprovechar las técnicas de compresión de imagen estándar y hacer que los datos sean pequeños (en comparación)!
En segundo lugar, los mapas de altura son una forma conveniente de generar, visualizar y editar el terreno. Es bastante intuitivo cuando ves uno. Se siente un poco como mirar un mapa. Esto demostró ser particularmente útil para Flight Arcade. ¡Diseñamos y editamos nuestra isla en Photoshop! Esto hizo que sea muy simple hacer pequeños ajustes según sea necesario. Cuando, por ejemplo, queríamos asegurarnos de que la pista fuera completamente plana, nos aseguramos de pintar esa área en un solo color..
Puedes ver el mapa de altura para Flight Arcade a continuación. Vea si puede detectar las áreas "planas" que creamos para la pista y el pueblo.
El mapa de altura para la isla de Arcade de vuelo. Fue creado en Photoshop y se basa en la "isla grande" en una famosa cadena de islas del Océano Pacífico. Cualquier conjetura?Una textura que se asigna a la malla 3D resultante después de que se decodifique el mapa de altura. Más sobre eso abajo.Construimos Flight Arcade con Babylon.js, y Babylon nos dio un camino bastante sencillo desde el mapa de altura a 3D. Babylon proporciona una API para generar una geometría de malla a partir de una imagen de mapa de altura:
var ground = BABYLON.Mesh.CreateGroundFromHeightMap ('your-mesh-name', '/path/to/heightmap.png', 100, // ancho de la malla del suelo (eje x) 100, // profundidad de la malla del suelo (eje z) 40, // número de subdivisiones 0, // min altura 50, // escena de altura máxima, falso, // actualizable? nulo // devolución de llamada cuando la malla está lista);
La cantidad de detalle está determinada por la propiedad de esa subdivisión. Es importante tener en cuenta que el parámetro se refiere al número de subdivisiones en cada lado de la imagen del mapa de altura, no al número total de celdas. Por lo tanto, aumentar ligeramente este número puede tener un gran efecto en el número total de vértices en su malla.
En la siguiente sección aprenderemos cómo texturizar el terreno, pero al experimentar con la creación de mapas de altura, es útil ver la estructura alámbrica. Aquí está el código para aplicar una textura de estructura de alambre simple para que sea fácil ver cómo los datos de mapa de altura se convierten en los vértices de nuestra malla:
// material de alambre simple var material = nuevo BABYLON.StandardMaterial ('material de suelo', escena); material.wireframe = true; material de suelo = material;
Una vez que tuvimos un modelo, mapear una textura fue relativamente sencillo. Para Flight Arcade, simplemente creamos una imagen muy grande que coincidía con la isla en nuestro mapa de altura. La imagen se estira sobre los contornos del terreno, por lo que la textura y el mapa de altura permanecen correlacionados. Esto fue realmente fácil de visualizar y, una vez más, todo el trabajo de producción se realizó en Photoshop..
La imagen original de la textura fue creada en 4096x4096. ¡Eso es bastante grande! (Finalmente, redujimos el tamaño en un nivel a 2048x2048 para mantener la descarga razonable, pero todo el desarrollo se realizó con la imagen a tamaño completo). Aquí hay una muestra de píxeles completos de la textura original..
Una muestra de píxeles completos de la textura original de la isla. Todo el pueblo está a solo unos 300 px cuadrados..Esos rectángulos representan los edificios de la ciudad en la isla. Rápidamente notamos una discrepancia en el nivel de detalle de texturas que podríamos alcanzar entre el terreno y los otros modelos 3D. Incluso con la textura de nuestra isla gigante, la diferencia era perturbadora.!
Para solucionar esto, "combinamos" detalles adicionales en la textura del terreno en forma de ruido aleatorio. Puedes ver el antes y el después a continuación. Observe cómo el ruido adicional mejora la apariencia de los detalles en el terreno..
Creamos un shader personalizado para agregar el ruido. Los sombreadores le dan una increíble cantidad de control sobre la representación de una escena 3D de WebGL, y este es un gran ejemplo de cómo un sombreador puede ser útil..
Un sombreador de WebGL consta de dos piezas principales: los sombreados de vértice y fragmento. El objetivo principal del sombreador de vértices es asignar los vértices a una posición en el marco renderizado. El sombreador de fragmentos (o píxeles) controla el color resultante de los píxeles.
Los Shaders están escritos en un lenguaje de alto nivel llamado GLSL (Graphics Library Shader Language), que se parece a C. Este código se ejecuta en la GPU. Para ver en profundidad cómo funcionan los sombreadores, consulte este tutorial sobre cómo crear su propio sombreador personalizado para Babylon.js, o consulte esta guía para principiantes sobre cómo codificar sombreadores gráficos.
No estamos cambiando la forma en que se mapea nuestra textura en la malla del suelo, por lo que nuestro sombreado de vértices es bastante simple. Simplemente calcula la asignación estándar y asigna la ubicación de destino.
flotador mediump de precisión; // Atributos atributo posición vec3; atributo vec3 normal; atributo vec2 uv; // Uniformes uniform mat4 worldViewProjection; // Variando variando vec4 vPosition; variando vec3 vNormal; variando vec2 vUV; void main () vec4 p = vec4 (posición, 1.0); vPosición = p; vNormal = normal; vUV = uv; gl_Position = worldViewProjection * p;
Nuestro shader de fragmentos es un poco más complicado. Combina dos imágenes diferentes: la base y la mezcla de imágenes. La imagen base se mapea a través de toda la malla de tierra. En Flight Arcade, esta es la imagen en color de la isla. La imagen de fusión es la imagen de ruido pequeño utilizada para dar al suelo un poco de textura y detalle a distancias cercanas. El sombreador combina los valores de cada imagen para crear una textura combinada en toda la isla.
La lección final en Flight Arcade se lleva a cabo en un día nublado, por lo que la otra tarea que tiene nuestro sombreador de píxeles es ajustar el color para simular la niebla. El ajuste se basa en qué tan lejos está el vértice de la cámara, y los píxeles distantes están más "ocultos" por la niebla. Verás este cálculo de distancia en el calcFogFactor
función por encima del código de sombreado principal.
#ifdef GL_ES precisión highp float; #endif uniforme mat4 worldView; variable vec4 vPosition; variando vec3 vNormal; variando vec2 vUV; // Refs uniforme sampler2D baseSampler; sampler2D blendSampler uniforme; mezcla de flotador uniformeScaleU; mezcla de flotador uniformeScaleV; #define FOGMODE_NONE 0. #define FOGMODE_EXP 1. #define FOGMODE_EXP2 2. #define FOGMODE_LINEAR 3. #define E 2.71828 uniform vec4 vFogInfos; uniforme vec3 vFogColor; float calcFogFactor () // obtiene la distancia de la cámara al vértice 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); pinza de retorno (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); // multiplicar el tipo de modo de fusión vec4 color = baseColor * blendColor; // factor en color de niebla float fog = calcFogFactor (); color.rgb = fog * color.rgb + (1.0 - fog) * vFogColor; gl_FragColor = color;
La última pieza de nuestro Blend shader personalizado es el código JavaScript utilizado por Babylon. El propósito principal de este código es preparar los parámetros pasados a nuestros sombreadores de vértices y píxeles..
function BlendMaterial (nombre, escena, opciones) this.name = name; this.id = nombre; this.options = opciones; this.blendScaleU = options.blendScaleU || 1; this.blendScaleV = options.blendScaleV || 1; this._scene = escena; scene.materials.push (esto); var asset = options.assetManager; var textureTask = asset.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 = verdadero;, esto); textureTask = asset.addTextureTask ('blend-material-blend-task', options.blendImage); textureTask.onSuccess = _.bind (function (task) this.blendTexture = task.texture; this.blendTexture.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODR) un tipo de actividad en el tipo de trabajo. 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 (); // asegúrese de que las texturas estén listas si (! this.baseTexture ||! this.blendTexture) return false; if (! this._effect) this._effect = engine.createEffect (// shader name "blend", // atributos que describen la topología de vértices ["position", "normal", "uv"], // uniforms ( variables externas) definidas por los sombreadores ["worldViewProjection", "world", "blendScaleU", "blendScaleV", "vFogInfos", "vFogColor"], // samplers (objetos utilizados para leer texturas) ["baseSampler", "blendSampler "], // opcional definir cadena" "); if (! this._effect.isReady ()) return false; devuelve 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 ("mundo", mundo); 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 facilita la creación de un material personalizado basado en sombreadores. Nuestro material de mezcla es relativamente simple, pero realmente marcó una gran diferencia en el aspecto de la isla cuando el avión voló hacia el suelo. Los Shaders llevan el poder de la GPU al navegador, expandiendo los tipos de efectos creativos que puedes aplicar a tus escenas 3D. En nuestro caso, ese fue el toque final.!
Microsoft tiene un montón de aprendizaje gratuito sobre muchos temas de JavaScript de código abierto, y estamos en la misión de crear mucho más con Microsoft Edge. Aquí hay algunos para ver:
Y algunas herramientas gratuitas para comenzar: Visual Studio Code, Azure Trial y herramientas de prueba en varios navegadores, todas disponibles para Mac, Linux o Windows.
Este artículo es parte de la serie web dev tech de Microsoft. Estamos muy contentos de compartir Microsoft Edge y lo nuevo Motor de renderizado EdgeHTML con usted. Obtenga máquinas virtuales gratuitas o realice pruebas a distancia en su dispositivo Mac, iOS, Android o Windows @ http://dev.modern.ie/.