Canalblog
Editer l'article Suivre ce blog Administration + Créer mon blog
Publicité
Fanfois et ses papillons
8 mai 2012

HTML5 02 (tutorial WebGL, 1ère partie)

Dans ce billet nous allons commencer à apprendre à faire une page HTML5 qui mette en œuvre WebGL pour faire un dessin.

Comme vous le savez probablement déjà, WebGL n'est pas supporté par tout le monde. Pour que notre page puisse espérer fonctionner avec Internet Explorer nous ferons donc appelle à IEWebGL.
Si vous ne l'avez pas déjà installé, je vous invite à le faire. Si vous ne le faites pas, il vous faudra utiliser un autre butineur qu'Internet Explorer. Pour l'installer une simple visite sur le site IEWebGL suffit, tout y est expliqué (en Anglais, mais le gros bouton "Download IEWebGL" ne devrait pas vous poser de problèmes).

Dans le précédent billet, nous avons vu qu'IEWebGL permettait très simplement, donc avec très peu de modifications, d'adapter une page écrite pour WebGL pour la rendre IE compatible. Par contre il n'y avait pas la moindre explication concernant la façon dont on écrit du code WebGL.
Le code de départ était issu du livre HTML5 Canvas Cookbook et le livre lui-même ne détaille pas vraiment le code WebGL.
Le but de ce billet, et des suivant, est justement de voir en détail comment faire un dessin simplissime en WebGL
En conséquence il est nécessaire de disposer d'un butineur qui permette de faire tourner WebGL. Dans le cas contraire vous ne pourrez pas voir par vous-même les pages HTML5 que nous allons réaliser.

Personnellement je vais utiliser Visual Web Developer 2010 Express pour écrire les pages à venir. D'origine il ne comprend pas HTML5, mais il est possible de lui en apprendre un peu avec Web Standards Update for Microsoft Visual Studio 2010 SP1. Malheureusement le Canvas n'est pas dans le lot des tags traités par le msi.
Attention, je ne dis pas que Visual Web Developer 2010 Express est requis. Il se trouve simplement que Visual Web Developer 2010 Express me plait plus pour écrire le code des pages que mon éditeur habituel qui est Scite. Surtout il est possible de définir assez simplement des templates, que ce soit de page ou de site.

Dans le cadre de Visual Web Developer 2010 Express, nous commençons par créer un nouveau projet de type "Application Web ASP.NET vide". Il nous faut choisir un langage .NET. Ce n'est pas fondamental car nous ne ferons que du Javascript dans les pages. Cela dit il faut tout de même en choisir un, je suis personnellement attaché à C#. Au niveau du nom, "TutoWebGL" le fait très bien.
Nous pouvons maintenant ajouter un nouvel élément de type "Page HTML" que nous nommons "WebGL_01.htm".
Comme nous l'avons vu dans le précédent billet, IEWebGL s'utilise de manière quasiment transparente à l'aide du Javascript webglhelper.js. Le bogue n'étant pas encore corrigé, au moment de l'écriture de ce billet, dans la version disponible sur le site, http://iewebgl.com/scripts/webglhelper.js, nous utiliserons une version locale pour notre projet. Nous ajoutons donc un nouveau dossier "script" à notre projet et y mettons le Javascript récupéré à l'adresse précédente. Cela fait nous corrigeons la ligne 72 pour qu'elle deviennent if (element.style.cssText) cnv.style.cssText = element.style.cssText;.
Nous pouvons maintenant remplacer le code de notre page web par le code suivant :

<!DOCTYPE HTML>
<html>
  <head>
    <!-- required to use WebGLHelper object -->
    <!-- <script type="text/javascript" src="http://iewebgl.com/scripts/webglhelper.js"></script> -->
    <script type="text/javascript" src="script/webglhelper.js"></script>
    <script id="vertex" type="x-shader"></script>
    <script id="fragment" type="x-shader"></script>
    <script type="text/javascript">
      function initWebGL(canvasElement) {
        canvas = canvasElement
        gl = WebGLHelper.GetGLContext(canvasElement);
      }
      function OnGLCanvasCreated(canvasElement, elementId) {
        window.setTimeout(function () { initWebGL(canvasElement); }, 1000);
      }
      function OnGLCanvasFailed(canvasElement, elementId) {
        alert("Votre butineur ne supporte pas WebGL !");
      }
    </script>
  </head>
  <body>
    <script id="WebGLCanvasCreationScript" type="text/javascript" width="800" height="500" style="border:1px solid black;">
      WebGLHelper.CreateGLCanvasInline('glCanvas', OnGLCanvasCreated, OnGLCanvasFailed)
    </script>
  </body>
