three.js CubeMapNode 深度解析:TSL 中自动完成等距柱状投影到立方体贴图的环境贴图转换
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。
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