Este artículo se basará en el marco introducido en la primera parte de esta miniserie, agregando un importador de modelos y una clase personalizada para objetos 3D. También se introducirá a la animación y los controles. Hay mucho por lo que pasar, así que empecemos!
Este artículo se basa en gran medida en el primer artículo, por lo tanto, si aún no lo ha leído, debe comenzar allí primero..
La forma en que WebGL manipula los elementos en el mundo 3D es mediante el uso de fórmulas matemáticas conocidas como transformaciones. Entonces, antes de comenzar a construir la clase 3D, les mostraré algunos de los diferentes tipos de transformaciones y cómo se implementan..
Hay tres transformaciones básicas al trabajar con objetos 3D.
Cada una de estas funciones se puede realizar en el eje X, Y o Z, haciendo una posibilidad total de nueve transformaciones básicas. Todo esto afecta a la matriz de transformación 4x4 del objeto 3D de diferentes maneras. Para realizar múltiples transformaciones en el mismo objeto sin problemas de superposición, tenemos que multiplicar la transformación en la matriz del objeto y no aplicarla directamente a la matriz del objeto. Moverse es lo más fácil de hacer, así que comencemos allí.
Mover un objeto 3D es una de las transformaciones más fáciles que puedes hacer, porque hay un lugar especial en la matriz de 4x4 para él. No hay necesidad de ninguna matemática; simplemente coloca las coordenadas X, Y y Z en la matriz y listo. Si está mirando la matriz 4x4, entonces son los primeros tres números en la fila inferior. Además, debes saber que Z positivo está detrás de la cámara. Por lo tanto, un valor Z de -100 coloca el objeto 100 unidades hacia adentro en la pantalla. Lo compensaremos en nuestro código..
Para realizar múltiples transformaciones, no puede simplemente cambiar la matriz real del objeto; debe aplicar la transformación a una nueva matriz en blanco, conocida como identidad Matriz, y multiplicarla con la matriz principal..
La multiplicación de matrices puede ser un poco difícil de entender, pero la idea básica es que cada columna vertical se multiplica por la fila horizontal de la segunda matriz. Por ejemplo, el primer número sería la primera fila multiplicada por la primera columna de la otra matriz. El segundo número en la nueva matriz sería la primera fila multiplicada por la segunda columna de la otra matriz, y así sucesivamente.
El siguiente fragmento de código es el código que escribí para multiplicar dos matrices en JavaScript. Agrega esto a tu .js
Archivo que hiciste en la primera parte de esta serie:
función MH (A, B) var Suma = 0; para (var i = 0; i < A.length; i++) Sum += A[i] * B[i]; return Sum; function MultiplyMatrix(A, B) var A1 = [A[0], A[1], A[2], A[3]]; var A2 = [A[4], A[5], A[6], A[7]]; var A3 = [A[8], A[9], A[10], A[11]]; var A4 = [A[12], A[13], A[14], A[15]]; var B1 = [B[0], B[4], B[8], B[12]]; var B2 = [B[1], B[5], B[9], B[13]]; var B3 = [B[2], B[6], B[10], B[14]]; var B4 = [B[3], B[7], B[11], B[15]]; return [ MH(A1, B1), MH(A1, B2), MH(A1, B3), MH(A1, B4), MH(A2, B1), MH(A2, B2), MH(A2, B3), MH(A2, B4), MH(A3, B1), MH(A3, B2), MH(A3, B3), MH(A3, B4), MH(A4, B1), MH(A4, B2), MH(A4, B3), MH(A4, B4)];
No creo que esto requiera ninguna explicación, ya que son solo las matemáticas necesarias para la multiplicación de matrices. Vamos a pasar a la escala.
La escala de un modelo también es bastante simple: es una simple multiplicación. Tienes que multiplicar los primeros tres números diagonales por cualquiera que sea la escala. Una vez más, el orden es X, Y y Z. Entonces, si desea escalar su objeto para que sea dos veces más grande en los tres ejes, multiplicaría los elementos primero, sexto y undécimo de su matriz por 2.
La rotación es la transformación más difícil porque hay una ecuación diferente para cada uno de los tres ejes. La siguiente imagen muestra las ecuaciones de rotación para cada eje:
No te preocupes si esta imagen no tiene sentido para ti; revisaremos la implementación de JavaScript pronto.
Es importante tener en cuenta que importa el orden en el que realices las transformaciones; Diferentes órdenes producen diferentes resultados..
Es importante tener en cuenta que importa el orden en el que realices las transformaciones; Diferentes órdenes producen diferentes resultados. Si primero mueve su objeto y luego lo gira, WebGL girará su objeto como si fuera un murciélago, en lugar de girar el objeto en su lugar. Si gira primero y luego mueve su objeto, tendrá un objeto en la ubicación especificada, pero se enfrentará a la dirección que ingresó. Esto se debe a que las transformaciones se realizan alrededor del punto de origen (0,0,0) en el mundo 3D. No hay orden correcto o incorrecto. Todo depende del efecto que estés buscando..
Podría requerir más de una de cada transformación para hacer algunas animaciones avanzadas. Por ejemplo, si desea que una puerta se abra en sus bisagras, la movería de modo que estén en el eje Y (es decir, 0 en los ejes X y Z). Luego giraría en el eje Y para que la puerta girara sobre sus bisagras. Finalmente, lo moverías nuevamente a la ubicación deseada en tu escena.
Este tipo de animaciones son un poco más personalizadas para cada situación, así que no voy a hacer una función para ello. Sin embargo, haré una función con el orden más básico que es: escalar, girar y luego mover. Esto asegura que todo se encuentre en la ubicación especificada y en la dirección correcta..
Ahora que tiene una comprensión básica de las matemáticas detrás de todo esto y cómo funcionan las animaciones, vamos a crear un tipo de datos JavaScript para mantener nuestros objetos 3D.
Recuerde en la primera parte de esta serie que necesita tres matrices para dibujar un objeto 3D básico: la matriz de vértices, la matriz de triángulos y la matriz de texturas. Esa será la base de nuestro tipo de datos. También necesitamos variables para las tres transformaciones en cada uno de los tres ejes. Por último, necesitamos una variable para la imagen de textura e indicar si el modelo ha terminado de cargarse..
Aquí está mi implementación de un objeto 3D en JavaScript:
función GLObject (VertexArr, TriangleArr, TextureArr, ImageSrc) 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; this.Triangles = TriangleArr; this.TriangleCount = TriangleArr.length; this.TextureMap = TextureArr; this.Image = new Image (); this.Image.onload = function () this.ReadyState = true; ; this.Image.src = ImageSrc; esto.Listo = falso; // Añadir función de transformación aquí
He añadido dos variables "listas" separadas: una para cuando la imagen está lista y otra para el modelo. Cuando la imagen esté lista, prepararé el modelo convirtiéndolo en una textura WebGL y almacenando en búfer los tres arreglos en búferes WebGL. Esto acelerará nuestra aplicación, como se aplica al almacenamiento en búfer de los datos en cada ciclo de sorteo. Como convertiremos las matrices en búferes, debemos guardar el número de triángulos en una variable separada.
Ahora, agreguemos la función que calculará la matriz de transformación del objeto. Esta función tomará todas las variables locales y las multiplicará en el orden que mencioné anteriormente (escala, rotación y luego traducción). Puedes jugar un poco con este orden para diferentes efectos. Reemplace la // Añadir la función de transformación aquí
comenta con el siguiente código:
this.GetTransforms = function () // Crear una Matriz de Identidad en Blanco var TMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1 ]; // Escalado var Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1]; Temp [0] * = this.Scale.X; Temp [5] * = this.Scale.Y; Temp [10] * = this.Scale.Z; TMatrix = MultiplyMatrix (TMatrix, Temp); // Temperatura de rotación X = [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1]; var X = this.Rotation.X * (Math.PI / 180.0); Temp [5] = Math.cos (X); Temp [6] = Math.sin (X); Temp [9] = -1 * Math.sin (X); Temp [10] = Math.cos (X); TMatrix = MultiplyMatrix (TMatrix, Temp); // Temperatura de rotación Y = [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1]; var Y = this.Rotation.Y * (Math.PI / 180.0); Temp [0] = Math.cos (Y); Temp [2] = -1 * Math.sin (Y); Temp [8] = Math.sin (Y); Temp [10] = Math.cos (Y); TMatrix = MultiplyMatrix (TMatrix, Temp); // Temperatura Z giratoria = [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1]; var Z = this.Rotation.Z * (Math.PI / 180.0); Temp [0] = Math.cos (Z); Temp [1] = Math.sin (Z); Temp [4] = -1 * Math.sin (Z); Temp [5] = Math.cos (Z); TMatrix = MultiplyMatrix (TMatrix, Temp); // Temp. De movimiento = [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1]; Temp [12] = this.Pos.X; Temp [13] = this.Pos.Y; Temp [14] = this.Pos.Z * -1; devuelve MultiplyMatrix (TMatrix, Temp);
Debido a que las fórmulas de rotación se superponen entre sí, deben realizarse una a la vez. Esta función sustituye a la MakeTransform
Función del último tutorial, para que pueda eliminarlo de su script.
Ahora que hemos creado nuestra clase 3D, necesitamos una forma de cargar los datos. Haremos un simple modelo de importador que convertiremos. .obj
archivos en los datos necesarios para hacer uno de nuestros recién creados GLObject
objetos. Estoy usando el .obj
Formato de modelo porque almacena todos los datos en forma cruda y tiene muy buena documentación sobre cómo almacena la información. Si su programa de modelado 3D no admite la exportación a .obj
, entonces siempre puedes crear un importador para algún otro formato de datos. .obj
es un tipo de archivo 3D estándar; Entonces, no debería ser un problema. Alternativamente, también puede descargar Blender, una aplicación de modelado 3D multiplataforma gratuita que admite la exportación a .obj
En .obj
archivos, las dos primeras letras de cada línea nos dicen qué tipo de datos contiene esa línea. "v
"es para una" línea de coordenadas de vértice "Vermont
"es para una línea de" coordenadas de textura ", y"F
"es para la línea de mapeo. Con esta información, escribí la siguiente función:
función LoadModel (ModelName, CB) var Ajax = new XMLHttpRequest (); Ajax.onreadystatechange = function () if (Ajax.readyState == 4 && Ajax.status == 200) // Datos del modelo de análisis var Script = Ajax.responseText.split ("\ n"); Vértices var = []; var VerticeMap = []; var triángulos = []; Texturas var = []; var TextureMap = []; Normales var = []; var NormalMap = []; Contador var = 0;
Esta función acepta el nombre de un modelo y una función de devolución de llamada. La devolución de llamada acepta cuatro matrices: vértice, triángulo, textura y matrices normales. Todavía no he cubierto las normales, así que puedes ignorarlas por ahora. Los analizaré en el artículo de seguimiento, cuando hablemos de la iluminación..
El importador comienza creando una XMLHttpRequest
objeto y definiendo su onreadystatechange
controlador de eventos. Dentro del manejador, dividimos el archivo en sus líneas y definimos algunas variables. .obj
Los archivos primero definen todas las coordenadas únicas y luego definen su orden. Es por eso que hay dos variables para los vértices, texturas y normales. La variable de contador se usa para completar la matriz de triángulos porque .obj
Los archivos definen los triángulos en orden..
A continuación, tenemos que revisar cada línea del archivo y verificar qué tipo de línea es:
para (var I en Script) var Line = Script [I]; // If Vertice Line if (Line.substring (0, 2) == "v") var Row = Line.substring (2) .split (""); Vertices.push (X: parseFloat (Fila [0]), Y: parseFloat (Fila [1]), Z: parseFloat (Fila [2])); // Texture Line else if (Line.substring (0, 2) == "vt") var Row = Line.substring (3) .split (""); Textures.push (X: parseFloat (Fila [0]), Y: parseFloat (Fila [1])); // Normals Line else if (Line.substring (0, 2) == "vn") var Row = Line.substring (3) .split (""); Normals.push (X: parseFloat (Fila [0]), Y: parseFloat (Fila [1]), Z: parseFloat (Fila [2]));
Los primeros tres tipos de líneas son bastante simples; Contienen una lista de coordenadas únicas para los vértices, texturas y normales. Todo lo que necesitamos hacer es insertar estas coordenadas en sus matrices respectivas. El último tipo de línea es un poco más complicado porque puede contener varias cosas. Podría contener solo vértices, o vértices y texturas, o vértices, texturas y normales. Como tal, tenemos que verificar cada uno de estos tres casos. El siguiente código hace esto:
// Mapping Line else if (Line.substring (0, 2) == "f") var Row = Line.substring (2) .split (""); para (var T en Fila) // Eliminar entradas en blanco si (Fila [T]! = "") // Si se trata de una entrada de múltiples valores si (Fila [T] .indexOf ("/")! = -1) // Dividir los diferentes valores var TC = Fila [T] .split ("/"); // Incrementar la matriz de triángulos Triangles.push (Counter); Counter ++; // Inserte los vértices var index = parseInt (TC [0]) - 1; VerticeMap.push (Vértices [índice] .X); VerticeMap.push (Vértices [índice] .Y); VerticeMap.push (Vértices [índice] .Z); // Insertar el índice de texturas = parseInt (TC [1]) - 1; TextureMap.push (Texturas [índice] .X); TextureMap.push (Texturas [índice] .Y); // Si esta entrada tiene datos normales si (TC.length> 2) // Insertar índice de Normals = parseInt (TC [2]) - 1; NormalMap.push (Normals [index] .X); NormalMap.push (Normals [index] .Y); NormalMap.push (Normals [index] .Z); // Para filas con solo vértices else Triangles.push (Counter); // Incrementar el contador de la matriz de triángulos ++; índice var = parseInt (Fila [T]) - 1; VerticeMap.push (Vértices [índice] .X); VerticeMap.push (Vértices [índice] .Y); VerticeMap.push (Vértices [índice] .Z);
Este código es más largo de lo que es complicado. Aunque cubrí el escenario donde el .obj
el archivo solo contiene datos de vértice, nuestro marco requiere vértices y coordenadas de textura. Si un .obj
el archivo solo contiene datos de vértice, tendrá que agregarle manualmente los datos de las coordenadas de la textura.
Pasemos ahora los arrays a la función de devolución de llamada y terminemos el LoadModel
función:
// Devolver The Arrays CB (VerticeMap, Triangles, TextureMap, NormalMap); Ajax.open ("GET", ModelName + ".obj", true); Ajax.send ();
Algo que debes tener en cuenta es que nuestro marco de WebGL es bastante básico y solo dibuja modelos que están hechos de triángulos. Es posible que tenga que editar sus modelos 3D en consecuencia. Afortunadamente, la mayoría de las aplicaciones 3D tienen una función o complemento para triangular sus modelos por usted. Hice un modelo simple de una casa con mis habilidades básicas de modelado, y lo incluiré en los archivos de origen para que los use, si así lo desea..
Ahora vamos a modificar el Dibujar
Función del último tutorial para incorporar nuestro nuevo tipo de datos de objeto 3D:
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); 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 (); // 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)); // Dibuja los triángulos this.GL.drawElements (this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0); ;
La nueva función de sorteo primero verifica si el modelo ha sido preparado para WebGL. Si la textura se ha cargado, preparará el modelo para el dibujo. Llegaremos a la PrepareModel
Funciona en un minuto. Si el modelo está listo, conectará sus búferes a los sombreadores y cargará las matrices de perspectiva y transformación como lo hizo antes. La única diferencia real es que ahora toma todos los datos del objeto modelo.
los PrepareModel
La función simplemente convierte las matrices de textura y datos en variables compatibles con WebGL. Aquí está la función; Añádelo justo antes de la función de sorteo:
this.PrepareModel = function (Model) Model.Image = this.LoadTexture (Model.Image); // Convertir Arrays a buffers var Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Buffer); this.GL.bufferData (this.GL.ARRAY_BUFFER, nuevo Float32Array (Model.Vertices), this.GL.STATIC_DRAW); Model.Vertices = Buffer; Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ELEMENT_ARRAY_BUFFER, Buffer); this.GL.bufferData (this.GL.ELEMENT_ARRAY_BUFFER, nuevo Uint16Array (Model.Triangles), this.GL.STATIC_DRAW); Model.Triangles = Buffer; Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Buffer); this.GL.bufferData (this.GL.ARRAY_BUFFER, nuevo Float32Array (Model.TextureMap), this.GL.STATIC_DRAW); Model.TextureMap = Buffer; Modelo.Listo = verdadero; ;
Ahora nuestro marco está listo y podemos pasar a la página HTML.
Puedes borrar todo lo que está dentro de la guión
etiquetas, ya que ahora podemos escribir el código de manera más concisa gracias a nuestro nuevo GLObject
tipo de datos.
Este es el JavaScript completo:
var GL; edificio var; función Ready () GL = new WebGL ("GLCanvas", "FragmentShader", "VertexShader"); LoadModel ("House", función (VerticeMap, Triangles, TextureMap) Building = new GLObject (VerticeMap, Triangles, TextureMap, "House.png"); Building.Pos.Z = 650; // Mi modelo era un poco demasiado grande Building.Scale.X = 0.5; Building.Scale.Y = 0.5; Building.Scale.Z = 0.5; // And Backwards Building.Rotation.Y = 180; setInterval (Update, 33);); función Update () Building.Rotation.Y + = 0.2 GL.Draw (Building);
Cargamos un modelo y le decimos a la página que lo actualice aproximadamente treinta veces por segundo. los Actualizar
La función gira el modelo en el eje Y, lo que se logra al actualizar la Y del objeto. Rotación
propiedad. Mi modelo era un poco demasiado grande para la escena WebGL y estaba al revés, así que tuve que realizar algunos ajustes en el código.
A menos que esté haciendo algún tipo de presentación cinematográfica de WebGL, es probable que desee agregar algunos controles. Veamos cómo podemos agregar algunos controles de teclado a nuestra aplicación.
Esta no es realmente una técnica de WebGL tanto como una característica nativa de JavaScript, pero es útil para controlar y posicionar sus modelos 3D. Todo lo que tienes que hacer es agregar un detector de eventos al teclado keydown
o tecla Arriba
eventos y comprobar qué tecla se pulsó. Cada clave tiene un código especial, y una buena manera de averiguar qué código corresponde a la clave es registrar los códigos de la clave en la consola cuando se produce el evento. Vaya al área donde cargué el modelo y agregue el siguiente código justo después de setInterval
línea:
document.onkeydown = handleKeyDown;
Esto establecerá la función handleKeyDown
para manejar el keydown
evento. Aquí está el código para el handleKeyDown
función:
function handleKeyDown (event) // Puede descomentar la línea siguiente para averiguar el código de cada clave //alert(event.keyCode); if (event.keyCode == 37) // Left Arrow Key Building.Pos.X - = 4; else if (event.keyCode == 38) // Up Arrow Key Building.Pos.Y + = 4; else if (event.keyCode == 39) // Right Arrow Key Building.Pos.X + = 4; else if (event.keyCode == 40) // Building Key Key Building.Pos.Y - = 4;
Todo lo que hace esta función es actualizar las propiedades del objeto; El framework WebGL se encarga del resto..
¡No hemos terminado! En la tercera y última parte de esta miniserie, repasaremos diferentes tipos de iluminación, y cómo combinar todo con algunas cosas en 2D!
Gracias por leer y, como siempre, si tiene alguna pregunta, no dude en dejar un comentario a continuación.!