
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 自动生成 mipmapmipmap 必须内嵌在纹理文件如 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 )参数类型默认值含义mipmapsArrayObject必填所有 mipmap含基础级的数据与尺寸每项包含data与各级的width/heightwidthnumber必填纹理宽度heightnumber必填纹理高度depthnumber必填纹理数组的层数深度formatnumberRGBAFormat纹理像素格式需与压缩数据实际格式一致如RGBAFormat表示未压缩或透传格式typenumberUnsignedByteType纹理数据类型需要说明的一处细节官方文档中对format与type的描述文字写的是 “The min filter value”这与 CompressedArrayTexture.js 源码 JSDoc 中的措辞一致属于上游文档的笔误从参数名与父类 CompressedTexture.js 的传递逻辑看二者实际语义分别是“纹理像素格式”与“纹理数据类型”本文按此解释。构造器在super()之后还做了三件事见 CompressedArrayTexture.jsthis.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()复制此类纹理时深度包裹方式不会丢失。四、MethodsaddLayerUpdate 与 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出该层数据再以(x0, y0, layerIndex, w, h, depthSize1)为区域调用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。加载器随后统一设置minFiltermipmap 数 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 的选型原始未压缩的层数据用DataArrayTextureGPU 不支持的压缩格式或需压缩带宽时再用CompressedArrayTexture两者共享同一套按层上传机制见 WebGLTextures.js 的isDataArrayTexture分支。参考资料均在当前仓库内文档原文docs/pages/CompressedArrayTexture.html.md类实现src/textures/CompressedArrayTexture.js、父类 src/textures/CompressedTexture.jsGPU 上传路径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),仅供参考