Principes de base de WebGL

Gregg Tavares
Gregg Tavares

WebGL permet d'afficher d'incroyables graphismes 3D en temps réel dans votre navigateur. Cependant, beaucoup de gens ignorent que WebGL est en réalité une API 2D, et non une API 3D. Voyons cela de plus près.

WebGL ne s'intéresse qu'à deux choses : Coordonnées de l'espace de découpage en 2D et couleurs. En tant que programmeur utilisant WebGL, votre tâche consiste à fournir ces deux éléments à WebGL. Pour ce faire, vous devez fournir deux "shaders". Un nuanceur de sommets qui fournit les coordonnées de l'espace de découpage et un nuanceur de fragments qui fournit la couleur. Les coordonnées de l'espace de découpage vont toujours de -1 à +1, quelle que soit la taille de votre canevas. Voici un exemple WebGL simple qui montre WebGL sous sa forme la plus simple.

// Get A WebGL context
var canvas = document.getElementById("canvas");
var gl = canvas.getContext("experimental-webgl");

// setup a GLSL program
var vertexShader = createShaderFromScriptElement(gl, "2d-vertex-shader");
var fragmentShader = createShaderFromScriptElement(gl, "2d-fragment-shader");
var program = createProgram(gl, [vertexShader, fragmentShader]);
gl.useProgram(program);

// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");

// Create a buffer and put a single clipspace rectangle in
// it (2 triangles)
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        -1.0, -1.0,
         1.0, -1.0,
        -1.0,  1.0,
        -1.0,  1.0,
         1.0, -1.0,
         1.0,  1.0]),
    gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

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

Voici les deux nuanceurs :

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

void main() {
  gl_Position = vec4(a_position, 0, 1);
}
</script>

<script id="2d-fragment-shader" type="x-shader/x-fragment">
void main() {
  gl_FragColor = vec4(0,1,0,1);  // green
}
</script>

Encore une fois, les coordonnées de l'espace de découpe vont toujours de -1 à +1, quelle que soit la taille du canevas. Dans le cas ci-dessus, vous pouvez voir que nous ne faisons que transmettre directement nos données de position. Comme les données de position sont déjà dans l'espace de découpage, il n'y a rien à faire. Si vous voulez de la 3D, c'est à vous de fournir des nuanceurs qui convertissent la 3D en 2D, car WebGL EST UNE API 2D ! Pour les éléments en 2D, vous préférerez probablement travailler en pixels plutôt qu'en espace de découpage. Modifions donc le nuanceur afin de pouvoir fournir des rectangles en pixels et de les convertir en espace de découpage pour nous. Voici le nouveau nuanceur de sommets :

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;

void main() {
   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = a_position / u_resolution;

   // convert from 0->1 to 0->2
   vec2 zeroToTwo = zeroToOne * 2.0;

   // convert from 0->2 to -1->+1 (clipspace)
   vec2 clipSpace = zeroToTwo - 1.0;

   gl_Position = vec4(clipSpace, 0, 1);
}
</script>

Nous pouvons maintenant convertir nos données de l'espace de découpage en pixels.

// set the resolution
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);

// setup a rectangle from 10,20 to 80,30 in pixels
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    10, 20,
    80, 20,
    10, 30,
    10, 30,
    80, 20,
    80, 30]), gl.STATIC_DRAW);

Vous remarquerez peut-être que le rectangle se trouve près du bas de cette zone. WebGL considère que l'angle inférieur gauche correspond à 0,0. Pour qu'il se trouve dans l'angle supérieur gauche, plus traditionnel pour les API graphiques 2D, il suffit d'inverser la coordonnée Y.

gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

Transformons le code qui définit un rectangle en fonction afin de pouvoir l'appeler pour des rectangles de différentes tailles. Tant que nous y sommes, nous allons rendre la couleur configurable. Tout d'abord, nous faisons en sorte que le nuanceur de fragment accepte une entrée uniforme de couleur.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

uniform vec4 u_color;

void main() {
   gl_FragColor = u_color;
}
</script>

