【WebGL】三维纹理的使用,如何正确设置纹理参数

465 阅读8分钟

如果对WebGL感兴趣,可以点击easy-webgl关注

前言

本节介绍WebGL纹理的使用、纹理尺寸、以及如何正确设置纹理参数才能正常显示纹理。

一、 纹理尺寸

WebGL 对纹理维度的限制主要源于其基于 OpenGL ES 2.0 规范,该规范要求纹理的宽度和高度必须是 2 的整数次幂(如 1x1, 2x2, 4x4, 8x8 等)。这一限制影响了纹理的渲染和性能。

1.1 限制的原因

  • 性能优化:2 的幂次方纹理可以更高效地使用硬件加速,提高渲染性能。
  • 纹理过滤和 mipmap 生成:2 的幂次方纹理支持更高质量的纹理过滤和 mipmap 生成,提升图像质量。

1.2 如何处理非 2 的幂次方纹理

  • 调整纹理尺寸:将纹理的宽度和高度调整为最接近的 2 的幂次方。
  • 使用纹理参数:设置纹理参数为 CLAMP_TO_EDGE 和 LINEAR,以支持非 2 的幂次方纹理的渲染

我们使用下面三张图片验证一下

“F”图像,尺寸:256 * 256。宽度和高度都是2的整数次幂

f.png

“猫”图像,尺寸:640 * 853。宽度和高度都不是2的整数次幂

cat.jpeg

"猫2"图像,尺寸:1024 * 600。宽度为2的整数次幂,高度不是。

cat2.jpeg

同时,本篇文章使用下面的Demo演示:

const main = (image) => {
    const canvas = document.getElementById('webgl')
    const gl = canvas.getContext('webgl')
    const vertexShaderSource1 = `
      attribute vec2 a_texCoord;
      attribute vec2 a_position;
      varying vec2 v_texCoord;
      void main(){
          gl_PointSize = 10.0;
          gl_Position = vec4(a_position, 0.0, 1.0);
          // 将纹理坐标传给片段着色器
          // GPU会在点之间进行插值
          v_texCoord = a_texCoord;
      }
    `
    const fragmentShaderSource1 = `
      precision mediump float;
      uniform sampler2D u_image;
      // 从顶点着色器传入的纹理坐标
      varying vec2 v_texCoord;
      void main(){
        // 在纹理上寻找对应颜色值
        gl_FragColor = texture2D(u_image, v_texCoord);
      }
    `
    const program1 = initShaders(gl, vertexShaderSource1, fragmentShaderSource1)
    const positionLocation1 = gl.getAttribLocation(program1, 'a_position')
    const texCoordLocation = gl.getAttribLocation(program1, "a_texCoord");
  
    // 给矩形提供纹理坐标
    const texCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
    const x = 1.0, y = 1.0;
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      0.0, 0.0,
      x, 0.0,
      0.0, y,
      x, 0.0,
      0.0, y,
      x, y
    ]), gl.STATIC_DRAW);
    gl.enableVertexAttribArray(texCoordLocation);
    gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
  
    // 创建纹理
    var texture = gl.createTexture();
    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_MIN_FILTER, gl.LINEAR);
    // gl.generateMipmap(gl.TEXTURE_2D);

    const rectX = 0.5, rectY =  0.5;
    let verticesInfo = [
      0.0, rectY,
      rectX, rectY,
      0, 0,
      rectX, rectY,
      0, 0,
      rectX, 0,
    ]
    verticesInfo = new Float32Array(verticesInfo)
  
    const vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
    gl.bufferData(gl.ARRAY_BUFFER, verticesInfo, gl.STATIC_DRAW)
  
  
    gl.vertexAttribPointer(
      positionLocation1,
      2,
      gl.FLOAT,
      false,
      8,
      0
    );
  
  
  
    gl.clearColor(0, 0, 0, 0.1)
  
    gl.clear(gl.COLOR_BUFFER_BIT);
  
    gl.useProgram(program1)
  
    gl.enableVertexAttribArray(positionLocation1);
  
    gl.drawArrays(gl.TRIANGLES, 0,6)
  
  }
  
  
  const image = new Image();
  // image.src = "./cat.jpeg";  // 640 * 853
  // image.src = "./cat2.jpeg";  // 1024 * 600
  // image.src = "./f.png";  // 尺寸:256 * 256

  image.onload = function () {
    main(image);
  }

二、纹理尺寸为 2 的整数次幂

