three.js CubeMapNode 深度解析:TSL 中自动完成等距柱状投影到立方体贴图的环境贴图转换
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
CubeMapNode是 three.js 节点系统(TSL,Three Shading Language)中的一个实用节点,用于在着色器构建阶段自动把等距柱状投影(equirectangular)格式的环境贴图转换为立方体贴图(cube map)格式,并在每帧渲染时自动更新纹理引用。读完本文,你可以掌握CubeMapNode的构造函数、属性与更新时机(updateBeforeType),理解其基于CubeRenderTarget的转换原理、WeakMap 缓存与 dispose 生命周期管理,并知道它在场景背景渲染与非 PBR 材质 IBL 照明中的两处内建调用位置。
一、CubeMapNode 是什么:定位与继承链
官方文档 docs/pages/CubeMapNode.html.md 对它的定义是:
This node can be used to automatically convert environment maps in the equirectangular format into the cube map format.
其继承链为EventDispatcher → Node → TempNode(见 src/nodes/utils/CubeMapNode.js)。选择TempNode而非Node作为基类并非偶然:TempNode提供了临时变量缓存机制——当同一个节点在一次构建中被多处引用时(hasDependencies返回usageCount > 1),构建器会将其求值结果先存入一个临时变量再复用,从而避免重复计算,相关逻辑见 src/nodes/core/TempNode.js。CubeMapNode本身携带一个内部CubeTexture状态、在构建期切换采样纹理,正符合"带状态的临时值节点"这一形态。
为什么需要这种转换
立方体贴图天然适合"按方向采样":TSL 中CubeTextureNode会根据贴图映射类型生成默认采样向量——反射映射(CubeReflectionMapping)使用reflectVector,折射映射(CubeRefractionMapping)使用refractVector,见 src/nodes/accessors/CubeTextureNode.js。而许多环境贴图(如全景照片、HDR 全景)天然是等距柱状投影格式。CubeMapNode填补的正是这个缺口:让节点材质/场景背景可以直接消费等距柱状贴图,而无需开发者手动预转立方体贴图。
二、API 完整参考
构造函数
new CubeMapNode( envNode : Node )- envNode(Node):表示环境贴图的节点。
从源码 src/nodes/utils/CubeMapNode.js 看,构造函数执行了以下初始化:
- 以
super( 'vec3' )声明输出类型为vec3; - 保存
this.envNode = envNode; - 创建内部状态:
this._cubeTexture = null:缓存转换后立方体贴图的引用;this._cubeTextureNode = cubeTexture( null ):一个值为空的CubeTextureNode占位,转换完成后其.value会被替换为真实贴图;this._defaultTexture:一个isRenderTargetTexture = true的默认CubeTexture,作为等距柱状贴图尚未加载完成时的兜底占位;
- 将
updateBeforeType设为NodeUpdateType.RENDER。
属性
.envNode : Node
表示环境贴图的节点,对应构造参数。
.updateBeforeType : string
覆盖自TempNode。由于CubeMapNode需要在其updateBefore方法中每渲染一次就检查/转换一次纹理,所以该属性被设为NodeUpdateType.RENDER,默认值'render'。
NodeUpdateType在 src/nodes/core/constants.js 中定义了四种更新时机:
| 值 | 字符串 | 含义 |
|---|---|---|
NONE | 'none' | 节点没有更新方法 |
FRAME | 'frame' | 每帧执行一次 |
RENDER | 'render' | 每次渲染前执行(CubeMapNode采用此模式) |
OBJECT | 'object' | 每个使用该节点渲染的Object3D各执行一次 |
更新与构建方法
文档中.updateBeforeType的说明指向了CubeMapNode#updateBefore,完整的两个关键方法如下(源码):
updateBefore( frame ) { const { renderer, material } = frame; const envNode = this.envNode; if ( envNode.isTextureNode || envNode.isMaterialReferenceNode ) { // 从 TextureNode 或材质引用节点取出真实贴图 const texture = ( envNode.isTextureNode ) ? envNode.value : material[ envNode.property ]; if ( texture && texture.isTexture ) { const mapping = texture.mapping; if ( mapping === EquirectangularReflectionMapping || mapping === EquirectangularRefractionMapping ) { // 等距柱状贴图:查缓存 → 未命中则用 CubeRenderTarget 转换并缓存 // 贴图尚未加载完成时回退到 _defaultTexture 占位 } else { // envNode 本身已是立方体贴图:直接透传 this._cubeTextureNode = this.envNode; } } } } setup( builder ) { this.updateBefore( builder ); return this._cubeTextureNode; }setup是节点参与着色器构建的入口:它先触发一次updateBefore(此时拿到当前帧的renderer与material),然后把内部_cubeTextureNode作为自身输出交给构建器。这也解释了updateBefore与setup双通道设计——前者由渲染循环按'render'时机周期性调用,后者保证构建阶段也能拿到最新转换结果。
源代码
实现位于 src/nodes/utils/CubeMapNode.js,并通过 src/nodes/Nodes.js 导出CubeMapNode类。
三、核心转换流程:updateBefore 逐段解析
updateBefore的完整决策路径(源码 L82-L151)可以分为四层:
1. 节点类型过滤
只处理envNode.isTextureNode(TextureNode)或envNode.isMaterialReferenceNode(材质属性引用节点)两种输入,并从对应位置取出真实Texture:前者直接取.value,后者按material[ envNode.property ]读取材质属性。这保证了cubeMapNode既能接texture( envMap ),也能接材质引用节点。
2. 映射类型判断
只有当贴图mapping为EquirectangularReflectionMapping或EquirectangularRefractionMapping时才执行转换;否则说明输入本身已经是立方体贴图,直接把envNode透传为输出(this._cubeTextureNode = this.envNode),零开销。
3. 等距柱状贴图转换(带缓存)
先查模块级
WeakMap缓存_cache:命中则复用已有立方体贴图,并调用mapTextureMapping校正映射标记;未命中时检查
isEquirectangularMapReady( image )(私有函数,判定条件仅为image !== null && image.height > 0,L173-L179)。就绪则:const renderTarget = new CubeRenderTarget( image.height ); renderTarget.fromEquirectangularTexture( renderer, texture ); mapTextureMapping( renderTarget.texture, texture.mapping ); this._cubeTexture = renderTarget.texture; _cache.set( texture, renderTarget.texture ); texture.addEventListener( 'dispose', onTextureDispose );目标立方体贴图的边长取原图等距柱状图的高度(等距柱状贴图宽高比一般为 2:1,取高度正好得到立方体单面尺寸);
未就绪(异步贴图尚未加载完)时回退到
_defaultTexture占位,下一帧渲染时updateBefore会再次检查,加载完成后自动切换为真实转换结果——这是该节点对异步加载友好的关键设计。
4. 映射标记校正
mapTextureMapping(L215-L227)保证生成的立方体贴图携带与来源一致的语义:
| 原映射 | 生成的立方体映射 |
|---|---|
EquirectangularReflectionMapping | CubeReflectionMapping |
EquirectangularRefractionMapping | CubeRefractionMapping |
这一点很关键,因为下游CubeTextureNode.getDefaultUV()正是依据CubeReflectionMapping/CubeRefractionMapping决定采样使用反射向量还是折射向量,映射标记错会导致采样方向错误。
四、底层转换实现:CubeRenderTarget.fromEquirectangularTexture
真正"画图"的工作由 src/renderers/common/CubeRenderTarget.js 完成。它是一个兼容WebGPURenderer的RenderTarget子类(构造时设置texture.isRenderTargetTexture = true,以对齐坐标系约定)。
fromEquirectangularTexture( renderer, texture )的转换手法(源码 L71 起):
保存并临时修改原贴图参数:
generateMipmaps = true,并继承type、colorSpace、minFilter、magFilter;构造一个
BoxGeometry( 5, 5, 5 )的立方体盒子,材质为NodeMaterial,其颜色节点为:material.colorNode = TSL_Texture( texture, equirectUV( positionWorldDirection ), 0 );即:以盒内表面每一点的世界空间方向计算等距柱状 UV(
equirectUV( positionWorldDirection )),采样原始等距柱状贴图,side = BackSide、blending = NoBlending;用
CubeCamera( 1, 10, renderTarget )从中心朝六个面各渲染一次,把球面全景"包裹"成六面立方贴图;细节优化:
- 若原贴图
minFilter === LinearMipmapLinearFilter,转换期间临时改为LinearFilter,注释说明是为了避免两极区域模糊("Avoid blurred poles"); - 转换前保存并临时清空渲染器 MRT 状态(
renderer.setMRT( null )),转换后恢复; - 结束后还原原贴图的
minFilter与generateMipmaps,并 dispose 临时盒子与材质。
- 若原贴图
由于转换走的是渲染器自身的 TSL 材质路径,因此该机制同时适用于 WebGL 与 WebGPU 渲染后端。
五、缓存策略与资源生命周期
CubeMapNode用模块级WeakMap(const _cache = new WeakMap(),L9)以原始等距柱状纹理为键缓存转换出的CubeRenderTarget.texture:
- 同一贴图被多个
CubeMapNode(多个材质/多个场景)使用时只转换一次; - 转换后向原贴图注册
dispose监听,触发onTextureDispose(L189-L205):先摘除监听,再从缓存中删除条目并调用renderTarget.dispose(),确保派生的立方体渲染目标随源贴图一起被释放,不留显存泄漏; - 使用
WeakMap意味着源纹理被 GC 回收后缓存条目自动失效,无需手动清理。
六、TSL 函数式用法与内建调用链
导出与 TSL 函数
文件末尾导出了 TSL 包装函数(L229-L237):
export const cubeMapNode = /*@__PURE__*/ nodeProxy( CubeMapNode ).setParameterLength( 1 );nodeProxy使cubeMapNode( envNode )可直接在 TSL 表达式中使用,参数个数固定为 1。对应的类型化类CubeMapNode则从 src/nodes/Nodes.js 导出,便于高级场景直接new。
典型用法
import * as THREE from 'three/webgpu'; import { texture, cubeMapNode } from 'three/tsl'; // 加载等距柱状环境贴图并声明映射类型 const envMap = new THREE.TextureLoader().load( 'equirectangular_env.jpg' ); envMap.mapping = THREE.EquirectangularReflectionMapping; // 包一层 TextureNode 再交给 cubeMapNode: // 渲染时若贴图未加载完成会得到占位贴图,加载完成后自动切换为转换结果 const cubeEnv = cubeMapNode( texture( envMap ) );注意两个前提:
- 传入的必须是
TextureNode(texture( ... ))或材质引用节点,其它节点类型会被updateBefore的类型检查过滤掉; - 贴图
mapping必须是两种等距柱状映射之一才会触发转换;传入已是CubeReflectionMapping的CubeTexture时节点直接透传。
仓库内建的两处调用
从源码结构看,cubeMapNode在仓库中被两个核心路径内建复用,说明它是节点渲染管线的标准件:
- 场景背景:src/renderers/common/nodes/NodeManager.js 中处理
scene.background时,若背景是等距柱状映射的贴图且backgroundBlurriness === 0,则构造cubeMapNode( envMap )作为背景节点;而backgroundBlurriness > 0或CubeUVReflectionMapping时改走pmremTexture路径。也就是说,在节点渲染器中把一张等距柱状贴图直接赋给scene.background,立方体转换由CubeMapNode在背后自动完成; - 非 PBR 材质的 IBL 照明:src/nodes/lighting/BasicEnvironmentNode.js 的
setup中执行builder.context.environment = cubeMapNode( this.envNode ),为MeshBasicNodeMaterial、MeshPhongNodeMaterial等非 PBR 节点材质提供基于图像的环境光照,环境贴图同样可以是等距柱状格式。
七、小结与适用前提
CubeMapNode的契约很简单:输入一个等距柱状环境贴图节点,输出一个可被方向向量采样的立方体贴图节点,转换时机绑定在'render'更新阶段(updateBeforeType = 'render');- 实现上依赖三块基础设施:
CubeRenderTarget.fromEquirectangularTexture的六面重投影、模块级WeakMap缓存、以及源贴图dispose事件驱动的资源回收; - 对异步加载的贴图,节点会先以
isRenderTargetTexture = true的默认占位贴图渲染,加载完成后自动切换,无需外部轮询或回调; - 适用前提:该节点属于 TSL/节点系统,经由节点材质或节点渲染器路径(
src/renderers/common/nodes/下的NodeManager等)生效;它不修改原始贴图对象,转换产物是独立的CubeRenderTarget。
如需进一步阅读,可参考 API 页面 docs/pages/CubeMapNode.html、基类 TempNode 的文档,以及实现文件 src/nodes/utils/CubeMapNode.js 与 src/renderers/common/CubeRenderTarget.js。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考