Voici le nouveau code qui dessine 50 rectangles à des emplacements aléatoires et avec des couleurs aléatoires.

...

  var colorLocation = gl.getUniformLocation(program, "u_color");
  ...
  // Create a buffer
  var buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.enableVertexAttribArray(positionLocation);
  gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

  // draw 50 random rectangles in random colors
  for (var ii = 0; ii < 50; ++ii) {
    // Setup a random rectangle
    setRectangle(
        gl, randomInt(300), randomInt(300), randomInt(300), randomInt(300));

    // Set a random color.
    gl.uniform4f(colorLocation, Math.random(), Math.random(), Math.random(), 1);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }
}

// Returns a random integer from 0 to range - 1.
function randomInt(range) {
  return Math.floor(Math.random() * range);
}

// Fills the buffer with the values that define a rectangle.
function setRectangle(gl, x, y, width, height) {
  var x1 = x;
  var x2 = x + width;
  var y1 = y;
  var y2 = y + height;
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
     x1, y1,
     x2, y1,
     x1, y2,
     x1, y2,
     x2, y1,
     x2, y2]), gl.STATIC_DRAW);
}

Comme vous pouvez le constater, WebGL est une API assez simple. Bien que la 3D puisse être plus complexe, cette complexité est ajoutée par vous, le programmeur, sous la forme de nuanceurs plus complexes. L'API WebGL elle-même est en 2D et assez simple.

Que signifient type="x-shader/x-vertex" et type="x-shader/x-fragment" ?

Les balises <script> contiennent du code JavaScript par défaut. Vous pouvez ne pas indiquer de type, ou indiquer type="javascript" ou type="text/javascript", et le navigateur interprétera le contenu comme du JavaScript. Si vous indiquez autre chose, le navigateur ignore le contenu du tag de script.

Nous pouvons utiliser cette fonctionnalité pour stocker des nuanceurs dans des balises de script. Mieux encore, nous pouvons créer notre propre type et, dans notre JavaScript, le rechercher pour décider de compiler le nuanceur en tant que nuanceur de sommets ou de fragments.

Dans ce cas, la fonction createShaderFromScriptElement recherche un script avec le id spécifié, puis examine le type pour décider du type de nuanceur à créer.

Traitement d'images WebGL

Le traitement d'images est facile dans WebGL. À quel point ? Lisez la suite.

Pour dessiner des images dans WebGL, nous devons utiliser des textures. De la même manière que WebGL attend des coordonnées d'espace de découpage lors du rendu au lieu de pixels, WebGL attend des coordonnées de texture lors de la lecture d'une texture. Les coordonnées de texture vont de 0,0 à 1,0, quelles que soient les dimensions de la texture. Comme nous ne dessinons qu'un seul rectangle (en fait, deux triangles), nous devons indiquer à WebGL à quel endroit de la texture correspond chaque point du rectangle. Nous allons transmettre ces informations du nuanceur de sommets au nuanceur de fragments à l'aide d'un type de variable spécial appelé "varying". On l'appelle "varying" parce qu'elle varie. WebGL interpolera les valeurs que nous fournissons dans le nuanceur de sommets lorsqu'il dessinera chaque pixel à l'aide du nuanceur de fragments. En utilisant le nuanceur de sommets de la fin de la section précédente, nous devons ajouter un attribut pour transmettre les coordonnées de texture, puis les transmettre au nuanceur de fragments.

attribute vec2 a_texCoord;
...
varying vec2 v_texCoord;

void main() {
   ...
   // pass the texCoord to the fragment shader
   // The GPU will interpolate this value between points
   v_texCoord = a_texCoord;
}

Nous fournissons ensuite un nuanceur de fragment pour rechercher les couleurs de la texture.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // Look up a color from the texture.
   gl_FragColor = texture2D(u_image, v_texCoord);
}
</script>

Enfin, nous devons charger une image, créer une texture et copier l'image dans la texture. Comme nous sommes dans un navigateur, les images se chargent de manière asynchrone. Nous devons donc réorganiser un peu notre code pour attendre que la texture se charge. Une fois qu'il sera chargé, nous le dessinerons.

