首页
/ three.js DataArrayTexture 实战:从 TypedArray 原始数据构建 2D 纹理数组

three.js DataArrayTexture 实战:从 TypedArray 原始数据构建 2D 纹理数组

2026-09-06 17:48:00作者:晏闻田Solitary

本文基于 three.js 官方文档 docs/pages/DataArrayTexture.html.md 展开,系统讲解 DataArrayTexture 的构造参数、默认属性覆写、addLayerUpdate() 逐层更新机制,并结合 src/ 下的渲染器源码说明数据如何经 texImage3D/texSubImage3D 上传为 WebGL 2 的 TEXTURE_2D_ARRAY。读完你可以掌握:如何用一行 new DataArrayTexture(data, width, height, depth) 从原始缓冲创建体积数据纹理,如何用 layerUpdates 只上传变化的层以降低 GPU 传输开销,以及在着色器中以 sampler2DArray 按 (U, V, layerIndex) 采样。

three.js webgl_texture2darray 示例:用 DataArrayTexture 采样头部扫描体积数据的单一切片

继承关系与类型标识

DataArrayTexture 的继承链为:

EventDispatcher → Texture → DataArrayTexture

它在 src/textures/DataArrayTexture.js 中定义,构造时立即设置只读标志 isDataArrayTexture = true,可用于 instanceof 之外的轻量类型判断。仓库自带的单元测试 test/unit/src/textures/DataArrayTexture.tests.js 也验证了三点:继承自 Texture、可正常实例化、isDataArrayTexture 恒为 true

除了类型标志,构造器还会向父类 Texturenull 作为 image,然后把原始数据包装成 Textureimage 结构(见下文“image 属性”),这一点是它与 DataTexture(单张)的结构性区别所在。

构造函数

new DataArrayTexture( data = null, width = 1, height = 1, depth = 1 )

