Table des matières

WebGL2Fundamentals.org

Fix, Fork, Contribute

WebGL2 Projection planaire et en perspective

Cet article suppose que vous avez lu l’article sur moins de code, plus de fun car il utilise la bibliothèque mentionnée là-bas pour alléger les exemples. Si vous ne comprenez pas ce que sont les tampons, les vertex arrays et les attributs, ou ce que fait une fonction nommée twgl.setUniforms, etc., vous devriez probablement revenir en arrière et lire les fondamentaux.

Il suppose également que vous avez lu les articles sur la perspective, l’article sur les caméras, l’article sur les textures, et l’article sur la visualisation de la caméra. Si vous ne les avez pas lus, commencez probablement par là.

La projection mapping est le processus de “projection” d’une image au sens d’un projecteur de cinéma pointé sur un écran et projetant un film dessus. Un projecteur de cinéma projette un plan en perspective. Plus l’écran est éloigné du projecteur, plus l’image est grande. Si vous inclinez l’écran de façon à ce qu’il ne soit pas perpendiculaire au projecteur, le résultat est un trapèze ou un quadrilatère quelconque.

Bien sûr, la projection mapping n’a pas à être plane. Il existe par exemple la projection cylindrique, la projection sphérique, etc.

Commençons par la projection planaire. Dans ce cas, imaginez que le projecteur a la même taille que l’écran, de sorte qu’au lieu que l’image grandisse à mesure que l’écran s’éloigne du projecteur, elle reste de la même taille.

Créons d’abord une scène simple qui dessine un plan et une sphère. Nous les texturerons tous les deux avec une simple texture de damier 8x8.

Les shaders sont similaires à ceux de l’article sur les textures, sauf que les différentes matrices sont séparées pour ne pas avoir à les multiplier en JavaScript.

const vs = `#version 300 es
in vec4 a_position;
in vec2 a_texcoord;

uniform mat4 u_projection;
uniform mat4 u_view;
uniform mat4 u_world;

out vec2 v_texcoord;

void main() {
  gl_Position = u_projection * u_view * u_world * a_position;

  // Passe la coordonnée de texture au fragment shader.
  v_texcoord = a_texcoord;
}
`;

J’ai aussi ajouté un uniform u_colorMult pour multiplier la couleur de la texture. En utilisant une texture monochrome, on peut changer sa couleur de cette façon.

const fs = `#version 300 es
precision highp float;

// Reçu du vertex shader.
in vec2 v_texcoord;

uniform vec4 u_colorMult;
uniform sampler2D u_texture;

out vec4 outColor;

void main() {
  outColor = texture(u_texture, v_texcoord) * u_colorMult;
}
`;

Voici le code pour configurer le programme, les tampons de la sphère et du plan :

// configure le programme GLSL
// compile les shaders, lie le programme, récupère les emplacements
const textureProgramInfo = twgl.createProgramInfo(gl, [vs, fs]);

const sphereBufferInfo = primitives.createSphereBufferInfo(
    gl,
    1,  // rayon
    12, // subdivisions autour
    6,  // subdivisions en bas
);
const sphereVAO = twgl.createVAOFromBufferInfo(
    gl, textureProgramInfo, sphereBufferInfo);
const planeBufferInfo = primitives.createPlaneBufferInfo(
    gl,
    20,  // largeur
    20,  // hauteur
    1,   // subdivisions horizontales
    1,   // subdivisions verticales
);
const planeVAO = twgl.createVAOFromBufferInfo(
    gl, textureProgramInfo, planeBufferInfo);

et le code pour créer une texture de damier 8x8 en utilisant les techniques couvertes dans l’article sur les textures de données.