function main() {
  var image = new Image();
  image.src = "http://someimage/on/our/server";  // MUST BE SAME DOMAIN!!!
  image.onload = function() {
    render(image);
  }
}

function render(image) {
  ...
  // all the code we had before.
  ...
  // look up where the texture coordinates need to go.
  var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");

  // provide texture coordinates for the rectangle.
  var texCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      0.0,  0.0,
      1.0,  0.0,
      0.0,  1.0,
      0.0,  1.0,
      1.0,  0.0,
      1.0,  1.0]), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(texCoordLocation);
  gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

  // Create a texture.
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set the parameters so we can render any size image.
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  // Upload the image into the texture.
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  ...
}

Ce n'est pas très intéressant, alors manipulons cette image. Et si on échangeait simplement le rouge et le bleu ?

...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...

Et si nous voulons effectuer un traitement d'image qui examine d'autres pixels ? Comme WebGL référence les textures dans des coordonnées de texture allant de 0,0 à 1,0, nous pouvons calculer la distance à parcourir pour un pixel à l'aide de l'opération mathématique simple onePixel = 1.0 / textureSize. Voici un nuanceur de fragment qui calcule la moyenne des pixels gauche et droit de chaque pixel de la texture.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // compute 1 pixel in texture coordinates.
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;

   // average the left, middle, and right pixels.
   gl_FragColor = (
       texture2D(u_image, v_texCoord) +
       texture2D(u_image, v_texCoord + vec2(onePixel.x, 0.0)) +
       texture2D(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
}
</script>

Nous devons ensuite transmettre la taille de la texture à partir de JavaScript.

...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height);
...

Maintenant que nous savons comment référencer d'autres pixels, utilisons un noyau de convolution pour effectuer un certain nombre de traitements d'image courants. Dans ce cas, nous utiliserons un noyau 3x3. Un noyau de convolution est simplement une matrice 3x3 dans laquelle chaque entrée représente le facteur de multiplication des huit pixels autour du pixel que nous affichons. Nous divisons ensuite le résultat par le poids du noyau (la somme de toutes les valeurs du noyau) ou par 1,0, selon la valeur la plus élevée. Voici un article intéressant sur le sujet. Voici un autre article qui montre du code réel si vous deviez l'écrire manuellement en C++. Dans notre cas, nous allons effectuer ce travail dans le nuanceur. Voici donc le nouveau nuanceur de fragment.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
   vec4 colorSum =
     texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  0)) * u_kernel[3] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  0)) * u_kernel[4] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  0)) * u_kernel[5] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  1)) * u_kernel[6] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  1)) * u_kernel[7] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  1)) * u_kernel[8] ;
   float kernelWeight =
     u_kernel[0] +
     u_kernel[1] +
     u_kernel[2] +
     u_kernel[3] +
     u_kernel[4] +
     u_kernel[5] +
     u_kernel[6] +
     u_kernel[7] +
     u_kernel[8] ;

   if (kernelWeight <= 0.0) {
     kernelWeight = 1.0;
   }

   // Divide the sum by the weight but just use rgb
   // we'll set alpha to 1.0
   gl_FragColor = vec4((colorSum / kernelWeight).rgb, 1.0);
}
</script>

En JavaScript, nous devons fournir un noyau de convolution.

...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
...
var edgeDetectKernel = [
    -1, -1, -1,
    -1,  8, -1,
    -1, -1, -1
];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
...

J'espère que cela vous a convaincu que le traitement d'images dans WebGL est assez simple. Je vais maintenant vous expliquer comment appliquer plusieurs effets à l'image.

À quoi correspondent les préfixes a, u et v_ devant les variables dans GLSL ?

Il s'agit simplement d'une convention d'attribution de noms. a_ pour les attributs, qui sont les données fournies par les tampons. u_ pour les variables uniformes qui sont des entrées des nuanceurs, v_ pour les variables de variation qui sont des valeurs transmises d'un nuanceur de sommets à un nuanceur de fragments et interpolées (ou variées) entre les sommets pour chaque pixel dessiné.

