Couverture de l'article Trucs et astuces WebGL

Vous avez choisi de créer une application WebGL, félicitations ! Nous allons vous donner ci-dessous des trucs et astuces qui pourront vous faire gagner du temps, éviter des galères et même améliorer la qualité et la performance de votre bijou.

Prendre le temps de développer ses outils de debug

Des bons outils de debug sont IN-DIS-PEN-SABLES ! Croyez-moi, vous allez peut-être investir quelques heures pour les développer, mais ils vous feront gagner un temps considérable par la suite. Vous pourrez aussi vous rendre compte de certaines fuites de performances invisibles au premier regard.

1 - Tout savoir sur sa scène 3D

Vos outils devront mettre à nu au possible le contenu de votre scène 3D. Une bonne ergonomie est d’afficher ce qui se trouve sous votre souris. Voici ce que j’utilise à titre personnel : debug info

 

Cette interface me permet rapidement d’identifier à quel mesh et material j’ai affaire. Je peux aussi vérifier son nombre de poly/vertices, car des opérations comme le CSG peuvent créer des maillages trop complexes et induire des problèmes de performances. J’ai également un aperçu des canaux UV pour vérifier la bonne application des textures en ce point.

(Anecdote : cette fonctionnalité m’a permis d’identifier un problème visuel que j’aurais eu beaucoup de mal à détecter autrement. En effet, un maillage était légèrement trop sombre, et incohérent avec la lumière incidente. Par hasard, j’ai survolé ma souris sur ce maillage et me suis rendu compte que toutes ses coordonnées UV2 étaient à 0, alors que la lightmap était activée. Ainsi, le pixel en bas à gauche de la lightmap, gris, était appliqué sur tout le maillage, l’assombrissant entièrement. J’ai donc remappé le modèle dans l’espace UV2, et miracle, la lumière fut ! )

2 - Debugger un shader

C’est là que la prise de tête commence. Debugger un shader peut être la source de nombreuses crises de nerfs si on ne sait pas par où commencer. En effet, oubliez le console.log ou les breakpoints si précieux, votre seul moyen de debugger un shader sera d’afficher des couleurs à l’écran dans le fragment shader. Oui, le rouge bien flashy, alias vec4(1.0, 0.0, 0.0, 1.0) sera votre ami. Rien ne vaut l’exemple :

  • Cas pratique : Mon image/objet ne ressemble à rien, ou pire, est tout noir. 0 ) Vérifiez tout d’abord que vous n’avez aucun warning WebGL dans votre console Javascript. Si vous en avez, le souci vient probablement de vos appels WebGL (attribute manquant etc…) 1 ) Vérifiez que votre shader est bien impliqué. Faites un simple gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); à la fin de votre fragment shader. Ceci équivaut au fameux console.log("coucou"); qui vérifie que votre code est bien appelé. Si vous obtenez un(e) image/objet entièrement rouge, votre shader est bien utilisé. Sinon, vérifiez côté Javascript que vous faites le nécessaire, comme résumé simplement ici. 2 ) Isolez maintenant une par une vos données en entrée. N’hésitez pas à afficher même le plus simple, comme par exemple les attributes. Un gl_FragColor = vec4(uv.x, uv.y, 0.0, 1.0); ou encore gl_FragColor = vec4(normal.x, normal.y, normal.z, 1.0); devrait vous afficher des couleurs cohérentes. Attention, vous devrez ramener vos données dans l’intervalle [0:1] pour qu’elles soient correctement affichées. Dans l’exemple ci-dessus, des valeurs négatives de normales s’afficheront noires, utilisez la valeur absolue. Pour la position, divisez/multipliez-la par un facteur lambda pour l’obtenir dans l’intervalle [0:1]. N’hésitez pas à tâtonner, le but est de localiser le souci, pas d’avoir un beau rendu de couleurs. 3 ) Si toutes vos données d’entrée sont cohérentes, le souci est alors dans les instructions de votre shader. Le plus courant est d’oublier que vous avez des valeurs hors de l’intervalle [0:1]. Aussi, si vous obtenez une sorte de motif répété, il est fort probable que vous ayez affaire à des erreurs de précision numérique. N’oubliez pas que chaque canal sur une texture (si vous n’utilisez pas de texture à précision flottante) est codé sur 8bits. C’est très peu de valeurs différentes pour représenter des grandeurs physiques comme la profondeur !

NB : Les navigateurs intègrent maintenant des outils pour aider au debug des shaders, comme pouvoir modifier en direct le code de votre shader et le recompiler. Ceci peut être pratique, mais attention, ils ne sont pas dépourvus de bugs ! Celui de Firefox par exemple vous fera des misères si vous changez un accès à une texture dans un texture2D vers une autre texture. L’accès se fera toujours dans la texture d’index 0, quelle que soit la texture invoquée !! (à vérifier cependant si c’est toujours d’actualité, ce bug a peut-être été corrigé depuis)

Garder un oeil sur la complexité de sa scène

Maintenant que vous avez de beaux outils de debug, il est très important de monitorer en permanence la complexité de sa scène. Les indicateurs sont les suivants :

  • Nombre d’objets
  • Nombre de matériaux affichés
  • Nombre de faces
  • Nombre de vertices

Je ne vais pas vous donner des indicateurs limites, car ces nombres varient énormément en fonction de la configuration que vous ciblez (n’oubliez pas aussi les smartphones et tablettes !). Mes recommandations sont les suivantes :

  • Partagez au maximum vos matériaux entre objets. Si votre framework est bien fait (comme Babylon.js ;p ), un système de cache accélèrera votre rendu si les matériaux successifs sont identiques.
  • Réduisez votre nombre d’objets au minimum nécessaire. Pour cela j’ai déjà fait un article sur le sujet.
  • Simplifiez votre géométrie - réduisez votre nombre de faces/sommets. Si votre application est plus avancée, considérez un système de LOD.

