WebGL Essentials Partie II

Cet article s'appuiera sur le cadre présenté dans la première partie de cette mini-série, en ajoutant un importateur de modèle et une classe personnalisée pour les objets 3D. Vous serez également initié à l'animation et aux contrôles. Il y a beaucoup à traverser, alors commençons!

Cet article s'appuie fortement sur le premier article, donc si vous ne l'avez pas encore lu, vous devriez commencer par là.

WebGL manipule des éléments dans le monde 3D en utilisant des formules mathématiques connues sous le nom de transformations. Donc, avant de commencer à construire la classe 3D, je vais vous montrer quelques types de transformations et leur implémentation..


Transformations

Il existe trois transformations de base lors de l'utilisation d'objets 3D.

  • En mouvement
  • Mise à l'échelle
  • Tournant

Chacune de ces fonctions peut être exécutée sur les axes X, Y ou Z, offrant ainsi une possibilité totale de neuf transformations de base. Tous ces éléments affectent la matrice de transformation 4x4 de l'objet 3D de différentes manières. Pour effectuer plusieurs transformations sur le même objet sans superposer de problèmes, nous devons multiplier la transformation dans la matrice de l'objet et ne pas l'appliquer directement à la matrice de l'objet. Le déménagement est le plus facile à faire, alors commençons par là.

Déplacement de A.K.A. "Traduction"

Le déplacement d'un objet 3D est l'une des transformations les plus faciles que vous puissiez effectuer, car la matrice 4x4 a une place spéciale dans celui-ci. Il n'y a pas besoin de maths; il suffit de mettre les coordonnées X, Y et Z dans la matrice et votre fait. Si vous regardez la matrice 4x4, alors ce sont les trois premiers chiffres de la rangée du bas. De plus, vous devez savoir que le positif Z est derrière la caméra. Par conséquent, une valeur Z de -100 place l'objet à 100 unités vers l'intérieur de l'écran. Nous allons compenser cela dans notre code.

Pour effectuer plusieurs transformations, vous ne pouvez pas simplement changer la matrice réelle de l'objet. vous devez appliquer la transformation à une nouvelle matrice vierge, appelée identité matrice, et le multiplier avec la matrice principale.

La multiplication de matrice peut être un peu difficile à comprendre, mais l'idée de base est que chaque colonne verticale est multipliée par la ligne horizontale de la seconde matrice. Par exemple, le premier nombre serait la première ligne multipliée par la première colonne de l'autre matrice. Le deuxième nombre dans la nouvelle matrice serait la première ligne multipliée par la deuxième colonne de l'autre matrice, etc..

L'extrait suivant est le code que j'ai écrit pour multiplier deux matrices en JavaScript. Ajoutez ceci à votre .js fichier que vous avez créé dans la première partie de cette série:

fonction MH (A, B) var Sum = 0; pour (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)]; 

Je ne pense pas que cela nécessite aucune explication, car il ne s’agit que du calcul nécessaire à la multiplication matricielle. Passons à la mise à l'échelle.

Mise à l'échelle

La mise à l'échelle d'un modèle est également assez simple: il s'agit d'une simple multiplication. Vous devez multiplier les trois premières diagonales par l’échelle. Encore une fois, l'ordre est X, Y et Z. Ainsi, si vous souhaitez redimensionner votre objet pour qu'il soit deux fois plus grand dans les trois axes, vous devez multiplier par 2 le premier, le sixième et le onzième éléments de votre tableau..

Tournant

La rotation est la transformation la plus délicate car il existe une équation différente pour chacun des trois axes. L'image suivante montre les équations de rotation pour chaque axe:

Ne vous inquiétez pas si cette image n’a aucun sens pour vous; nous examinerons bientôt l'implémentation de JavaScript.

Il est important de noter que l'ordre dans lequel vous effectuez les transformations est important; des ordres différents produisent des résultats différents.

