three.js TSL 光照上下文节点 LightingContextNode 深度解析:构造、上下文对象与在 NodeMaterial 光照管线中的真实作用
LightingContextNode 是 three.js 节点材质(Node Material)/ TSL 体系中专门承载光照上下文的节点类型,它继承自 ContextNode,为 LightsNode(场景光照节点)提供运行时上下文数据,也是 NodeMaterial 组装"出射光"(outgoing light)时内部实际使用的核心组件。读完本文,你将掌握 LightingContextNode 的继承关系、构造参数与默认值、.getContext() 返回的完整上下文对象结构,以及它如何在 NodeBuilder 中推动 LightingModel 完成直接光/间接光的求值。
继承与定位:它不是"一盏灯",而是灯光求值的"上下文容器"
从文档给出的继承链可以清晰看出其类层次:
EventDispatcher → Node → ContextNode → LightingContextNode
LightingContextNode 是对 ContextNode 模块的扩展,区别在于它额外注入了光照专用的上下文数据。按照源码注释的原话,它表示 LightsNode 的运行时上下文("It represents the runtime context of LightsNode")。
要理解它的价值,先要理解父类 ContextNode 的机制。在 ContextNode 中,setup() / analyze() / generate() 都会在构建内部节点前后执行同一套逻辑:
const previousContext = builder.addContext( this.value );
// ... 在临时上下文中构建子节点
builder.setContext( previousContext );
也就是说,ContextNode 能在 NodeBuilder 构建期间临时向 builder 注入上下文数据,构建结束后再恢复原状。这正是材质/渲染管线在不同阶段切换求值上下文(如覆写 getUV()、注入 AO 与阴影回调,参见 builtinAOContext、builtinShadowContext 等 TSL 工具函数)的基础设施。LightingContextNode 则是这一机制在光照领域的专用实现:它把 lightsNode 包进一个携带光照数据的上下文中执行构建,让光照模型求值所需的全部中间量都"就位"。
核心源码位于 src/nodes/lighting/LightingContextNode.js,全文仅百余行,却承担了材质光照链路中承上启下的职责。
构造函数与全部参数
文档给出的构造函数签名为:
new LightingContextNode( lightsNode, lightingModel, materialLightings, backdropNode, backdropAlphaNode )
对照源码实现(LightingContextNode.js#L28),各参数的类型、语义与默认值如下表:
| 参数 | 类型 | 默认值 | 语义 |
|---|---|---|---|
lightsNode |
LightsNode |
无(必填) | 要为其建立上下文的灯光节点,即场景内全部光源在节点系统中的聚合表示 |
lightingModel |
?LightingModel |
null |
当前使用的光照模型;为 null 时在 setup() 阶段回退到 builder.context.lightingModel |
materialLightings |
?Array.<LightingNode> |
[](文档表格遗漏时源码以 [] 为准) |
材质级光照节点列表,如光照贴图、环境贴图、AO 等非"逐光源"的照明项 |
backdropNode |
?Node.<vec3> |
null |
背景/背板节点,用于实现类似滤镜的效果(把对象背后的画面参与光照合成) |
backdropAlphaNode |
?Node.<float> |
null |
背景节点的混合透明度,控制 backdropNode 对出射光的影响程度 |
注意:默认值与文档表格的差异
官方 API 文档的属性表格中未列出 materialLightings 的默认值,但源码中明确给出:
constructor( lightsNode, lightingModel = null, materialLightings = [], backdropNode = null, backdropAlphaNode = null )
因此该参数默认是空数组 [],其余参数的默认 null 与文档一致。构造时这些值会被逐一存到实例属性上,并初始化一个缓存用的私有字段 this._value = null。
公开属性一览
LightingContextNode 的公开属性即构造函数参数的镜像,全部为可读写:
.backdropAlphaNode : Node.<float>— 背景混合透明度节点,默认null;.backdropNode : Node.<vec3>— 背景节点,默认null;.lightingModel : LightingModel— 当前光照模型实例,默认null;.materialLightings : Array.<LightingNode>— 材质级照明节点数组,默认[]。
在类层面,它还通过静态 getter 暴露节点类型标识 type,返回字符串 'LightingContextNode',供节点系统做类型判断与序列化识别。
.getContext():一次生成整个光照求值上下文
getContext() 是该类最核心的方法,它负责构建并返回一个完整的光照上下文对象。源码实现(LightingContextNode.js#L79-L108)清晰展示了上下文里包含哪些"中间变量":
getContext() {
const { materialLightings, backdropNode, backdropAlphaNode } = this;
const directDiffuse = vec3().toVar( 'directDiffuse' ),
directSpecular = vec3().toVar( 'directSpecular' ),
indirectDiffuse = vec3().toVar( 'indirectDiffuse' ),
indirectSpecular = vec3().toVar( 'indirectSpecular' );
const reflectedLight = {
directDiffuse, directSpecular, indirectDiffuse, indirectSpecular
};
const context = {
radiance: vec3().toVar( 'radiance' ),
irradiance: vec3().toVar( 'irradiance' ),
iblIrradiance: vec3().toVar( 'iblIrradiance' ),
ambientOcclusion: float( 1 ).toVar( 'ambientOcclusion' ),
reflectedLight,
materialLightings,
backdrop: backdropNode,
backdropAlpha: backdropAlphaNode
};
return context;
}
返回对象的字段类型、初始值与作用归纳如下:
| 字段 | 类型 | 初始值 | 求值期作用 |
|---|---|---|---|
radiance |
Node.<vec3> 变量 |
vec3() |
直接/间接辐射度中间量 |
irradiance |
Node.<vec3> 变量 |
vec3() |
辐照度中间量 |
iblIrradiance |
Node.<vec3> 变量 |
vec3() |
基于图像的光照(IBL)辐照度 |
ambientOcclusion |
Node.<float> 变量 |
float(1) |
环境光遮蔽,默认完全不遮蔽 |
reflectedLight |
对象 | 内部含四个 vec3 变量 |
反射光四元组,见下 |
materialLightings |
Array.<LightingNode> |
构造传入 | 材质级照明项,由 LightsNode.setupLightsNode() 读取合并 |
backdrop |
Node.<vec3> |
构造传入(默认 null) |
背景/背板节点 |
backdropAlpha |
Node.<float> |
构造传入(默认 null) |
背板混合透明度 |
其中 reflectedLight 嵌套结构如下(这是后续 LightingModel 求值的"账本"):
{
directDiffuse: Node.<vec3>, // 直接漫反射
directSpecular: Node.<vec3>, // 直接镜面反射
indirectDiffuse: Node.<vec3>, // 间接漫反射
indirectSpecular: Node.<vec3> // 间接镜面反射
}
值得注意的工程细节:这些中间量并非普通 vec3(),而是通过 .toVar() 声明为着色器变量,保证它们在整个光照求值链中共享同一份存储、可被反复赋值读取,从而让"分项累加"式光照模型可以在不同位置增量写入。
setup() 中的上下文装配与 lightingModel 回退
除 getContext() 外,setup() 完成了最后一步装配(LightingContextNode.js#L110-L117):
setup( builder ) {
this.value = this._value || ( this._value = this.getContext() );
this.value.lightingModel = this.lightingModel || builder.context.lightingModel;
return super.setup( builder );
}
其行为可以拆解为三点:
- 惰性缓存上下文对象:
this._value为空时才调用getContext()生成,避免重复分配;ContextNode的value属性被用来承载这个上下文对象。 - 解析 lightingModel 的优先级:优先使用构造传入的
lightingModel;若为null,则回退到当前builder.context.lightingModel(即外层环境已声明的光照模型)。这意味着LightingContextNode可以"借用"渲染管线上下文里已经装好的光照模型,具备很强的可组合性。 - 调用父类
setup():由 ContextNode 完成真正的上下文推入/子节点构建/上下文恢复动作。
谁在使用它:NodeMaterial 光照组装的真实调用链
LightingContextNode 并非仅供进阶开发者手动使用的孤立节点,而是 three.js 节点材质内建光照管线的一部分。其 TSL 便捷函数 lightingContext 由 nodeProxy( LightingContextNode ) 生成(同文件末行),并同时从 Three.TSL.js 对外再导出,因此可以通过 TSL.lightingContext(...) 或具名导入直接使用。
在 NodeMaterial.setupLighting() 中,材质出射光的组装逻辑清晰可见:
const lightsNode = lights ? ( this.lightsNode || builder.lightsNode ) : null;
// 只有存在光源/材质光项时才进入光照上下文分支
if ( lightsNode && ( materialLightings.length > 0 || lightsNode.getScope().hasLights ) ) {
const lightingModel = this.setupLightingModel( builder ) || null;
outgoingLightNode = lightingContext( lightsNode, lightingModel, materialLightings, backdropNode, backdropAlphaNode );
} else if ( backdropNode !== null ) {
// 无光源时的纯背板合成分支
outgoingLightNode = vec3( backdropAlphaNode !== null ? mix( outgoingLightNode, backdropNode, backdropAlphaNode ) : backdropNode );
}
也就是说:每种内置 NodeMaterial 只需实现自己的 setupLightingModel(),其余交给 LightingContextNode 统一驱动。例如:
- MeshBasicNodeMaterial 返回
BasicLightingModel; - MeshLambertNodeMaterial 返回关闭高光的
PhongLightingModel( false )(强制 Lambert); - MeshStandardNodeMaterial 返回
PhysicalLightingModel; - MeshPhysicalNodeMaterial 依据是否开启 clearcoat、sheen、iridescence、anisotropy、transmission、dispersion、retroreflection 等特性构造参数化的
PhysicalLightingModel。
这些 LightingModel 的基类定义在 src/nodes/core/LightingModel.js,其生命周期接口 start() / direct() / directRectArea() / indirect() / finish() / ambientOcclusion() 恰与 LightingContextNode 提供的上下文中间量一一对应。
下行消费:LightsNode 如何"读取"该上下文
光照模型的求值发生在 LightsNode.setup() 内,它直接解构 builder.context,印证了 getContext() 中各字段的实际消费方式:
const { backdrop, backdropAlpha } = context;
const { directDiffuse, directSpecular, indirectDiffuse, indirectSpecular } = context.reflectedLight;
let totalDiffuse = directDiffuse.add( indirectDiffuse );
// backdrop 合成:有 alpha 则按 alpha 混合,否则整体替换漫反射总量
if ( backdrop !== null ) {
totalDiffuse = backdropAlpha !== null
? vec3( backdropAlpha.mix( totalDiffuse, backdrop ) )
: vec3( backdrop );
}
totalDiffuseNode.assign( totalDiffuse );
totalSpecularNode.assign( directSpecular.add( indirectSpecular ) );
outgoingLightNode.assign( totalDiffuseNode.add( totalSpecularNode ) );
同时,直接光/区域光的落账也是通过上下文完成的(LightsNode.js#L303-L332):
setUpDirectLight( builder, lightNode, lightData ) {
const { lightingModel, reflectedLight } = builder.context;
lightingModel.direct( { ...lightData, lightNode, reflectedLight }, builder );
}
而 materialLightings(材质级光照项)则被 LightsNode.setupLightsNode() 通过 builder.context.materialLightings 读出,与场景内建光源一起排序、生成对应的 LightingNode(如 EnvironmentNode、AONode 等,参见 src/nodes/lighting 目录下的各类光源节点)。这正是"上下文容器"承上启下的完整闭环:NodeMaterial 构造上下文 → LightingContextNode 注入 builder → LightsNode 读取中间量并驱动 LightingModel 分项求值 → 汇聚成出射光。
进阶用法:背板(backdrop)滤镜效果
backdropNode / backdropAlphaNode 在 NodeMaterial 上以同名属性公开,文档注释给出了典型场景——把对象背后的画面采样成纹理并施加滤镜。例如:
const material = new NodeMaterial();
material.transparent = true;
// 对象背后的所有内容都会被处理成单色(饱和度归零)
material.backdropNode = saturation( viewportSharedTexture().rgb, 0 );
注释同时强调了两条限制(见 NodeMaterial.js#L195):
- 背板计算属于光照的一部分,因此只有受光照的(lit)材质才能使用
backdropNode; - 当场景没有任何光源且仅设背板时,会走
setupLighting()中backdropNode !== null的旁路分支,用mix(outgoingLight, backdropNode, backdropAlphaNode)直接在节点层完成合成,此时不再经过LightingContextNode。
小结
- 定位:
LightingContextNode= 灯光版的ContextNode,是LightsNode的运行时上下文,位于继承链EventDispatcher → Node → ContextNode → LightingContextNode末端。 - 职责:把
lightsNode与lightingModel、materialLightings、背板节点绑定为一个上下文对象,并在 NodeBuilder 构建期间临时注入,供LightsNode.setup()与 LightingModel 生命周期方法读写。 - 核心产出:
getContext()一次性构造radiance / irradiance / iblIrradiance / ambientOcclusion / reflectedLight(4 个 vec3 分项)/ materialLightings / backdrop / backdropAlpha,其中反射光中间量全部为toVar()声明着色的变量。 - 上下文贯通:
materialLightings由LightsNode.setupLightsNode()消费,reflectedLight与lightingModel被直接光/间接光求值读取,最终总漫反射 + 总镜面反射汇聚为出射光。 - 使用入口:内部由
NodeMaterial.setupLighting()通过lightingContext(...)统一创建;TSL 侧可用TSL.lightingContext,文件内还导出nodeProxy生成的同名便捷函数。
若想继续深挖,可从 LightingContextNode.js、ContextNode.js、LightsNode.js、LightingModel.js 与 NodeMaterial.js 这几个源文件沿调用链逐层阅读,即可完整还原 three.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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00