// crée une texture de damier 8x8
const checkerboardTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, checkerboardTexture);
gl.texImage2D(
    gl.TEXTURE_2D,
    0,                // niveau mip
    gl.LUMINANCE,     // format interne
    8,                // largeur
    8,                // hauteur
    0,                // bordure
    gl.LUMINANCE,     // format
    gl.UNSIGNED_BYTE, // type
    new Uint8Array([  // données
      0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC,
      0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF,
      0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC,
      0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF,
      0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC,
      0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF,
      0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC,
      0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF, 0xCC, 0xFF,
    ]));
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

Pour dessiner, nous allons créer une fonction qui prend une matrice de projection et une matrice de caméra, calcule la matrice de vue à partir de la matrice de caméra puis dessine la sphère et le plan :

// Uniforms pour chaque objet.
const planeUniforms = {
  u_colorMult: [0.5, 0.5, 1, 1],  // bleu clair
  u_texture: checkerboardTexture,
  u_world: m4.translation(0, 0, 0),
};
const sphereUniforms = {
  u_colorMult: [1, 0.5, 0.5, 1],  // rose
  u_texture: checkerboardTexture,
  u_world: m4.translation(2, 3, 4),
};

function drawScene(projectionMatrix, cameraMatrix) {
  // Crée une matrice de vue à partir de la matrice de caméra.
  const viewMatrix = m4.inverse(cameraMatrix);

  gl.useProgram(textureProgramInfo.program);

  // Définit l'uniform partagé par la sphère et le plan
  twgl.setUniforms(textureProgramInfo, {
    u_view: viewMatrix,
    u_projection: projectionMatrix,
  });

  // ------ Dessine la sphère --------

  // Configure tous les attributs nécessaires.
  gl.bindVertexArray(sphereVAO);

  // Définit les uniforms propres à la sphère
  twgl.setUniforms(textureProgramInfo, sphereUniforms);

  // appelle gl.drawArrays ou gl.drawElements
  twgl.drawBufferInfo(gl, sphereBufferInfo);

  // ------ Dessine le plan --------

  // Configure tous les attributs nécessaires.
  gl.bindVertexArray(planeVAO);

  // Définit les uniforms propres au plan
  twgl.setUniforms(textureProgramInfo, planeUniforms);

  // appelle gl.drawArrays ou gl.drawElements
  twgl.drawBufferInfo(gl, planeBufferInfo);
}

Nous pouvons utiliser ce code depuis une fonction render comme ceci :

const settings = {
  cameraX: 2.75,
  cameraY: 5,
};
const fieldOfViewRadians = degToRad(60);

function render() {
  twgl.resizeCanvasToDisplaySize(gl.canvas);

  // Indique à WebGL comment convertir du clip space en pixels
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  gl.enable(gl.CULL_FACE);
  gl.enable(gl.DEPTH_TEST);

  // Efface le canvas ET le tampon de profondeur.
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  // Calcule la matrice de projection
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const projectionMatrix =
      m4.perspective(fieldOfViewRadians, aspect, 1, 2000);

  // Calcule la matrice de la caméra avec lookAt.
  const cameraPosition = [settings.cameraX, settings.cameraY, 7];
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const cameraMatrix = m4.lookAt(cameraPosition, target, up);

  drawScene(projectionMatrix, cameraMatrix);
}
render();

Nous avons donc une scène simple avec un plan et une sphère. J’ai ajouté quelques curseurs pour permettre de changer la position de la caméra afin de mieux comprendre la scène.

Maintenant, projetons une texture sur la sphère et le plan.

La première chose à faire est de charger une texture.

function loadImageTexture(url) {
  // Crée une texture.
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  // Remplit la texture avec un pixel bleu 1x1.
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
                new Uint8Array([0, 0, 255, 255]));
  // Charge une image de façon asynchrone
  const image = new Image();
  image.src = url;
  image.addEventListener('load', function() {
    // Maintenant que l'image est chargée, on la copie dans la texture.
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    // suppose que cette texture est une puissance de 2
    gl.generateMipmap(gl.TEXTURE_2D);
    render();
  });
  return texture;
}

const imageTexture = loadImageTexture('resources/f-texture.png');