Il est important de noter que l'ordre dans lequel vous effectuez les transformations est important; des ordres différents produisent des résultats différents. Si vous déplacez d'abord votre objet, puis le faites pivoter, WebGL fera pivoter votre objet comme une chauve-souris, au lieu de le faire pivoter à la place. Si vous faites d'abord pivoter puis déplacez votre objet, vous aurez un objet à l'emplacement spécifié, mais il fera face à la direction que vous avez entrée. En effet, les transformations sont effectuées autour du point d'origine - 0,0,0 - dans le monde 3D. Il n'y a pas de bon ou de mauvais ordre. Tout dépend de l'effet recherché.

Plusieurs transformations peuvent être nécessaires pour créer des animations avancées. Par exemple, si vous souhaitez qu'une porte s'ouvre sur ses charnières, vous devez la déplacer de manière à ce que ses charnières soient sur l'axe Y (c'est-à-dire 0 sur les axes X et Z). Vous feriez ensuite pivoter l'axe Y afin que la porte pivote sur ses charnières. Enfin, vous le déplaceriez à nouveau à l'emplacement souhaité dans votre scène.

Ce type d'animation est un peu plus personnalisé pour chaque situation, je ne vais donc pas en créer une fonction. Je vais toutefois créer une fonction avec l'ordre le plus fondamental, à savoir: la mise à l'échelle, la rotation, puis le déplacement. Cela garantit que tout se trouve à l'emplacement spécifié et dans le bon sens.

Maintenant que vous avez compris les bases mathématiques et le fonctionnement des animations, créons un type de données JavaScript pour contenir nos objets 3D..


Objets GL

N'oubliez pas, dans la première partie de cette série, que vous avez besoin de trois tableaux pour dessiner un objet 3D de base: le tableau de sommets, le tableau de triangles et le tableau de textures. Ce sera la base de notre type de données. Nous avons également besoin de variables pour les trois transformations sur chacun des trois axes. Enfin, nous avons besoin d’une variable pour l’image de texture et pour indiquer si le chargement du modèle est terminé..

Voici ma mise en œuvre d'un objet 3D en JavaScript:

fonction 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; this.Ready = false; // Ajout de la fonction de transformation ici

J'ai ajouté deux variables "prêtes" distinctes: une pour le moment où l'image est prête et une pour le modèle. Lorsque l'image est prête, je prépare le modèle en convertissant l'image en une texture WebGL et en mettant en tampon les trois tableaux en tampons WebGL. Cela accélérera notre application, par opposition à la mise en mémoire tampon des données à chaque cycle de tirage. Puisque nous allons convertir les tableaux en tampons, nous devons enregistrer le nombre de triangles dans une variable séparée.

Ajoutons maintenant la fonction qui calculera la matrice de transformation de l'objet. Cette fonction prendra toutes les variables locales et les multipliera dans l’ordre que j’ai mentionné précédemment (échelle, rotation, puis traduction). Vous pouvez jouer avec cet ordre pour obtenir différents effets. Remplace le // Ajout de la fonction de transformation ici commenter avec le code suivant:

this.GetTransforms = function () // Créer une matrice d’identité vide var TMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 ]; // Scaling var Temp = [1, 0, 0, 0, 0, 1, 0, 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); // Rotation de X Temp = [1, 0, 0, 0, 0, 1, 0, 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); // Rotation de Y Temp = [1, 0, 0, 0, 0, 1, 0, 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); // rotation Z Temp = [1, 0, 0, 0, 0, 1, 0, 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érature de déplacement = [1, 0, 0, 0, 0, 1, 0, 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; renvoyer MultiplyMatrix (TMatrix, Temp); 

Comme les formules de rotation se chevauchent, elles doivent être exécutées une par une. Cette fonction remplace le MakeTransform fonction du dernier tutoriel, vous pouvez donc le supprimer de votre script.


Importateur OBJ

