首页
/ three.js TSL 光照模型基类 LightingModel 深度解析:抽象方法契约、求值生命周期与自定义扩展指南

three.js TSL 光照模型基类 LightingModel 深度解析:抽象方法契约、求值生命周期与自定义扩展指南

2026-09-07 20:17:49作者:何举烈Damon

LightingModel 是 three.js Node 材质体系(TSL/WebGPU Node)中承载「光照模型」的抽象基类。它为直接光、间接光、环境光遮蔽等光照分量定义了一组清晰的抽象方法,并规定了它们在一次光照求值中的先后顺序。阅读完本文,你将理解这些方法何时被调用、各自应实现什么物理项、如何被 NodeMaterial 与 LightsNode 驱动,以及如何基于它编写一个自定义光照模型接入渲染管线。

一、LightingModel 是什么

类文档 的定义看,LightingModel 是用于实现光照模型的抽象类。模块内定义了一系列方法,具体的光照模型子类可以有选择地实现这些方法,而它们会在光照求值过程的不同阶段被执行。

在 three.js 中,LightingModel 与一组节点类型共同构成"节点化"的光照求值框架:

  • LightsNode:代表场景光照,负责管理整个光照求值生命周期;
  • LightingContextNode:在 ContextNode 基础上注入光照上下文(radiance、irradiance、reflectedLight、ambientOcclusion 等);
  • 各类 Light 节点(如 DirectionalLightNodePointLightNodeSpotLightNodeRectAreaLightNode)负责把传统光源换算成可求值的光照数据;
  • 具体的 LightingModel 子类(BasicLightingModelPhysicalLightingModelPhongLightingModel 等)则把这些光照数据最终合成为片元的漫反射、镜面反射与出射光。

LightingModel 处于这条链路的最末端——定义"用光照数据怎样算出最终颜色"的策略层

二、构造函数

new LightingModel()

LightingModel 构造函数不接收任何参数,也不执行任何初始化逻辑,其全部源码可在 src/nodes/core/LightingModel.js 中查看:

class LightingModel {

	start( builder ) { /* ... */ }
	finish( /*builder*/ ) { }
	direct( /*lightData, builder*/ ) { }
	directRectArea( /*lightData, builder*/ ) { }
	indirect( /*builder*/ ) { }
	ambientOcclusion( /*input, stack, builder*/ ) { }

}

可以看到基类中除 start() 外,其余方法默认都是空实现;它们只是以 JSDoc @abstract 标注的抽象方法契约,需要子类覆写来提供实际物理语义。值得强调的是,基类的 start() 并非空实现,它提供了默认编排逻辑(见下文第四节),这也是三个内置模型的 start() 通常会先做准备工作、再以 super.start( builder ) 收尾的原因。

三、六个抽象方法:光照模型的生命周期契约

这些方法分别对应光照求值过程中的不同阶段,先给出全景表,再逐一剖析。

方法 阶段 职责
.start( builder ) 求值前 设置光照模型自身与上下文数据
.direct( lightData, builder ) 直接光项 方向光 / 点光 / 聚光灯的直接光照
.directRectArea( lightData, builder ) 直接光项(面光) 矩形区域光的直接光照
.indirect( builder ) 间接光项 间接光照(IBL、辐照度图、AO 调制等)
.ambientOcclusion( builder ) AO 项 环境光遮蔽项,须由光照模型在 indirect 中手动调用
.finish( builder ) 求值后 收尾任务,如对出射光的最终修正

所有方法都接收 builder 参数,它是对当前 NodeBuilder 的引用;NodeBuilder 的完整定义可参阅 NodeBuilder 文档

.start( builder )

意图是"为光照模型和上下文数据做准备,供后续求值阶段使用"。在 src/nodes/core/LightingModel.js 中,基类为该方法提供了一套默认编排:

start( builder ) {

	// lights ( direct )
	builder.lightsNode.setupLights( builder, builder.lightsNode.getLightNodes( builder ) );

	// indirect
	this.indirect( builder );

}

即默认流程是:

  1. 通过 lightsNode.setupLights() 逐个 build 场景中每个光源对应的 LightingNode——这正是触发下方 direct / directRectArea 求值的起点;
  2. 紧接着调用 this.indirect( builder ) 求值间接光项。