Rappelons-nous de l’article sur la visualisation de la caméra : nous avons créé un cube allant de -1 à +1 et l’avons dessiné pour représenter le frustum de la caméra. Nos matrices faisaient en sorte que l’espace à l’intérieur de ce frustum représente une zone en forme de frustum dans l’espace monde, convertie vers le clip space -1 à +1. Nous pouvons faire quelque chose de similaire ici.

Essayons. D’abord, dans notre fragment shader, nous allons dessiner la texture projetée partout où ses coordonnées de texture sont entre 0.0 et 1.0. En dehors de cette plage, nous utiliserons la texture de damier :

const fs = `#version 300 es
precision highp float;

// Reçu du vertex shader.
in vec2 v_texcoord;
+in vec4 v_projectedTexcoord;

uniform vec4 u_colorMult;
uniform sampler2D u_texture;
+uniform sampler2D u_projectedTexture;

out vec4 outColor;

void main() {
-  outColor = texture(u_texture, v_texcoord) * u_colorMult;
+  // divise par w pour obtenir la valeur correcte. Voir l'article sur la perspective
+  vec3 projectedTexcoord = v_projectedTexcoord.xyz / v_projectedTexcoord.w;
+
+  bool inRange = 
+      projectedTexcoord.x >= 0.0 &&
+      projectedTexcoord.x <= 1.0 &&
+      projectedTexcoord.y >= 0.0 &&
+      projectedTexcoord.y <= 1.0;
+
+  vec4 projectedTexColor = texture(u_projectedTexture, projectedTexcoord.xy);
+  vec4 texColor = texture(u_texture, v_texcoord) * u_colorMult;
+
+  float projectedAmount = inRange ? 1.0 : 0.0;
+  outColor = mix(texColor, projectedTexColor, projectedAmount);
}
`;

Pour calculer les coordonnées de texture projetées, nous allons créer une matrice qui représente un espace 3D orienté et positionné dans une certaine direction, tout comme la caméra dans l’article sur la visualisation de la caméra. Nous projetterons ensuite les positions mondiales des sommets de la sphère et du plan à travers cet espace. Là où ils se trouvent entre 0 et 1, le code que nous venons d’écrire affichera la texture.

Ajoutons du code au vertex shader pour projeter les positions mondiales de la sphère et du plan à travers cet espace :

