Bienvenido de nuevo a esta tercera y última entrega en nuestra miniserie de WebGL Essentials. En esta lección, analizaremos la iluminación y agregaremos objetos 2D a su escena. Hay mucha información nueva aquí, así que vamos a bucear directamente!
La iluminación puede ser el aspecto más técnico y difícil de entender de una aplicación 3D. Una comprensión firme de la iluminación es absolutamente esencial..
Antes de entrar en los diferentes tipos de técnicas de luz y código, es importante saber cómo funciona la luz en el mundo real. Cada fuente de luz (por ejemplo, una bombilla, el sol, etc.) genera partículas llamadas fotones. Estos fotones rebotan alrededor de los objetos hasta que finalmente entran en nuestros ojos. Nuestros ojos convierten los fotones para producir una "imagen" visual. Así es como lo vemos. La luz también es aditiva, lo que significa que un objeto con más color es más brillante que un objeto sin color (negro). El negro es la ausencia total de color, mientras que el blanco contiene todos los colores. Esta es una distinción importante cuando se trabaja con luces muy brillantes o "sobre saturadas".
El brillo es solo un principio que tiene múltiples estados. La reflexión, por ejemplo, puede tener una variedad de niveles diferentes. Un objeto, como un espejo, puede ser completamente reflectivo, mientras que otros objetos pueden tener una superficie mate. La transparencia determina cómo los objetos doblan la luz y causan refracción; un objeto puede ser completamente transparente mientras que otros pueden ser opacos (o en cualquier etapa intermedia).
La lista continúa, pero creo que ya puedes ver que la luz no es simple.
Si quisieras que incluso una pequeña escena simulara la luz real, se ejecutaría en aproximadamente 4 cuadros por hora, y eso es en una computadora de gran potencia. Para solucionar este problema, los programadores utilizan trucos y técnicas para simular una iluminación semi-realista a una velocidad de cuadro razonable. Tienes que llegar a algún tipo de compromiso entre el realismo y la velocidad. Echemos un vistazo a algunas de estas técnicas..
Antes de comenzar a elaborar diferentes técnicas, me gustaría darle un pequeño aviso legal. Existe una gran controversia sobre los nombres exactos de las diferentes técnicas de iluminación, y diferentes personas le darán diferentes explicaciones sobre qué es "Ray Casting" o "Light Mapping". Entonces, antes de que empiece a recibir el correo de odio, me gustaría decir que voy a usar los nombres que aprendí; Algunas personas podrían no estar de acuerdo con mis títulos exactos. En cualquier caso, lo importante es saber cuáles son las diferentes técnicas. Así que sin más preámbulos, vamos a empezar..
Tienes que llegar a algún tipo de compromiso entre el realismo y la velocidad..
El trazado de rayos es una de las técnicas de iluminación más realistas, pero también es una de las más costosas. El trazado de rayos emula luz real; emite "fotones" o "rayos" de la fuente de luz y los rebota. En la mayoría de las implementaciones de trazado de rayos, los rayos provienen de la "cámara" y rebotan en la escena en la dirección opuesta. Esta técnica se usa generalmente en películas o escenas que pueden reproducirse antes de tiempo. Esto no quiere decir que no pueda usar el trazado de rayos en una aplicación en tiempo real, pero hacerlo lo obliga a atenuar otras cosas en la escena. Por ejemplo, puede que tenga que reducir la cantidad de "rebotes" que deben realizar los rayos, o puede asegurarse de que no haya objetos que tengan superficies reflectantes o refractivas. El trazado de rayos también puede ser una opción viable si su aplicación tiene muy pocas luces y objetos..
Si tiene una aplicación en tiempo real, puede precompilar partes de su escena.
Si las luces en su aplicación no se mueven o solo se mueven en un área pequeña a la vez, puede precompilar la iluminación con un algoritmo muy avanzado de trazado de rayos y recalcular un área pequeña alrededor de la fuente de luz móvil. Por ejemplo, si estás haciendo un juego en el que las luces no se mueven, puedes precompilar el mundo con todas las luces y efectos deseados. Luego, puedes agregar una sombra alrededor de tu personaje cuando se mueva. Esto produce un aspecto de muy alta calidad con una cantidad mínima de procesamiento.
El lanzamiento de rayos es muy similar al trazado de rayos, pero los "fotones" no rebotan objetos ni interactúan con diferentes materiales. En una aplicación típica, básicamente comenzarías con una escena oscura y luego dibujarías líneas desde la fuente de luz. Todo lo que golpea la luz está encendido; Todo lo demás permanece oscuro. Esta técnica es significativamente más rápida que el trazado de rayos, a la vez que te brinda un efecto de sombra realista. Pero el problema con el lanzamiento de rayos es su restricción; no tiene mucho espacio para trabajar cuando intenta agregar efectos como reflejos. Por lo general, tiene que llegar a algún tipo de compromiso entre el lanzamiento de rayos y el trazado de rayos, equilibrando la velocidad y los efectos visuales.
El principal problema con estas dos técnicas es que WebGL no le da acceso a ningún vértice excepto el activo en ese momento..
Esto significa que tiene que realizar todo en la CPU (como se adjunta a la tarjeta gráfica), o ha creado un segundo sombreador que calcula toda la iluminación y almacena la información en una textura falsa. A continuación, deberá descomprimir los datos de textura de nuevo en la información de iluminación y asignarlos a los vértices. Básicamente, la versión actual de WebGL no es muy adecuada para esto. No digo que no se pueda hacer, solo digo que WebGL no te ayudará.
El trazado de rayos también puede ser una opción viable si su aplicación tiene muy pocas luces y objetos..
Una alternativa mucho mejor a la emisión de rayos en WebGL se llama mapeo de sombras. Te da el mismo efecto que el lanzamiento de rayos, pero utiliza un enfoque diferente. El mapeo instantáneo no resolverá todos sus problemas, pero WebGL está semi-optimizado para ello. Puede pensarlo como una especie de pirateo, pero el mapeo en la sombra se utiliza en aplicaciones reales de PC y consola..
Entonces, ¿qué es lo que preguntas??
Tienes que entender cómo WebGL interpreta sus escenas para responder a esta pregunta. WebGL empuja todos los vértices en el sombreado de vértices, que calcula las coordenadas finales para cada vértice después de que se aplican las transformaciones. Luego, para ahorrar tiempo, WebGL descarta los vértices que están ocultos detrás de otros objetos y solo dibuja los objetos esenciales. Si recuerdas cómo funciona el lanzamiento de rayos, solo proyecta rayos de luz sobre los objetos visibles. Así que ajustamos la "cámara" de nuestra escena a las coordenadas de la fuente de luz y la apuntamos en la dirección en la que queremos que se vea la luz. Luego, WebGL elimina automáticamente todos los vértices que no están a la vista de la luz. Luego podemos guardar estos datos y usarlos cuando renderizamos la escena para saber cuáles de los vértices están iluminados.
Esta técnica suena bien en el papel pero tiene algunas desventajas:
Todas estas técnicas requieren una buena cantidad de retoques con WebGL. Pero te mostraré una técnica muy básica para producir una luz difusa para dar un poco de personalidad a tus objetos. No lo llamaría luz realista, pero da definición a tus objetos. Esta técnica utiliza la matriz normal del objeto para calcular el ángulo de la luz en comparación con la superficie del objeto. Es rápido, eficiente y no requiere ningún tipo de piratería con WebGL. Empecemos.
Empecemos actualizando los shaders para incorporar la iluminación. Necesitamos agregar un valor booleano que determine si el objeto debe estar encendido o no. Luego, necesitamos el vértice normal real y lo transformamos para que se alinee con el modelo. Finalmente, necesitamos hacer una variable para pasar el resultado final al fragmento shader. Este es el nuevo sombreador de vértices:
Si no usamos luces, entonces solo pasamos un vértice en blanco al fragmento shader y su color permanece igual. Cuando se encienden las luces, calculamos el ángulo entre la dirección de la luz y la superficie del objeto utilizando la función de puntos en la normal, y multiplicamos el resultado por el color de la luz como una especie de máscara para superponer sobre el objeto..
Imagen de normales de superficie por Oleg Alexandrov.
Esto funciona porque las normales ya están perpendiculares a la superficie del objeto, y la función de punto nos da un número basado en el ángulo de la luz con respecto a la normal. Si la normal y la luz son casi paralelas, entonces la función de punto devuelve un número positivo, lo que significa que la luz está mirando hacia la superficie. Cuando la normal y la luz son perpendiculares, la superficie es paralela a la luz y la función devuelve cero. Cualquier cosa superior a 90 grados entre la luz y lo normal da como resultado un número negativo, pero lo filtramos con la función "máximo cero".
Ahora déjame mostrarte el fragmento shader:
Este sombreador es prácticamente el mismo de partes anteriores de la serie. La única diferencia es que multiplicamos el color de la textura por el nivel de luz. Esto aclara u oscurece diferentes partes del objeto, dándole algo de profundidad..
Eso es todo para los shaders, ahora vamos a la WebGL.js
archivar y modificar nuestras dos clases.
Comencemos con el GLObject
clase. Necesitamos agregar una variable para la matriz normal. Esto es lo que la parte superior de su GLObject
ahora debería verse como
función GLObject (VertexArr, TriangleArr, TextureArr, ImageSrc, NormalsArr) this.Pos = X: 0, Y: 0, Z: 0; this.Scale = X: 1.0, Y: 1.0, Z: 1.0; this.Rotation = X: 0, Y: 0, Z: 0; this.Vertices = VertexArr; // Array para mantener los datos normales this.Normals = NormalsArr; // El resto de GLObject continúa aquí.
Este código es bastante sencillo. Ahora volvamos al archivo HTML y agreguemos la matriz normal a nuestro objeto.
En el Listo()
En la función en la que cargamos nuestro modelo 3D, debemos agregar el parámetro para la matriz normal. Una matriz vacía significa que el modelo no contenía ningún dato normal, y tendremos que dibujar el objeto sin luz. En el caso de que la matriz normal contenga datos, simplemente los pasaremos a la GLObject
objeto.
También necesitamos actualizar el WebGL
clase. Necesitamos vincular las variables a los shaders justo después de cargar los shaders. Añadamos el vértice normal; Tu código ahora debería verse así:
// Enlace del atributo de posición de vértice desde Shader this.VertexPosition = this.GL.getAttribLocation (this.ShaderProgram, "VertexPosition"); this.GL.enableVertexAttribArray (this.VertexPosition); // Enlace del atributo de coordenadas de la textura desde Shader this.VertexTexture = this.GL.getAttribLocation (this.ShaderProgram, "TextureCoord"); this.GL.enableVertexAttribArray (this.VertexTexture); // Este es el nuevo atributo de matriz Normales this.VertexNormal = this.GL.getAttribLocation (this.ShaderProgram, "VertexNormal"); this.GL.enableVertexAttribArray (this.VertexNormal);
A continuación, vamos a actualizar el PrepareModel ()
Funciona y agrega algo de código para almacenar los datos normales cuando esté disponible. Agregue el nuevo código justo antes de Modelo.Listo
declaración en la parte inferior:
if (false! == Model.Normals) Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Buffer); this.GL.bufferData (this.GL.ARRAY_BUFFER, nuevo Float32Array (Model.Normals), this.GL.STATIC_DRAW); Model.Normals = Buffer; Model.Ready = true;
Por último, pero no menos importante, actualizar el real Dibujar
Funciona para incorporar todos estos cambios. Hay un par de cambios aquí, así que tengan paciencia conmigo. Voy a ir paso a paso a través de toda la función:
this.Draw = function (Model) if (Model.Image.ReadyState == true && Model.Ready == false) this.PrepareModel (Model); if (Model.Ready) this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.Vertices); this.GL.vertexAttribPointer (this.VertexPosition, 3, this.GL.FLOAT, false, 0, 0); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.TextureMap); this.GL.vertexAttribPointer (this.VertexTexture, 2, this.GL.FLOAT, false, 0, 0);
Hasta aquí es lo mismo que antes. Ahora viene la parte normal:
// Compruebe si hay Normales if (false! == Model.Normals) // Conecte el búfer normal al Shader this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Model.Normals); this.GL.vertexAttribPointer (this.VertexNormal, 3, this.GL.FLOAT, false, 0, 0); // Indica al sombreador que use la iluminación var UseLights = this.GL.getUniformLocation (this.ShaderProgram, "UseLights"); this.GL.uniform1i (UseLights, true); else // Incluso si nuestro objeto no tiene datos normales, todavía tenemos que pasar algo // Por lo tanto, en lugar de pasar los Vértices, esto es. this.GL.vertexAttribPointer (this.VertexNormal, 3, this.GL.FLOAT, false, 0, 0); // Indica al sombreador que use la iluminación var UseLights = this.GL.getUniformLocation (this.ShaderProgram, "UseLights"); this.GL.uniform1i (UseLights, false);
Verificamos si el modelo tiene datos normales. Si es así, conecta el búfer y establece el booleano. Si no, el shader todavía necesita algún tipo de datos o le dará un error. Así que en vez de eso, pasé el búfer de vértices y UseLight
booleano a falso
. Podrías evitar esto usando múltiples sombreadores, pero pensé que esto sería más simple para lo que estamos tratando de hacer..
this.GL.bindBuffer (this.GL.ELEMENT_ARRAY_BUFFER, Model.Triangles); // Generar la matriz de perspectiva var PerspectiveMatrix = MakePerspective (45, this.AspectRatio, 1, 1000.0); var TransformMatrix = Model.GetTransforms ();
De nuevo, esta parte de la función sigue siendo la misma..
var NormalsMatrix = MatrixTranspose (InverseMatrix (TransformMatrix));
Aquí calculamos la matriz de transformación normal. Voy a discutir el MatrixTranspose ()
y Matriz inversa ()
Funciona en un minuto. Para calcular la matriz de transformación para la matriz normal, debe transponer la matriz inversa de la matriz de transformación regular del objeto. Más sobre esto más adelante.
// Establecer la ranura 0 como la textura activa this.GL.activeTexture (this.GL.TEXTURE0); // Cargar en la textura a la memoria this.GL.bindTexture (this.GL.TEXTURE_2D, Model.Image); // Actualice The Texture Sampler en el fragmento de sombreado para usar la ranura 0 this.GL.uniform1i (this.GL.getUniformLocation (this.ShaderProgram, "uSampler"), 0); // Establezca las matrices de perspectiva y transformación var pmatrix = this.GL.getUniformLocation (this.ShaderProgram, "PerspectiveMatrix"); this.GL.uniformMatrix4fv (pmatrix, false, new Float32Array (PerspectiveMatrix)); var tmatrix = this.GL.getUniformLocation (this.ShaderProgram, "TransformationMatrix"); this.GL.uniformMatrix4fv (tmatrix, false, new Float32Array (TransformMatrix)); var nmatrix = this.GL.getUniformLocation (this.ShaderProgram, "NormalTransformation"); this.GL.uniformMatrix4fv (nmatrix, false, new Float32Array (NormalsMatrix)); // Dibuja los triángulos this.GL.drawElements (this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0); ;
Puede ver fácilmente la fuente de cualquier aplicación WebGL para obtener más información..
Este es el resto de la Dibujar()
función. Es casi lo mismo que antes, pero hay un código agregado que conecta la matriz normal con los sombreadores. Ahora, volvamos a esas dos funciones que usé para obtener la matriz de transformación normal..
los Matriz inversa ()
La función acepta una matriz y devuelve su matriz inversa. Una matriz inversa es una matriz que, cuando se multiplica por la matriz original, devuelve una matriz de identidad. Veamos un ejemplo de álgebra básica para aclarar esto. El inverso del número 4 es 1/4 porque cuando 1/4 x 4 = 1
. El "uno" equivalente en matrices es una matriz de identidad. por lo tanto, el Matriz inversa ()
La función devuelve la matriz de identidad para el argumento. Aquí está esta función:
función InverseMatrix (A) var s0 = A [0] * A [5] - A [4] * A [1]; var s1 = A [0] * A [6] - A [4] * A [2]; var s2 = A [0] * A [7] - A [4] * A [3]; var s3 = A [1] * A [6] - A [5] * A [2]; var s4 = A [1] * A [7] - A [5] * A [3]; var s5 = A [2] * A [7] - A [6] * A [3]; var c5 = A [10] * A [15] - A [14] * A [11]; var c4 = A [9] * A [15] - A [13] * A [11]; var c3 = A [9] * A [14] - A [13] * A [10]; var c2 = A [8] * A [15] - A [12] * A [11]; var c1 = A [8] * A [14] - A [12] * A [10]; var c0 = A [8] * A [13] - A [12] * A [9]; var invdet = 1.0 / (s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0); var B = []; B [0] = (A [5] * c5 - A [6] * c4 + A [7] * c3) * invdet; B [1] = (-A [1] * c5 + A [2] * c4 - A [3] * c3) * invdet; B [2] = (A [13] * s5 - A [14] * s4 + A [15] * s3) * invdet; B [3] = (-A [9] * s5 + A [10] * s4 - A [11] * s3) * invdet; B [4] = (-A [4] * c5 + A [6] * c2 - A [7] * c1) * invdet; B [5] = (A [0] * c5 - A [2] * c2 + A [3] * c1) * invdet; B [6] = (-A [12] * s5 + A [14] * s2 - A [15] * s1) * invdet; B [7] = (A [8] * s5 - A [10] * s2 + A [11] * s1) * invdet; B [8] = (A [4] * c4 - A [5] * c2 + A [7] * c0) * invdet; B [9] = (-A [0] * c4 + A [1] * c2 - A [3] * c0) * invdet; B [10] = (A [12] * s4 - A [13] * s2 + A [15] * s0) * invdet; B [11] = (-A [8] * s4 + A [9] * s2 - A [11] * s0) * invdet; B [12] = (-A [4] * c3 + A [5] * c1 - A [6] * c0) * invdet; B [13] = (A [0] * c3 - A [1] * c1 + A [2] * c0) * invdet; B [14] = (-A [12] * s3 + A [13] * s1 - A [14] * s0) * invdet; B [15] = (A [8] * s3 - A [9] * s1 + A [10] * s0) * invdet; retorno B;
Esta función es bastante complicada y, para decirte la verdad, no entiendo completamente por qué funcionan las matemáticas. Pero ya he explicado la esencia de esto arriba. No se me ocurrió esta función; Fue escrito en ActionScript por Robin Hilliard..
La proxima funcion, MatrixTranspose ()
, Es mucho más sencillo de entender. Devuelve la versión "transpuesta" de su matriz de entrada. En resumen, simplemente gira la matriz de lado. Aquí está el código:
función MatrixTranspose (A) return [A [0], A [4], A [8], A [12], A [1], A [5], A [9], A [13], A [ 2], A [6], A [10], A [14], A [3], A [7], A [11], A [15]];
En lugar de ir en filas horizontales (es decir, A [0], A [1], A [2] ...) esta función baja verticalmente (A [0], A [4], A [8] ...).
Eres bueno para ir después de agregar estas dos funciones a tu WebGL.js
archivo, y cualquier modelo que contenga los datos normales debe estar sombreado. Puedes jugar con la dirección y el color de la luz en el sombreado de vértice para obtener diferentes efectos.
Hay un último tema que deseo cubrir, y es agregar contenido 2D a nuestra escena. Agregar componentes 2D en una escena 3D puede tener muchos beneficios. Por ejemplo, se puede usar para mostrar información de coordenadas, un mini mapa, instrucciones para su aplicación y la lista continúa. Este proceso no es tan sencillo como podría pensar, así que echemos un vistazo.
HTML no le permitirá utilizar la API de WebGL y la API de 2D desde el mismo lienzo.
Podría estar pensando, "¿Por qué no usar simplemente el lienzo integrado en HTML5 2D API?" Bueno, el problema es que HTML no te permitirá usar la API de WebGL y la API 2D desde el mismo lienzo. Una vez que asigna el contexto del lienzo a WebGL, no puede usarlo con la API 2D. HTML5 simplemente devuelve nulo
Cuando intentas obtener el contexto 2D. Entonces, ¿cómo te las arreglas? Bueno, te daré dos opciones..
2.5D, para aquellos que no lo saben, es cuando colocas objetos 2D (objetos sin profundidad) en una escena 3D. Agregar texto a una escena es un ejemplo de 2.5D. Puede tomar el texto de una imagen y aplicarlo como una textura a un plano 3D, o puede obtener un modelo 3D para el texto y representarlo en su pantalla..
Los beneficios de este enfoque es que no necesita dos lonas, y sería más rápido dibujar si solo utilizara formas simples en su aplicación.
Pero para hacer cosas como texto, o bien necesitas imágenes de todo lo que quieres escribir, o un modelo 3D para cada letra (un poco exagerado, en mi opinión).
La alternativa es crear un segundo lienzo y superponerlo sobre el lienzo 3D. Prefiero este enfoque porque parece estar mejor equipado para dibujar contenido 2D. No voy a comenzar a crear un nuevo marco 2D, pero creamos un ejemplo simple en el que mostramos las coordenadas del modelo junto con su rotación actual. Agreguemos un segundo lienzo al archivo HTML justo después del lienzo de WebGL. Aquí está el nuevo lienzo junto con el actual:
También agregué algo de CSS en línea para superponer el segundo lienzo sobre el primero. El siguiente paso es crear una variable para el lienzo 2D y obtener su contexto. Voy a hacer esto en el Listo()
función. Su código actualizado debe verse algo como esto:
var GL; edificio var; var Canvas2D; Función Ready () // Gl Declaración y función del modelo de carga Aquí Canvas2D = document.getElementById ("2DCanvas"). getContext ("2d"); Canvas2D.fillStyle = "# 000";
En la parte superior, puedes ver que agregué una variable global para el lienzo 2D. Luego, agregué dos líneas al fondo del Listo()
función. La primera línea nueva obtiene el contexto 2D, y la segunda línea nueva establece el color en negro.
El último paso es dibujar el texto dentro de la Actualizar()
función:
función Actualizar () Building.Rotation.Y + = 0.3 // Borrar el lienzo del dibujo anterior Canvas2D.clearRect (0, 0, 600, 400); // Texto del título Canvas2D.font = "25px sans-serif"; Canvas2D.fillText ("Building", 20, 30); // Propiedades del objeto Canvas2D.font = "16px sans-serif"; Canvas2D.fillText ("X:" + Building.Pos.X, 20, 55); Canvas2D.fillText ("Y:" + Building.Pos.Y, 20, 75); Canvas2D.fillText ("Z:" + Building.Pos.Z, 20, 95); Canvas2D.fillText ("Rotación:" + Math.floor (Building.Rotation.Y), 20, 115); GL.GL.clear (16384 | 256); GL.Draw (Edificio);
Comenzamos girando el modelo en su eje Y, y luego borramos el lienzo 2D de cualquier contenido anterior. A continuación, establecemos el tamaño de fuente y dibujamos un texto para cada eje. los fillText ()
El método acepta tres parámetros: el texto a dibujar, la coordenada x y la coordenada y.
La simplicidad habla por sí misma. Esto puede haber sido un poco excesivo para dibujar un texto simple; Usted podría fácilmente haber escrito el texto en una posición o
elemento. Pero si estás haciendo algo como dibujar formas, sprites, una barra de salud, etc., entonces esta es probablemente tu mejor opción..
En el ámbito de los últimos tres tutoriales, creamos un motor 3D bastante bueno, aunque básico. A pesar de su naturaleza primitiva, te da una base sólida para trabajar. En el futuro, sugiero mirar otros marcos como three.js o glge para tener una idea de lo que es posible. Además, WebGL se ejecuta en el navegador, y puede ver fácilmente la fuente de cualquier aplicación WebGL para obtener más información..
Espero que haya disfrutado esta serie de tutoriales y, como siempre, deje sus comentarios y preguntas en la sección de comentarios a continuación..