</html>

Que trouve-t-on dans ce code ?

  • insertion du Javascript webglhelper.js. C'est lui gère pour nous le fait que le butineur dispose de ce qu'il faut ou pas. En conséquence nous ne mettons pas un tag canvas dans notre page mais un tag script id="WebGLCanvasCreationScript" ;
  • le tag script id="WebGLCanvasCreationScript". C'est lui qui sera remplacé, soit par un Canvas, soit par un objet IEWebGL. Il contient l'appel à la méthode CreateGLCanvasInline qui effectue la création de l'élément qui va bien et l'insert dans le DOM de la page. Les indications width, height et style seront en fait reportées sur l'objet qui sera substitué au script. Suivant la façon dont la création se passe, les méthodes OnGLCanvasCreated et OnGLCanvasFailed passées en paramètre seront appelées ;
  • OnGLCanvasFailed est appelée si la création d'un contexte WebGL (ou IEWebGL) c'est avérée impossible. Le code proposé est simpliste et peut être modifié en cas de besoin ;
  • OnGLCanvasCreated est appelée si la création d'un contexte WebGL (ou IEWebGL) a été possible. Cette fonction devrait pouvoir directement faire ce qui est voulu, mais un bogue d'initialisation d'IEWebGL fait qu'il est nécessaire d'attendre "un peu" si vous voulez pouvoir manipuler le contexte WebGL sans souci. Le code qui devrait s'y trouver à été déporté dans initWebGL ;
  • initWebGL contient le code Javascript qui permet de réaliser le dessin WebGL. La première chose à faire est donc la récupération du contexte WebGL dans lequel nous travaillerons. En WebGL tout le code n'est pas du Javascript. Il y a aussi du code "shader". Deux emplacements ont été prévus pour l'accueillir ;
  • les deux scripts de type x-shader accueilleront le code "shader".

Maintenant que le cadre est défini essayons de l'utiliser.

Une remarque avant de se lancer.
LE grand classique pour apprendre OpenGL c'est "NeHe OpenGL tutorials" qui commence .
Du coup le grand classique pour aborder WebGL, qui repose sur OpenGL, c'est de reprendre les mêmes exemples adaptés (WebGL ne contient pas tout OpenGL). Pleins de sites le font et souvent très bien, à votre moteur de recherche préféré.
Du coup je ne le fais pas. Déjà parce qu'à part le fait que le texte serait en français, je ne voie pas l'intérêt. L'autre raison, la vraie, c'est que mon besoin concret n'est pas couvert, du moins au début. Du coup je ne vais pas présenter les choses de la même façon, je vais suivre une approche plus géométrique de la question. Dis autrement, je me moque un peu de dessiner des objets avec des textures, je veux comprendre la 3D et comment l'utiliser. Si cela est bon, les textures c'est plus un problème graphique (graphiste et carte graphique) qu'un problème de programmation.

Nous allons donc commencer par les trois axes de notre espace de dessin. Notre but est donc de les matérialiser à l'écran.

Avant de dessiner nos trois axes, nous allons commencer par le fond de notre dessin. L'idée n'étant pas de se faire mal à la tête, nous allons faire simple avec un fond uniforme.
La bonne nouvelle c'est que c'est super simple à faire.
Il suffit de changer la couleur avec laquelle le fond est effacé et d'effacer.
Un dernier détail, il faut définir la zone occupée par notre contexte WebGL dans le Canvas, et de préférence avant d'effacer.
Remis dans le bon ordre cela donne le code suivant :

