three.js CompressedArrayTexture 深度解析:压缩 2D 纹理数组的构造、属性与按层更新机制
本文基于 three.js 官方 API 文档 docs/pages/CompressedArrayTexture.html.md,完整覆盖 CompressedArrayTexture 的构造参数、属性与方法,并结合 CompressedArrayTexture.js 源码实现、WebGLTextures.js 中的 GPU 上传路径,以及官方示例 webgl_texture2darray_layerupdate.html,讲清楚这类纹理是如何从压缩数据进入 GPU、以及 addLayerUpdate 按层局部更新相比整幅重传为何更省性能。读完本文,你可以直接在项目中构造压缩纹理数组(如加载 KTX2 动画帧序列),并掌握低开销的逐层刷新实战技巧。
一、CompressedArrayTexture 是什么:继承体系与定位
官方文档将其定义为一类“基于压缩数据创建的 2D 纹理数组(texture 2D array)”,并说明这类纹理通常由 CompressedTextureLoader 加载创建。其继承链为:
EventDispatcher → Texture → CompressedTexture → CompressedArrayTexture
在 CompressedArrayTexture.js 中可以直接看到这一点:
class CompressedArrayTexture extends CompressedTexture {
constructor( mipmaps, width, height, depth, format, type ) {
super( mipmaps, width, height, format, type );
// ...
}
}
父类 CompressedTexture.js 决定了它的几个关键行为,理解这些对正确使用该类至关重要:
image属性只描述尺寸:压缩纹理没有 DOM 图像,父类将其设为this.image = { width, height },CompressedArrayTexture在此基础上追加this.image.depth = depth,因此image最终是{ width, height, depth }三个纯数值字段,供渲染器判断纹理尺寸;flipY恒为false:压缩数据无法在上传时做垂直翻转,父类显式覆写了该标志;generateMipmaps恒为false:压缩格式不能由 GPU 自动生成 mipmap,mipmap 必须内嵌在纹理文件(如 KTX2)中,随mipmaps数组一并传入;mipmaps数组:保存所有 mipmap(包含 0 号基础级)的数据与尺寸,每个元素形如{ data, width, height }。
单元测试 CompressedArrayTexture.tests.js 也验证了继承关系(instanceof CompressedTexture 为真)与 isCompressedArrayTexture 标志的默认值。
二、构造函数参数逐项说明
文档给出的签名与默认值如下(以 CompressedArrayTexture.js 第 24 行构造器为准):
new CompressedArrayTexture( mipmaps, width, height, depth, format, type )
| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
mipmaps |
Array<Object> |
必填 | 所有 mipmap(含基础级)的数据与尺寸,每项包含 data 与各级的 width / height |
width |
number |
必填 | 纹理宽度 |
height |
number |
必填 | 纹理高度 |
depth |
number |
必填 | 纹理数组的层数(深度) |
format |
number |
RGBAFormat |
纹理像素格式,需与压缩数据实际格式一致(如 RGBAFormat 表示未压缩或透传格式) |
type |
number |
UnsignedByteType |
纹理数据类型 |
需要说明的一处细节:官方文档中对 format 与 type 的描述文字写的是 “The min filter value”,这与 CompressedArrayTexture.js 源码 JSDoc 中的措辞一致,属于上游文档的笔误;从参数名与父类 CompressedTexture.js 的传递逻辑看,二者实际语义分别是“纹理像素格式”与“纹理数据类型”,本文按此解释。
构造器在 super() 之后还做了三件事(见 CompressedArrayTexture.js):
this.isCompressedArrayTexture = true; // 类型检测标志
this.image.depth = depth; // image 追加深度维度
this.wrapR = ClampToEdgeWrapping; // 深度方向包裹方式
this.layerUpdates = new Set(); // 待更新层注册表
三、Properties:.image / .isCompressedArrayTexture / .layerUpdates / .wrapR
.image : Object
文档指出:“压缩纹理的 image 属性只定义其尺寸”。由于父类 CompressedTexture 已把 image 覆写为 { width, height },CompressedArrayTexture 再补上 depth。因此对这类纹理而言,image 不是图像数据,而是一个纯尺寸描述对象,渲染器用它来确定 texStorage3D / texImage3D 的体尺寸参数。
.isCompressedArrayTexture : boolean (readonly)
只读类型标志,默认为 true,用于类型测试(instanceof 之外的 duck-typing 判断)。three.js 渲染管线内部大量使用此类标志区分上传路径,例如 WebGLTextures.js 中根据 texture.isCompressedArrayTexture 决定 GPU 纹理目标为 gl.TEXTURE_2D_ARRAY。
.layerUpdates : Set
一个 Set,记录当前需要向 GPU 重新上传的纹理层索引。默认空集。这是 addLayerUpdate 机制的底层数据结构,在上传完成后由渲染器自动清空(详见下节原理分析)。
.wrapR : RepeatWrapping | ClampToEdgeWrapping | MirroredRepeatWrapping
定义纹理在深度方向的包裹方式,对应 UVW 映射中的 W 分量,默认为 ClampToEdgeWrapping。采样 2D 纹理数组时使用 sampler2DArray 并以 vec3(uv, layerIndex) 采样,wrapR 决定 W 分量(即层坐标)越界时的行为。
另外,CompressedArrayTexture.js 中的 copy( source ) 方法在调用 super.copy 后会额外拷贝 wrapR,因此用 texture.copy() 复制此类纹理时深度包裹方式不会丢失。
四、Methods:addLayerUpdate 与 clearLayerUpdates
.addLayerUpdate( layerIndex : number )
文档原文的解释是:通常把 needsUpdate 设为 true 时,整个压缩纹理数组会被发送到 GPU;而标记具体层后,只会传输与某个深度关联的所有 mipmap 子集,这通常高效得多。实现极简:
addLayerUpdate( layerIndex ) {
this.layerUpdates.add( layerIndex );
}
.clearLayerUpdates()
重置层更新注册表(this.layerUpdates.clear())。一般不需要手动调用——渲染器在完成一次按层上传后会自动清理,手动调用适用于“取消尚未渲染的更新标记”这类边缘场景。
五、源码级原理:layerUpdates 如何节省 GPU 传输
理解 addLayerUpdate 的价值,必须看 WebGLTextures.js 中 uploadTexture 对 isCompressedArrayTexture 的处理分支:
- 首次上传(分配显存):当
useTexStorage && allocateMemory时,先调用state.texStorage3D( gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, mipmaps[0].width, mipmaps[0].height, image.depth )一次性分配整个数组的显存; - 逐 mipmap 上传:遍历
mipmaps,对非RGBAFormat的压缩数据调用compressedTexSubImage3D:- 若
layerUpdates.size > 0,则计算单层字节数layerByteLength = getByteLength( mipmap.width, mipmap.height, texture.format, texture.type ),对layerUpdates中的每个layerIndex从mipmap.data中subarray出该层数据,再以(x=0, y=0, layerIndex, w, h, depthSize=1)为区域调用compressedTexSubImage3D——每一级 mipmap 只上传被标记的那一层; - 若无层标记,则以
image.depth为深度一次性上传整幅数组(compressedTexSubImage3D( ..., 0, 0, image.depth, glFormat, mipmap.data ));
- 若
- 自动清理:上传循环结束后执行
if ( texture.layerUpdates.size > 0 ) texture.clearLayerUpdates();,保证标记不重复生效。
也就是说:一次动画帧只改了 3 个层中的 1 个时,整幅重传的数据量与层数成正比(depth × 每级 mipmap 大小),而按层更新只传输 1 × 每级 mipmap 单层大小,层数越多节省越明显。同样的按层上传逻辑也存在于 DataArrayTexture 分支(WebGLTextures.js),可见这是 three.js 对 2D 纹理数组统一的局部更新基础设施。
六、实战示例:KTX2 动画帧的按层更新
官方示例 webgl_texture2darray_layerupdate.html 完整演示了该类的典型用法:加载一张 KTX2 压缩动画(多帧),把其中 3 帧写入一个 3 层的 CompressedArrayTexture,再通过 GUI 把任意源层“搬运”到目标层。关键代码(省略场景与 GUI 搭建):
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const ktx2Loader = new KTX2Loader();
ktx2Loader.detectSupport( renderer );
// KTX2 动画源纹理(压缩数据)
const spiritedaway = await ktx2Loader.loadAsync( 'textures/spiritedaway.ktx2' );
// 单帧的字节长度:用于在压缩数据流中切分每一层
const layerByteLength = THREE.TextureUtils.getByteLength(
spiritedaway.image.width, spiritedaway.image.height,
spiritedaway.format, spiritedaway.type,
);
// 构造 3 层的压缩纹理数组(1 级 mipmap 占位,随后填充基础级数据)
const textureArray = new THREE.CompressedArrayTexture( [
{ data: new Uint8Array( layerByteLength * 3 ),
width: spiritedaway.image.width,
height: spiritedaway.image.height }
], spiritedaway.image.width, spiritedaway.image.height, 3,
spiritedaway.format, spiritedaway.type );
“搬运”某一帧到目标层的核心逻辑,正是文档中 addLayerUpdate 的标准姿势:
const layerElementLength = layerByteLength / spiritedaway.mipmaps[ 0 ].data.BYTES_PER_ELEMENT;
// 1) 在 CPU 端把源层压缩数据写入目标层对应的字节区间
textureArray.mipmaps[ 0 ].data.set(
spiritedaway.mipmaps[ 0 ].data.subarray(
layerElementLength * srcLayer,
layerElementLength * ( srcLayer + 1 )
),
layerByteLength * destLayer,
);
// 2) 标记该层 + 置位 needsUpdate,渲染时只上传这一层
textureArray.addLayerUpdate( destLayer );
textureArray.needsUpdate = true;
renderer.render( scene, camera );
采样端使用 sampler2DArray,第三维坐标即层索引:
precision highp sampler2DArray;
uniform sampler2DArray diffuse;
// ...
outColor = texture( diffuse, vec3( vUv, diffuseIndex ) );
该示例值得注意的两个细节:其一,压缩数据无法逐像素读写,因此“更新一层”必须在 CPU 端按 getByteLength 计算的字节偏移做 subarray/set 级别的搬运,再交给 GPU 上传路径完成局部刷新;其二,初始填充(mipmaps[0].data.set(...) + needsUpdate = true)走的是整幅上传路径,之后的单帧切换才走 addLayerUpdate 的局部路径。
七、自动创建路径:KTX2Loader 何时产出 CompressedArrayTexture
除手动构造外,three.js 生态中最常见的创建方式是通过加载器自动产出。KTX2Loader.js 的 _createTextureFrom 中:
if ( container.faceCount === 6 ) {
texture = new CompressedCubeTexture( faces, format, type );
} else {
const mipmaps = faces[ 0 ].mipmaps;
texture = container.layerCount > 1
? new CompressedArrayTexture( mipmaps, width, height, container.layerCount, format, type )
: new CompressedTexture( mipmaps, width, height, format, type );
}
即:KTX2 容器层数为 1 时得到普通 CompressedTexture,层数大于 1 时自动升级为 CompressedArrayTexture(立方体 6 面则走 CompressedCubeTexture)。加载器随后统一设置 minFilter(mipmap 数 > 1 时用 LinearMipmapLinearFilter,否则 LinearFilter)、magFilter = LinearFilter、generateMipmaps = false、needsUpdate = true,并按容器元数据设置 colorSpace 与 premultiplyAlpha。使用 KTX2Loader.js 前务必先调用 ktx2Loader.detectSupport( renderer ) 探测当前显卡支持的压缩格式——这一点在示例 webgl_texture2darray_layerupdate.html 与 WebGL 上传分支的告警逻辑(glFormat === null 时警告 “Attempt to load unsupported compressed texture format”)中都有体现。
八、使用注意事项小结
- mipmap 必须自备:
generateMipmaps被强制为false,若只传基础级数据且期望 trilinear 采样,应像KTX2Loader那样把minFilter设为LinearFilter,否则会出现缺 mipmap 的采样问题; - flipY 无效:压缩纹理上传不能翻转,图像方向需在编码阶段处理(示例着色器中手动做了
vUv.y = 1.0 - vUv.y的处理); - format/type 必须与压缩数据匹配:上传路径按
format判断走compressedTexSubImage3D还是普通texSubImage3D,格式不匹配会导致警告或错误采样; - 按层更新的触发条件:
addLayerUpdate只是登记意图,真正生效需要下一次needsUpdate = true且渲染器执行上传;上传完成后layerUpdates会被自动清空,重复登记同一层无副作用(Set去重); - 与 DataArrayTexture 的选型:原始未压缩的层数据用
DataArrayTexture,GPU 不支持的压缩格式或需压缩带宽时再用CompressedArrayTexture;两者共享同一套按层上传机制(见 WebGLTextures.js 的isDataArrayTexture分支)。
参考资料(均在当前仓库内)
- 文档原文:docs/pages/CompressedArrayTexture.html.md
- 类实现:src/textures/CompressedArrayTexture.js、父类 src/textures/CompressedTexture.js
- GPU 上传路径:src/renderers/webgl/WebGLTextures.js
- 自动创建:examples/jsm/loaders/KTX2Loader.js
- 可运行示例:examples/webgl_texture2darray_layerupdate.html
- 单元测试:test/unit/src/textures/CompressedArrayTexture.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 StartedRust0624
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