const vs = `#version 300 es
in vec4 a_position;
in vec2 a_texcoord;

uniform mat4 u_projection;
uniform mat4 u_view;
uniform mat4 u_world;
+uniform mat4 u_textureMatrix;

out vec2 v_texcoord;
+out vec4 v_projectedTexcoord;

void main() {
+  vec4 worldPosition = u_world * a_position;

-  gl_Position = u_projection * u_view * u_world * a_position;
+  gl_Position = u_projection * u_view * worldPosition;

  // Passe la coordonnée de texture au fragment shader.
  v_texcoord = a_texcoord;

+  v_projectedTexcoord = u_textureMatrix * worldPosition;
}

Il ne reste plus qu’à calculer la matrice qui définit cet espace orienté. Tout ce qu’on a à faire, c’est calculer une matrice monde comme pour n’importe quel autre objet, puis prendre son inverse. Cela nous donnera une matrice qui nous permet d’orienter les positions mondiales d’autres objets relativement à cet espace. C’est exactement ce que fait la matrice de vue dans l’article sur les caméras.

Nous utiliserons notre fonction lookAt créée dans ce même article :

const settings = {
  cameraX: 2.75,
  cameraY: 5,
+  posX: 3.5,
+  posY: 4.4,
+  posZ: 4.7,
+  targetX: 0.8,
+  targetY: 0,
+  targetZ: 4.7,
};

function drawScene(projectionMatrix, cameraMatrix) {
  // Crée une matrice de vue à partir de la matrice de caméra.
  const viewMatrix = m4.inverse(cameraMatrix);

  let textureWorldMatrix = m4.lookAt(
      [settings.posX, settings.posY, settings.posZ],          // position
      [settings.targetX, settings.targetY, settings.targetZ], // cible
      [0, 1, 0],                                              // haut
  );

  // utilise l'inverse de cette matrice monde pour obtenir
  // une matrice qui transformera d'autres positions
  // relativement à cet espace monde.
  const textureMatrix = m4.inverse(textureWorldMatrix);

  // définit les uniforms identiques pour la sphère et le plan
  twgl.setUniforms(textureProgramInfo, {
    u_view: viewMatrix,
    u_projection: projectionMatrix,
+    u_textureMatrix: textureMatrix,
+    u_projectedTexture: imageTexture,
  });

  ...
}

Bien sûr, vous n’êtes pas obligé d’utiliser lookAt. Vous pouvez créer une matrice monde de n’importe quelle façon, par exemple en utilisant un graphe de scène ou une pile de matrices.

Avant d’exécuter, ajoutons une échelle :

const settings = {
  cameraX: 2.75,
  cameraY: 5,
  posX: 3.5,
  posY: 4.4,
  posZ: 4.7,
  targetX: 0.8,
  targetY: 0,
  targetZ: 4.7,
+  projWidth: 2,
+  projHeight: 2,
};

function drawScene(projectionMatrix, cameraMatrix) {
  // Crée une matrice de vue à partir de la matrice de caméra.
  const viewMatrix = m4.inverse(cameraMatrix);

  let textureWorldMatrix = m4.lookAt(
      [settings.posX, settings.posY, settings.posZ],          // position
      [settings.targetX, settings.targetY, settings.targetZ], // cible
      [0, 1, 0],                                              // haut
  );
+  textureWorldMatrix = m4.scale(
+      textureWorldMatrix,
+      settings.projWidth, settings.projHeight, 1,
+  );

  // utilise l'inverse de cette matrice monde pour obtenir
  // une matrice qui transformera d'autres positions
  // relativement à cet espace monde.
  const textureMatrix = m4.inverse(textureWorldMatrix);

  ...
}

et avec ça, nous obtenons une texture projetée.

Il est peut-être difficile de voir l’espace dans lequel se trouve la texture. Ajoutons un cube en fil de fer pour aider à visualiser.

Il nous faut d’abord un ensemble séparé de shaders. Ces shaders peuvent simplement dessiner une couleur unie, sans textures.

const colorVS = `#version 300 es
in vec4 a_position;

uniform mat4 u_projection;
uniform mat4 u_view;
uniform mat4 u_world;

void main() {
  // Multiplie la position par les matrices.
  gl_Position = u_projection * u_view * u_world * a_position;
}
`;
const colorFS = `#version 300 es
precision highp float;

uniform vec4 u_color;

out vec4 outColor;

