Principes de base de WebGL

Gregg Tavares
Gregg Tavares

Principes de base de WebGL

WebGL permet d'afficher d'incroyables graphismes 3D en temps réel dans votre navigateur. Mais beaucoup de gens ne savent pas 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. Clipspace en 2D et en couleurs En tant que programmeur utilisant WebGL, vous devez fournir ces deux éléments à WebGL. Pour ce faire, vous devez indiquer deux "nuanceurs". Un nuanceur Vertex qui fournit les coordonnées de l'espace des extraits et un nuanceur de fragments qui fournit la couleur. Les coordonnées Clipspace vont toujours de -1 à +1, quelle que soit la taille du canevas. Voici un exemple WebGL simple qui illustre WebGL dans 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 vide vont toujours de -1 à +1, quelle que soit la taille du canevas. Dans le cas ci-dessus, vous pouvez constater que nous ne faisons rien d'autre que la transmission directe de nos données de position. Étant donné que les données de position se trouvent déjà dans l'espace d'extraits, vous n'avez rien à faire. Si vous voulez utiliser la 3D, c'est à vous de fournir des nuanceurs qui convertissent les nuanceurs 3D en 2D, car WebGL est une API 2D. Pour les éléments 2D, vous préféreriez probablement travailler en pixels plutôt qu'en espace clip. Nous allons donc modifier le nuanceur afin de fournir des rectangles en pixels et de les convertir en espace de rognage. 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 modifier nos données de l'espace des extraits 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 que le rectangle est près du bas de cette zone. WebGL considère que l'angle inférieur gauche est 0,0. Pour qu'il s'agisse de l'angle 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 l'appeler pour des rectangles de différentes tailles. Pendant que nous y sommes, nous allons rendre la couleur réglable. Tout d'abord, le nuanceur de fragments utilise une entrée de couleur uniforme.

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

uniform vec4 u_color;

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

Et voici le nouveau code qui trace 50 rectangles à des endroits 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);
}

J'espère que vous voyez que WebGL est en fait une API assez simple. Bien qu'il puisse devenir plus compliqué de faire de la 3D, c'est vous, le programmeur, qui ajoute une complication 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" ?

Par défaut, les balises <script> contiennent du code JavaScript. Vous pouvez ne saisir aucun type, ou insérer type="javascript" ou type="text/javascript" pour que le navigateur interprète le contenu comme JavaScript. Si vous spécifiez autre chose, le navigateur ignore le contenu du tag de script.

Nous pouvons utiliser cette fonctionnalité pour stocker des nuanceurs dans des tags de script. Mieux encore, nous pouvons créer notre propre type et, dans notre recherche JavaScript, 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 des images avec WebGL

Le traitement des images est facile avec WebGL. C'est facile ? Lisez ce qui suit.

Pour dessiner des images dans WebGL, nous devons utiliser des textures. Tout comme WebGL attend des coordonnées de l'espace d'écrêtage lors du rendu au lieu des pixels, WebGL attend des coordonnées de texture lors de la lecture d'une texture. Les coordonnées d'une texture vont de 0.0 à 1.0, quelles que soient ses dimensions. Comme nous ne dessinons qu'un seul rectangle (en d'autres termes, 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 spécial de variable appelé "variable". C'est ce qu'on appelle une variation, car elle varie. WebGL interpolera les valeurs fournies dans le nuanceur de sommets lorsqu'il dessine chaque pixel à l'aide du nuanceur de fragments. À l'aide du nuanceur de sommets que nous avons vu à 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 la copier dans la texture. Comme nous sommes dans un navigateur, les images se chargent de manière asynchrone. Nous devons donc légèrement réorganiser le code pour attendre le chargement de la texture. Une fois 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);
  ...
}

Pas trop excitant, alors manipulons cette image. Pourquoi ne pas simplement alterner le rouge et le bleu ?

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

Comment traiter l'image qui examine les autres pixels ? Étant donné que WebGL fait référence aux textures dans des coordonnées de texture qui vont de 0,0 à 1, nous pouvons calculer le nombre de mouvements à déplacer pour un pixel à l'aide du calcul simple onePixel = 1.0 / textureSize. Voici un nuanceur de fragments qui calcule la moyenne des pixels gauches et droits 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 faire référence à d'autres pixels, utilisons un noyau à convolution pour effectuer un certain nombre de traitements courants des images. Dans ce cas, nous utiliserons un noyau 3x3. Un noyau à convolution n'est qu'une matrice 3x3 dans laquelle chaque entrée de la matrice représente la proportion de 8 pixels autour du pixel rendu à calculer. Nous divisons ensuite le résultat par le poids du noyau (la somme de toutes les valeurs qu'il contient) ou par 1,0, qui est le plus élevé. Voici un très bon article à ce sujet. Voici un autre article montrant du code réel si vous deviez l'écrire à la main en C++. Dans notre cas, nous allons effectuer cette tâche dans le nuanceur. Voici donc le nouveau nuanceur de fragments.

<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 à 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 voir comment appliquer plusieurs effets à l'image.

Quelle est la différence entre les préfixes a, u et v_ des variables en GLSL ?

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

Appliquer plusieurs effets

La deuxième question évidente pour le traitement d'image est de savoir comment appliquer plusieurs effets.

