three.js TSL 中的 BasicLightingModel:非光照材质的间接光照与环境映射实现解析
本文基于 three.js 官方 API 文档 BasicLightingModel.html.md,结合仓库源码,讲解 three.js 节点材质体系(TSL)中 BasicLightingModel 的设计与实现。它是面向“非光照(unlit)”材质的光照模型:唯一的光照贡献来自烘焙的间接光(baked indirect lighting),并用环境光遮蔽(AO)与材质漫反射颜色进行调制,同时支持环境映射(environment mapping)。读完本文,你可以理解该模型在渲染管线中的调用时机、indirect() 与 finish() 两个核心方法的节点图构造逻辑,以及它与 MeshBasicNodeMaterial 的协作方式。
一、BasicLightingModel 是什么:定位与继承体系
按官方文档的定义,BasicLightingModel 表示非光照材质的光照模型(lighting model for unlit materials):
The only light contribution is baked indirect lighting modulated with ambient occlusion and the material's diffuse color. Environment mapping is supported.
它位于节点(Node)体系的函数模块中,继承自抽象基类 LightingModel:
- 源码:src/nodes/functions/BasicLightingModel.js
- 基类:src/nodes/core/LightingModel.js
- 文档:BasicLightingModel.html.md、LightingModel.html.md
基类 LightingModel 定义了一组“在不同时间点执行”的抽象方法,构成光照模型的生命周期钩子:
| 方法 | 职责 | 执行时机 |
|---|---|---|
start(builder) |
初始化光照模型与上下文数据:先调用 lightsNode.setupLights() 收集光节点,再执行 this.indirect(builder) |
光照求值开始前 |
direct(lightData, builder) |
直接光项,由 directional/point/spot 光节点构建时调用 | 每个灯节点求值时 |
directRectArea(lightData, builder) |
面光源(rect area light)的直接光项 | 面光源求值时 |
indirect(builder) |
间接光项 | start() 内部 |
ambientOcclusion(input, stack, builder) |
AO 项,需由具体模型在间接项中手动调用 | 模型自行决定 |
finish(builder) |
对出射光(outgoing light)做最终更新 | 所有光项累计之后 |
可以推断,这套“钩子式”接口让不同光照模型(如 BasicLightingModel、PhongLightingModel)在统一的调度框架下各自实现光照数学,而不必关心调度细节。
调度方:LightsNode 如何驱动光照模型
从 src/nodes/lighting/LightsNode.js 的源码可以看到完整调用链:
lightingModel.start(builder)启动模型,其间执行间接项;- 框架把
reflectedLight中的directDiffuse + indirectDiffuse、directSpecular + indirectSpecular合并,写入outgoingLightNode; lightingModel.finish(builder)对outgoingLight做最后一次修改(例如环境映射混合)。
这解释了为什么文档把 finish() 描述为“执行诸如对出射光做最终更新之类的最终任务”——它发生在所有光项累计完成之后。
二、构造方法与间接光实现:indirect(builder)
文档给出的构造接口非常简单:
new BasicLightingModel()
在 src/nodes/functions/BasicLightingModel.js 中,构造函数仅调用 super(),没有额外状态。其核心逻辑全部集中在 indirect() 方法中。
indirect() 的完整实现
indirect( { context } ) {
const ambientOcclusion = context.ambientOcclusion;
const reflectedLight = context.reflectedLight;
const irradianceLightMap = context.irradianceLightMap;
reflectedLight.indirectDiffuse.assign( vec4( 0.0 ) );
// accumulation (baked indirect lighting only)
if ( irradianceLightMap ) {
reflectedLight.indirectDiffuse.addAssign( irradianceLightMap );
} else {
reflectedLight.indirectDiffuse.addAssign( vec4( 1.0, 1.0, 1.0, 0.0 ) );
}
// modulation
reflectedLight.indirectDiffuse.mulAssign( ambientOcclusion );
reflectedLight.indirectDiffuse.mulAssign( diffuseColor.rgb );
}
结合源码可以拆解出三个步骤:
- 清零:
reflectedLight.indirectDiffuse先赋值为vec4(0.0),保证间接漫反射从干净状态开始累计; - 累计(accumulation):这是文档所说 “baked indirect lighting only” 的含义——该模型不处理任何直接光(未覆写
direct()),唯一来源是context.irradianceLightMap(烘焙光照贴图);若没有光照贴图,则退化为全白vec4(1,1,1,0),等价于“均匀环境光”; - 调制(modulation):依次乘以 AO 项与材质漫反射颜色
diffuseColor.rgb。
因此最终公式为:
indirectDiffuse = ( irradianceLightMap 或 白色 ) × AO × diffuseColor.rgb
irradianceLightMap 从何而来:BasicLightMapNode
context.irradianceLightMap 由 src/nodes/lighting/BasicLightMapNode.js 提供。其源码注释明确指出:
A specific version of IrradianceNode that is only relevant for MeshBasicNodeMaterial. Since the material is unlit, it requires a special scaling factor for the light map.
setup( builder ) {
// irradianceLightMap property is used in the indirectDiffuse() method of BasicLightingModel
const RECIPROCAL_PI = float( 1 / Math.PI );
builder.context.irradianceLightMap = this.lightMapNode.mul( RECIPROCAL_PI );
}
即光照贴图会乘以 1/π 的缩放系数——这是 non-PBR 材质下对齐 Lambert 辐照度归一化的做法。而该节点的创建则由 MeshBasicNodeMaterial.setupLightMap() 负责(见 src/materials/nodes/MeshBasicNodeMaterial.js):只有当 builder.material.lightMap 存在时才生成 BasicLightMapNode。
三、环境映射实现:finish(builder)
文档将 .finish( builder : NodeBuilder ) 描述为“Implements the environment mapping”(实现环境映射),覆写自 LightingModel#finish。其源码(src/nodes/functions/BasicLightingModel.js)按材质的 combine 模式分支处理:
finish( builder ) {
const { material, context } = builder;
const outgoingLight = context.outgoingLight;
const envNode = builder.context.environment;
if ( envNode ) {
switch ( material.combine ) {
case MultiplyOperation:
outgoingLight.rgb.assign( mix( outgoingLight.rgb, outgoingLight.rgb.mul( envNode.rgb ), materialSpecularStrength.mul( materialReflectivity ) ) );
break;
case MixOperation:
outgoingLight.rgb.assign( mix( outgoingLight.rgb, envNode.rgb, materialSpecularStrength.mul( materialReflectivity ) ) );
break;
case AddOperation:
outgoingLight.rgb.addAssign( envNode.rgb.mul( materialSpecularStrength.mul( materialReflectivity ) ) );
break;
default:
warn( 'BasicLightingModel: Unsupported .combine value:', material.combine );
break;
}
}
}
关键点:
- 前置条件:只有
builder.context.environment存在时才执行任何混合,否则直接返回,出射光保持indirect()的结果; - 三种混合模式对应 three.js 材质经典的
combine取值(常量定义于 src/constants.js),混合权重统一为materialSpecularStrength × materialReflectivity(这两个属性节点定义于 src/nodes/accessors/MaterialNode.js):MultiplyOperation:mix( outgoingLight, outgoingLight × env, weight )——环境贴图与表面颜色相乘后按权重过渡;MixOperation:mix( outgoingLight, env, weight )——出射光与环境贴图直接线性插值;AddOperation:outgoingLight += env × weight——环境贴图作为附加亮度叠加;- 其他取值会触发
warn( 'BasicLightingModel: Unsupported .combine value:' )。
combine的默认值可在 src/materials/MeshBasicMaterial.js 中确认:this.combine = MultiplyOperation;。
environment 从何而来:BasicEnvironmentNode
builder.context.environment 由 src/nodes/lighting/BasicEnvironmentNode.js 填充,源码注释写明 “environment property is used in the finish() method of BasicLightingModel”:
setup( builder ) {
// environment property is used in the finish() method of BasicLightingModel
builder.context.environment = cubeMapNode( this.envNode );
}
该节点将环境节点统一转为 cube map 形式供采样,其类注释说明 BasicEnvironmentNode 是面向 non-PBR 材质(如 MeshBasicNodeMaterial、MeshPhongNodeMaterial)的 IBL 基础模型。
四、与 MeshBasicNodeMaterial 的协作:完整装配链
文档明确 BasicLightingModel 用于 MeshBasicNodeMaterial。从该材质源码可以看到完整的装配关系:
// src/materials/nodes/MeshBasicNodeMaterial.js
this.lights = true; // 虽然材质定义上是 unlit,但仍需光照模型计算 reflectedLight
setupOutgoingLight() {
return diffuseColor.rgb;
}
setupEnvironment( builder ) {
const envNode = super.setupEnvironment( builder );
return envNode ? new BasicEnvironmentNode( envNode ) : null;
}
setupLightMap( builder ) {
let node = null;
if ( builder.material.lightMap ) {
node = new BasicLightMapNode( materialLightMap );
}
return node;
}
setupLightingModel() {
return new BasicLightingModel();
}
由此可以梳理出一条清晰的装配链(均以仓库相对路径为证):
MeshBasicNodeMaterial.setupLightingModel()返回new BasicLightingModel();MeshBasicNodeMaterial.setupEnvironment()用BasicEnvironmentNode包装环境节点,写入builder.context.environment;- 若设置了
lightMap,setupLightMap()生成BasicLightMapNode,写入带1/π缩放的irradianceLightMap; LightsNode调度start()→indirect()得到间接漫反射(AO × 漫反射颜色调制),合并后finish()再按combine模式混入环境映射。
值得注意的是源码注释中的一处设计决策:setupOutgoingLight() 仍直接返回 diffuseColor.rgb,因为“lights 虽为 true,但我们希望出射光以漫反射颜色为基底”。此外 setupNormal() 返回 negateOnBackSide( normalViewGeometry ),表明基础材质不使用法线贴图参与光照。
五、实践要点与边界
综合文档与源码,使用 BasicLightingModel 相关能力时需注意:
- 适用材质:它服务于节点版基础材质(
MeshBasicNodeMaterial)等 unlit 场景,典型用途包括 UI 贴图层、无光照底图、纯烘焙光照场景,以及只需要环境映射“染色”的物体; - 没有直接光项:该模型未覆写
direct()/directRectArea(),场景中摆放 DirectionalLight、PointLight 等不会改变其结果,颜色由diffuseColor、lightMap、aoMap与环境贴图决定; - combine 仅支持三种操作:
MultiplyOperation、MixOperation、AddOperation,其他取值仅打印警告不生效; - 环境映射权重:混合强度由
materialSpecularStrength与materialReflectivity相乘控制,调整这两个属性即可控制环境贴图对最终颜色的影响比例; - 光照贴图需配合
1/π缩放:BasicLightMapNode已自动处理,自行在 TSL 中复现类似效果时应对齐这一约定。
延伸阅读
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 StartedRust0624
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