首页
/ three.js TSL 光照上下文节点 LightingContextNode 深度解析:构造、上下文对象与在 NodeMaterial 光照管线中的真实作用

three.js TSL 光照上下文节点 LightingContextNode 深度解析:构造、上下文对象与在 NodeMaterial 光照管线中的真实作用

2026-09-07 17:53:36作者:盛欣凯Ernestine

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 与阴影回调,参见 builtinAOContextbuiltinShadowContext 等 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 );
}

其行为可以拆解为三点:

  1. 惰性缓存上下文对象this._value 为空时才调用 getContext() 生成,避免重复分配;ContextNodevalue 属性被用来承载这个上下文对象。
  2. 解析 lightingModel 的优先级:优先使用构造传入的 lightingModel;若为 null,则回退到当前 builder.context.lightingModel(即外层环境已声明的光照模型)。这意味着 LightingContextNode 可以"借用"渲染管线上下文里已经装好的光照模型,具备很强的可组合性。
  3. 调用父类 setup():由 ContextNode 完成真正的上下文推入/子节点构建/上下文恢复动作。

谁在使用它:NodeMaterial 光照组装的真实调用链

LightingContextNode 并非仅供进阶开发者手动使用的孤立节点,而是 three.js 节点材质内建光照管线的一部分。其 TSL 便捷函数 lightingContextnodeProxy( 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 统一驱动。例如:

这些 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(如 EnvironmentNodeAONode 等,参见 src/nodes/lighting 目录下的各类光源节点)。这正是"上下文容器"承上启下的完整闭环:NodeMaterial 构造上下文 → LightingContextNode 注入 builder → LightsNode 读取中间量并驱动 LightingModel 分项求值 → 汇聚成出射光

进阶用法:背板(backdrop)滤镜效果

backdropNode / backdropAlphaNodeNodeMaterial 上以同名属性公开,文档注释给出了典型场景——把对象背后的画面采样成纹理并施加滤镜。例如:

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 末端。
  • 职责:把 lightsNodelightingModelmaterialLightings、背板节点绑定为一个上下文对象,并在 NodeBuilder 构建期间临时注入,供 LightsNode.setup() 与 LightingModel 生命周期方法读写。
  • 核心产出getContext() 一次性构造 radiance / irradiance / iblIrradiance / ambientOcclusion / reflectedLight(4 个 vec3 分项)/ materialLightings / backdrop / backdropAlpha,其中反射光中间量全部为 toVar() 声明着色的变量。
  • 上下文贯通materialLightingsLightsNode.setupLightsNode() 消费,reflectedLightlightingModel 被直接光/间接光求值读取,最终总漫反射 + 总镜面反射汇聚为出射光。
  • 使用入口:内部由 NodeMaterial.setupLighting() 通过 lightingContext(...) 统一创建;TSL 侧可用 TSL.lightingContext,文件内还导出 nodeProxy 生成的同名便捷函数。

若想继续深挖,可从 LightingContextNode.jsContextNode.jsLightsNode.jsLightingModel.jsNodeMaterial.js 这几个源文件沿调用链逐层阅读,即可完整还原 three.js 节点化光照系统从"场景光源集合"到"最终出射光颜色"的求值全景。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391