Maintenant que notre classe 3D est construite, nous avons besoin d’un moyen de charger les données. Nous allons faire un importateur de modèle simple qui va convertir .obj fichiers dans les données nécessaires pour rendre l'un de nos nouvellement créé GLObject objets. J'utilise le .obj format de modèle car il stocke toutes les données sous une forme brute et dispose d’une très bonne documentation sur la manière dont il stocke les informations. Si votre programme de modélisation 3D ne prend pas en charge l'exportation vers .obj, alors vous pouvez toujours créer un importateur pour un autre format de données. .obj est un type de fichier 3D standard; alors, ça ne devrait pas être un problème. Vous pouvez également télécharger Blender, une application de modélisation 3D multiplate-forme gratuite prenant en charge l’exportation vers .obj

Dans .obj Dans les fichiers, les deux premières lettres de chaque ligne nous indiquent le type de données que cette ligne contient. "v"est pour une ligne" coordonnées du sommet ","Vermont"est pour une ligne" coordonnées de texture ", et"F"est pour la ligne de mappage. Avec cette information, j’ai écrit la fonction suivante:

function LoadModel (ModelName, CB) var Ajax = new XMLHttpRequest (); Ajax.onreadystatechange = function () if (Ajax.readyState == 4 && Ajax.status == 200) // Analyse des données de modèle var Script = Ajax.responseText.split ("\ n"); var vertices = []; var VerticeMap = []; var Triangles = []; var Textures = []; var TextureMap = []; var Normales = []; var NormalMap = []; var Counter = 0;

Cette fonction accepte le nom d'un modèle et une fonction de rappel. Le rappel accepte quatre tableaux: le sommet, le triangle, la texture et les tableaux normaux. Je n'ai pas encore couvert les normales, vous pouvez donc les ignorer pour le moment. Je les passerai en revue dans l'article de suivi, lorsque nous discuterons de l'éclairage.

L’importateur commence par créer un XMLHttpRequest objet et définissant sa onstateystatechange gestionnaire d'événements. Dans le gestionnaire, nous scindons le fichier en lignes et définissons quelques variables.. .obj Les fichiers définissent d'abord toutes les coordonnées uniques, puis définissent leur ordre. C'est pourquoi il existe deux variables pour les sommets, les textures et les normales. La variable counter est utilisée pour remplir le tableau de triangles car .obj les fichiers définissent les triangles dans l'ordre.

Ensuite, nous devons passer par chaque ligne du fichier et vérifier de quel type de ligne il s’agit:

 for (var I dans le script) var Line = Script [I]; // If Vertice Line if (Line.substring (0, 2) == "v") var Row = Line.substring (2) .split (""); Vertices.push (X: parseFloat (Row [0]), Y: parseFloat (Row [1]), Z: parseFloat (Row [2]));  // Ligne de texture else if (Line.substring (0, 2) == "vt") var Row = Line.substring (3) .split (""); Textures.push (X: parseFloat (Row [0]), Y: parseFloat (Row [1]));  // Normals Ligne else if (Line.substring (0, 2) == "vn") var Row = Line.substring (3) .split (""); Normals.push (X: parseFloat (Row [0]), Y: parseFloat (Row [1]), Z: parseFloat (Row [2])); 