Vous pourriez essayer de générer des nuanceurs à la volée. Fournir une UI qui permet à l'utilisateur de sélectionner les effets qu'il souhaite utiliser, puis de générer un nuanceur qui exécute tous les effets. Cela n'est pas toujours possible, bien que cette technique soit souvent utilisée pour créer des effets pour les éléments graphiques en temps réel. Une méthode plus flexible consiste à utiliser deux textures supplémentaires et à effectuer le rendu de chaque texture l'une après l'autre, en effectuant un ping-pong dans les deux sens 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, un Framebuffer n'est pas un bon nom. Un framebuffer WebGL/OpenGL n'est en réalité qu'une collection d'états et non un tampon. Toutefois, en associant 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 chacune d'elles, en ping-pong avec la texture qui est également affichée

// 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);
}

Certaines choses que je devrais passer en revue.

En appelant gl.bindFramebuffer avec null, vous indiquez à WebGL que vous souhaitez effectuer le rendu dans le canevas plutôt que dans l'un de vos tampons de frames. WebGL doit convertir l'espace des extraits en pixels. Pour cela, 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 WebGL est initialisé. Étant donné que les tampons de frame dans lesquels nous effectuons le rendu ont une taille différente, nous devons définir la fenêtre d'affichage de manière appropriée sur le canevas. Enfin, dans les exemples de 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 à la coordonnée 2D en haut à gauche. Cela n'est pas nécessaire pour le rendu dans un framebuffer. Comme le framebuffer n'est jamais affiché, quelle partie située en haut et en bas n'a pas d'importance. Tout ce qui compte, c'est que le pixel 0,0 dans le framebuffer correspond à 0,0 dans nos calculs. Pour résoudre ce problème, j'ai permis de définir s'il faut inverser ou non les données en ajoutant une entrée supplémentaire 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>

Nous pouvons ensuite 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 gardé cet exemple simple en utilisant un seul programme GLSL qui peut produire plusieurs effets. Si vous souhaitez exploiter tout le potentiel du traitement d'image, vous aurez probablement besoin de nombreux programmes GLSL. Programme d'ajustement de la teinte, de la saturation et de la luminance. une autre pour la luminosité et le contraste. un pour l'inversion, un autre pour l'ajustement des 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 c'est un exercice qu'il vaut mieux laisser au lecteur, car plusieurs programmes GLSL ayant chacun leurs propres besoins de paramètres signifient probablement une refactorisation majeure pour éviter que cela ne devienne un gros bazar. J'espère que cet exemple et les exemples précédents ont rendu WebGL un peu plus accessible. J'espère qu'en commençant par la 2D, j'espère que WebGL sera un peu plus facile à comprendre. Si j'ai un peu de temps, j'essaierai d'écrire quelques articles supplémentaires sur l'utilisation de la 3D ainsi que sur le fonctionnement de WebGL.

WebGL et alpha

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

La principale différence entre OpenGL et WebGL est qu'OpenGL effectue le rendu sur un tampon d'arrière-plan qui n'est pas composé avec quoi que ce soit, ou n'est en fait pas composé avec quoi que ce soit par le gestionnaire de fenêtres de l'OS, ce qui n'a pas d'importance pour votre alpha. WebGL est composé par le navigateur avec la page Web, et la valeur par défaut consiste à utiliser une valeur alpha pré-multipliée, comme les balises <img> .png avec des tags de transparence et de canevas 2D. WebGL propose plusieurs façons d'utiliser OpenGL.

1) Indiquer à WebGL que vous voulez 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 n'importe quelle couleur d'arrière-plan qui finit par se trouver 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ément derrière le canevas si le z-index du canevas est supérieur à 0, etc.). En d'autres termes, la couleur CSS définie pour cette zone de la page Web. Le bon moyen de vérifier si vous rencontrez des problèmes alpha consiste à 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 alpha éventuels.

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 agira ainsi davantage comme OpenGL, car le tampon d'arrière-plan n'utilisera que le mode RVB. Il s'agit probablement de la meilleure option, car un bon navigateur pourrait voir que vous n'avez pas de version 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. Peu d'applications que je connais utilisent la version alpha dans le tampon d'arrière-plan. Je pense qu'il aurait dû être la valeur par défaut.

3) Supprimer la valeur 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);

Le nettoyage est généralement très rapide, car il existe un cas particulier pour la plupart des matériels. C'est ce que j'ai fait dans la plupart de mes démonstrations. Si j'étais intelligent, je passerais à la méthode n°2 ci-dessus. Peut-être que je le ferai tout de suite après avoir publié ça. Il semble que la plupart des bibliothèques WebGL devraient utiliser cette méthode par défaut. Les quelques développeurs qui utilisent la version alpha pour les effets de composition peuvent la demander. Les autres bénéficient des meilleures performances et du moins de surprises.

4) Effacer la version alpha une fois pour 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 tampons de frames, vous devrez peut-être réactiver le rendu en version 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 du code alpha dans les textures, la valeur par défaut est que leur valeur alpha est pré-multipliée, ce qui n'est généralement PAS le cas de la plupart des jeux. Si vous voulez empêcher ce comportement, vous devez indiquer à WebGL

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6) Utilisation d'une équation de mélange qui fonctionne avec la valeur alpha pré-multipliée

Presque toutes les applications OpenGL que j'ai écrites ou que j'ai utilisées

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,

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Ce sont les méthodes que je connais. Si vous en savez plus, veuillez les publier ci-dessous.