three.js TSL 深入解析:CubeTextureNode 立方纹理节点的原理与实现
本文基于 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 标志模式,同 isTextureNode、isUniformNode)。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
作为对照,父类 TextureNode 的 setUpdateMatrix(value) 会写入 this.updateMatrix(TextureNode.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;
}
分三段理解:
- 深度纹理(立方阴影贴图)分支:不做环境旋转,仅在 WebGPU 后端(
WebGPUCoordinateSystem,Y 轴向上)对方向向量的 y 分量取反。WebGL 后端(Y 轴向下)则原样返回。 - 环境旋转:普通立方纹理先乘以
materialEnvRotation。该 uniform 定义在 MaterialProperties.js#L35-L57,其语义是:当材质设置了material.envMap时取material.envMapRotation,当场景使用scene.environment/scene.environmentNode时取scene.environmentRotation;由于旋转矩阵是正交的,源码用transpose()代替invert()以提高效率。 - 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 在编译期的处理顺序为:
- 校验:
value必须是THREE.Texture实例,否则抛出NodeError(TextureNode.js#L360-L364); - 解析 UV:
uvNode(或上下文的getUV回调,或getDefaultUV()按 mapping 推导)→(立方纹理此处跳过getTransformedUV(),因为updateMatrix无法被置位)→setupUV()做旋转与翻转; - 解析 level/bias:
levelNode、biasNode,或上下文的getTextureLevel回调; - 生成片段:
generateUV()输出vec3方向,generateSnippet()交给builder.generateTexture/generateTextureLevel/generateTextureBias/generateTextureCompare/...输出最终的 GLSL/WGSL 采样语句(TextureNode.js#L493-L539); - 着色空间转换:若需要,采样结果经
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.js 用
cubeTexture( 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.js 用
material.fragmentNode = cubeTexture( envTexture, _outputDirection )从源立方图向 PMREM 各 mip 层级重采样,_outputDirection正是利用反射/折射几何推导出的采样方向。 - 场景环境与背景自动接入:NodeManager.js 中当场景设置
scene.background为立方纹理时执行envMap = cubeTexture( background ),#L883 处scene.environment则以默认 UV(即第 4.2 节的 mapping 推导)供 PBR 材质采样。这说明getDefaultUV()的 mapping 分支直接决定了material.envMap/scene.environment在 TSL 管线中的默认行为。 - 立方图工具节点:CubeMapNode.js 以
cubeTexture( null )作为占位,随后再替换 value,展示了节点值可后置绑定的用法。
8. 关键结论小结
| 特性 | CubeTextureNode 的行为 | 依据 |
|---|---|---|
| 坐标类型 | vec3 世界空间方向(vec2 是 2D 纹理的行为) |
generateUV(),CubeTextureNode.js#L150-L154 |
| 默认方向 | 按 mapping 取 reflectVector/refractVector,否则报错 |
getDefaultUV() |
| UV 变换矩阵 | 完全忽略(setUpdateMatrix 空实现) |
与父类 getTransformedUV() 路径形成对比 |
| 环境旋转 | 普通纹理乘以 materialEnvRotation;深度纹理不旋转 |
setupUV() |
| 坐标修正 | WebGPU 下深度纹理 y 取反;WebGPU 或非 RenderTarget 纹理 x 取反 | setupUV() |
| uniform 输入类型 | cubeTexture / cubeDepthTexture(后者启用比较采样器) |
getInputType() |
参考资料(仓库内路径):
- API 文档源:docs/pages/CubeTextureNode.html.md
- 核心实现:src/nodes/accessors/CubeTextureNode.js
- 父类采样框架:src/nodes/accessors/TextureNode.js
- 内置反射/折射方向:src/nodes/accessors/ReflectVector.js
- 环境旋转 uniform:src/nodes/accessors/MaterialProperties.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 StartedRust0625
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