gl.viewport(0, 0, canvas.width, canvas.height); 
gl.clearColor(1, 0.98, 0.8, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

La première ligne ne devrait pas vous poser de problème, on dessine bien sur tout le Canvas.
La seconde ligne définie une couleur d'effacement. Au lieu d'utiliser des valeurs comprises entre 0 et 255, il faut des valeurs comprise entre 0 et 1. Le "Lemon Chiffon" ne s'obtient pas avec un R-G-B-A de 255-250-205-255 mais plutôt de 1-0.98-0.8-1.
Pour "peindre" le fond avec la couleur que nous venons de définir, nous demandons donc l'effacement du contexte WebGL.
Ce code diffère de ce que l'on rencontre pour la manipulation classique du Canvas, mais reste du Javascript. Il est donc assez facilement lisible. L'écrire peut par contre demander un peu de temps et la consultation de la documentation, WebGL Specification.

Passons maintenant au dessin de nos axes X, Y et Z.
Les choses se compliquent immédiatement. Nous allons devoir mettre le doigt, et probablement plus, dans du code "shader".
Là si vous n'en avez jamais fait, comme moi, la chose peut être un poil plus douloureuse. Si vous ne savez pas ce qu'est une matrice la chose peut même être assez compliquée à aborder.
Alors non, une matrice n'est pas un concept détaillé dans une série de films du début des années 2000. C'est un objet mathématique bien plus ancien. Si vous en doutez, jetez un œil sur Wikipédia.
J'ai parlé des matrices en premier, mais qui dit matrices dit généralement vecteurs. Le code des shaders est donc bourré de vecteurs, généralement à quatre dimensions.
Pour dessiner quelque chose en WebGL il faut passer par deux shaders. L'un pour définir la zone dessinée, le vertex shader, l'autre pour définir la couleur de chaque élément (point, trait ou triangle) dessiné, le fragment shader.

Pour dessiner un simple point nous avons donc besoin de définir nos deux "shaders". Le premier permet de définir le point lui-même, le second la couleur.
Pour dessiner un fragment de droite il nous faut définir, de manière relativement naturelle, deux points.
Pour dessiner un triangle, deviner quoi, il en faut trois, des points. Pour dessiner une figure plus complexe, les choses se gâtent nettement car il faut assembler des triangles. Si vous avez un doute sur la faisabilité de la chose, il n'y a pas à en avoir. Tous les dessins en 3D que vous pouvez voir sont faits, soit à l'aide d'assemblages de triangles, soit point par point. La seconde solution donne un résultat meilleur au niveau des textures, mais fait suffisamment exploser les temps de calcul et la taille mémoire nécessaire pour que la chose soit moins employée et surtout soit réservée aux objets fixes. Vous pouvez tourner au tour, zoomer, mais l'objet n'est pas recalculé, il ne change pas.

Le vertex shader se met dans le script vertexque nous lui avons préparé. Nous y plaçons un code qui nous permettra de dessiner un point dont nous fourniront les trois coordonnées (x, y et z). Le code et le suivant :

attribute vec3 aVertexPosition;
 
void main() {
  gl_Position = vec4(aVertexPosition, 1.0);
}

Avec un poil d'intuition, on comprend que l'on fournit un vecteur à trois dimensions (vec3) nommé aVertexPosition, qui est utilisé comme base pour construire un vecteur à quatre dimensions (vec4). Par contre il peut être un peu plus compliqué de comprendre pourquoi ce vecteur est placé dans gl_Position. En fait gl_Position est le nom, imposé, de l'élément retourné par notre méthode main. C'est en quelque sorte un return.
Dernier point, que contient le vecteur à quatre dimensions ?
Les trois premières sont donc x, y et z. La quatrième correspond à la coordonnée homogène. Je vous laisse consulter Coordonnées homogènespour plus d'informations. Ici nous imposons 1 ce qui nous permet d'avoir aucune "déformation".

Le fragment shader se place dans le script fragment. Dans notre cas il retournera simplement la couleur que nous lui fournirons. Le code est le suivant :

#ifdef GL_ES
  precision highp float;
#endif
 
uniform vec4 uColor;
 
void main() {
  gl_FragColor = uColor;
}

Là encore le nom gl_FragColor n'est pas libre, il remplace le return.
Ce shader conduira tous les points à être dessinés avec la même couleur.

Comme cela les shaders ne sont pas utilisables. Il convient de les compiler et de les lier pour former un "program". Nous ajoutons donc le code suivant à notre méthode Javascrip InitWebGL:

var v = document.getElementById("vertex").firstChild.nodeValue;
var f = document.getElementById("fragment").firstChild.nodeValue;
 
var vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, v);
gl.compileShader(vs);
 
var fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, f);
gl.compileShader(fs);
 
program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
 
gl.useProgram(program);

Si ce code ne s'invente pas forcement tout seul, il se lit par contre assez facilement.
Comme souvent, s'il y a un problème il ne se passe rien. Rien, de rien. Donc pour savoir si quelque chose ne se passe pas bien, il est préférable d'aller à la pêche aux infos. C'est ce que fait ce code, que nous ajoutons à InitWebGL avant l'instruction useProgram :

if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS))
  console.log(gl.getShaderInfoLog(vs));
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS))
  console.log(gl.getShaderInfoLog(fs));
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
  console.log(gl.getProgramInfoLog(program));

Passons au dessin lui-même. Pour ceux qui ont oublié, nous voulons dessiner les trois axes.
La première chose à connaître c'est le système de coordonnées.
Alors là c'est extrêmement simple, pour chaque axe, la zone visible se situe entre -1 et 1. Cela est vrai quel que soit l'axe. C'est donc à nous de faire ce qu'il faut pour que l'on voit, ou pas, les points. Le centre de l'espace WebGL se confond avec celui du Canvas hôte. Le coin supérieur gauche est à -1, 1, 0 et le coin inférieur droit à 1, -1, 0. Il est normal que z soit toujours à 0 vu que notre Canvas est comme notre écran en 2D.
Pour dessiner nos trois axes nous devons donc définir six points. Pour qu'ils nous permettent de nous faire une idée des distances, nous allons les dessiner sur leur section -0,5 0,5. Nous définissons donc un tableau qui contient toutes les coordonnées de nos points, axe par axe, deux points par axe :

var vertices = new Float32Array([
  -0.5,  0.0,  0.0, 0.5, 0.0, 0.0, // axe des X
   0.0, -0.5,  0.0, 0.0, 0.5, 0.0, // axe des Y
   0.0,  0.0, -0.5, 0.0, 0.0, 0.5  // axe des Z
  ]);

Nous devons maintenant les placer dans le tampon courant de WebGL. Une fois la chose faite, toutes le actions à venir leurs seront appliquées. C'est le principe d'OpenGL, et donc de WebGL, les tampons sont globaux. Avantage, il n'est pas nécessaire de passer beaucoup de paramètres aux méthodes. Inconvénient, il faut savoir ce qui est placé dans les tampons pour comprendre ce qui se passe. Le code est le suivant :

vbuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
 
itemSize = 3;
numItems = vertices.length / itemSize;

Nous pouvons maintenant les lier aux codes des shaders pour qu'ils les traitent :

program.uColor = gl.getUniformLocation(program, "uColor");
gl.uniform4fv(program.uColor, [1.0, 0.0, 0.0, 1.0]);
 
program.aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(program.aVertexPosition);
gl.vertexAttribPointer(program.aVertexPosition, itemSize, gl.FLOAT, false, 0, 0);

A ce stade tout est prêt mais rien n'est visible. Il faut maintenant demander l'affichage :

gl.drawArrays(gl.LINES, 0, numItems);

Si votre butineur le permet vous devriez voir le résultat suivant :

HTML5 02 01

Evidement certains seront déçus. Ils ne trouvent pas l'axe des Z. En fait il est bien là, mais compte tenu de la projection, qui est faite par défaut en le suivant, il est réduit à un point qui se confond avec l'origine.
Pour le voir, il va falloir, soit déplacer notre objet 3D que forment les trois axes, soit modifier le point de vue.
Ce sera l'objet du prochain billet.

Pour ceux qui se serait loupé dans les copier/coller, ils trouveront ici un rar correspondant au projet sous Visual Web Developer 2010 Express.

Publicité
Publicité
Commentaires
Fanfois et ses papillons
Publicité
Archives
Publicité