首页
/ three.js CubeMapNode 深度解析:TSL 中自动完成等距柱状投影到立方体贴图的环境贴图转换

three.js CubeMapNode 深度解析:TSL 中自动完成等距柱状投影到立方体贴图的环境贴图转换

2026-09-06 16:22:49作者:姚月梅Lane

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.jsCubeMapNode 本身携带一个内部 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 看,构造函数执行了以下初始化:

  1. super( 'vec3' ) 声明输出类型为 vec3
  2. 保存 this.envNode = envNode
  3. 创建内部状态:
    • this._cubeTexture = null:缓存转换后立方体贴图的引用;
    • this._cubeTextureNode = cubeTexture( null ):一个值为空的 CubeTextureNode 占位,转换完成后其 .value 会被替换为真实贴图;
    • this._defaultTexture:一个 isRenderTargetTexture = true 的默认 CubeTexture,作为等距柱状贴图尚未加载完成时的兜底占位;
  4. updateBeforeType 设为 NodeUpdateType.RENDER

属性

.envNode : Node

表示环境贴图的节点,对应构造参数。

.updateBeforeType : string

覆盖自 TempNode。由于 CubeMapNode 需要在其 updateBefore 方法中每渲染一次就检查/转换一次纹理,所以该属性被设为 NodeUpdateType.RENDER,默认值 'render'

NodeUpdateTypesrc/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(此时拿到当前帧的 renderermaterial),然后把内部 _cubeTextureNode 作为自身输出交给构建器。这也解释了 updateBeforesetup 双通道设计——前者由渲染循环按 'render' 时机周期性调用,后者保证构建阶段也能拿到最新转换结果。

源代码

实现位于 src/nodes/utils/CubeMapNode.js,并通过 src/nodes/Nodes.js 导出 CubeMapNode 类。

三、核心转换流程:updateBefore 逐段解析

updateBefore 的完整决策路径(源码 L82-L151)可以分为四层:

1. 节点类型过滤

只处理 envNode.isTextureNodeTextureNode)或 envNode.isMaterialReferenceNode(材质属性引用节点)两种输入,并从对应位置取出真实 Texture:前者直接取 .value,后者按 material[ envNode.property ] 读取材质属性。这保证了 cubeMapNode 既能接 texture( envMap ),也能接材质引用节点。

2. 映射类型判断

只有当贴图 mappingEquirectangularReflectionMappingEquirectangularRefractionMapping 时才执行转换;否则说明输入本身已经是立方体贴图,直接把 envNode 透传为输出(this._cubeTextureNode = this.envNode),零开销。

3. 等距柱状贴图转换(带缓存)

  • 先查模块级 WeakMap 缓存 _cache:命中则复用已有立方体贴图,并调用 mapTextureMapping 校正映射标记;

  • 未命中时检查 isEquirectangularMapReady( image )(私有函数,判定条件仅为 image !== null && image.height > 0L173-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. 映射标记校正

mapTextureMappingL215-L227)保证生成的立方体贴图携带与来源一致的语义:

原映射 生成的立方体映射
EquirectangularReflectionMapping CubeReflectionMapping
EquirectangularRefractionMapping CubeRefractionMapping

这一点很关键,因为下游 CubeTextureNode.getDefaultUV() 正是依据 CubeReflectionMapping / CubeRefractionMapping 决定采样使用反射向量还是折射向量,映射标记错会导致采样方向错误。

四、底层转换实现:CubeRenderTarget.fromEquirectangularTexture

真正"画图"的工作由 src/renderers/common/CubeRenderTarget.js 完成。它是一个兼容 WebGPURendererRenderTarget 子类(构造时设置 texture.isRenderTargetTexture = true,以对齐坐标系约定)。

fromEquirectangularTexture( renderer, texture ) 的转换手法(源码 L71 起):

  1. 保存并临时修改原贴图参数:generateMipmaps = true,并继承 typecolorSpaceminFiltermagFilter

  2. 构造一个 BoxGeometry( 5, 5, 5 ) 的立方体盒子,材质为 NodeMaterial,其颜色节点为:

    material.colorNode = TSL_Texture( texture, equirectUV( positionWorldDirection ), 0 );
    

    即:以盒内表面每一点的世界空间方向计算等距柱状 UV(equirectUV( positionWorldDirection )),采样原始等距柱状贴图,side = BackSideblending = NoBlending

  3. CubeCamera( 1, 10, renderTarget ) 从中心朝六个面各渲染一次,把球面全景"包裹"成六面立方贴图;

  4. 细节优化:

    • 若原贴图 minFilter === LinearMipmapLinearFilter,转换期间临时改为 LinearFilter,注释说明是为了避免两极区域模糊("Avoid blurred poles");
    • 转换前保存并临时清空渲染器 MRT 状态(renderer.setMRT( null )),转换后恢复;
    • 结束后还原原贴图的 minFiltergenerateMipmaps,并 dispose 临时盒子与材质。

由于转换走的是渲染器自身的 TSL 材质路径,因此该机制同时适用于 WebGL 与 WebGPU 渲染后端。

五、缓存策略与资源生命周期

CubeMapNode 用模块级 WeakMapconst _cache = new WeakMap()L9)以原始等距柱状纹理为键缓存转换出的 CubeRenderTarget.texture

  • 同一贴图被多个 CubeMapNode(多个材质/多个场景)使用时只转换一次;
  • 转换后向原贴图注册 dispose 监听,触发 onTextureDisposeL189-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 ) );

注意两个前提:

  1. 传入的必须是 TextureNodetexture( ... ))或材质引用节点,其它节点类型会被 updateBefore 的类型检查过滤掉;
  2. 贴图 mapping 必须是两种等距柱状映射之一才会触发转换;传入已是 CubeReflectionMappingCubeTexture 时节点直接透传。

仓库内建的两处调用

从源码结构看,cubeMapNode 在仓库中被两个核心路径内建复用,说明它是节点渲染管线的标准件:

  1. 场景背景src/renderers/common/nodes/NodeManager.js 中处理 scene.background 时,若背景是等距柱状映射的贴图且 backgroundBlurriness === 0,则构造 cubeMapNode( envMap ) 作为背景节点;而 backgroundBlurriness > 0CubeUVReflectionMapping 时改走 pmremTexture 路径。也就是说,在节点渲染器中把一张等距柱状贴图直接赋给 scene.background,立方体转换由 CubeMapNode 在背后自动完成;
  2. 非 PBR 材质的 IBL 照明src/nodes/lighting/BasicEnvironmentNode.jssetup 中执行 builder.context.environment = cubeMapNode( this.envNode ),为 MeshBasicNodeMaterialMeshPhongNodeMaterial 等非 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.jssrc/renderers/common/CubeRenderTarget.js

登录后查看全文
热门项目推荐
相关项目推荐