WebGL之使用纹理

现在我们已经创建好了一个可以旋转的 3D 的立方体,接下来是时候使用贴图来代替每个面的单一的颜色了。

加载纹理

首先加入加载纹理的代码。现在我们只使用一张单一的纹理贴到立方体的 6 个面上,但是同样的方法可以用来加载任意数量的纹理贴图。

// 定义了一个名为loadTexture的函数,接受两个参数:gl表示WebGL上下文对象,url表示要加载的图像的URL
function loadTexture(gl, url) { // 在这两行中,创建一个新的纹理对象并将其绑定到gl.TEXTURE_2D
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  // 设置了纹理的初始像素值为一个单像素的蓝色(RGBA值为[0, 0, 255, 255])。这样做是为了在图像加载之前,提供一个占位像素,以便立即使用
  const level = 0;
  const internalFormat = gl.RGBA;
  const width = 1;
  const height = 1;
  const border = 0;
  const srcFormat = gl.RGBA;
  const srcType = gl.UNSIGNED_BYTE;
  const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue
  gl.texImage2D(
    gl.TEXTURE_2D,
    level,
    internalFormat,
    width,
    height,
    border,
    srcFormat,
    srcType,
    pixel,
  );
  // 这里创建了一个新的Image对象,并设置了onload事件处理程序,以便在图像加载完成后执行。
  const image = new Image();
  image.onload = () => { // 是将一个纹理对象绑定到指定的纹理目标上,以便后续的纹理操作可以作用在这个纹理对象上。它并不涉及将图像数据加载到纹理对象中,只是将一个已经创建好的纹理对象与指定的纹理目标进行关联
    gl.bindTexture(gl.TEXTURE_2D, texture);
    // 将图像数据加载到纹理对象中,用于创建纹理。它将图像数据加载到指定的纹理目标上,可以是2D纹理(TEXTURE_2D)或其他类型的纹理。这个函数负责将图像数据拷贝到纹理对象中,以便后续在渲染过程中使用
    gl.texImage2D(
      gl.TEXTURE_2D,
      level,
      internalFormat,
      srcFormat,
      srcType,
      image,
    );
    // 根据图像的宽度和高度是否为2的幂次方,设置纹理的参数。如果是2的幂次方,则生成mipmap;否则,关闭mipmap并设置纹理的包裹模式为CLAMP_TO_EDGE,并将纹理的缩小滤波设置为LINEAR
    // 如果不使用上面这些参数的话,任何非 2 的幂纹理使用都会失败然后返回一张纯黑图片
    if (isPowerOf2(image.width) && isPowerOf2(image.height)) { // Yes, it's a power of 2. Generate mips.
      gl.generateMipmap(gl.TEXTURE_2D);
    } else { // 用来设置纹理对象的参数,其中包括纹理的水平(S)和垂直(T)方向的环绕方式(wrapping mode)以及纹理的缩小过滤方式(minification filter)
      // 这行代码设置了纹理在水平方向(S轴)的环绕方式为CLAMP_TO_EDGE,表示超出纹理坐标范围时,纹理坐标会被夹在0和1之间,防止纹理坐标超出范围而出现重复贴图或拉伸的情况
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      // 这行代码设置了纹理在垂直方向(T轴)的环绕方式也为CLAMP_TO_EDGE,同样避免了纹理坐标超出范围的情况
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
      // 这行代码设置了纹理的缩小过滤方式为LINEAR,表示在纹理被缩小的情况下,使用线性插值来确定采样纹理的颜色值,以平滑地过渡纹理像素
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    }
  };
  image.src = url;
  return texture;
}
function isPowerOf2(value) { return (value & (value - 1)) === 0;
}

texImage2D是WebGL中用于将图像数据传输到纹理对象的函数。它的作用是将图像数据加载到指定的纹理目标上,可以是2D纹理(TEXTURE_2D)或立方体贴图等。

