three.js CompressedArrayTexture 深度解析:压缩 2D 纹理数组的构造、属性与按层更新机制
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
本文基于 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
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考