Utiliser les textures avec WebGL

Maintenant que notre programme peut faire tourner un cude 3D, appliquons lui une texture à la place de ses faces unies.

Charger les textures

La première chose à faire est d'ajouter le code de chargement des textures. Dans notre cas, nous utiliserons une texture unique, appliquée à chaqu'une des six faces de notre cube. La même technique peut être utilisée pour n'importe quel nombre de textures.

Note: Il est important de noter que le chargement de textures suis les cross-domain rules; Donc vous pouvez seulement charger des textures depuis les sites dont votre contenut à été approuvé CORS. Voir Cross-domain textures pour plus de détails.

Le code qui charge les textures ressemble à ça : 

function initTextures() {
  cubeTexture = gl.createTexture();
  cubeImage = new Image();
  cubeImage.onload = function() { handleTextureLoaded(cubeImage, cubeTexture); }
  cubeImage.src = "cubetexture.png";

function handleTextureLoaded(image, texture) {
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.bindTexture(gl.TEXTURE_2D, null);

La routine initTextures() commence par créer la texture GL cubeTexture en appelant la fonction GL createTexture(). Pour charger l'image dans la texture, elle crée ensuite un objet Image et charge le fichier graphique que nous voulons utiliser. La routine de callback handleTextureLoaded() est appelée quand l'image a été chargée.

Pour créer la texture, nous spécifions que nous voulons utiliser la nouvelle texture en la liant à gl.TEXTURE_2D. L'image est ensuite passée à texImage2D() pour écrire l'image dans la texture.

Note: Les dimensions des textures doivent dans la plupart des cas avoir un nombre de pixels qui soit une puissance de 2 (donc 1, 2, 4, 8, 16, etc). En cas d'exception, voir la section "Non power-of-two textures" ci-dessous.

Les deux lignes qui suivent mettent en place les filtres pour la textures; cela contrôle l'affichage de l'image en fonction de sa taille. Dans ce cas, on utilise un filtre linéaire quand on agrandit l'image, et un mipmap quand on la rétrécit. Ensuite la mipmap est générée en appelant la méthode generateMipMap(), et on finit par dire a WebGL que l'on a finit de manipuler la texture en associant null a gl.TEXTURE_2D.

Non power-of-two textures

Generally speaking, using textures whose sides are a power of two is ideal. They are efficiently stored in video memory and are not restricted in how they could be used. Artist-created textures should be scaled up or down to a nearby power of two and, really, should have been authored in power-of-two to begin with. Each side should be: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, or 2048 pixels. Many, but not all, devices can support 4096 pixels; some can support 8192 and above.

Occasionally, it is difficult to use power-of-two textures due to your specific circumstance. If the source is some 3rd party, often the best results come from modifying the images using HTML5 canvas into power-of-two sizes before they are passed to WebGL; UV coordinates may also require adjustment if stretching is apparent.

But, if you must have a non power-of-two (NPOT) texture, WebGL does include limited native support. NPOT textures are mostly useful if your texture dimensions must be the same resolution as something else, such as your monitor resolution, or if the above suggestions are just not worth the hassle. The catch: these textures cannot be used with mipmapping and they must not "repeat" (tile or wrap).

An example of a repeated texture is tiling an image of a few bricks to cover a brick wall.

Mipmapping and UV repeating can be disabled with texParameteri() when you create your texture using bindTexture(). This will allow NPOT textures at the expense of mipmapping, UV wrapping, UV tiling, and your control over how the device will handle your texture.

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); //gl.NEAREST is also allowed, instead of gl.LINEAR, as neither mipmap.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); //Prevents s-coordinate wrapping (repeating).
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); //Prevents t-coordinate wrapping (repeating).

Again, with these parameters, compatible WebGL devices will automatically accept any resolution for that texture (up to their maximum dimensions). Without performing the above configuration, WebGL requires all samples of NPOT textures to fail by returning solid black: rgba(0,0,0,1).

