Une fois que vous avez correctement créé un contexte WebGL, vous pouvez commencer à dessiner dedans. La chose la plus simple est de dessiner un objet 2D et sans texture, l'article vous propose donc d'apprendre à dessiner un simple carré.
Illuminez la scène
La chose la plus importante à comprendre avant que nous commencions est que, bien que nous allons dans cet exemple afficher un objet à deux dimensions, nous le dessinons dans un espace 3D. Pour cela il nous faut tout d'abord créer les shaders qui vont illuminer notre scène et dessiner notre objet.
Initialiser les shaders
Les shaders sont décrit dans le OpenGL ES Shading Language. Pour faciliter la réutilisabilité du code, nous allons écrire une fonction qui charge les shaders appelés depuis le document HTML au lieu de tout écrire directement dans le JavaScript. Intéressons nous maintenant à la fonction initShaders()
qui s'occupe de gérer cette tâche :
function initShaders() { var fragmentShader = getShader(gl, "shader-fs"); var vertexShader = getShader(gl, "shader-vs"); // Créer le programme shader var shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); // Faire une alerte si le chargement du shader échoue if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert("Impossible d'initialiser le shader."); } gl.useProgram(shaderProgram); vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); gl.enableVertexAttribArray(vertexPositionAttribute); }
Il y a deux programmes de shader qui sont chargés dans cette fonction ; le premier, le fragment shader, est chargé depuis l'élément script
avec "shader-fs" comme ID. Le second, le vertex shader, est chargé depuis l'élément script
avec "shader-vs" comme ID. Nous regarderons dans le prochain paragraphe la fonction getShader()
qui va en fait inclure les shaders depuis le DOM.
Ensuite nous créons le programme shader en appelant la function createProgram()
de l'objet WebGL. Nous attachons ensuite les deux shaders à ce programme que nous allons lier au contexte de WebGL.
Après avoir fait ceci, le paramètre LINK_STATUS
de l'objet gl
est vérifié pour être sûr que notre programme s'est lié correctement. Si c'est fait, on active le nouveau programme.
Charger les shaders depuis le DOM
La fonction getShader()
insère le contenu d'un programme shader à partir d'un objet DOM (récupéré ici par l'id). La fonction retourne null si le programme n'a pas pu être chargé ou compilé.
Dans la première partie de la fonction on récupère l'élément DOM puis on insère son texte dans la variable theSource.
function getShader(gl, id) { var shaderScript, theSource, currentChild, shader; shaderScript = document.getElementById(id); if (!shaderScript) { return null; } theSource = ""; currentChild = shaderScript.firstChild; while(currentChild) { if (currentChild.nodeType == currentChild.TEXT_NODE) { theSource += currentChild.textContent; } currentChild = currentChild.nextSibling; }
Une fois que le code du shader a été lu, on regarde le type MIME du shader afin de déterminer si il s'agit d'un vertex shader (avec pour type MIME "x-shader/x-vertex") ou un fragment shader (type MIME "x-shader/x-fragment"), puis on crée un shader ayant le même type que son contenu.
if (shaderScript.type == "x-shader/x-fragment") { shader = gl.createShader(gl.FRAGMENT_SHADER); } else if (shaderScript.type == "x-shader/x-vertex") { shader = gl.createShader(gl.VERTEX_SHADER); } else { // type de shader inconnu return null; }
Finalement, le contenu de la variable theSource
est passé au shader. Si une erreur apparaît lors de la compilation, on affiche une alerte décrivant l'erreur et on retourne null.
Si tout se passe bien, le shader compilé est renvoyé.
gl.shaderSource(shader, theSource); // Compile le programme shader gl.compileShader(shader); // Vérifie si la compilation s'est bien déroulée if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert("Une erreur est survenue au cours de la compilation des shaders: " + gl.getShaderInfoLog(shader)); return null; } return shader; }
Les shaders
Nous avons besoin de créer le contenu des shaders à notre HTML. Les shaders sont décrits par un langage qui leur est propre, le GLSL. Il est possible de faire de nombreuses choses avec tel qu'illuminer une scène.
Dans cet exemple, nous l'utiliserons de la manière la plus simple qui soit.
Le Vertex shader
Le vertex shader défini la position de chaque vertex (sommet de triangle).
La position du sommet est défini par la variable GL gl_Position
. Nous ne nous préoccuperons pas pour l'instant de ce que contiennent les matrices uMVMatrix et uPMatrix.
<script id="shader-vs" type="x-shader/x-vertex"> attribute vec3 aVertexPosition; uniform mat4 uMVMatrix; uniform mat4 uPMatrix; void main(void) { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); } </script>
Le Fragment shader
Chaque pixel d'un triangle est appelé un fragment. Le but du fragment shader est d'établir la couleur de chaque pixel. Dans cet exemple on va simplement mettre chaque pixel sur la couleur blanche.
gl_FragColor
est une variable de GL qui est utilisée pour la couleur du fragment. Assigner sa valeur établit la couleur du pixel.
Ici il y a quatres composantes de couleur, le rouge, le vert, le bleu et l'opacité.
<script id="shader-fs" type="x-shader/x-fragment"> void main(void) { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); } </script>
Créer un objet
Avant de commencer à dessiner notre carré, on doit créer un buffer qui va contenir nos sommets. On va pour cela utiliser la fonction initBuffers()
. On pourra améliorer cette fonction plus tard pour créer des objets 3D plus complexes.
var horizAspect = 480.0/640.0; function initBuffers() { squareVerticesBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer); var vertices = [ 1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0 ]; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); }
Cette fonction est très courte car nous allons dessiner une scène très simple. La fonction appelle d'abord la méthode createBuffer()
de l'objet gl afin d'obtenir un buffer dans lequel nous allons mettre nos sommets (vertices). On ajoute ensuite ce buffer au contexte avec la méthode bindBuffer()
.
Une fois que le buffer est prêt, on crée un tableau JavaScript qui va contenir les coordonnées 3D de chaque sommet de notre carré.
Le tout est passé à la méthode bufferData()
de l'objet gl pour établir les sommets de notre objets.
Vous noterez au passage que l'on a converti le tableau JavaScript en tableau de nombre flottant WebGL.
Dessiner la scène
Maintenant que les shaders sont prêts et que l'objet a été construit, on peut vraiment commencer à rendre la scène. Puisque notre scène n'est pas animée, la fonction drawScene()
est très simple. Elle utilise quelques fonctions dont nous verrons rapidement l'utilité.
function drawScene() { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); perspectiveMatrix = makePerspective(45, 640.0/480.0, 0.1, 100.0); loadIdentity(); mvTranslate([-0.0, 0.0, -6.0]); gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer); gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0); setMatrixUniforms(); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); }
La première étape est de nettoyer la scène avec notre couleur d'arrière plan, ensuite on établit la perspective de la caméra. On met un angle d'ouverture de 45° et un rapport largeur/hauteur de 640 par 480 (la taille de notre canvas). On indique aussi que l'on ne veut rendre que les objets compris entre 0.1 et 100 unités par rapport à la caméra.
Ensuite on positionne notre carré en chargeant la matrice identité et en faisant une translation en arrière de 6 unités. Après ça on lie le vertex buffer de notre carré au contexte, on le configure puis on le dessine avec la méthode drawArrays().
Si vous avez un navigateur qui supporte le WebGL, vous pouvez essayer la démo en cliquant ici.
Opérations sur les matrices
Les opérations sur les matrices sont assez compliquées. Personne n'a vraiment envie d'écrire le code pour les gérer. Heureusement , il y a Sylvester, c'est une bibliothèque très pratique pour gérer les opérations sur les vecteurs et les matrices depuis JavaScript.
Le fichier glUtils.js
utilisé dans cette démo est présent dans de nombreuses démos WebGL que l'on peut voir sur le Web. Personne n'est vraiment clair sur sa provenance, mais elle fait des simplifications encore plus grandes pour construire des matrices d'un certains types ainsi que pour générer du HTML pour les afficher.
De plus, cette démo définit un certain nombre de fonctions utiles pour utiliser ces librairies. Ce qu'elles font exactement ne fait pas partie de ce que vous apprendrez dans cette démo, mais il y a plein de bonnes références en ligne pour bien comprendre leur utilité. N'hésitez pas à jeter un oeil à la section A voir pour une liste de quelques unes.
function loadIdentity() { mvMatrix = Matrix.I(4); } function multMatrix(m) { mvMatrix = mvMatrix.x(m); } function mvTranslate(v) { multMatrix(Matrix.Translation($V([v[0], v[1], v[2]])).ensure4x4()); } function setMatrixUniforms() { var pUniform = gl.getUniformLocation(shaderProgram, "uPMatrix"); gl.uniformMatrix4fv(pUniform, false, new Float32Array(perspectiveMatrix.flatten())); var mvUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix"); gl.uniformMatrix4fv(mvUniform, false, new Float32Array(mvMatrix.flatten())); }