Les trois premiers types de lignes sont assez simples. ils contiennent une liste de coordonnées uniques pour les sommets, les textures et les normales. Tout ce que nous avons à faire, c'est d'insérer ces coordonnées dans leurs tableaux respectifs. Le dernier type de ligne est un peu plus compliqué car il peut contenir plusieurs éléments. Il peut contenir uniquement des sommets, ou des sommets et des textures, ou des sommets, des textures et des normales. En tant que tel, nous devons vérifier chacun de ces trois cas. Le code suivant fait ceci:

 // Mapping Line else if (Line.substring (0, 2) == "f") var Row = Line.substring (2) .split (""); for (var T in Row) // Supprime les entrées vides if (Row [T]! = "") // S'il s'agit d'une entrée à valeurs multiples if (Row [T] .indexOf ("/")! = -1) // Divise les différentes valeurs var TC = Ligne [T] .split ("/"); // Incrémente les Triangles Array Triangles.push (Counter); Counter ++; // Insérer les sommets var index = parseInt (TC [0]) - 1; VerticeMap.push (Vertices [index] .X); VerticeMap.push (Vertices [index] .Y); VerticeMap.push (Vertices [index] .Z); // Insérer le Textures index = parseInt (TC [1]) - 1; TextureMap.push (Textures [index] .X); TextureMap.push (Textures [index] .Y); // Si cette entrée contient des données normales si (TC.length> 2) // Insère des normales, index = parseInt (TC [2]) - 1; NormalMap.push (Normals [index] .X); NormalMap.push (Normals [index] .Y); NormalMap.push (Normals [index] .Z);  // Pour les lignes contenant uniquement des sommets else Triangles.push (Counter); // Incrémente le compteur de tableaux de triangles ++; var index = parseInt (Ligne [T]) - 1; VerticeMap.push (Vertices [index] .X); VerticeMap.push (Vertices [index] .Y); VerticeMap.push (Vertices [index] .Z); 

Ce code est plus long que compliqué. Bien que j'ai couvert le scénario où le .obj fichier ne contient que des données de sommet, notre structure requiert des sommets et des coordonnées de texture. Si un .obj fichier ne contient que des données de sommet, vous devrez y ajouter manuellement les données de coordonnées de texture.

Passons maintenant les tableaux à la fonction de rappel et finissons la LoadModel une fonction:

  // Retourne les tableaux CB (VerticeMap, Triangles, TextureMap, NormalMap);  Ajax.open ("GET", ModelName + ".obj", true); Ajax.send (); 

Il convient de faire attention au fait que notre infrastructure WebGL est assez basique et ne dessine que des modèles composés de triangles. Vous devrez peut-être modifier vos modèles 3D en conséquence. Heureusement, la plupart des applications 3D ont une fonction ou un plug-in pour trianguler vos modèles à votre place. J'ai fait un modèle simple de maison avec mes compétences de base en modélisation et je l'inclurai dans les fichiers source que vous pourrez utiliser si vous le souhaitez..

Modifions maintenant le Dessiner fonction du dernier tutoriel pour incorporer notre nouveau type de données d'objet 3D:

this.Draw = fonction (Modèle) if (Model.Image.ReadyState == true && Modèle.Ready == false) this.PrepareModel (Modèle);  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); // Génère la matrice de perspective var PerspectiveMatrix = MakePerspective (45, this.AspectRatio, 1, 1000.0); var TransformMatrix = Model.GetTransforms (); // Définit l'emplacement 0 comme texture active this.GL.activeTexture (this.GL.TEXTURE0); // Charge dans la texture à la mémoire this.GL.bindTexture (this.GL.TEXTURE_2D, Model.Image); // Mise à jour du sampler de texture dans le fragment shader pour utiliser l'emplacement 0 this.GL.uniform1i (this.GL.get.UniformLocation (this.ShaderProgram, "uSampler"), 0); // Définition des matrices de perspective et de transformation 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)); // Dessine les triangles this.GL.drawElements (this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0); ;

La nouvelle fonction draw vérifie d'abord si le modèle a été préparé pour WebGL. Si la texture est chargée, le modèle sera préparé pour le dessin. Nous arriverons au PrepareModel fonctionner dans une minute. Si le modèle est prêt, il connectera ses tampons aux shaders et chargera les matrices de perspective et de transformation comme auparavant. La seule vraie différence est qu’il prend maintenant toutes les données de l’objet modèle..

le PrepareModel function convertit simplement les matrices de texture et de données en variables compatibles WebGL. Voici la fonction; ajoutez-le juste avant la fonction draw:

