Principes de base de WebGL

Gregg Tavares
Gregg Tavares

Principes de base de WebGL

WebGL permet d'afficher des graphismes 3D en temps réel incroyables dans votre navigateur. Mais beaucoup de gens ne savent pas que WebGL est en fait une API 2D, et non une API 3D. Voyons cela de plus près.

WebGL ne se soucie que de deux choses. Coordonnées de l'espace de découpe 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 vertex qui fournit les coordonnées de l'espace de découpe et un nuanceur de fragment qui fournit la couleur. Les coordonnées de l'espace de découpe vont toujours de -1 à +1, quelle que soit la taille de votre canevas. Voici un exemple simple de WebGL 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 l'exemple ci-dessus, vous pouvez voir que nous ne faisons que transmettre directement nos données de position. Étant donné que les données de position sont déjà dans l'espace de découpe, aucune tâche n'est requise. Si vous souhaitez utiliser 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 2D, vous préférerez probablement travailler en pixels plutôt qu'en espace de découpe. Modifions donc le nuanceur pour que nous puissions fournir des rectangles en pixels et qu'il les convertisse en espace de découpe. 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écoupe 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 en bas de cette zone. WebGL considère l'angle inférieur gauche comme étant 0,0. Pour que ce soit le coin supérieur gauche plus traditionnel utilisé pour les API graphiques 2D, il suffit d'inverser la coordonnée y.

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

Convertissons le code qui définit un rectangle en fonction afin de pouvoir l'appeler pour des rectangles de différentes tailles. En même temps, nous allons rendre la couleur configurable. Nous commençons par faire en sorte que le nuanceur de fragments prenne 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 et dans 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);
}

J'espère que vous pouvez constater que WebGL est en réalité une API assez simple. Bien que la 3D puisse être plus complexe, cette complication 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 les valeurs "type="x-shader/x-vertex" et "type="x-shader/x-fragment" ?

Les balises <script> contiennent par défaut du code JavaScript. Vous pouvez ne pas spécifier de type, ou indiquer type="javascript" ou type="text/javascript". Le navigateur interprétera alors le contenu comme du code JavaScript. Si vous ajoutez un autre élément, le navigateur ignore le contenu de la balise 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 code JavaScript, le rechercher pour décider de compiler le nuanceur en tant que nuanceur de sommet ou de fragment.

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

Traitement des images WebGL

Le traitement des images est facile en WebGL. C'est facile ? Lisez la suite.

Pour dessiner des images en WebGL, nous devons utiliser des textures. De même que WebGL attend des coordonnées d'espace de découpe au lieu de pixels pour le rendu, il 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. Étant donné que nous ne dessinons qu'un seul rectangle (deux triangles, en fait), nous devons indiquer à WebGL l'emplacement dans la texture auquel chaque point du rectangle correspond. Nous allons transmettre ces informations du nuanceur de sommet au nuanceur de fragment à l'aide d'un type de variable spécial appelé "variante". On l'appelle "variable", car elle varie. WebGL interpole les valeurs que nous fournissons dans le nuanceur de sommets lorsqu'il dessine chaque pixel à l'aide du nuanceur de fragment. À l'aide du nuanceur de vertex 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 fragment.

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. Étant donné que 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 est chargé, nous allons le dessiner.

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. Nous allons donc manipuler cette image. Que pensez-vous de simplement échanger le rouge et le bleu ?

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

Que se passe-t-il si nous voulons effectuer un traitement d'image qui examine d'autres pixels ? Étant donné que 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 1 pixel à l'aide d'une simple formule mathématique onePixel = 1.0 / textureSize. Voici un nuanceur de fragment qui calcule la moyenne des pixels de gauche et de droite 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'images courants. Dans ce cas, nous utiliserons un noyau 3x3. Un noyau de convolution n'est qu'une matrice 3x3, où chaque entrée de la matrice 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 très intéressant à ce sujet. Cet autre article présente du code concret si vous deviez écrire ce code 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 vous avoir convaincu que le traitement des images en WebGL est assez simple. Je vais maintenant vous expliquer comment appliquer plusieurs effets à l'image.

Pourquoi les préfixes a, u et v_ sont-ils utilisés devant les variables dans GLSL ?

Il ne s'agit que 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 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 la plus évidente qui suit concerne le traitement d'image : comment appliquer plusieurs effets ?

Vous pouvez essayer de générer des nuanceurs en temps réel. Fournissez une UI qui permet à l'utilisateur de sélectionner les effets qu'il souhaite utiliser, puis de générer un nuanceur qui applique tous les effets. Cela n'est pas toujours possible, même si cette technique est souvent utilisée pour créer des effets pour les graphiques en temps réel. Une méthode plus flexible consiste à utiliser deux autres textures et à effectuer le rendu sur chacune d'elles à tour de rôle, en effectuant un ping-pong 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 un mauvais nom. Un frame buffer WebGL/OpenGL n'est en réalité qu'un ensemble d'états et non un tampon de quelque sorte que ce soit. 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 ceux à 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-les chacune, 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 quelques points.

