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。
除了类型标志,构造器还会向父类Texture传null作为 image,然后把原始数据包装成Texture的image结构(见下文“image 属性”),这一点是它与DataTexture(单张)的结构性区别所在。
构造函数
new DataArrayTexture( data = null, width = 1, height = 1, depth = 1 )四个参数及默认值(来源:src/textures/DataArrayTexture.js#L19-L37):
| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
data | ?TypedArray | null | 原始缓冲数据(如Uint8Array、Float32Array) |
width | number | 1 | 每层纹理的宽度(像素) |
height | number | 1 | 每层纹理的高度(像素) |
depth | number | 1 | 纹理数组的层数 |
data.length需要与width × height × depth × 每通道数匹配,通道数由format(默认继承Texture的RGBAFormat)与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 就把它当作纹理数组处理。Texture的width/height/depthgetter 也直接取自该 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默认true(src/textures/Texture.js#L281),面向 DOM 图像;但原始缓冲数据没有“图像朝上”的语义,因此DataArrayTexture覆写为false。.generateMipmaps:默认关闭,配合NearestFilter的 min/mag 过滤——对体积数据、查找表这类数据纹理而言,mipmap 通常没有意义且会额外占用显存。.magFilter/.minFilter:取值范围是NearestFilter | NearestMipmapNearestFilter | NearestMipmapLinearFilter | LinearFilter | LinearMipmapNearestFilter | LinearMipmapLinearFilter。DataArrayTexture默认两者都是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.js的uploadTexture()在处理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 ); }也就是说:
- 若
layerUpdates非空,则按getByteLength( width, height, format, type )算出单层字节长度,用subarray只切出被标记层的数据,对每一层调用texSubImage3D(..., layerIndex, ..., 1, ...)上传该层; - 上传完成后渲染器会自行调用
texture.clearLayerUpdates()重置集合——因此你不需要手动清,下一轮更新前再次addLayerUpdate即可; - 若集合为空,则一次性
texSubImage3D上传全部depth层。
这正是文档所述:“设置Texture#needsUpdate为true时,通常整个数组都会被发送到 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:
目标纹理类型选择(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 纹理目标。版本检查触发上传(L914-L916):
source.version !== sourceProperties.__version时才执行上传。Texture#needsUpdate的 setter 会递增version并置位source.needsUpdate(src/textures/Texture.js#L754-L763),这就是“改数据 → 置needsUpdate = true→ 渲染时自动上传”的底层机制。像素存储参数:上传前依次设置
UNPACK_FLIP_Y_WEBGL(对应.flipY)、UNPACK_PREMULTIPLY_ALPHA_WEBGL、UNPACK_ALIGNMENT(对应.unpackAlignment),见 L926-L932。分配与填充(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.0(
sampler2DArray与texImage3D/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.data中destLayer * layerByteLength处的偏移,然后addLayerUpdate( destLayer ); texture.needsUpdate = true;。渲染器在上传该层后自动清空layerUpdates,下一轮更新互不干扰(对照 src/renderers/webgl/WebGLTextures.js#L1194 的texture.clearLayerUpdates()调用)。
其他可直接参考的示例:
- examples/webgl_rendertarget_texture2darray.html:把 2D 纹理数组用作渲染目标(
WebGLRenderTarget配Texture2DArrayTarget); - 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=false、generateMipmaps=false、NearestFilter、unpackAlignment=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),仅供参考