[WebGL] sampler2DArray demo 多纹理渲染

发布时间 2023-10-24 11:55:45作者: 拂晓风起-Kenko

背景

之前尝试过利用多个纹理单元,再基于传入给 shader 的 vertexBuffer 信息决定选 1 号纹理单元还是 2 号纹理单元。
虽然理论上,这个方式确实行得通,但是一次 drawcall 绘制多个纹理,本来目的是为了提高绘制性能,而实际上却无法提高性能,甚至还有反作用。
因为有说法是 shader 分支会降低 GPU 性能,详见:https://blog.csdn.net/qq_31788759/article/details/107248224

那么,是否还有其他批量绘制不同纹理的方式呢?

WebGL 2 sampler2DArray

WebGL 2 带来了一个新的数据结构:sampler2DArray。这个东西,还是占用一个纹理单元,但传入的不是一个纹理,而是一个纹理数组。
关键 shader 代码:

#version 300 es
precision mediump float;
precision mediump sampler2DArray;

// our texture
uniform sampler2DArray u_image;
// the texCoords passed in from the vertex shader.
in vec3 v_texCoord;
// we need to declare an output for the fragment shader
out vec4 outColor;

void main() {
    outColor = texture(u_image, v_texCoord); // 这里从一般的 vec2 变成了 vec3,第三个元素是纹理 index
}
</script>

相对于普通的 2d 纹理渲染,最关键的是 JS 如何传递纹理到 GPU。这里关键是类型 gl.TEXTURE_2D 变成 gl.TEXTURE_2D_ARRAY,texImage2D 变成 texImage3D。
有两种方式传递纹理:

  • 可以把多个纹理合并到一个大图,一次性推送;
  • 也可以先预申请空间,再用 texSubImage3D 逐个推送。
    关键 JS 代码:

    var canvas = document.createElement('canvas');
    canvas.width = imgs[0].width;
    canvas.height = imgs[0].height * imgs.length;
    var ctx = canvas.getContext('2d');
    for (let i = 0; i < imgs.length; i++) {
      ctx.drawImage(imgs[i], 0, imgs[0].height * i);
    }
    var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    var pixels = new Uint8Array(imageData.data.buffer);
    gl.texImage3D(
      gl.TEXTURE_2D_ARRAY,
      0, // level 暂时不知道什么地方会用到
      gl.RGBA, // internalFormat
      imgs[0].width, // width
      imgs[0].height, // height
      imgs.length, // depth 多少个纹理
      0, // border
      gl.RGBA, // format
      gl.UNSIGNED_BYTE, // type
      pixels
    );

    // 利用 texImage3D 设定空间,再用 texSubImage3D 设置图片。texImage3D 创建空间时,宽高设定很重要,后续的尺寸不能超过这个尺寸。如果后续尺寸比这个小,会导致渲染时图片偏小,也很好理解,因为 subImage 传递的只是左上角一小部分的图片,其他面积为空白。
    gl.texImage3D(
      gl.TEXTURE_2D_ARRAY,
      0, // level 暂时不知道什么地方会用到
      gl.RGBA, // internalFormat
      imgs[0].width, // width
      imgs[0].height, // height
      imgs.length, // depth 多少个纹理
      0, // border
      gl.RGBA, // format
      gl.UNSIGNED_BYTE, // type
      null
    );
    /**
     * 用 texStorage3D 搭配 texSubImage3D 也可以。但这里需要时 RGBA8 不是 RGBA。
     * 因为选项里边没有 RGBA https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/texStorage3D
     * 另外,这里不是指输入图片的格式,是指把图片转为什么格式存储。
     * texImage3D 的 gl.RGBA 默认情况下跟 gl.RGBA8 一致
     */
    // gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, imgs[0].width, imgs[0].height,3);
    gl.texSubImage3D(
      gl.TEXTURE_2D_ARRAY,
      0, // level 暂时不知道什么地方会用到
      0, // x offset
      0, // y offset
      0, // z offset 不能超过前边预留的 depth
      imgs[0].width, // width
      imgs[0].height, // height
      1, // depth 多少个纹理
      gl.RGBA, // format
      gl.UNSIGNED_BYTE, // type
      imgs[0]
    );
    gl.texSubImage3D(
      gl.TEXTURE_2D_ARRAY,
      0, // level 暂时不知道什么地方会用到
      0,
      0,
      1,
      imgs[0].width, // width
      imgs[0].height, // height
      1, // depth 多少个纹理
      gl.RGBA, // format
      gl.UNSIGNED_BYTE, // type
      imgs[1]
    );
    gl.texSubImage3D(
      gl.TEXTURE_2D_ARRAY,
      0, // level 暂时不知道什么地方会用到
      0,
      0,
      2,
      imgs[0].width, // width
      imgs[0].height, // height
      1, // depth 多少个纹理
      gl.RGBA, // format
      gl.UNSIGNED_BYTE, // type
      imgs[2]
    );

参考

详细代码请见:https://github.com/kenkozheng/HTML5_research/tree/master/WebGL/sampler2DArray
https://www.nxrte.com/jishu/19240.html
https://juejin.cn/post/6844903846678888461
https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/texImage3D
https://github.com/WebGLSamples/WebGL2Samples/blob/master/samples/texture_2d_array.html