Appliquer plusieurs effets

La question suivante la plus évidente pour le traitement d'images est la suivante : comment appliquer plusieurs effets ?

Vous pouvez essayer de générer des nuanceurs à la volée. Fournissez une UI qui permet à l'utilisateur de sélectionner les effets qu'il souhaite utiliser, puis générez un nuanceur qui applique tous les effets. Cela n'est pas toujours possible, bien que cette technique soit souvent utilisée pour créer des effets pour les graphismes en temps réel. Une méthode plus flexible consiste à utiliser deux textures supplémentaires et à effectuer un rendu sur chaque texture à tour de rôle, en alternant entre les deux et en appliquant l'effet suivant à chaque fois.

Original Image -> [Blur]        -> Texture 1
Texture 1      -> [Sharpen]     -> Texture 2
Texture 2      -> [Edge Detect] -> Texture 1
Texture 1      -> [Blur]        -> Texture 2
Texture 2      -> [Normal]      -> Canvas

Pour ce faire, nous devons créer des framebuffers. Dans WebGL et OpenGL, le terme "framebuffer" est en fait mal choisi. Un framebuffer WebGL/OpenGL n'est en réalité qu'un ensemble d'états, et non un tampon d'un type quelconque. Toutefois, en associant une texture à un framebuffer, nous pouvons effectuer un rendu dans cette texture. Commençons par transformer l'ancien code de création de texture en fonction.

function createAndSetupTexture(gl) {
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set up texture so we can render any size image and so we are
  // working with pixels.
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  return texture;
}

// Create a texture and put the image in it.
var originalImageTexture = createAndSetupTexture(gl);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

Utilisons maintenant cette fonction pour créer deux autres textures et les associer à deux framebuffers.

// create 2 textures and attach them to framebuffers.
var textures = [];
var framebuffers = [];
for (var ii = 0; ii < 2; ++ii) {
  var texture = createAndSetupTexture(gl);
  textures.push(texture);

  // make the texture the same size as the image
  gl.texImage2D(
      gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
      gl.RGBA, gl.UNSIGNED_BYTE, null);

  // Create a framebuffer
  var fbo = gl.createFramebuffer();
  framebuffers.push(fbo);
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Attach a texture to it.
  gl.framebufferTexture2D(
      gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}

Créons maintenant un ensemble de noyaux, puis une liste de noyaux à appliquer.

// Define several convolution kernels
var kernels = {
  normal: [
    0, 0, 0,
    0, 1, 0,
    0, 0, 0
  ],
  gaussianBlur: [
    0.045, 0.122, 0.045,
    0.122, 0.332, 0.122,
    0.045, 0.122, 0.045
  ],
  unsharpen: [
    -1, -1, -1,
    -1,  9, -1,
    -1, -1, -1
  ],
  emboss: [
     -2, -1,  0,
     -1,  1,  1,
      0,  1,  2
  ]
};

// List of effects to apply.
var effectsToApply = [
  "gaussianBlur",
  "emboss",
  "gaussianBlur",
  "unsharpen"
];

Enfin, appliquons chacune d'elles, en alternant la texture que nous affichons

// start with the original image
gl.bindTexture(gl.TEXTURE_2D, originalImageTexture);

// don't y flip images while drawing to the textures
gl.uniform1f(flipYLocation, 1);

// loop through each effect we want to apply.
for (var ii = 0; ii < effectsToApply.length; ++ii) {
  // Setup to draw into one of the framebuffers.
  setFramebuffer(framebuffers[ii % 2], image.width, image.height);

  drawWithKernel(effectsToApply[ii]);

  // for the next draw, use the texture we just rendered to.
  gl.bindTexture(gl.TEXTURE_2D, textures[ii % 2]);
}

// finally draw the result to the canvas.
gl.uniform1f(flipYLocation, -1);  // need to y flip for canvas
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");

function setFramebuffer(fbo, width, height) {
  // make this the framebuffer we are rendering to.
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Tell the shader the resolution of the framebuffer.
  gl.uniform2f(resolutionLocation, width, height);

  // Tell webgl the viewport setting needed for framebuffer.
  gl.viewport(0, 0, width, height);
}

function drawWithKernel(name) {
  // set the kernel
  gl.uniform1fv(kernelLocation, kernels[name]);

  // Draw the rectangle.
  gl.drawArrays(gl.TRIANGLES, 0, 6);
}

Je dois vous expliquer certaines choses.

L'appel de gl.bindFramebuffer avec null indique à WebGL que vous souhaitez effectuer le rendu sur le canevas plutôt que sur l'un de vos framebuffers. WebGL doit convertir l'espace de découpage en pixels. Pour ce faire, il se base sur les paramètres de gl.viewport. Les paramètres de gl.viewport sont définis par défaut sur la taille du canevas lorsque nous initialisons WebGL. Comme les framebuffers dans lesquels nous effectuons le rendu ont une taille différente de celle du canevas, nous devons définir la fenêtre d'affichage de manière appropriée. Enfin, dans les exemples de principes de base de WebGL, nous avons inversé la coordonnée Y lors de l'affichage, car WebGL affiche le canevas avec la position 0,0 dans l'angle inférieur gauche, au lieu de l'angle supérieur gauche, plus traditionnel pour la 2D. Cela n'est pas nécessaire lors de l'affichage dans un framebuffer. Comme le framebuffer n'est jamais affiché, l'orientation (haut/bas) n'a pas d'importance. Tout ce qui compte, c'est que le pixel 0,0 du framebuffer corresponde à 0,0 dans nos calculs. Pour résoudre ce problème, j'ai ajouté une entrée au nuanceur afin de pouvoir définir si l'image doit être inversée ou non.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
...
uniform float u_flipY;
...

void main() {
   ...
   gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);
   ...
}
</script>

