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 fournissez deux "nuanceurs". 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>

Là encore, les coordonnées de l'espace de séparation 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 pouvoir fournir des rectangles en pixels et le faire convertir 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);

Transformons 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 dans des emplacements et 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 autre chose, 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 sommets ou de fragments.

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 avec WebGL. C'est facile ? Lisez la suite.

Pour dessiner des images en WebGL, nous devons utiliser des textures. Tout comme WebGL attend des coordonnées de l'espace de séparation pour le 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. Étant donné que nous ne dessinons qu'un seul rectangle (deux triangles, en fait), nous devons indiquer à WebGL à quel emplacement de la texture chaque point du rectangle correspond. Nous allons transmettre ces informations du nuanceur de vertex au nuanceur de fragment à l'aide d'un type de variable spécial appelé "varying". 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. En utilisant le nuanceur de sommets mentionné à 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 fragments 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 le dessinons.

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 trop excitant, alors manipulons 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 de la formule mathématique simple 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 est simplement une matrice 3x3 où chaque entrée de la matrice représente la multiplication des 8 pixels autour du pixel que nous restituons. 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 très bon article à 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 que cela vous a convaincu que le traitement d'images avec 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 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 pourriez 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 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 des aller-retours 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 joignant une texture à un framebuffer, nous pouvons effectuer le 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 d'entre eux à 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 alors reconvertir l'espace de stockage en pixels. Cela se 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 relatifs aux principes de base de WebGL, nous avons inversé la coordonnée Y lors du rendu, car WebGL affiche le canevas avec 0,0 correspondant à l'angle inférieur gauche, et non à l'angle supérieur gauche, plus traditionnel pour la 2D. Cela n'est pas nécessaire pour le rendu dans un framebuffer. Comme le framebuffer n'est jamais affiché, la partie supérieure et inférieure n'a pas d'importance. Tout ce qui compte est que le pixel 0,0 du frame buffer corresponde à 0,0 dans nos calculs. Pour résoudre ce problème, j'ai permis de définir s'il faut inverser ou non l'image en ajoutant une autre entrée dans le nuanceur.

<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>

Ensuite, nous pouvons le définir lorsque nous effectuons le rendu avec

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

J'ai simplifié cet exemple en utilisant un seul programme GLSL qui permet d'obtenir 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 d'autres articles sur la 3D, ainsi que des informations plus détaillées sur ce que WebGL fait vraiment sous le capot.

WebGL et alpha

J'ai remarqué que certains développeurs OpenGL avaient des problèmes avec la façon dont WebGL traite alpha dans le backbuffer (c'est-à-dire le canevas). J'ai donc pensé qu'il serait intéressant d'aborder certaines des différences entre WebGL et OpenGL liées à la version 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. Avec WebGL, plusieurs possibilités s'offrent à vous.

1) Indiquer à WebGL que vous souhaitez qu'il soit composé avec une valeur alpha non prémultipliée

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 trouvera 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 z-index supérieur à 0, etc.). En d'autres termes, la couleur CSS définit 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 les problèmes d'alpha que vous rencontrez.

#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é. Bien sûr, cela signifie également qu'il n'y aura pas de valeur alpha dans le tampon d'arrière-plan. Par conséquent, si vous utilisez alpha dans le tampon d'arrière-plan à certaines fins, cela pourrait ne pas fonctionner pour vous. À ma connaissance, peu d'applications utilisent alpha dans le tampon d'arrière-plan. Je pense que c'est la méthode par défaut qui aurait dû être utilisée.

#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émos. 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 obtiendront les meilleurs résultats et le moins de surprises.

#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);

N° 6) Utiliser une équation de combinaison qui fonctionne avec une valeur alpha prémultipliée

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 savez d'autres, n'hésitez pas à les publier ci-dessous.