上面的图像“F”尺寸为 256*256,为 2 的整数次幂。当纹理尺寸为 2 的整数次幂时,必须要设置下面至少一项

  • 调用并设置gl.TEXTURE_MIN_FILTERgl.NEAREST或者gl.LINEAR以关闭纹理映射
  • 调用gl.generateMipmap生成纹理映射,这样webgl就可以选择使用纹理映射绘制

修改上面的DEMO,使用图像"F"

  // image.src = "./cat.jpeg";  // 640 * 853
  // image.src = "./cat2.jpeg";  // 1024 * 600
  image.src = "./f.png";  // 尺寸:256 * 256

下面的两行注释中,至少有一行是放开的,图像都可以正常绘制

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// gl.generateMipmap(gl.TEXTURE_2D);

比如只设置gl.TEXTURE_MIN_FILTER

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// gl.generateMipmap(gl.TEXTURE_2D);

或者只调用generateMipmap

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.generateMipmap(gl.TEXTURE_2D);

上面几种方式,纹理都可以正常绘制,如下所示:

texture_09.jpg

2.1 gl.TEXTURE_MIN_FILTER的取值

gl.TEXTURE_MIN_FILTER的取值:

  • NEAREST
  • LINEAR
  • NEAREST_MIPMAP_NEAREST
  • LINEAR_MIPMAP_NEAREST
  • NEAREST_MIPMAP_LINEAR
  • LINEAR_MIPMAP_LINEAR

如果不调用gl.generateMipmap生成纹理映射,那么gl.TEXTURE_MIN_FILTER只能取值为gl.LINEAR或者gl.NEAREST。下面的代码绘制将出错:

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);
// gl.generateMipmap(gl.TEXTURE_2D);

这是因为我们没有调用gl.generateMipmap生成纹理映射,但gl.TEXTURE_MIN_FILTER参数却设置成从纹理映射中选择贴图。在着色器中当 texture2D 被调用的时候由于纹理没有正确设置,就会使用颜色 (0, 0, 0, 1)也就是黑色

texture_10.jpg

因此需要调用generateMipmap,下面的代码才能正确绘制纹理。

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);
gl.generateMipmap(gl.TEXTURE_2D);

2.2 环绕模式

在 WebGL 中,如果纹理的尺寸是2的整数次幂,可以自由设置 gl.TEXTURE_WRAP_S 和 gl.TEXTURE_WRAP_T 为 gl.REPEAT、gl.CLAMP_TO_EDGE 或 gl.MIRRORED_REPEAT,比如:

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);
gl.generateMipmap(gl.TEXTURE_2D);

三、纹理宽和高都不是 2 的整数次幂

上面的图像“猫”尺寸为 640 * 853,宽度和高度都不是 2 的整数次幂。

修改上面的DEMO,使用图像"猫"

image.src = "./cat.jpeg";  // 640 * 853
// image.src = "./cat2.jpeg";  // 1024 * 600
// image.src = "./f.png";  // 尺寸:256 * 256

3.1 不能使用纹理映射

当纹理的宽度和高度都不是2的整数次幂的纹理,不能生成纹理映射,下面的代码绘制将会出错

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);

显示纹理失败,在着色器中当 texture2D 被调用的时候由于纹理没有正确设置, 就会使用颜色 (0, 0, 0, 1) 也就是黑色。如果打开 JavaScript 控制台或者浏览器控制台, 根据浏览器不同可能会显示不同的错误信息,像这样

texture_11.jpg

3.2 如何正确显示

如果需要让宽度和高度都不是2的整数次幂的贴图能够正确显示,只需要同时将水平和垂直包裹模式设置为 CLAMP_TO_EDGE 并且通过设置过滤器为 LINEAR or NEAREST 来关闭贴图映射。

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, 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);

3.3 环绕模式

在 WebGL 中,如果纹理的宽度和高度 不是 2 的整数次幂(即非幂次方纹理),默认情况下 无法 将 gl.TEXTURE_WRAP_S 和 gl.TEXTURE_WRAP_T 设置为 gl.REPEAT或者gl.MIRRORED_REPEAT。这是因为 WebGL 对于非幂次方纹理的环绕模式有严格的限制。

对于非2的整数次幂方纹理,gl.TEXTURE_WRAP_S 和 gl.TEXTURE_WRAP_T 的取值只能是 gl.CLAMP_TO_EDGE。下面的代码可以正常绘制:

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, 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.REPEAT 或 gl.MIRRORED_REPEAT,WebGL 会抛出错误(例如,INVALID_OPERATION,有些浏览器可能不会抛出错误,但会绘制黑色的背景)

