首页
/ three.js TSL 深入解析:CubeTextureNode 立方纹理节点的原理与实现

three.js TSL 深入解析:CubeTextureNode 立方纹理节点的原理与实现

2026-09-06 16:59:55作者:江焘钦

本文基于 three.js 官方 API 文档中 CubeTextureNode 的页面,结合仓库源码,系统讲解 TSL(three.js Shading Language)中立方纹理采样节点 CubeTextureNode 的类继承关系、构造参数、五个核心方法的源码实现,以及它如何通过 cubeTexture() 函数被阴影贴图、PMREM 环境贴图管线和场景环境采样等模块复用。读完后你将能够理解 TSL 中立方纹理采样的完整代码生成链路,并能在自定义 Node 材质中正确使用立方纹理采样。

1. 类定位与继承体系

根据 API 文档,CubeTextureNode 的继承链为:

EventDispatcher → Node → InputNode → UniformNode → TextureNode → CubeTextureNode

也就是说,它是一个均匀(uniform)节点,在着色器中代表一张立方纹理(CubeTexture),其父类 TextureNode 负责通用的纹理采样框架(mip level、bias、depth、compare、grad、gather 等采样变体),而 CubeTextureNode 只覆写与"立方纹理"这一特定采样几何相关的部分。源码中类定义位于 CubeTextureNode.js,静态类型标识为:

static get type() {
    return 'CubeTextureNode';
}

与 2D 的 TextureNode 相比,最本质的区别在于:立方纹理的"坐标"不是二维 UV,而是一个三维方向向量(vec3),因此其默认 UV 计算、输入类型声明和坐标修正逻辑都被重写。

2. 构造函数与参数

API 文档给出的构造签名为:

new CubeTextureNode( value : CubeTexture, uvNode : Node.<vec3>, levelNode : Node.<int>, biasNode : Node.<float> )

