three.js DataArrayTexture 实战:从 TypedArray 原始数据构建 2D 纹理数组
2026/9/7 19:23:20 网站建设 项目流程

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

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

本文基于 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) 采样。

继承关系与类型标识

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?TypedArraynull原始缓冲数据(如Uint8ArrayFloat32Array
widthnumber1每层纹理的宽度(像素)
heightnumber1每层纹理的高度(像素)
depthnumber1纹理数组的层数

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/depthgetter 也直接取自该 image 对象(src/textures/Texture.js#L385-L407)。

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

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

属性DataArrayTexture 默认值Texture 基类默认值说明
.flipYfalsetrue上传 GPU 时是否沿垂直轴翻转
.generateMipmapsfalsetrue是否生成 mipmap
.magFilterNearestFilterLinearFilter纹素覆盖多个像素时的采样方式
.minFilterNearestFilterLinearMipmapLinearFilter纹素覆盖不足一个像素时的采样方式
.unpackAlignment14内存中每行像素起始处的对齐要求
.wrapRClampToEdgeWrapping—(基类无此属性)深度方向(W)的环绕方式
.image{data, width, height, depth}视具体纹理而定纹理图像定义
.isDataArrayTexturetrue(readonly)类型测试标志
.layerUpdatesnew Set()待更新层索引的集合

逐项说明:

  • .flipY:基类Texture默认true(src/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(双字对齐)。基类默认4(src/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)的重复上传。

  • .layerUpdates : Set<number>:保存“需要更新的层索引”的集合,构造时初始化为空Set(src/textures/DataArrayTexture.js#L99-L104)。
  • .addLayerUpdate( layerIndex : number ):把指定层索引加入集合。
  • .clearLayerUpdates():清空集合,重置更新登记(src/textures/DataArrayTexture.js#L133-L146)。

从渲染器源码看这两者的分工非常清晰。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.needsUpdate(src/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 纹理的某一层拷贝到目标数组的指定层,随后只上传被写入的那一层:

// 计算单层的字节长度(与 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#L1194 的texture.clearLayerUpdates()调用)。

其他可直接参考的示例:

  • examples/webgl_rendertarget_texture2darray.html:把 2D 纹理数组用作渲染目标(WebGLRenderTargetTexture2DArrayTarget);
  • examples/webgpu_textures_2d-array.html 与 examples/webgpu_rendertarget_2d-array_3d.html:WebGPU 路径下的 2D 数组纹理与 2D 数组/3D 渲染目标;
  • examples/webgl_texture2darray_compressed.html:压缩格式数组纹理(CompressedArrayTexture),与本文逐层更新机制互为对照。

与相关纹理类的对照

数据结构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#L904 与 src/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.js、src/textures/Texture.js 与 src/renderers/webgl/WebGLTextures.js,行为边界则由 test/unit/src/textures/DataArrayTexture.tests.js 固化。

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询