下面的代码显示纹理失败,在着色器中当 texture2D 被调用的时候由于纹理没有正确设置, 就会使用颜色 (0, 0, 0, 1) 也就是黑色

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

texture_12.jpg

四、纹理宽度和高度其中之一是2的整数次幂

当纹理的宽度和高度其中之一是2的整数次幂,比如下面的场景之一:

  • 宽度为2的整数次幂,高度不是。
  • 或者宽度不是2的整数次幂,高度是。

以上两种场景,在环绕模式的设置会有些差异。

上面的图像“猫2”尺寸为 1024 * 600,宽度是2的整数次幂,而高度不是。

修改上面的DEMO,使用图像"猫2"

// image.src = "./cat.jpeg";  // 640 * 853
image.src = "./cat2.jpeg";  // 1024 * 600
// image.src = "./f.png";  // 尺寸:256 * 256

4.1不能使用纹理映射

实际上,只要纹理有一个尺寸不是2的整数次幂,都不能使用纹理映射,下面的代码将绘制出错。

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);

纹理不能正常显示,如下所示:

texture_11.jpg

4.2如何正确显示

需要将gl.gl.TEXTURE_MIN_FILTER设置为gl.LINEAR或者gl.NEAREST以关闭纹理映射,同时需要将垂直方向的环绕模式gl.TEXTURE_WRAP_T设置为gl.CLAMP_TO_EDGE。以下代码可以正常绘制:

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // 垂直方向
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

4.3 环绕模式

为2的整数幂的那一边,可以自由设置

  • 宽度为2的整数次幂,高度不是。此时gl.TEXTURE_WRAP_S可以取gl.CLAMP_TO_EDGE、gl.REPEAT或者gl.MIRRORED_REPEAT之一,但gl.TEXTURE_WRAP_T只能取值gl.CLAMP_TO_EDGE
  • 或者宽度不是2的整数次幂,高度是。此时gl.TEXTURE_WRAP_T可以取gl.CLAMP_TO_EDGE、gl.REPEAT或者gl.MIRRORED_REPEAT之一,但gl.TEXTURE_WRAP_S只能取值gl.CLAMP_TO_EDGE

比如下面的代码可以正常绘制

// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); // 水平方向
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

如何绘制所有尺寸的图像

下面的代码可以正常绘制所有尺寸的纹理,这里为了方便直观的看到不同尺寸下,纹理参数如何正确设置,所以使用if else穷举了各种尺寸场景。

function isPowerOf2(value) {
    return (value & (value - 1)) === 0;
}
// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// 检查每个维度是否是 2 的幂
if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
    // 宽度和高度都是 2 的幂,一般用纹理映射中的贴图
    gl.generateMipmap(gl.TEXTURE_2D);
    // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST); // 可以自由设置,也可以不设置
} else if(!isPowerOf2(image.width) && !isPowerOf2(image.height)){
    // 宽度和高度都不是 2 的幂,关闭纹理映射贴图并设置水平和垂直方向的环绕模式为到gl.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); // 关闭纹理映射贴图,只能取值gl.LINEAR或者gl.NEAREST
} else if(!isPowerOf2(image.width) && isPowerOf2(image.height)){
    // 宽度不是 2 的幂,高度是,关闭纹理映射贴图并设置水平方向环绕模式为到gl.CLAMP_TO_EDGE
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); // 水平方向环绕模式只能设置成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); // 关闭纹理映射贴图,只能取值gl.LINEAR或者gl.NEAREST
} else if(isPowerOf2(image.width) && !isPowerOf2(image.height)){
    // 宽度是 2 的幂,高度不是,关闭纹理映射贴图并设置垂直方向环绕模式为到gl.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.CLAMP_TO_EDGE
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // 关闭纹理映射贴图,只能取值gl.LINEAR或者gl.NEAREST
}

当然也可以这么写:

function isPowerOf2(value) {
    return (value & (value - 1)) === 0;
}
    
// 将图像上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// 检查每个维度是否是 2 的幂
if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
    // 宽度和高度都是 2 的幂,一般用贴图
    gl.generateMipmap(gl.TEXTURE_2D);
 } else {
    // 宽度或者高度只要有一个不是 2 的幂,关闭贴图并设置包裹模式为到边缘
    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);
 }