因此,自定义模型若需要先分配一些中间变量(如 PhysicalLightingModel 为 clearcoat / sheen / iridescence 预留的累加向量),可以覆写 start 并在末尾调用 super.start( builder );若完全不需要直接光驱动,也可以不调用父类实现。

.direct( lightData, builder )

该方法用于实现直接光项,在方向光、点光与聚光灯节点 build 的过程中被执行。lightData 中包含该光源的颜色、方向等数据,builder 为当前节点构建器。

src/nodes/lighting/LightsNode.js 可以看到该方法是被光源节点通过 LightsNode 间接触发的:

setupDirectLight( builder, lightNode, lightData ) {

	const { lightingModel, reflectedLight } = builder.context;

	lightingModel.direct( {
		...lightData,
		lightNode,
		reflectedLight
	}, builder );

}

也就是说,direct 实际收到的 lightData 已经由 LightsNode 展开,附带了 lightNode(产生该直接光的光源节点)与 reflectedLight(用于累加输出结果的引用)。例如 PhysicalLightingModel.direct()PhysicalLightingModel.js 中会据此计算 dotNLBRDF_GGX 高光、clearcoat 分量等,并累加到 reflectedLight.directDiffuse / directSpecular 上。

.directRectArea( lightData, builder )

该方法用于实现矩形区域光(rect area light)的直接光项。区域光不能像理想点光源那样只用一个方向向量近似,通常需要使用 LTC(Linearly Transformed Cosines)矩阵进行面光源 BRDF 积分。对应触发点在 src/nodes/lighting/LightsNode.js

setupDirectRectAreaLight( builder, lightNode, lightData ) {

	const { lightingModel, reflectedLight } = builder.context;

	lightingModel.directRectArea( {
		...lightData,
		lightNode,
		reflectedLight
	}, builder );

}

PhysicalLightingModel 对它的实现位于 src/nodes/functions/PhysicalLightingModel.js,内部会解构 lightColorlightPositionhalfWidthhalfHeightltc_1ltc_2 等区域光特有数据完成求值。

.indirect( builder )

该方法用于实现间接光项。间接光包括基于图像的光照(IBL)、辐照度图(irradiance light map)、天空盒环境反射等,并且通常还需要乘以环境光遮蔽。

因为间接项内部往往还要再细分(AO 调制、clearcoat/specular 各自的间接分量),所以框架刻意不替子类规定 indirect 的内部流程,只要求子类自行实现。示例见 src/nodes/functions/PhysicalLightingModel.js,其 indirect() 会累加 IBL 漫反射/镜面反射项,并在内部手动调用 this.ambientOcclusion( builder )(见源码 L749)。这与 BasicLightingModel 只在 indirect 中做"烘焙间接光 × AO × 漫反射颜色"调制(BasicLightingModel.js)形成鲜明对比。

.ambientOcclusion( builder )

该方法专门用于实现环境光遮蔽项。注释与文档均特别强调:与其它方法不同,ambientOcclusion 不会被渲染系统自动调用,必须由光照模型在自己的 indirect 项中手动调用

PhysicalLightingModel 的 AO 实现位于 src/nodes/functions/PhysicalLightingModel.js,它会读取 builder.context.ambientOcclusionreflectedLight,对间接漫反射、clearcoat 与 sheen 的间接镜面反射分别做遮蔽调制。AO 数值的来源则是 LightingContextNode.js 中以 float( 1 ).toVar( 'ambientOcclusion' ) 初始化的上下文变量——默认 1.0(无遮蔽),随后由材质侧的 AO 贴图节点或 AONode 乘入实际遮蔽值。

.finish( builder )

该方法用于执行收尾任务,例如对出射光(outgoing light)做最终更新。典型用途是在所有直接/间接分量累加完毕后统一处理环境贴图合成或最终的色调微调:

  • BasicLightingModel.finish() 根据 material.combine(Multiply / Mix / Add)将环境节点合并进出射光,BasicLightingModel.js
  • ShadowMaskModel.finish() 则用累积得到的 shadow mask 调整片元透明度并把漫反射色赋给出射光,ShadowMaskModel.js

四、执行时序:一次光照求值中这些方法怎样被驱动