Mapping the texture onto the faces

At this point, the texture is loaded and ready to use. But before we can use it, we need to establish the mapping of the texture coordinates to the vertices of the faces of our cube. This replaces all the previously existing code for configuring colors for each of the cube's faces in initBuffers().

  cubeVerticesTextureCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, cubeVerticesTextureCoordBuffer);
  var textureCoordinates = [
    // Front
    0.0,  0.0,
    1.0,  0.0,
    1.0,  1.0,
    0.0,  1.0,
    // Back
    0.0,  0.0,
    1.0,  0.0,
    1.0,  1.0,
    0.0,  1.0,
    // Top
    0.0,  0.0,
    1.0,  0.0,
    1.0,  1.0,
    0.0,  1.0,
    // Bottom
    0.0,  0.0,
    1.0,  0.0,
    1.0,  1.0,
    0.0,  1.0,
    // Right
    0.0,  0.0,
    1.0,  0.0,
    1.0,  1.0,
    0.0,  1.0,
    // Left
    0.0,  0.0,
    1.0,  0.0,
    1.0,  1.0,
    0.0,  1.0

  gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(textureCoordinates),

First, this code creates a GL buffer into which we'll store the texture coordinates for each face, then we bind that buffer as the array we'll be writing into.

The textureCoordinates array defines the texture coordinates corresponding to each vertex of each face. Note that the texture coordinates range from 0.0 to 1.0; the dimensions of textures are normalized to a range of 0.0 to 1.0 regardless of their actual size, for the purpose of texture mapping.

Once we've set up the texture mapping array, we pass the array into the buffer, so that GL has that data ready for its use.

Note: You will have to use Float32Array in place of WebGLFloatArray in WebKit based browsers. 

Updating the shaders

The shader program -- and the code that initializes the shaders -- also needs to be updated to use the textures instead of solid colors.

First, let's take a look at the very simple change needed in initShaders():

  textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord");

This replaces the code that set up the vertex color attribute with one that contains the texture coordinate for each vertex.

The vertex shader

Next, we need to replace the vertex shader so that instead of fetching color data, it instead fetches the texture coordinate data.

    <script id="shader-vs" type="x-shader/x-vertex">
      attribute vec3 aVertexPosition;
      attribute vec2 aTextureCoord;
      uniform mat4 uMVMatrix;
      uniform mat4 uPMatrix;
      varying highp vec2 vTextureCoord;
      void main(void) {
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
        vTextureCoord = aTextureCoord;

The key change here is that instead of fetching the vertex color, we're setting the texture coordinates; this will indicate the location within the texture corresponding to the vertex.

The fragment shader

The fragment shader likewise needs to be updated:

    <script id="shader-fs" type="x-shader/x-fragment">
      varying highp vec2 vTextureCoord;
      uniform sampler2D uSampler;
      void main(void) {
        gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));

Instead of assigning a color value to the fragment's color, the fragment's color is computed by fetching the texel (that is, the pixel within the texture) that the sampler says best maps to the fragment's position.

Drawing the textured cube

The change to the drawScene() function is simple (except that for the purpose of clarity, I've removed the code that causes the cube to translate through space while animating; instead it just rotates).

The code to map colors to the texture is gone, replaced with this:

  gl.bindTexture(gl.TEXTURE_2D, cubeTexture);
  gl.uniform1i(gl.getUniformLocation(shaderProgram, "uSampler"), 0);

GL provides 32 texture registers; the first of these is gl.TEXTURE0. We bind our previously-loaded texture to that register, then set the shader sampler uSampler (specified in the shader program) to use that texture.

At this point, the rotating cube should be good to go. If your browser supports WebGL, you can try the live demo.

Étiquettes et contributeurs liés au document

Étiquettes : 
 Contributeurs à cette page : teoli, fscholz, Nasso, LDelhez
 Dernière mise à jour par : teoli,