源码实现(CubeTextureNode.js#L34-L47):

constructor( value, uvNode = null, levelNode = null, biasNode = null ) {

    super( value, uvNode, levelNode, biasNode );

    /**
     * This flag can be used for type testing.
     *
     * @type {boolean}
     * @readonly
     * @default true
     */
    this.isCubeTextureNode = true;

}

各参数含义与文档一一对应:

参数 类型 默认值 说明
value CubeTexture 必填 要采样的立方纹理
uvNode Node<vec3> null 三维采样方向;为 null 时由 getDefaultUV() 根据纹理的 mapping 类型自动推导
levelNode Node<int> null 显式指定 mip 层级
biasNode Node<float> null LOD 计算的偏置(数值越大采样越模糊)

注意:与 2D 场景不同,这里的 uvNode 类型是 vec3 方向向量,而不是 vec2 平面坐标。

3. 类型标志 isCubeTextureNode

this.isCubeTextureNode = true;

只读布尔标志,默认为 true,用于运行时类型判断(three.js 惯用的 duck typing 标志模式,同 isTextureNodeisUniformNode)。TSL 的 cubeTexture() 函数就依赖该标志来区分"传入的是一个纹理"还是"传入的是已存在的立方纹理节点"(见第 6 节)。

4. 核心方法逐一解析

4.1 getInputType():声明立方纹理输入类型

API 文档说明该方法"覆写默认实现,返回合适的立方纹理类型"。源码(CubeTextureNode.js#L55-L65):

getInputType( /*builder*/ ) {

    if ( this.value.isDepthTexture === true ) {

        return 'cubeDepthTexture';

    }

    return 'cubeTexture';

}

父类 TextureNode.getInputType() 固定返回 'texture'(见 TextureNode.js#L257-L261),而立方纹理节点根据底层纹理是否为深度纹理,返回 'cubeTexture''cubeDepthTexture'。这两个输入类型是 NodeBuilder 类型系统的一部分:从源码结构看,NodeBuilder.js'cubeTexture''texture' 并列为合法的"纹理族"输入类型,且在 #L1667 处对 'cubeTexture' 类型的采样结果统一按 vec4 处理。返回深度类型的关键意义在于:渲染器据此为该 uniform 绑定比较采样器(comparison sampler),这是点光源立方阴影贴图能执行硬件深度比较的前提。

4.2 getDefaultUV():按 mapping 类型推导默认采样方向

API 文档说明:返回基于立方纹理 mapping 类型的默认 UV。源码(CubeTextureNode.js#L72-L92):

getDefaultUV() {

    const texture = this.value;

    if ( texture.mapping === CubeReflectionMapping ) {

        return reflectVector;

    } else if ( texture.mapping === CubeRefractionMapping ) {

        return refractVector;

    } else {

        error( 'CubeTextureNode: Mapping "%s" not supported.', texture.mapping );

        return vec3( 0, 0, 0 );

    }

}

要点:

  • CubeReflectionMapping → 使用 reflectVector:视角向量(view 空间)经法线反射后变换到世界空间的方向;
  • CubeRefractionMapping → 使用 refractVector:视角向量按 materialRefractionRatio 折射后的世界空间方向;
  • 其他 mapping 会触发 error() 并退化为 vec3(0,0,0)(即全黑/原点方向采样)。

这两个内置方向向量的定义在 ReflectVector.js

export const reflectView = positionViewDirection.negate().reflect( normalView );
export const refractView = positionViewDirection.negate().refract( normalView, materialRefractionRatio );

export const reflectVector = reflectView.transformDirection( cameraWorldMatrix ).toVar( 'reflectVector' );
export const refractVector = refractView.transformDirection( cameraWorldMatrix ).toVar( 'refractVector' );

也就是说,"默认 UV"本身就是一段可组合的 TSL 表达式:视角方向取反(得到视线方向)→ 在 view 空间做反射/折射 → 用 cameraWorldMatrix 变换到世界空间。toVar() 将其缓存为命名变量,避免重复计算。这也解释了 API 文档中 uvNode 默认值为 null 时的行为——TextureNode.setup()TextureNode.js#L368-L378)在 uvNode 为空时会调用 this.getDefaultUV() 兜底。

4.3 setUpdateMatrix():立方纹理忽略 UV 变换矩阵

API 文档说明:以空实现覆写,因为立方纹理会忽略 updateMatrix 标志,UV 变换矩阵不会应用到立方纹理上。源码只有一行(CubeTextureNode.js#L100):

setUpdateMatrix( /*updateMatrix*/ ) { } // Ignore .updateMatrix for CubeTextureNode

作为对照,父类 TextureNodesetUpdateMatrix(value) 会写入 this.updateMatrixTextureNode.js#L306-L312),进而在 setup() 中触发 getTransformedUV()——即用纹理自身的 texture.matrix(含 offset/repeat/rotation/center)变换 2D UV。立方纹理的采样方向是几何/光学意义上的世界空间向量,不应被纹理的 2D 平铺变换影响,因此这里直接丢弃该标志。这是一个值得记住的语义差异:CubeTexture.matrix 的 2D 变换语义对 TSL 采样无效。

4.4 setupUV():旋转、翻转与坐标系适配

这是本类中最复杂、也最体现工程细节的方法。API 文档概括为:"根据后端以及纹理类型,可能需要修改 uv 节点以获得正确采样。"源码(CubeTextureNode.js#L110-L141):

setupUV( builder, uvNode ) {

    const texture = this.value;

    // Depth textures (shadow maps) - no environment rotation, Y flip for WebGPU
    if ( texture.isDepthTexture === true ) {

        if ( builder.renderer.coordinateSystem === WebGPUCoordinateSystem ) {

            return vec3( uvNode.x, uvNode.y.negate(), uvNode.z );

        }

        return uvNode;

    }

    // rotate first

    uvNode = materialEnvRotation.mul( uvNode );

    // flip

    if ( builder.renderer.coordinateSystem === WebGPUCoordinateSystem || ! texture.isRenderTargetTexture ) {

        uvNode = vec3( uvNode.x.negate(), uvNode.yz );

    }

    return uvNode;

}

分三段理解:

  1. 深度纹理(立方阴影贴图)分支:不做环境旋转,仅在 WebGPU 后端(WebGPUCoordinateSystem,Y 轴向上)对方向向量的 y 分量取反。WebGL 后端(Y 轴向下)则原样返回。
  2. 环境旋转:普通立方纹理先乘以 materialEnvRotation。该 uniform 定义在 MaterialProperties.js#L35-L57,其语义是:当材质设置了 material.envMap 时取 material.envMapRotation,当场景使用 scene.environment/scene.environmentNode 时取 scene.environmentRotation;由于旋转矩阵是正交的,源码用 transpose() 代替 invert() 以提高效率。
  3. x 分量翻转:WebGPU 后端,或纹理不是渲染目标纹理(即普通立方纹理,非 CubeRenderTarget 输出)时,对 x 取反。从源码结构看,这是在补偿"世界空间方向"与"立方体贴图布局约定"(以及 WebGL/WebGPU 坐标系差异)之间的符号约定,保证 CubeTexture.mapping 为反射/折射映射时采样结果与经典着色器行为一致。

4.5 generateUV():生成 vec3 坐标代码片段

API 文档说明:生成 UV 代码片段。源码(CubeTextureNode.js#L150-L154):

generateUV( builder, cubeUV ) {

    return cubeUV.build( builder, this.sampler === true ? 'vec3' : 'ivec3' );

}

父类版本(TextureNode.js#L458-L462)生成的是 vec2/ivec2;立方纹理节点将输出类型改为 vec3(插值采样 sampler=true)或 ivec3(显式取像素 sampler=false,即 load() 路径)。cubeUV 参数正是 setup() 阶段解析好的、已经过 getDefaultUV() + setupUV() 加工的最终方向节点。

5. 完整采样链路

把父类 TextureNode.setup()/generate() 与子类覆写串起来,一个 CubeTextureNode 在编译期的处理顺序为:

  1. 校验value 必须是 THREE.Texture 实例,否则抛出 NodeErrorTextureNode.js#L360-L364);
  2. 解析 UVuvNode(或上下文的 getUV 回调,或 getDefaultUV() 按 mapping 推导)→(立方纹理此处跳过 getTransformedUV(),因为 updateMatrix 无法被置位)→ setupUV() 做旋转与翻转;
  3. 解析 level/biaslevelNodebiasNode,或上下文的 getTextureLevel 回调;
  4. 生成片段generateUV() 输出 vec3 方向,generateSnippet() 交给 builder.generateTexture/generateTextureLevel/generateTextureBias/generateTextureCompare/... 输出最终的 GLSL/WGSL 采样语句(TextureNode.js#L493-L539);
  5. 着色空间转换:若需要,采样结果经 colorSpaceToWorking 转换到工作色空间(TextureNode.js#L651-L655)。

其中深度纹理的 compare 回退逻辑(在不支持 Compatibility.TEXTURE_COMPARE 的后端上用 step() 模拟比较)同样继承自父类,这意味着立方阴影贴图在不同 WebGL/WebGPU 后端上都能得到一致结果。

6. TSL 函数式 API:cubeTexture() 与 uniformCubeTexture()

API 文档描述的是类本身,而日常编写 TSL 代码时更多使用同文件导出的函数式接口(CubeTextureNode.js#L160-L215):

// 内部代理:等价于 new CubeTextureNode(...),接受 1~4 个参数
export const cubeTextureBase = nodeProxy( CubeTextureNode ).setParameterLength( 1, 4 ).setName( 'cubeTexture' );

// 推荐入口
export const cubeTexture = ( value = EmptyTexture, uvNode = null, levelNode = null, biasNode = null ) => { ... };

// 无 UV/level/bias 参数的便捷入口
export const uniformCubeTexture = ( value = EmptyTexture ) => cubeTextureBase( value );

cubeTexture() 的关键分支:

if ( value && value.isCubeTextureNode === true ) {

    textureNode = nodeObject( value.clone() );
    textureNode.referenceNode = value; // 确保引用指向原始节点

    if ( uvNode !== null ) textureNode.uvNode = nodeObject( uvNode );
    if ( levelNode !== null ) textureNode.levelNode = nodeObject( levelNode );
    if ( biasNode !== null ) textureNode.biasNode = nodeObject( biasNode );

} else {

    textureNode = cubeTextureBase( value, uvNode, levelNode, biasNode );

}
  • 传入普通 CubeTexture 时,走 cubeTextureBase 创建新的 uniform 节点(缺省值为模块级单例 EmptyTexture);
  • 传入已存在的 CubeTextureNode 时,克隆该节点并设置 referenceNode 指回原节点,再按需提供性地覆写 uv/level/bias。这与父类 sample(uvNode) 的行为一致(TextureNode.js#L696-L704),目的是让多个采样共享同一纹理 uniform(uniform 以纹理 uuid 作为 hash 去重,见 TextureNode.js#L227-L231),同时各自的 UV 节点相互独立。

一个最小可运行的采样示例(概念上等价于仓库中 NodeManager 对场景背景的处理):

import { cubeTexture, reflectVector } from 'three/tsl';

// 最简形式:mapping 决定默认方向(反射/折射)
const envSample = cubeTexture( cubeMap );

// 显式方向:例如按表面法线采样
const customDir = normalWorld;
const envSample2 = cubeTexture( cubeMap, customDir );

// 指定 mip 层级与偏置
const envSample3 = cubeTexture( cubeMap, reflectVector, 2, 0.5 );

7. 仓库内的真实应用场景

在 three.js 仓库中,cubeTexture() 是多个核心渲染路径的底层原语:

  • 点光源阴影(PCSS 风格软阴影)PointShadowNode.jscubeTexture( depthTexture, bd3D ).compare( dp ) 做单次深度比较,其 PCSS 变体则对 5 个偏移方向各采样一次后累加(#L88-L92)。这里 getInputType() 返回 'cubeDepthTexture' 的深度比较语义直接支撑了 compare() 调用。
  • 通用阴影节点ShadowNode.js 中点光源分支 shadowColor = cubeTexture( shadowMap.texture, shadowCoord.xyz );等距柱状(equirectangular)阴影图则用 equirectDirection() 作为方向(#L541-L560)。
  • PMREM 预滤波环境贴图生成PMREMGenerator.jsmaterial.fragmentNode = cubeTexture( envTexture, _outputDirection ) 从源立方图向 PMREM 各 mip 层级重采样,_outputDirection 正是利用反射/折射几何推导出的采样方向。
  • 场景环境与背景自动接入NodeManager.js 中当场景设置 scene.background 为立方纹理时执行 envMap = cubeTexture( background )#L883scene.environment 则以默认 UV(即第 4.2 节的 mapping 推导)供 PBR 材质采样。这说明 getDefaultUV() 的 mapping 分支直接决定了 material.envMap / scene.environment 在 TSL 管线中的默认行为。
  • 立方图工具节点CubeMapNode.jscubeTexture( null ) 作为占位,随后再替换 value,展示了节点值可后置绑定的用法。

8. 关键结论小结

特性 CubeTextureNode 的行为 依据
坐标类型 vec3 世界空间方向(vec2 是 2D 纹理的行为) generateUV()CubeTextureNode.js#L150-L154
默认方向 mappingreflectVector/refractVector,否则报错 getDefaultUV()
UV 变换矩阵 完全忽略(setUpdateMatrix 空实现) 与父类 getTransformedUV() 路径形成对比
环境旋转 普通纹理乘以 materialEnvRotation;深度纹理不旋转 setupUV()
坐标修正 WebGPU 下深度纹理 y 取反;WebGPU 或非 RenderTarget 纹理 x 取反 setupUV()
uniform 输入类型 cubeTexture / cubeDepthTexture(后者启用比较采样器) getInputType()

参考资料(仓库内路径)

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