news 2026/9/7 7:08:25

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

作者头像

张小明

前端开发工程师

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

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 看,构造函数执行了以下初始化:

  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'

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(此时拿到当前帧的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 > 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)保证生成的立方体贴图携带与来源一致的语义:

原映射生成的立方体映射
EquirectangularReflectionMappingCubeReflectionMapping
EquirectangularRefractionMappingCubeRefractionMapping

这一点很关键,因为下游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监听,触发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 ) );

注意两个前提:

  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.js 的setup中执行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.js 与 src/renderers/common/CubeRenderTarget.js。

【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 7:07:44

Vibe Coding实战指南:零基础用AI编程工具从需求到完整项目

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 7:06:56

ComfyUI漫剧工作流从零搭建:角色统一与批量出片全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 7:06:19

Modem待机功耗高?从电流分流到NV调参的完整排查思路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 7:06:06

Mini-LED显示器怎么选?从分区数到光晕控制的硬核实用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华