四个参数及默认值(来源:src/textures/DataArrayTexture.js#L19-L37):

参数 类型 默认值 含义
data ?TypedArray null 原始缓冲数据(如 Uint8ArrayFloat32Array
width number 1 每层纹理的宽度(像素)
height number 1 每层纹理的高度(像素)
depth number 1 纹理数组的层数

data.length 需要与 width × height × depth × 每通道数 匹配,通道数由 format(默认继承 TextureRGBAFormat)与 type 决定。构造器随后把数据封装为:

this.image = { data, width, height, depth };

这个 {data, width, height, depth} 正是渲染器区分“数组纹理”的关键。在基类 src/textures/Texture.js#L353-L359 中,isArrayTexture 的判定逻辑是 image.depth && image.depth > 1 ? true : false,也就是说只要深度大于 1 的 image 对象,three.js 就把它当作纹理数组处理。Texturewidth / height / depth getter 也直接取自该 image 对象(src/textures/Texture.js#L385-L407)。

默认值覆写:与基类 Texture 的差异

DataArrayTexture 在构造时对若干基类属性做了覆写(源码逐行见 src/textures/DataArrayTexture.js#L39-L104)。下表汇总了文档中列出的全部覆写属性,并与 Texture 基类默认值对照:

属性 DataArrayTexture 默认值 Texture 基类默认值 说明
.flipY false true 上传 GPU 时是否沿垂直轴翻转
.generateMipmaps false true 是否生成 mipmap
.magFilter NearestFilter LinearFilter 纹素覆盖多个像素时的采样方式
.minFilter NearestFilter LinearMipmapLinearFilter 纹素覆盖不足一个像素时的采样方式
.unpackAlignment 1 4 内存中每行像素起始处的对齐要求
.wrapR ClampToEdgeWrapping —(基类无此属性) 深度方向(W)的环绕方式
.image {data, width, height, depth} 视具体纹理而定 纹理图像定义
.isDataArrayTexture true(readonly) 类型测试标志
.layerUpdates new Set() 待更新层索引的集合

逐项说明:

  • .flipY:基类 Texture 默认 truesrc/textures/Texture.js#L281),面向 DOM 图像;但原始缓冲数据没有“图像朝上”的语义,因此 DataArrayTexture 覆写为 false
  • .generateMipmaps:默认关闭,配合 NearestFilter 的 min/mag 过滤——对体积数据、查找表这类数据纹理而言,mipmap 通常没有意义且会额外占用显存。
  • .magFilter / .minFilter:取值范围是 NearestFilter | NearestMipmapNearestFilter | NearestMipmapLinearFilter | LinearFilter | LinearMipmapNearestFilter | LinearMipmapLinearFilterDataArrayTexture 默认两者都是 NearestFilter,保证按整数坐标取样时精确命中原始数据。
  • .unpackAlignment:指定每行像素在内存中的起始对齐,合法值为 1(字节对齐)、2(偶数字节)、4(字对齐)、8(双字对齐)。基类默认 4src/textures/Texture.js#L291),DataArrayTexture 覆写为 1,即不施加额外行对齐约束,这对任意宽度的原始数据更安全。该值最终通过 state.pixelStorei( _gl.UNPACK_ALIGNMENT, texture.unpackAlignment ) 生效(src/renderers/webgl/WebGLTextures.js#L932)。
  • .wrapR:文档中特别指出它“对应 UVW 映射中的 W”。纹理数组的第三维采样坐标就是层索引,ClampToEdgeWrapping 意味着层索引越界时取边界层。copy() 方法中 wrapR 也是唯一在基类拷贝之外额外处理的属性(src/textures/DataArrayTexture.js#L114-L122),因为它属于纹理数组/3D 纹理特有的维度。

.layerUpdates.addLayerUpdate().clearLayerUpdates()

这三个 API 是 DataArrayTexture 最具实战价值的部分:当纹理内容每帧只变化少数几层时,可以避免整块数组(可能几十 MB)的重复上传。

从渲染器源码看这两者的分工非常清晰。WebGLTextures.jsuploadTexture() 在处理 DataArrayTexture 分支时(src/renderers/webgl/WebGLTextures.js#L1168-L1208):

if ( texture.layerUpdates.size > 0 ) {

    const layerByteLength = getByteLength( image.width, image.height, texture.format, texture.type );

    for ( const layerIndex of texture.layerUpdates ) {

        const layerData = image.data.subarray(
            layerIndex * layerByteLength / image.data.BYTES_PER_ELEMENT,
            ( layerIndex + 1 ) * layerByteLength / image.data.BYTES_PER_ELEMENT
        );
        state.texSubImage3D( _gl.TEXTURE_2D_ARRAY, 0, 0, 0, layerIndex, image.width, image.height, 1, glFormat, glType, layerData );

    }

    texture.clearLayerUpdates();

} else {

    state.texSubImage3D( _gl.TEXTURE_2D_ARRAY, 0, 0, 0, 0, image.width, image.height, image.depth, glFormat, glType, image.data );

}

也就是说:

  1. layerUpdates 非空,则按 getByteLength( width, height, format, type ) 算出单层字节长度,用 subarray 只切出被标记层的数据,对每一层调用 texSubImage3D(..., layerIndex, ..., 1, ...) 上传该层
  2. 上传完成后渲染器会自行调用 texture.clearLayerUpdates() 重置集合——因此你不需要手动清,下一轮更新前再次 addLayerUpdate 即可;
  3. 若集合为空,则一次性 texSubImage3D 上传全部 depth 层。

这正是文档所述:“设置 Texture#needsUpdatetrue 时,通常整个数组都会被发送到 GPU;标记特定层则只传输该深度对应的数据子集,往往高效得多”。CompressedArrayTexture 拥有完全相同的 layerUpdates / addLayerUpdate / clearLayerUpdates 机制(src/textures/CompressedArrayTexture.js),可对照阅读。

GPU 上传流程:TEXTURE_2D_ARRAY 的完整链路

DataArrayTexture 最终落到 WebGL 2 的 GL_TEXTURE_2D_ARRAY。以 WebGL 后端为例,关键调用链全部集中在 src/renderers/webgl/WebGLTextures.js

  1. 目标纹理类型选择L900-L905):

    let textureType = _gl.TEXTURE_2D;
    if ( texture.isDataArrayTexture || texture.isCompressedArrayTexture ) textureType = _gl.TEXTURE_2D_ARRAY;
    if ( texture.isData3DTexture ) textureType = _gl.TEXTURE_3D;
    

    可见 isDataArrayTexture 标志直接决定绑定到哪个 GPU 纹理目标。

  2. 版本检查触发上传L914-L916):source.version !== sourceProperties.__version 时才执行上传。Texture#needsUpdate 的 setter 会递增 version 并置位 source.needsUpdatesrc/textures/Texture.js#L754-L763),这就是“改数据 → 置 needsUpdate = true → 渲染时自动上传”的底层机制。

  3. 像素存储参数:上传前依次设置 UNPACK_FLIP_Y_WEBGL(对应 .flipY)、UNPACK_PREMULTIPLY_ALPHA_WEBGLUNPACK_ALIGNMENT(对应 .unpackAlignment),见 L926-L932

  4. 分配与填充L1168-L1208):优先走 texStorage3D 一次性分配不可变存储(texStorage3D( _gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, image.width, image.height, image.depth )),随后按上文逻辑用 texSubImage3D 填充;当 texStorage3D 不可用时退回 texImage3D 全量上传。

WebGPU 后端同样支持:src/renderers/webgpu/utils/WebGPUTextureUtils.js#L674-L700 中对 isArrayTexture || isDataArrayTexture || isData3DTexture 走 3D 纹理上传路径,且同样消费 texture.layerUpdates;采样端由 src/renderers/webgpu/nodes/WGSLNodeBuilder.js#L601 判断。

另外,DataArrayTexture 还可以作为渲染目标纹理:src/renderers/WebGLRenderer.js#L2994 中把 isData3DTexture || isDataArrayTexture || isCompressedArrayTexture 归入数组/3D 纹理目标处理分支;跨纹理拷贝时(L3349)也会识别 isDataArrayTexture 源。Texture#dispose() 会在销毁纹理时派发 dispose 事件并释放 GPU 资源(src/textures/Texture.js#L647-L657),不再使用时应调用。

实战示例一:体积渲染中的切片采样

仓库示例 examples/webgl_texture2darray.html 是最典型的用法:加载 256×256×109 的头部 CT 扫描原始数据(8-bit 灰度),构建 DataArrayTexture,然后在着色器里用 sampler2DArray 按层取样,实现逐层“切片”动画。核心代码:

// 原始数据 256 x 256 x 109,8-bit,zip 压缩
const array = new Uint8Array( zip[ 'head256x256x109' ].buffer );

const texture = new THREE.DataArrayTexture( array, 256, 256, 109 );
texture.format = THREE.RedFormat;   // 每像素仅 1 通道,与 8-bit 灰度数据匹配
texture.needsUpdate = true;

const material = new THREE.ShaderMaterial( {
    uniforms: {
        diffuse: { value: texture },
        depth: { value: 55 },                              // 采样哪一层
        size: { value: new THREE.Vector2( planeWidth, planeHeight ) }
    },
    vertexShader: /* ... 把 position 归一化为 uv ... */,
    fragmentShader: /* ... 见下方 shader ... */,
    glslVersion: THREE.GLSL3
} );
// 片元着色器:sampler2DArray 的第三个坐标就是层索引
precision highp float;
precision highp int;
precision highp sampler2DArray;

uniform sampler2DArray diffuse;
in vec2 vUv;
uniform int depth;
out vec4 outColor;

void main() {

    vec4 color = texture( diffuse, vec3( vUv, depth ) );
    outColor = vec4( color.rrr * 1.5, 1.0 );

}

几个要点:

  • format 必须与数据一致:示例数据是每像素 1 字节的灰度,所以显式设为 RedFormat(否则按默认 RGBAFormat 会错位)。
  • 示例注释明确指出 2D 纹理数组依赖 WebGL 2.0sampler2DArraytexImage3D/texStorage3D 均为 WebGL 2 能力),这也是为什么该对象在 WebGL 2 环境下才有完整语义。
  • 默认 NearestFilter + ClampToEdgeWrapping(含 wrapR)的组合使切片边界行为可预期。

实战示例二:用 addLayerUpdate 只上传变化的层

examples/webgl_texture2darray_layerupdate.html 演示了 addLayerUpdate() 的标准调用模式:一个三层数组纹理作为画布,GUI 允许把源 KTX2 纹理的某一层拷贝到目标数组的指定层,随后只上传被写入的那一层:

three.js webgl_texture2darray_layerupdate 示例:通过 addLayerUpdate 只更新纹理数组的指定层

// 计算单层的字节长度(与 WebGLTextures.js 内部的 getByteLength 同一套规则)
const layerByteLength = THREE.TextureUtils.getByteLength(
    spiritedaway.image.width,
    spiritedaway.image.height,
    spiritedaway.format,
    spiritedaway.type,
);

// ...构造目标数组纹理(示例用 CompressedArrayTexture,机制相同)...

function transfer() {

    // 1) 在 CPU 侧把源数据写入 image.data 中对应层的偏移位置
    const layerElementLength = layerByteLength / spiritedaway.mipmaps[ 0 ].data.BYTES_PER_ELEMENT;
    textureArray.mipmaps[ 0 ].data.set(
        spiritedaway.mipmaps[ 0 ].data.subarray(
            layerElementLength * ( formData.srcLayer % spiritedaway.image.depth ),
            layerElementLength * ( ( formData.srcLayer % spiritedaway.image.depth ) + 1 ),
        ),
        layerByteLength * formData.destLayer,
    );

    // 2) 登记需要上传的层
    textureArray.addLayerUpdate( formData.destLayer );
    textureArray.needsUpdate = true;   // 递增 version,触发下一帧上传

    renderer.render( scene, camera );

}

DataArrayTexture 的用法完全同构,只是 image 为单个 {data, width, height, depth} 而非 mipmaps 数组:写入 texture.image.datadestLayer * layerByteLength 处的偏移,然后 addLayerUpdate( destLayer ); texture.needsUpdate = true;。渲染器在上传该层后自动清空 layerUpdates,下一轮更新互不干扰(对照 src/renderers/webgl/WebGLTextures.js#L1194texture.clearLayerUpdates() 调用)。

其他可直接参考的示例:

与相关纹理类的对照

数据结构 GPU 目标 典型用途
DataTexture 单层 {data, width, height} TEXTURE_2D 单张程序化/数据纹理
DataArrayTexture {data, width, height, depth} TEXTURE_2D_ARRAY 多层灰度/体积切片、逐帧 LUT 序列
Data3DTexture 同维度 3D 数据 TEXTURE_3D 连续体积,采样时 W 为连续坐标
CompressedArrayTexture 压缩 mipmaps 数组 TEXTURE_2D_ARRAY 压缩纹理数组,同样支持 layerUpdates

四者均实现 isDataArrayTexture / isData3DTexture / isCompressedArrayTexture 等标志位,渲染器正是靠这些标志在 src/renderers/webgl/WebGLTextures.js#L904src/renderers/WebGLRenderer.js#L2994 处选择正确的上传/绑定分支。

小结

DataArrayTexture 是 three.js 中“原始缓冲 → 2D 纹理数组”的直接通道:构造参数 (data, width, height, depth) 决定了 image 结构,而 flipY=falsegenerateMipmaps=falseNearestFilterunpackAlignment=1 这组覆写默认值使它天然适配数据纹理场景;wrapR 控制层索引的 W 方向环绕。当数据动态更新时,addLayerUpdate() + needsUpdate 的组合让渲染器只 texSubImage3D 上传被标记的层,避免了整块数组的重传。完整实现可继续查看 src/textures/DataArrayTexture.jssrc/textures/Texture.jssrc/renderers/webgl/WebGLTextures.js,行为边界则由 test/unit/src/textures/DataArrayTexture.tests.js 固化。

登录后查看全文
热门项目推荐
相关项目推荐