要真正理解这六个方法,需要看清驱动方——LightsNode.setup()。在 src/nodes/lighting/LightsNode.js 中,整个求值流程如下:

  1. 建立上下文与光照模型:从 builder.context 取出 lightingModel(由材质经 LightingContextNode 注入);
  2. start:调用 lightingModel.start( builder )。基类默认实现会触发"所有光源节点 build + indirect";而各光源节点 build 时,AnalyticLightNode 会依据光源类型调用 builder.lightsNode.setupDirectLight(...) / setupDirectRectAreaLight(...)(见 AnalyticLightNode.js),从而进入 lightingModel.direct / directRectArea
  3. 汇总分量LightsNodecontext.reflectedLight 读出 directDiffuse / directSpecular / indirectDiffuse / indirectSpecular,累加得到总漫反射与总镜面反射:
    • totalDiffuse = directDiffuse + indirectDiffuse(若存在 backdrop 还会与 backdrop 混合);
    • totalSpecular = directSpecular + indirectSpecular
    • outgoingLight = totalDiffuse + totalSpecular
  4. finish:调用 lightingModel.finish( builder ),对 outgoingLight 做最终修正;
  5. 返回 outgoingLight 作为节点图光照输出。

其中 LightingContextNode.getContext()src/nodes/lighting/LightingContextNode.js)负责搭建求值期间共享的上下文对象,结构如下:

{
	radiance,          // vec3 变量
	irradiance,        // vec3 变量
	iblIrradiance,     // vec3 变量
	ambientOcclusion,  // float 变量,默认 1
	reflectedLight: {
		directDiffuse,   // vec3 变量
		directSpecular,  // vec3 变量
		indirectDiffuse, // vec3 变量
		indirectSpecular // vec3 变量
	},
	backdrop,          // 背板(backdrop)节点,默认 null
	backdropAlpha,     // 背板透明度节点,默认 null
	lightingModel,     // 在 setup 时从参数或上层上下文解析
}

这也是 directindirect 等方法能够直接操作 reflectedLight.directDiffuse 等字段的原因。

把上面的生命周期用一句话串起来就是:start() 打开光照求值并调用 indirect() → 光源节点逐盏触发 direct()/directRectArea()LightsNode 汇总四路反射分量算出 outgoingLightfinish() 对出射光做最终处理

五、内置 LightingModel 实现族谱

仓库 src/nodes/functions/ 目录下提供了一批现成的光照模型,对应不同材质风格:

光照模型 继承 用途 / 关联材质 参考源码
BasicLightingModel LightingModel 无光照/基础材质,仅烘焙间接光 + AO + 环境贴图,用于 MeshBasicNodeMaterial BasicLightingModel.js
PhongLightingModel BasicLightingModel Blinn-Phong 高光,用于 MeshPhongNodeMaterialMeshLambertNodeMaterial 通过 new PhongLightingModel( false ) 关闭高光实现 Lambert PhongLightingModel.js
ToonLightingModel LightingModel 渐变分级光照的卡通渲染,用于 MeshToonNodeMaterial ToonLightingModel.js
PhysicalLightingModel LightingModel 基于物理的 PBR(GGX/多散射能量补偿/clearcoat/sheen/iridescence/anisotropy/transmission/dispersion),用于 MeshStandardNodeMaterialMeshPhysicalNodeMaterial PhysicalLightingModel.js
SSSLightingModel PhysicalLightingModel 在 PBR 基础上叠加次表面散射,定义于 MeshSSSNodeMaterial MeshSSSNodeMaterial.js
ShadowMaskModel LightingModel 专用于阴影材质:逐光累积 shadow mask 并据此输出透明度,用于 ShadowNodeMaterial ShadowMaskModel.js
VolumetricLightingModel LightingModel 体积光散射/透射(含体绘制射线步进),用于 VolumeNodeMaterial VolumetricLightingModel.js

各模型的实例化与材质绑定关系十分直接:每个 Node 材质的 setupLightingModel() 工厂方法返回对应模型实例,例如 MeshBasicNodeMaterial.jssetupLightingModel() 直接返回 new BasicLightingModel()MeshStandardNodeMaterial.js 返回 new PhysicalLightingModel();而 MeshPhysicalNodeMaterial.js 会把 useClearcoatuseSheenuseIridescenceuseAnisotropyuseTransmissionuseDispersionuseRetroreflection 等开关透传给构造函数。

