three.js DataArrayTexture 实战:从 TypedArray 原始数据构建 2D 纹理数组
本文基于 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 / 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默认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 固化。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