void main() {
  outColor = u_color;
}
`;

Ensuite, nous devons compiler et lier ces shaders :

// configure les programmes GLSL
const textureProgramInfo = twgl.createProgramInfo(gl, [vs, fs]);
+const colorProgramInfo = twgl.createProgramInfo(gl, [colorVS, colorFS]);

Et nous avons besoin de données pour dessiner un cube fait de lignes :

const sphereBufferInfo = primitives.createSphereBufferInfo(
    gl,
    1,  // rayon
    12, // subdivisions autour
    6,  // subdivisions en bas
);
const sphereVAO = twgl.createVAOFromBufferInfo(
    gl, textureProgramInfo, sphereBufferInfo);
const planeBufferInfo = primitives.createPlaneBufferInfo(
    gl,
    20,  // largeur
    20,  // hauteur
    1,   // subdivisions horizontales
    1,   // subdivisions verticales
);
const planeVAO = twgl.createVAOFromBufferInfo(
    gl, textureProgramInfo, planeBufferInfo);
+const cubeLinesBufferInfo = twgl.createBufferInfoFromArrays(gl, {
+  position: [
+     0,  0, -1,
+     1,  0, -1,
+     0,  1, -1,
+     1,  1, -1,
+     0,  0,  1,
+     1,  0,  1,
+     0,  1,  1,
+     1,  1,  1,
+  ],
+  indices: [
+    0, 1,
+    1, 3,
+    3, 2,
+    2, 0,
+
+    4, 5,
+    5, 7,
+    7, 6,
+    6, 4,
+
+    0, 4,
+    1, 5,
+    3, 7,
+    2, 6,
+  ],
+});
+const cubeLinesVAO = twgl.createVAOFromBufferInfo(
+    gl, colorProgramInfo, cubeLinesBufferInfo);

Remarquez que ce cube va de 0 à 1 en X et Y pour correspondre aux coordonnées de texture. En Z, il va de -1 à 1. Cela nous permettra de le mettre à l’échelle pour l’étirer dans les deux directions.

Pour l’utiliser, on peut simplement utiliser la textureWorldMatrix d’avant puisque tout ce qu’on veut faire c’est dessiner le cube là où cet espace existe.

function drawScene(projectionMatrix, cameraMatrix) {

  ...
+  // ------ Dessine le cube ------
+
+  gl.useProgram(colorProgramInfo.program);
+
+  // Configure tous les attributs nécessaires.
+  gl.bindVertexArray(cubeLinesVAO);
+
+  // met le cube à l'échelle en Z pour qu'il soit très long
+  // pour représenter que la texture est projetée à l'infini
+  const mat = m4.scale(textureWorldMatrix, 1, 1, 1000);
+
+  // Définit les uniforms qu'on vient de calculer
+  twgl.setUniforms(colorProgramInfo, {
+    u_color: [0, 0, 0, 1],
+    u_view: viewMatrix,
+    u_projection: projectionMatrix,
+    u_world: mat,
+  });
+
+  // appelle gl.drawArrays ou gl.drawElements
+  twgl.drawBufferInfo(gl, cubeLinesBufferInfo, gl.LINES);
}

Et maintenant on peut voir plus facilement où se fait la projection.

Il est important de noter que nous ne projetons pas vraiment la texture. Nous faisons plutôt le contraire. Pour chaque pixel d’un objet rendu, on cherche quelle partie de la texture y serait projetée, puis on lit la couleur à cette partie de la texture.

Puisque nous avons mentionné les projecteurs de cinéma, comment simulerions-nous un projecteur de cinéma ? Fondamentalement, on peut juste multiplier par une matrice de projection :

const settings = {
  cameraX: 2.75,
  cameraY: 5,
  posX: 2.5,
  posY: 4.8,
  posZ: 4.3,
  targetX: 2.5,
  targetY: 0,
  targetZ: 3.5,
  projWidth: 1,
  projHeight: 1,
+  perspective: true,
+  fieldOfView: 45,
};

...

function drawScene(projectionMatrix, cameraMatrix) {
  // Crée une matrice de vue à partir de la matrice de caméra.
  const viewMatrix = m4.inverse(cameraMatrix);

  const textureWorldMatrix = m4.lookAt(
      [settings.posX, settings.posY, settings.posZ],          // position
      [settings.targetX, settings.targetY, settings.targetZ], // cible
      [0, 1, 0],                                              // haut
  );
-  textureWorldMatrix = m4.scale(
-      textureWorldMatrix,
-      settings.projWidth, settings.projHeight, 1,
-  );
  
+  const textureProjectionMatrix = settings.perspective
+      ? m4.perspective(
+          degToRad(settings.fieldOfView),
+          settings.projWidth / settings.projHeight,
+          0.1,  // near
+          200)  // far
+      : m4.orthographic(
+          -settings.projWidth / 2,   // gauche
+           settings.projWidth / 2,   // droite
+          -settings.projHeight / 2,  // bas
+           settings.projHeight / 2,  // haut
+           0.1,                      // near
+           200);                     // far

  // utilise l'inverse de cette matrice monde pour obtenir
  // une matrice qui transformera d'autres positions
  // relativement à cet espace monde.
-  const textureMatrix = m4.inverse(textureWorldMatrix);
+  const textureMatrix = m4.multiply(
+      textureProjectionMatrix,
+      m4.inverse(textureWorldMatrix));

Notez qu’il y a une option pour utiliser une matrice de projection en perspective ou orthographique.

Nous devons aussi utiliser cette matrice de projection lors du dessin des lignes :

// ------ Dessine le cube ------

...

-// met le cube à l'échelle en Z pour qu'il soit très long
-// pour représenter que la texture est projetée à l'infini
-const mat = m4.scale(textureWorldMatrix, 1, 1, 1000);

+// oriente le cube pour correspondre à la projection.
+const mat = m4.multiply(
+    textureWorldMatrix, m4.inverse(textureProjectionMatrix));

et avec ça on obtient :

Ça fonctionne en partie, mais notre projection et les lignes du cube utilisent l’espace 0 à 1, donc ça n’utilise qu’un quart du frustum de projection.

Pour corriger ça, faisons d’abord en sorte que notre cube soit un cube -1 à +1 dans toutes les directions :

const cubeLinesBufferInfo = twgl.createBufferInfoFromArrays(gl, {
  position: [
-     0,  0, -1,
-     1,  0, -1,
-     0,  1, -1,
-     1,  1, -1,
-     0,  0,  1,
-     1,  0,  1,
-     0,  1,  1,
-     1,  1,  1,
+    -1, -1, -1,
+     1, -1, -1,
+    -1,  1, -1,
+     1,  1, -1,
+    -1, -1,  1,
+     1, -1,  1,
+    -1,  1,  1,
+     1,  1,  1,
  ],
  indices: [
    0, 1,
    1, 3,
    3, 2,
    2, 0,

    4, 5,
    5, 7,
    7, 6,
    6, 4,

    0, 4,
    1, 5,
    3, 7,
    2, 6,
  ],
});

Ensuite, nous devons faire en sorte que l’espace à l’intérieur du frustum aille de 0 à 1 pour notre matrice de texture, ce qu’on peut faire en décalant l’espace de 0.5 et en le mettant à l’échelle de 0.5 :

const textureWorldMatrix = m4.lookAt(
    [settings.posX, settings.posY, settings.posZ],          // position
    [settings.targetX, settings.targetY, settings.targetZ], // cible
    [0, 1, 0],                                              // haut
);
const textureProjectionMatrix = settings.perspective
    ? m4.perspective(
        degToRad(settings.fieldOfView),
        settings.projWidth / settings.projHeight,
        0.1,  // near
        200)  // far
    : m4.orthographic(
        -settings.projWidth / 2,   // gauche
         settings.projWidth / 2,   // droite
        -settings.projHeight / 2,  // bas
         settings.projHeight / 2,  // haut
         0.1,                      // near
         200);                     // far

-// utilise l'inverse de cette matrice monde pour obtenir
-// une matrice qui transformera d'autres positions
-// relativement à cet espace monde.
-const textureMatrix = m4.multiply(
-    textureProjectionMatrix,
-    m4.inverse(textureWorldMatrix));

+let textureMatrix = m4.identity();
+textureMatrix = m4.translate(textureMatrix, 0.5, 0.5, 0.5);
+textureMatrix = m4.scale(textureMatrix, 0.5, 0.5, 0.5);
+textureMatrix = m4.multiply(textureMatrix, textureProjectionMatrix);
+// utilise l'inverse de cette matrice monde pour obtenir
+// une matrice qui transformera d'autres positions
+// relativement à cet espace monde.
+textureMatrix = m4.multiply(
+    textureMatrix,
+    m4.inverse(textureWorldMatrix));

Et maintenant ça fonctionne :

À quoi sert la projection planaire d’une texture ?

L’une des raisons est simplement parce qu’on le veut. La plupart des logiciels de modélisation 3D offrent un moyen de faire de la projection planaire avec une texture.

Une autre est les décalcomanies (decals). Les décalcomanies permettent de mettre des éclaboussures de peinture ou des marques d’explosion sur une surface. Elles ne fonctionnent généralement pas via des shaders comme ci-dessus. Au lieu de ça, on écrit une fonction qui parcourt la géométrie des modèles sur lesquels on veut appliquer la décalcomanie. Pour chaque triangle, on vérifie s’il est dans la zone où la décalcomanie s’appliquerait, comme la vérification inRange dans le shader. Pour chaque triangle qui est dans la plage, on l’ajoute à une nouvelle géométrie avec les coordonnées de texture projetées. On ajoute ensuite cette décalcomanie à la liste des choses à dessiner.

Générer de la géométrie est la bonne approche, sinon il faudrait des shaders différents pour 2 décalcomanies, 3 décalcomanies, 4 décalcomanies, etc., et les shaders deviendraient rapidement trop complexes et atteindraient la limite de textures du shader GPU.

Une autre utilisation est la simulation du vidéo mapping réel. On construit un modèle 3D de la chose sur laquelle on va projeter de la vidéo, puis on fait la projection avec le code ci-dessus mais avec de la vidéo comme texture. On peut ensuite perfectionner et éditer la vidéo pour correspondre au modèle sans avoir à être sur place avec un vrai projecteur.

Ce type de projection est aussi utile pour calculer les ombres avec les shadow maps.

Références de textures conditionnelles

Dans le fragment shader ci-dessus, nous lisons les deux textures dans tous les cas.


  vec4 projectedTexColor = texture(u_projectedTexture, projectedTexcoord.xy);
  vec4 texColor = texture(u_texture, v_texcoord) * u_colorMult;

  float projectedAmount = inRange ? 1.0 : 0.0;
  gl_FragColor = mix(texColor, projectedTexColor, projectedAmount);

Pourquoi ne pas faire quelque chose comme ça ?


  if (inRange) {
    gl_FragColor = texture(u_projectedTexture, projectedTexcoord.xy);
  } else {
    gl_FragColor = texture(u_texture, v_texcoord) * u_colorMult;
  }

D'après la spécification GLSL ES 3.0 Section 8.8

Fonctions de recherche de texture

Certaines fonctions de texture (les versions non-"Lod" et non-"Grad") peuvent nécessiter des dérivées implicites. Les dérivées implicites sont indéfinies dans un flux de contrôle non uniforme et pour les accès de texture dans le vertex shader.

En d'autres termes, si nous allons utiliser des textures, nous devons toujours y accéder. Nous pouvons utiliser les résultats conditionnellement. Par exemple, nous aurions pu écrire ceci :


  vec4 projectedTexColor = texture(u_projectedTexture, projectedTexcoord.xy);
  vec4 texColor = texture(u_texture, v_texcoord) * u_colorMult;

  if (inRange) {
    gl_FragColor = projectedTexColor;
  } else {
    gl_FragColor = texColor;
  }

ou ceci


  vec4 projectedTexColor = texture(u_projectedTexture, projectedTexcoord.xy);
  vec4 texColor = texture(u_texture, v_texcoord) * u_colorMult;

  gl_FragColor = inRange ? projectedTexColor : texColor;

Mais nous ne pouvons pas accéder aux textures elles-mêmes de façon conditionnelle. Ça peut fonctionner sur votre GPU mais ne fonctionnera pas sur tous les GPUs.

Dans tous les cas, c'est important à savoir.

Quant à la raison pour laquelle j'ai utilisé mix plutôt que de simplement brancher sur inRange, c'est une préférence personnelle. mix est plus flexible donc je l'écris généralement ainsi.

Problème ou bug ? Créez un ticket sur github.
Utilisez <pre><code>le code ici</code></pre> pour les blocs de code
comments powered by Disqus