Ombres ou occlusion ambiante ?

Les ombres coûtent cher. Mettre en place la technique de shadow mapping implémentée dans tous les framework WebGL qui se respectent vous coûtera un DC (draw call) en plus par objet. Vous doublez donc implicitement la complexité de la scène si tous vos objets projettent des ombres. Dans certains cas, il est possible de précalculer ce qu’on appelle l’occlusion ambiante. Cette technique peut vous offrir des ombres plus réalistes, et à moindre coût, vu que tout est précalculé. Ci dessous, nous avons précalculé l’occlusion ambiante sur des plans en dessous de chaque meuble sur Wanaplan. Ce plan se déplace avec le meuble. Comparez la qualité avec l’ancienne technique de shadow mapping. En plus cette technique coûte beaucoup moins cher en temps CPU !

Sans Ambient Occlusion :

 

Avec Ambient Occlusion :

plan3D_withAO

Utiliser un shader de traitement d’image

Vous voulez flouter une image ? Dessiner un contour autour d’un objet ? Appliquer de l’antialiasing ? Toutes ces opérations se font merveilleusement bien via des fragment shaders. Aussi je vous conseille de développer une classe qui vous permettra l’application d’un shader sur la totalité de l’écran. Cette classe répondra à tous vos besoins de traitement de l’image, si tant est que vous sachiez manipuler les shaders.

Voici une partie de mon implémentation, qui me simplifie la vie comme vous avez pas idée :

IP.Render = function(effect, inTex, outTex, engine, _uniformCb) {
        var gl = engine._gl;

        if (!inTex || !outTex || !effect || !engine) {
            console.warn("[ImageProcessor] Render : at least one parameter is missing, aborting.")
            return;
        }

        if (!effect.isReady() || !inTex.isReady() || !outTex.isReady()) {
            return;
        }

        gl.useProgram(effect.getProgram());

        // Only created once
        if (screenQuadVBO == null) {
            // These vertices make a simple unscaled quad
            var verts = [
                1.0, 1.0, -1.0, 
                1.0, -1.0, -1.0,
                -1.0, -1.0, 1.0, 
                -1.0, 1.0, 1.0
            ];
            screenQuadVBO = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, screenQuadVBO);
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
        }

        // Bind the framebuffer to the output RenderTarget's framebuffer
        engine.bindFramebuffer(outTex._texture);

        // Bind the vertices
        gl.bindBuffer(gl.ARRAY_BUFFER, screenQuadVBO);
        gl.enableVertexAttribArray(effect.getAttributeLocationByName("position"));
        gl.vertexAttribPointer(effect.getAttributeLocationByName("position"), 2, gl.FLOAT, false, 0, 0);

        // _uniformCb is a callback where the uniforms are set
        // Use what you need for your shader
        if (_uniformCb)
            _uniformCb(effect);
        // In my fragment shader, i'll always have 2 uniform automatically set : 
        //     textureSampler, which will be the input texture
        //     texelSize, which will be the actual texel size, as displayed on the screen
        effect.setTexture("textureSampler", inTex);
        effect.setFloat2("texelSize", 1 / wanaplan.getWidth(), 1 / wanaplan.getHeight());

        gl.drawArrays(gl.TRIANGLES, 0, 6);

        // Cleanup:
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
        engine.unBindFramebuffer(outTex);
    };

A partir de là, une fois mes shaders codés et compilés dans mon effect, je spécifie la texture d’entrée inTex, la texture de sortie outTex, le contexte gl, et mon callback pour régler mes uniforms avant le dessin _uniformCb. Et je récupère ma texture passée sous mon merveilleux shader.

Attendre WebGL 2 avec impatience

WebGL2 nous réserve des merveilles en termes de possibilités. Cet article (de 2013 quand même…) nous résume ce qui est dans la spec.

En bref, les techniques suivantes seront exploitables en production :

  • Des buffers d’uniforms qui, bien utilisés vont permettre d’accélérer les draw calls et donc autoriser plus d’objets/matériaux sur la scène.
  • Le deferred rendering qui permet d’afficher de nombreuses lumières point.
  • L’antialiasing MSAA sur les renderTargets hors écran. Cette feature va éviter l’aliasing horrible qui existe actuellement sur tous les postProcess comme la profondeur de champ ou encore le SSAO
  • Des techniques d’illumination globale. Je vous recommande d’ailleurs cet excellent article de Florian Boesch, qui implémente une technique à base d’irradiance, de lightmapping et de lightprobes dans WebGL (+extensions).
  • Bien d’autres choses encore…

Commentaires

Shadertoy est aussi une source inestimable de shader de debug interressant, juste celui-là est priceless, basiquement un printf en shader
https://www.shadertoy.com/view/4sBSWW

(plein d’autre sur le site, les basiques ‘splitscreen’ selon glFragCoord aident toujours beaucoup par exemple)
Autre exemple de debug avec shader avancé https://www.shadertoy.com/view/4dlGzX

J’aurais rajouté deux outils indispensable sur chrome en extension qui permettent le debug webgl

https://benvanik.github.io/WebGL-Inspector/
https://github.com/spite/ShaderEditorExtension

ET dans le code js, il est possible de récuperer les infos précise des compilers GLSL pour logger direct lignes d’erreur
(un exemple là https://github.com/cedricpinson/osgjs/blob/develop/sources/osg/Shader.js#L138)

Merci pour tous ces précieux liens ! J’oublie de regarder Shadertoy, mais c’est effectivement très intéressant…
Vivement la suite @Benjamin :) !