Nous pouvons ensuite le définir lorsque nous effectuons l'affichage avec

...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);

J'ai gardé cet exemple simple en utilisant un seul programme GLSL capable d'obtenir plusieurs effets. Si vous vouliez effectuer un traitement d'image complet, vous auriez probablement besoin de nombreux programmes GLSL. Un programme d'ajustement de la teinte, de la saturation et de la luminance. Un autre pour la luminosité et le contraste. Un pour inverser, un autre pour ajuster les niveaux, etc. Vous devriez modifier le code pour changer de programme GLSL et mettre à jour les paramètres de ce programme spécifique. J'avais envisagé d'écrire cet exemple, mais il est préférable de laisser le lecteur s'en charger, car plusieurs programmes GLSL, chacun avec ses propres besoins en paramètres, nécessitent probablement une refactorisation majeure pour éviter que tout cela ne devienne un gros désordre. J'espère que cet exemple et les précédents vous ont permis de mieux appréhender WebGL et que le fait de commencer par la 2D vous a aidé à mieux comprendre WebGL. Si j'en ai le temps, j'essaierai d'écrire d'autres articles sur la façon de faire de la 3D, ainsi que sur ce que WebGL fait réellement en arrière-plan.

WebGL et alpha

J'ai remarqué que certains développeurs OpenGL rencontraient des problèmes avec la façon dont WebGL traite l'alpha dans le tampon d'arrière-plan (c'est-à-dire le canevas). J'ai donc pensé qu'il serait utile de passer en revue certaines des différences entre WebGL et OpenGL concernant l'alpha.

La plus grande différence entre OpenGL et WebGL est qu'OpenGL effectue le rendu dans un tampon d'arrière-plan qui n'est pas composé avec quoi que ce soit, ou qui n'est pas composé avec quoi que ce soit par le gestionnaire de fenêtres de l'OS. Votre alpha n'a donc pas d'importance. WebGL est composé par le navigateur avec la page Web. Par défaut, il utilise l'alpha prémultiplié, comme les balises <img> .png avec transparence et les balises de canevas 2D. WebGL propose plusieurs façons de rendre ce processus plus semblable à OpenGL.

