three.js CompressedArrayTexture 深度解析:压缩 2D 纹理数组的构造、属性与按层更新机制
2026/9/7 18:09:26 网站建设 项目流程

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 )
参数类型默认值含义
mipmapsArray<Object>必填所有 mipmap(含基础级)的数据与尺寸,每项包含data与各级的width/height
widthnumber必填纹理宽度
heightnumber必填纹理高度
depthnumber必填纹理数组的层数(深度)
formatnumberRGBAFormat纹理像素格式,需与压缩数据实际格式一致(如RGBAFormat表示未压缩或透传格式)
typenumberUnsignedByteType纹理数据类型

需要说明的一处细节:官方文档中对formattype的描述文字写的是 “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 中uploadTextureisCompressedArrayTexture的处理分支:

  1. 首次上传(分配显存):当useTexStorage && allocateMemory时,先调用state.texStorage3D( gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, mipmaps[0].width, mipmaps[0].height, image.depth )一次性分配整个数组的显存;
  2. 逐 mipmap 上传:遍历mipmaps,对非RGBAFormat的压缩数据调用compressedTexSubImage3D
    • layerUpdates.size > 0,则计算单层字节数layerByteLength = getByteLength( mipmap.width, mipmap.height, texture.format, texture.type ),对layerUpdates中的每个layerIndexmipmap.datasubarray出该层数据,再以(x=0, y=0, layerIndex, w, h, depthSize=1)为区域调用compressedTexSubImage3D——每一级 mipmap 只上传被标记的那一层
    • 若无层标记,则以image.depth为深度一次性上传整幅数组(compressedTexSubImage3D( ..., 0, 0, image.depth, glFormat, mipmap.data ));
  3. 自动清理:上传循环结束后执行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 = LinearFiltergenerateMipmaps = falseneedsUpdate = true,并按容器元数据设置colorSpacepremultiplyAlpha。使用 KTX2Loader.js 前务必先调用ktx2Loader.detectSupport( renderer )探测当前显卡支持的压缩格式——这一点在示例 webgl_texture2darray_layerupdate.html 与 WebGL 上传分支的告警逻辑(glFormat === null时警告 “Attempt to load unsupported compressed texture format”)中都有体现。

八、使用注意事项小结

  1. mipmap 必须自备generateMipmaps被强制为false,若只传基础级数据且期望 trilinear 采样,应像KTX2Loader那样把minFilter设为LinearFilter,否则会出现缺 mipmap 的采样问题;
  2. flipY 无效:压缩纹理上传不能翻转,图像方向需在编码阶段处理(示例着色器中手动做了vUv.y = 1.0 - vUv.y的处理);
  3. format/type 必须与压缩数据匹配:上传路径按format判断走compressedTexSubImage3D还是普通texSubImage3D,格式不匹配会导致警告或错误采样;
  4. 按层更新的触发条件addLayerUpdate只是登记意图,真正生效需要下一次needsUpdate = true且渲染器执行上传;上传完成后layerUpdates会被自动清空,重复登记同一层无副作用(Set去重);
  5. 与 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),仅供参考

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

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

立即咨询