this.PrepareModel = function (Modèle) Model.Image = this.LoadTexture (Model.Image); // Conversion de tableaux en tampons var Buffer = this.GL.createBuffer (); this.GL.bindBuffer (this.GL.ARRAY_BUFFER, Buffer); this.GL.bufferData (this.GL.ARRAY_BUFFER, nouveau 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, nouvelle 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, nouveau Float32Array (Model.TextureMap), this.GL.STATIC_DRAW); Model.TextureMap = Buffer; Model.Ready = true; ;

Maintenant, notre framework est prêt et nous pouvons passer à la page HTML.


La page HTML

Vous pouvez effacer tout ce qui se trouve à l'intérieur du scénario balises que nous pouvons maintenant écrire le code de manière plus concise grâce à notre nouvelle GLObject Type de données.

C'est le code JavaScript complet:

var GL; bâtiment var; function Ready () GL = nouveau WebGL ("GLCanvas", "FragmentShader", "VertexShader"); LoadModel ("Maison", fonction (VerticeMap, Triangles, TextureMap) Bâtiment = nouveau GLObject (VerticeMap, Triangles, TextureMap, "Maison.png"); Building.Pos.Z = 650; // Mon modèle était un peu trop grand. Building.Scale.X = 0.5; Building.Scale.Y = 0.5; Building.Scale.Z = 0.5; // Et en arrière Building.Rotation.Y = 180; setInterval (Update, 33););  function Update () Building.Rotation.Y + = 0.2 GL.Draw (Bâtiment); 

Nous chargeons un modèle et disons à la page de le mettre à jour environ trente fois par seconde. le Mettre à jour la fonction fait pivoter le modèle sur l'axe des Y, ce qui est accompli en mettant à jour le Y de l'objet Rotation propriété. Mon modèle était un peu trop gros pour la scène WebGL et c'était en arrière, donc je devais effectuer quelques ajustements dans le code.

À moins de faire une présentation WebGL cinématographique, vous souhaiterez probablement ajouter des contrôles. Voyons comment nous pouvons ajouter des commandes de clavier à notre application..


Contrôles du clavier

Ce n'est pas vraiment une technique WebGL, mais bien une fonctionnalité JavaScript native, mais elle est pratique pour contrôler et positionner vos modèles 3D. Tout ce que vous avez à faire est d’ajouter un écouteur d’événement au clavier. touche Bas ou keyup événements et vérifier quelle touche a été enfoncée. Chaque clé a un code spécial et un bon moyen de savoir quel code correspond à la clé consiste à enregistrer les codes de clé sur la console lorsque l'événement est déclenché. Donc, allez à la zone où j'ai chargé le modèle, et ajoutez le code suivant juste après le setInterval ligne:

document.onkeydown = handleKeyDown;

Cela va définir la fonction handleKeyDown pour gérer le touche Bas un événement. Voici le code pour le handleKeyDown une fonction:

function handleKeyDown (event) // Vous pouvez supprimer la mise en commentaire de la ligne suivante pour connaître le code de chaque clé //alert(event.keyCode); if (event.keyCode == 37) // Flèche gauche Building.Pos.X - = 4;  else if (event.keyCode == 38) // Up Arrow Key Building.Pos.Y + = 4;  else if (event.keyCode == 39) // Touche Flèche Droite Building.Pos.X + = 4;  else if (event.keyCode == 40) // Down Arrow Key Building.Pos.Y - = 4; 

Toute cette fonction ne fait que mettre à jour les propriétés de l'objet; le framework WebGL s'occupe du reste.


Conclusion

Nous n'avons pas fini! Dans la troisième et dernière partie de cette mini-série, nous examinerons différents types d’éclairage et la manière de les associer à des éléments 2D.!

Merci de votre lecture et, comme toujours, si vous avez des questions, n'hésitez pas à laisser un commentaire ci-dessous!