各具体模型的文档页见 BasicLightingModel 文档PhongLightingModel 文档PhysicalLightingModel 文档ToonLightingModel 文档ShadowMaskModel 文档,可对照阅读每个实现覆写了哪些抽象方法。

六、模型与材质的接线:NodeMaterial.setupLightingModel

NodeMaterialsrc/materials/nodes/NodeMaterial.js)在基类中把 setupLightingModel 声明为抽象接口(源码 L1062-L1066),文档注释明确指出"绝大多数派生材质都应实现该方法,因为它定义了材质的光照模型"。

材质在 build 出射光节点时(src/materials/nodes/NodeMaterial.js)完成装配:

const lightingModel = this.setupLightingModel( builder ) || null;

outgoingLightNode = lightingContext(
	lightsNode,
	lightingModel,
	materialLightings,
	backdropNode,
	backdropAlphaNode
);

即:调用材质覆写的 setupLightingModel() 取得具体 LightingModel 实例,再把 LightsNode + lightingModel + 材质附加光照节点 一起包进 LightingContextNode。后续 LightsNode.setup() 在执行光照求值时,正是从该上下文里取出 lightingModel 开始生命周期调用的。这与第四节的执行时序闭合起来:材质决定"用什么模型",LightsNode 决定"何时调哪些方法"

七、实践:编写一个自定义 LightingModel

理解了契约之后,自定义一个光照模型只需三步:新建类继承 LightingModel、按需覆写对应阶段方法、通过自定义 NodeMaterial 的 setupLightingModel() 接入。以下给出一个"只在间接项里叠加 AO 与 IBL 辐照度"的最小骨架(示意,非仓库内代码):

import LightingModel from './three.js/src/nodes/core/LightingModel.js';

class MySimpleLightingModel extends LightingModel {

	// 1) 在 start 里完成变量初始化后,交由基类驱动光源求值与 indirect
	start( builder ) {

		super.start( builder );

	}

	// 2) 处理方向光/点光/聚光灯直接项
	direct( { lightDirection, lightColor, reflectedLight }, builder ) {

		const dotNL = /* 法线与光线方向点积 */;
		reflectedLight.directDiffuse.addAssign( /* 漫反射贡献 */ );

	}

	// 3) 间接项内手动调用 AO
	indirect( builder ) {

		const { ambientOcclusion, irradiance, reflectedLight } = builder.context;

		reflectedLight.indirectDiffuse.assign( irradiance );
		reflectedLight.indirectDiffuse.mulAssign( ambientOcclusion );
		reflectedLight.indirectDiffuse.mulAssign( /* 漫反射颜色 */ );

		this.ambientOcclusion( builder ); // 必须手动调用

	}

	// 4) AO 实现
	ambientOcclusion( builder ) {

		const { ambientOcclusion, reflectedLight } = builder.context;
		reflectedLight.indirectDiffuse.mulAssign( ambientOcclusion );

	}

	// 5) 收尾
	finish( { context } ) {

		// 例如对 context.outgoingLight 做最后处理

	}

}

再将材质子类的 setupLightingModel() 覆写为返回 new MySimpleLightingModel(),即可让该材质的所有片元走自定义光照模型。接入后可在运行时通过切换不同的 LightingModel 实例,在同一个节点管线内体验完全不同的着色风格——这正是 TSL 光照模型解耦设计的价值所在。

八、小结

  • LightingModelsrc/nodes/core/LightingModel.js 中定义的抽象基类,通过 start / direct / directRectArea / indirect / ambientOcclusion / finish 六个方法划分光照求值生命周期;
  • 求值顺序由 LightsNode 驱动,光源节点(如 AnalyticLightNode)触发 direct / directRectArea,AO 必须由模型自身在 indirect 内手动调用;
  • 上下文数据(反射光分量、AO、backdrop 等)由 LightingContextNode 统一提供;
  • 仓库内置 Basic / Phong / Toon / Physical / SSS / ShadowMask / Volumetric 等多种实现,材质通过 NodeMaterial.setupLightingModel() 选择其一,开发者也可轻松注入自定义模型。

如需查阅官方 API 条目,可访问 LightingModel 官方文档页;进一步学习上下文与光照节点体系,可参考 LightingContextNode 文档LightsNode 文档

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

项目优选

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