1.  Indiquez à WebGL que vous souhaitez qu'il soit composé avec un canal alpha non prémultiplié.

gl = canvas.getContext("experimental-webgl", {premultipliedAlpha: false});

La valeur par défaut est "true". Bien entendu, le résultat sera toujours composé sur la page avec la couleur d'arrière-plan qui se trouve sous le canevas (la couleur d'arrière-plan du canevas, la couleur d'arrière-plan du conteneur du canevas, la couleur d'arrière-plan de la page, les éléments derrière le canevas si le canevas a un z-index > 0, etc.). En d'autres termes, la couleur CSS définit cette zone de la page Web. Pour détecter d'éventuels problèmes d'alpha, vous pouvez définir l'arrière-plan du canevas sur une couleur vive, comme le rouge. Vous verrez immédiatement ce qui se passe.

<canvas style="background: red;"></canvas>

Vous pouvez également la définir sur noir, ce qui masquera les problèmes d'alpha que vous rencontrez.

2. Indiquer à WebGL que vous ne voulez pas d'alpha dans le tampon d'arrière-plan

gl = canvas.getContext("experimental-webgl", {alpha: false});

Il se comportera ainsi davantage comme OpenGL, car le tampon d'arrière-plan n'aura que du RVB. C'est probablement la meilleure option, car un bon navigateur peut voir que vous n'avez pas d'alpha et optimiser la façon dont WebGL est composé. Bien sûr, cela signifie également qu'il n'y aura pas d'alpha dans le tampon d'arrière-plan. Si vous utilisez l'alpha dans le tampon d'arrière-plan pour une raison quelconque, cela risque de ne pas fonctionner. À ma connaissance, peu d'applications utilisent l'alpha dans le tampon d'arrière-plan. Je pense que cela aurait dû être le comportement par défaut.

3.  Effacer l'alpha à la fin de l'affichage

..
renderScene();
..
// Set the backbuffer's alpha to 1.0
gl.clearColor(1, 1, 1, 1);
gl.colorMask(false, false, false, true);
gl.clear(gl.COLOR_BUFFER_BIT);

L'effacement est généralement très rapide, car il existe un cas particulier pour cela dans la plupart des matériels. C'est ce que j'ai fait dans la plupart de mes démonstrations. Si j'étais malin, je passerais à la méthode 2 ci-dessus. Je le ferai peut-être juste après avoir publié ceci. Il semble que la plupart des bibliothèques WebGL devraient utiliser cette méthode par défaut. Les quelques développeurs qui utilisent réellement l'alpha pour les effets de composition peuvent en faire la demande. Les autres bénéficieront simplement des meilleures performances et du moins de surprises possible.

4.  Effacer l'alpha une fois, puis ne plus l'afficher

// At init time. Clear the back buffer.
gl.clearColor(1,1,1,1);
gl.clear(gl.COLOR_BUFFER_BIT);

// Turn off rendering to alpha
gl.colorMask(true, true, true, false);

Bien sûr, si vous effectuez le rendu dans vos propres framebuffers, vous devrez peut-être réactiver le rendu alpha, puis le désactiver à nouveau lorsque vous passerez au rendu dans le canevas.

5. Gérer les images

De plus, si vous chargez des fichiers PNG avec un canal alpha dans des textures, leur canal alpha est prémultiplié par défaut, ce qui n'est généralement PAS la façon dont la plupart des jeux procèdent. Si vous souhaitez empêcher ce comportement, vous devez l'indiquer à WebGL avec

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6. Utiliser une équation de fusion qui fonctionne avec l'alpha prémultiplié

Presque toutes les applications OpenGL que j'ai écrites ou sur lesquelles j'ai travaillé utilisent

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Cela fonctionne pour les textures alpha non prémultipliées. Si vous souhaitez utiliser des textures alpha prémultipliées, vous devez probablement

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Ce sont les seules méthodes que je connais. Si vous en connaissez d'autres, n'hésitez pas à les partager ci-dessous.