Appeler 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écoupe en pixels. Il le fait en fonction des 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. Étant donné que les framebuffers dans lesquels nous effectuons le rendu sont de taille différente du canevas, nous devons définir la fenêtre d'affichage de manière appropriée. Enfin, dans les exemples sur les principes fondamentaux de WebGL, nous avons inversé la coordonnée Y lors du rendu, car WebGL affiche le canevas avec 0,0 en bas à gauche au lieu du coin supérieur gauche plus traditionnel pour la 2D. Cela n'est pas nécessaire lors du rendu dans un framebuffer. Étant donné que le framebuffer n'est jamais affiché, la partie supérieure et inférieure est sans importance. Tout ce qui compte est que le pixel 0,0 du frame buffer corresponde à 0,0 dans nos calculs. Pour y remédier, j'ai ajouté une autre entrée au nuanceur pour 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 prenons en charge

...
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 pouvant générer plusieurs effets. Si vous souhaitez effectuer un traitement d'image complet, vous aurez probablement besoin de nombreux programmes GLSL. Programme permettant d'ajuster la teinte, la saturation et la luminance. Un autre pour la luminosité et le contraste. L'un pour l'inversion, l'autre pour ajuster les niveaux, etc. Vous devez modifier le code pour changer de programme GLSL et mettre à jour les paramètres de ce programme particulier. J'ai envisagé d'écrire cet exemple, mais il est préférable de le laisser au lecteur, car plusieurs programmes GLSL ayant chacun leurs propres besoins en paramètres impliquent probablement une refonte majeure pour éviter que tout cela ne devienne un grand fouillis de spaghettis. J'espère que cet article et les exemples précédents vous ont permis de mieux comprendre WebGL. J'espère également que commencer par la 2D vous aidera à mieux comprendre WebGL. Si j'ai le temps, j'essaierai d'écrire quelques articles supplémentaires sur la 3D, ainsi que des informations plus détaillées sur ce que WebGL fait vraiment sous le capot.

WebGL et version 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 que OpenGL effectue le rendu sur un tampon d'arrière-plan qui n'est pas composite avec quoi que ce soit, ou qui n'est pas composite avec quoi que ce soit par le gestionnaire de fenêtres de l'OS. Par conséquent, votre alpha n'a aucune importance. WebGL est composite avec la page Web par le navigateur, et l'alpha prémultiplié est utilisé par défaut, comme les balises <img> .png avec transparence et les balises de canevas 2D. WebGL propose plusieurs façons de rendre cela plus semblable à OpenGL.

#1) Indiquez à WebGL que vous souhaitez qu'il soit composite avec un 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 (couleur d'arrière-plan du canevas, couleur d'arrière-plan du conteneur du canevas, couleur d'arrière-plan de la page, éléments situés derrière le canevas si le canevas a un indice Z supérieur à 0, etc.). En d'autres termes, la couleur définie par le CSS pour cette zone de la page Web. Un excellent moyen de savoir si vous rencontrez des problèmes d'alpha est de 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 le définir sur noir, ce qui masquera tous les problèmes d'alpha.

#2) Indiquer à WebGL que vous ne souhaitez pas d'alpha dans le tampon d'arrière-plan

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

Cela le fera agir davantage comme OpenGL, car le tampon d'arrière-plan ne contiendra que du RVB. Il s'agit probablement de 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é. Cela signifie bien sûr qu'il n'y aura pas d'alpha dans le tampon arrière. Par conséquent, si vous utilisez l'alpha dans le tampon arrière à des fins spécifiques, cela risque de ne pas fonctionner. Peu d'applications que je connais utilisent l'alpha dans le backbuffer. Je pense que cela aurait dû être l'option par défaut.

#3) Effacer l'alpha à la fin du rendu

..
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. Je l'ai fait dans la plupart de mes démonstrations. Si j'étais intelligent, je passerais à la méthode 2 ci-dessus. Je vais peut-être le faire juste après avoir publié ce message. 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 utilisateurs bénéficieront des meilleures performances et des surprises les moins nombreuses.

#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 entendu, si vous effectuez le rendu dans vos propres framebuffers, vous devrez peut-être réactiver le rendu en alpha, puis le désactiver lorsque vous passerez au rendu dans le canevas.

#5) Gestion des images

De plus, si vous chargez des fichiers PNG avec alpha dans des textures, leur alpha est prémultiplié par défaut, ce qui n'est généralement PAS le cas dans la plupart des jeux. Si vous souhaitez éviter ce comportement, vous devez indiquer à WebGL avec

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

#6) Utiliser une équation de mélange 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 travailler avec des textures alpha prémultipliées, vous devez probablement

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Ce sont les méthodes que je connais. Si vous en connaissez d'autres, veuillez les indiquer ci-dessous.