函数签名如下:

void gl.texImage2D(target, level, internalformat, format, type, HTMLImageElement image);

参数解释:

  • target: 指定纹理目标,通常为gl.TEXTURE_2D。
  • level: 指定要生成的纹理级别,通常为0。
  • internalformat: 指定纹理的内部格式,比如gl.RGBA表示纹理包含RGBA四个颜色通道。
  • format: 指定纹理数据的格式,通常与internalformat相同。
  • type: 指定纹理数据的数据类型,比如gl.UNSIGNED_BYTE表示每个颜色通道为无符号字节。
  • image: 要加载到纹理的图像数据,通常是HTMLImageElement对象。

    在代码中的使用情景是将图像数据加载到纹理中,这样可以将图像作为纹理应用到3D模型上进行渲染。texImage2D函数在这里的作用是将图像数据拷贝到纹理对象中,以便后续在渲染过程中使用。


    功能解释:

    函数 loadTexture() 首先调用 WebGL 的 createTexture() 函数来创建一个 WebGL 纹理对象 texture。接下来使用 texImage2D() 以上传一个蓝色的像素点。这样我们就可以在图片下载完成之前使用这个蓝色的纹理了。

    要从图片文件加载纹理,接下来创建一个 Image 对象,并为 src 设置我们想要用作纹理的图片的 URL。我们为 image.onload 设置的函数会在图片下载完成时被调用。那时我们再次调用 texImage2D(),这次我们将图片作为纹理的数据源。之后,我们根据下载的图像在两个维度上是否为 2 的幂来设置纹理的过滤(filter)和平铺(wrap)。

    WebGL1 中,对于非 2 的幂纹理只能使用 NEAREST 和 LINEAR 过滤,且不会生成贴图。此外,平铺模式也必须设置为 CLAMP_TO_EDGE。另一方面,如果纹理在两个维度上都是 2 的幂,那么 WebGL 就可以使用更高质量的过滤,可以使用贴图,还能够将平铺模式设置为 REPEAT 或 MIRRORED_REPEAT。

    使用重复纹理寻址的一个例子就是使用一张砖块的纹理来平铺满一面墙壁。

    多级渐进纹理和纹理坐标重复可以通过调用 texParameteri() 来禁用,当然首先你已经通过调用 bindTexture() 绑定过纹理了。这样虽然已经可以使用非 2 的幂纹理了,但是你将无法使用多级渐进纹理,纹理坐标包装,纹理坐标重复,而且无法控制设备如何处理你的纹理。

    但需要注意的是:浏览器会从加载的图像中按从左上角开始的自上而下顺序复制像素,而 WebGL 需要按自下而上的顺序——从左下角开始的像素顺序。(参见为什么我的 WebGL 纹理是颠倒的?以了解详情。)

    所以为了防止渲染时图像纹理方向错误,我们还需要调用 pixelStorei() 并将 gl.UNPACK_FLIP_Y_WEBGL 参数设置为 true,以调整像素顺序,使其翻转成 WebGL 需要的自下而上顺序。

    映射纹理到面

    现在,纹理已加载并准备就绪。但在我们使用它之前,我们需要建立纹理坐标到立方体上的面的顶点的映射。这将取代 initBuffers() 中为设置每个立方体面颜色而存在的所有先前的代码。

    function initTextureBuffer(gl) { const textureCoordBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
      const 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 Float32Array(textureCoordinates),
        gl.STATIC_DRAW,
      );
      return textureCoordBuffer;
    }
    

    首先,这段代码创建了一个 WebGL 缓冲区,我们将在其中存储每个面的纹理坐标,然后将该缓冲区绑定为将要写入的数组。

    textureCoordinates 数组定义好了与每个面上的每个顶点一一对应的纹理坐标。请注意,纹理坐标的取值范围只能从 0.0 到 1.0,所以不论纹理贴图的实际大小是多少,为了实现纹理映射,我们要使用的纹理坐标只能规范化到 0.0 到 1.0 的范围内。

    一旦我们建立了纹理映射数组,我们将数组传递到缓冲区中,这样 WebGL 就可以使用该数据了。

    然后我们返回新的缓冲区。

    更新着色器

    为了使用纹理来代替单一的颜色,着色器程序和着色器程序的初始化代码都需要进行修改。

    顶点着色器

    我们需要更改顶点着色器,使其不再获取颜色数据,而是获取纹理坐标数据。

    const vsSource = `
        attribute vec4 aVertexPosition;
        attribute vec2 aTextureCoord;
        uniform mat4 uModelViewMatrix;
        uniform mat4 uProjectionMatrix;
        varying highp vec2 vTextureCoord;
        void main(void) {
          gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
          vTextureCoord = aTextureCoord;
        }
      `;
    

    代码的关键更改在于不再获取顶点颜色数据转而获取和设置纹理坐标数据;这样就能把顶点与其对应的纹理联系在一起了。

    片段着色器

    那么片段着色器也要相应地进行更改,像这样更新 main() 函数中的 fsSource 声明:

    const fsSource = `
        varying highp vec2 vTextureCoord;
        uniform sampler2D uSampler;
        void main(void) {
          gl_FragColor = texture2D(uSampler, vTextureCoord);
        }
      `;
    

    在 WebGL 中,vTextureCoord 和 uSampler 这两个变量的值通常是这样的:

    • vTextureCoord:这是一个 varying 变量,用于在顶点着色器和片元着色器之间传递纹理坐标信息。通常情况下,vTextureCoord 的值是在顶点着色器中计算得到的纹理坐标,然后插值传递给片元着色器。纹理坐标通常是二维的,范围在 [0, 1] 之间,用来指定纹理上的位置。

    • uSampler:这是一个 uniform 变量,用于在片元着色器中指定要采样的纹理单元。通常情况下,uSampler 的值是一个整数,表示纹理单元的索引。在 WebGL 中,可以同时绑定多个纹理,每个纹理都有一个唯一的索引。通过设置 uSampler 的值,可以告诉片元着色器从哪个纹理单元中采样纹理数据。

      综合起来,vTextureCoord 用于指定在纹理上的位置,而 uSampler 用于指定要从哪个纹理单元中采样纹理数据。这种结合使得在片元着色器中能够根据纹理坐标从指定的纹理单元中获取对应位置的颜色值,从而实现纹理映射和纹理渲染。

      Attribute 与 Uniform 位置

      因为我们修改了 attribute 并添加了 uniform,所以我们需要查找它们的位置。

      const programInfo = { program: shaderProgram,
        attribLocations: { vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
          textureCoord: gl.getAttribLocation(shaderProgram, "aTextureCoord"),
        },
        uniformLocations: { projectionMatrix: gl.getUniformLocation(shaderProgram, "uProjectionMatrix"),
          modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
          uSampler: gl.getUniformLocation(shaderProgram, "uSampler"),
        },
      };
      

      绘制具体纹理贴图的立方体

      // 告诉 WebGL 如何从缓冲区中提取纹理坐标
      function setTextureAttribute(gl, buffers, programInfo) { const num = 2; // 每个坐标由 2 个值组成
        const type = gl.FLOAT; // 缓冲区中的数据为 32 位浮点数
        const normalize = false; // 不做标准化处理
        const stride = 0; // 从一个坐标到下一个坐标要获取多少字节
        const offset = 0; // 从缓冲区内的第几个字节开始获取数据
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
        gl.vertexAttribPointer(
          programInfo.attribLocations.textureCoord,
          num,
          type,
          normalize,
          stride,
          offset,
        );
        gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord);
      }
      

      完整源码如下:

         WebGL Demo      
      // webgl-demo.js
      import { initBuffers } from "./init-buffers.js";
      import { drawScene } from "./draw-scene.js";
      let cubeRotation = 0.0;
      let deltaTime = 0;
      main();
      //
      // start here
      //
      function main() { const canvas = document.querySelector("#glcanvas");
        // Initialize the GL context
        const gl = canvas.getContext("webgl");
        // Only continue if WebGL is available and working
        if (gl === null) { alert(
            "Unable to initialize WebGL. Your browser or machine may not support it."
          );
          return;
        }
        // Set clear color to black, fully opaque
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        // Clear the color buffer with specified clear color
        gl.clear(gl.COLOR_BUFFER_BIT);
        // Vertex shader program
        // 定义了aTextureCoord变量
        const vsSource = `
        attribute vec4 aVertexPosition;
        attribute vec2 aTextureCoord;
        uniform mat4 uModelViewMatrix;
        uniform mat4 uProjectionMatrix;
        varying highp vec2 vTextureCoord;
        void main(void) {
          gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
          vTextureCoord = aTextureCoord;
        }
      `;
        // Fragment shader program
        // 获取顶点着色器的vTextureCoord数据
        const fsSource = `
        varying highp vec2 vTextureCoord;
        uniform sampler2D uSampler;
        void main(void) {
          gl_FragColor = texture2D(uSampler, vTextureCoord);
        }
      `;
        // Initialize a shader program; this is where all the lighting
        // for the vertices and so forth is established.
        const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
        // Collect all the info needed to use the shader program.
        // Look up which attributes our shader program is using
        // for aVertexPosition, aVertexColor and also
        // look up uniform locations.
        const programInfo = { program: shaderProgram,
          attribLocations: { vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
            textureCoord: gl.getAttribLocation(shaderProgram, "aTextureCoord"),
          },
          uniformLocations: { projectionMatrix: gl.getUniformLocation(
              shaderProgram,
              "uProjectionMatrix"
            ),
            modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
            uSampler: gl.getUniformLocation(shaderProgram, "uSampler"),
          },
        };
        // Here's where we call the routine that builds all the
        // objects we'll be drawing.
        const buffers = initBuffers(gl);
        // Load texture
        const texture = loadTexture(gl, "cubetexture.png");
        // 为了防止渲染时图像纹理方向错误,我们还需要调用 pixelStorei() 并将 gl.UNPACK_FLIP_Y_WEBGL 参数设置为 true,以调整像素顺序,使其翻转成 WebGL 需要的自下而上顺序
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
        let then = 0;
        // Draw the scene repeatedly
        function render(now) { now *= 0.001; // convert to seconds
          deltaTime = now - then;
          then = now;
          drawScene(gl, programInfo, buffers, texture, cubeRotation);
          cubeRotation += deltaTime;
          requestAnimationFrame(render);
        }
        requestAnimationFrame(render);
      }
      //
      // Initialize a shader program, so WebGL knows how to draw our data
      //
      function initShaderProgram(gl, vsSource, fsSource) { const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
        const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
        // Create the shader program
        const shaderProgram = gl.createProgram();
        gl.attachShader(shaderProgram, vertexShader);
        gl.attachShader(shaderProgram, fragmentShader);
        gl.linkProgram(shaderProgram);
        // If creating the shader program failed, alert
        if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert(
            `Unable to initialize the shader program: ${gl.getProgramInfoLog(
              shaderProgram
            )}`
          );
          return null;
        }
        return shaderProgram;
      }
      //
      // creates a shader of the given type, uploads the source and
      // compiles it.
      //
      function loadShader(gl, type, source) { const shader = gl.createShader(type);
        // Send the source to the shader object
        gl.shaderSource(shader, source);
        // Compile the shader program
        gl.compileShader(shader);
        // See if it compiled successfully
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert(
            `An error occurred compiling the shaders: ${gl.getShaderInfoLog(shader)}`
          );
          gl.deleteShader(shader);
          return null;
        }
        return shader;
      }
      // 这段代码的主要作用是在图像加载完成前提供一个临时的纹理数据,以便在图像加载完成后立即使用。这种方式可以确保在图像下载过程中不会出现空白的纹理,提高用户体验
      function loadTexture(gl, url) { const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        // Because images have to be downloaded over the internet
        // they might take a moment until they are ready.
        // Until then put a single pixel in the texture so we can
        // use it immediately. When the image has finished downloading
        // we'll update the texture with the contents of the image.
        const level = 0;
        const internalFormat = gl.RGBA;
        const width = 1;
        const height = 1;
        const border = 0;
        const srcFormat = gl.RGBA;
        const srcType = gl.UNSIGNED_BYTE;
        const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue
        gl.texImage2D(
          gl.TEXTURE_2D,
          level,
          internalFormat,
          width,
          height,
          border,
          srcFormat,
          srcType,
          pixel
        );
        const image = new Image();
        image.onload = () => { gl.bindTexture(gl.TEXTURE_2D, texture);
          gl.texImage2D(
            gl.TEXTURE_2D,
            level,
            internalFormat,
            srcFormat,
            srcType,
            image
          );
          // WebGL1 has different requirements for power of 2 images
          // vs non power of 2 images so check if the image is a
          // power of 2 in both dimensions.
          if (isPowerOf2(image.width) && isPowerOf2(image.height)) { // Yes, it's a power of 2. Generate mips.
            gl.generateMipmap(gl.TEXTURE_2D);
          } else { // No, it's not a power of 2. Turn off mips and set
            // wrapping to clamp to edge
            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.LINEAR);
          }
        };
        image.src = url;
        return texture;
      }
      function isPowerOf2(value) { return (value & (value - 1)) === 0;
      }
      
      // init-buffers.js
      function initBuffers(gl) { const positionBuffer = initPositionBuffer(gl);
        const textureCoordBuffer = initTextureBuffer(gl);
        const indexBuffer = initIndexBuffer(gl);
        return { position: positionBuffer,
          textureCoord: textureCoordBuffer,
          indices: indexBuffer,
        };
      }
      function initPositionBuffer(gl) { // Create a buffer for the square's positions.
        const positionBuffer = gl.createBuffer();
        // Select the positionBuffer as the one to apply buffer
        // operations to from here out.
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        const positions = [
          // Front face
          -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
          // Back face
          -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0,
          // Top face
          -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0,
          // Bottom face
          -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0,
          // Right face
          1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,
          // Left face
          -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0,
        ];
        // Now pass the list of positions into WebGL to build the
        // shape. We do this by creating a Float32Array from the
        // JavaScript array, then use it to fill the current buffer.
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
        return positionBuffer;
      }
      function initColorBuffer(gl) { const faceColors = [
          [1.0, 1.0, 1.0, 1.0], // Front face: white
          [1.0, 0.0, 0.0, 1.0], // Back face: red
          [0.0, 1.0, 0.0, 1.0], // Top face: green
          [0.0, 0.0, 1.0, 1.0], // Bottom face: blue
          [1.0, 1.0, 0.0, 1.0], // Right face: yellow
          [1.0, 0.0, 1.0, 1.0], // Left face: purple
        ];
        // Convert the array of colors into a table for all the vertices.
        var colors = [];
        for (var j = 0; j < faceColors.length; ++j) { const c = faceColors[j];
          // Repeat each color four times for the four vertices of the face
          colors = colors.concat(c, c, c, c);
        }
        const colorBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
        return colorBuffer;
      }
      function initIndexBuffer(gl) { const indexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        // This array defines each face as two triangles, using the
        // indices into the vertex array to specify each triangle's
        // position.
        const indices = [
          0,
          1,
          2,
          0,
          2,
          3, // front
          4,
          5,
          6,
          4,
          6,
          7, // back
          8,
          9,
          10,
          8,
          10,
          11, // top
          12,
          13,
          14,
          12,
          14,
          15, // bottom
          16,
          17,
          18,
          16,
          18,
          19, // right
          20,
          21,
          22,
          20,
          22,
          23, // left
        ];
        // Now send the element array to GL
        gl.bufferData(
          gl.ELEMENT_ARRAY_BUFFER,
          new Uint16Array(indices),
          gl.STATIC_DRAW
        );
        return indexBuffer;
      }
      // 设置每个面4个顶点的纹理坐标
      function initTextureBuffer(gl) { const textureCoordBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
        const 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 Float32Array(textureCoordinates),
          gl.STATIC_DRAW
        );
        return textureCoordBuffer;
      }
      export { initBuffers };
      
      // draw-scene.js
      function drawScene(gl, programInfo, buffers, texture, cubeRotation) { gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
        gl.clearDepth(1.0); // Clear everything
        gl.enable(gl.DEPTH_TEST); // Enable depth testing
        gl.depthFunc(gl.LEQUAL); // Near things obscure far things
        // Clear the canvas before we start drawing on it.
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        // Create a perspective matrix, a special matrix that is
        // used to simulate the distortion of perspective in a camera.
        // Our field of view is 45 degrees, with a width/height
        // ratio that matches the display size of the canvas
        // and we only want to see objects between 0.1 units
        // and 100 units away from the camera.
        const fieldOfView = (45 * Math.PI) / 180; // in radians
        const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
        const zNear = 0.1;
        const zFar = 100.0;
        const projectionMatrix = mat4.create();
        // note: glmatrix.js always has the first argument
        // as the destination to receive the result.
        mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
        // Set the drawing position to the "identity" point, which is
        // the center of the scene.
        const modelViewMatrix = mat4.create();
        // Now move the drawing position a bit to where we want to
        // start drawing the square.
        mat4.translate(
          modelViewMatrix, // destination matrix
          modelViewMatrix, // matrix to translate
          [-0.0, 0.0, -6.0]
        ); // amount to translate
        mat4.rotate(
          modelViewMatrix, // destination matrix
          modelViewMatrix, // matrix to rotate
          cubeRotation, // amount to rotate in radians
          [0, 0, 1]
        ); // axis to rotate around (Z)
        mat4.rotate(
          modelViewMatrix, // destination matrix
          modelViewMatrix, // matrix to rotate
          cubeRotation * 0.7, // amount to rotate in radians
          [0, 1, 0]
        ); // axis to rotate around (Y)
        mat4.rotate(
          modelViewMatrix, // destination matrix
          modelViewMatrix, // matrix to rotate
          cubeRotation * 0.3, // amount to rotate in radians
          [1, 0, 0]
        ); // axis to rotate around (X)
        // Tell WebGL how to pull out the positions from the position
        // buffer into the vertexPosition attribute.
        setPositionAttribute(gl, buffers, programInfo);
        setTextureAttribute(gl, buffers, programInfo);
        // Tell WebGL which indices to use to index the vertices
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);
        // Tell WebGL to use our program when drawing
        gl.useProgram(programInfo.program);
        // Set the shader uniforms
        gl.uniformMatrix4fv(
          programInfo.uniformLocations.projectionMatrix,
          false,
          projectionMatrix
        );
        gl.uniformMatrix4fv(
          programInfo.uniformLocations.modelViewMatrix,
          false,
          modelViewMatrix
        );
        // 激活了纹理单元0,这意味着接下来的纹理操作将会影响到纹理单元0。在激活的纹理单元上,我们可以执行加载纹理、绑定纹理等操作,以便在渲染过程中正确地使用纹理数据
        gl.activeTexture(gl.TEXTURE0);
        // 将名为 texture 的纹理对象绑定到了 2D 纹理目标上。一旦纹理对象绑定到了特定的纹理目标上,后续的纹理操作(比如设置纹理参数、加载纹理图像等)都会作用在这个被绑定的纹理对象上
        // 这样,在绘制时,当我们在着色器中通过纹理单元索引指定要使用的纹理时,WebGL 就会从这个被绑定的纹理对象中获取相应的纹理数据进行渲染
        gl.bindTexture(gl.TEXTURE_2D, texture);
        // 这行代码将纹理单元索引0传递给着色器中的uniform变量uSampler。这样,着色器就知道要从纹理单元0中采样纹理数据进行渲染
        // 这行代码并不是说 uSampler 的值是固定的 0,而是告诉着色器程序将纹理绑定到纹理单元索引为 0 的位置上。在着色器程序中,uSampler 会接收这个索引值,以便从正确的纹理单元中获取纹理数据进行渲染
        gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
        { const vertexCount = 36;
          const type = gl.UNSIGNED_SHORT;
          const offset = 0;
          gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
        }
      }
      // Tell WebGL how to pull out the positions from the position
      // buffer into the vertexPosition attribute.
      function setPositionAttribute(gl, buffers, programInfo) { const numComponents = 3;
        const type = gl.FLOAT; // the data in the buffer is 32bit floats
        const normalize = false; // don't normalize
        const stride = 0; // how many bytes to get from one set of values to the next
        // 0 = use type and numComponents above
        const offset = 0; // how many bytes inside the buffer to start from
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
        gl.vertexAttribPointer(
          programInfo.attribLocations.vertexPosition,
          numComponents,
          type,
          normalize,
          stride,
          offset
        );
        gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
      }
      // Tell WebGL how to pull out the colors from the color buffer
      // into the vertexColor attribute.
      function setColorAttribute(gl, buffers, programInfo) { const numComponents = 4;
        const type = gl.FLOAT;
        const normalize = false;
        const stride = 0;
        const offset = 0;
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);
        gl.vertexAttribPointer(
          programInfo.attribLocations.vertexColor,
          numComponents,
          type,
          normalize,
          stride,
          offset
        );
        gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
      }
      // 设置数据步长将纹理数据赋值给programInfo.attribLocations.textureCoord
      function setTextureAttribute(gl, buffers, programInfo) { const num = 2; // every coordinate composed of 2 values
        const type = gl.FLOAT; // the data in the buffer is 32-bit float
        const normalize = false; // don't normalize
        const stride = 0; // how many bytes to get from one set to the next
        const offset = 0; // how many bytes inside the buffer to start from
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
        gl.vertexAttribPointer(
          programInfo.attribLocations.textureCoord,
          num,
          type,
          normalize,
          stride,
          offset
        );
        gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord);
      }
      export { drawScene };
      

      gl.uniform1i(programInfo.uniformLocations.uSampler, 0)

      这行代码是在WebGL中用来将纹理单元索引传递给着色器中的uniform变量的操作。

      具体来说:

      • gl.uniform1i 是 WebGL 中用于将整型值传递给 uniform 变量的函数。
      • programInfo.uniformLocations.uSampler 是一个 uniform 变量的位置,通常在着色器程序链接后通过 gl.getUniformLocation 获取。
      • 0 是要传递给 uniform 变量的整型值,这里代表纹理单元的索引。

        在这个例子中,gl.uniform1i(programInfo.uniformLocations.uSampler, 0); 的作用是将纹理单元索引 0 传递给着色器中名为 uSampler 的 uniform 变量。这样,着色器就知道要从纹理单元索引为 0 的纹理中采样纹理数据进行渲染。

        通过这种方式,我们可以在 JavaScript 程序中控制着色器中的 uniform 变量,从而实现对纹理的使用和渲染。

        总结

        1. 首先将图片加载到纹理对象中
        2. 设置各个面的各个顶点的纹理坐标
        3. 赋值纹理坐标给着色器并设置纹理坐标的读取方式