首页
/ three.js TSL 中的 BasicLightingModel:非光照材质的间接光照与环境映射实现解析

three.js TSL 中的 BasicLightingModel:非光照材质的间接光照与环境映射实现解析

2026-09-04 21:05:46作者:牧宁李

本文基于 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

基类 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)做最终更新 所有光项累计之后

可以推断,这套“钩子式”接口让不同光照模型(如 BasicLightingModelPhongLightingModel)在统一的调度框架下各自实现光照数学,而不必关心调度细节。

调度方:LightsNode 如何驱动光照模型

src/nodes/lighting/LightsNode.js 的源码可以看到完整调用链:

  1. lightingModel.start(builder) 启动模型,其间执行间接项;
  2. 框架把 reflectedLight 中的 directDiffuse + indirectDiffusedirectSpecular + indirectSpecular 合并,写入 outgoingLightNode
  3. 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 );

}

结合源码可以拆解出三个步骤:

  1. 清零reflectedLight.indirectDiffuse 先赋值为 vec4(0.0),保证间接漫反射从干净状态开始累计;
  2. 累计(accumulation):这是文档所说 “baked indirect lighting only” 的含义——该模型不处理任何直接光(未覆写 direct()),唯一来源是 context.irradianceLightMap(烘焙光照贴图);若没有光照贴图,则退化为全白 vec4(1,1,1,0),等价于“均匀环境光”;
  3. 调制(modulation):依次乘以 AO 项与材质漫反射颜色 diffuseColor.rgb

因此最终公式为:

indirectDiffuse = ( irradianceLightMap 或 白色 ) × AO × diffuseColor.rgb

irradianceLightMap 从何而来:BasicLightMapNode

context.irradianceLightMapsrc/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):
    • MultiplyOperationmix( outgoingLight, outgoingLight × env, weight )——环境贴图与表面颜色相乘后按权重过渡;
    • MixOperationmix( outgoingLight, env, weight )——出射光与环境贴图直接线性插值;
    • AddOperationoutgoingLight += env × weight——环境贴图作为附加亮度叠加;
    • 其他取值会触发 warn( 'BasicLightingModel: Unsupported .combine value:' )
  • combine 的默认值可在 src/materials/MeshBasicMaterial.js 中确认:this.combine = MultiplyOperation;

environment 从何而来:BasicEnvironmentNode

builder.context.environmentsrc/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 材质(如 MeshBasicNodeMaterialMeshPhongNodeMaterial)的 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();
}

由此可以梳理出一条清晰的装配链(均以仓库相对路径为证):

  1. MeshBasicNodeMaterial.setupLightingModel() 返回 new BasicLightingModel()
  2. MeshBasicNodeMaterial.setupEnvironment()BasicEnvironmentNode 包装环境节点,写入 builder.context.environment
  3. 若设置了 lightMapsetupLightMap() 生成 BasicLightMapNode,写入带 1/π 缩放的 irradianceLightMap
  4. LightsNode 调度 start()indirect() 得到间接漫反射(AO × 漫反射颜色调制),合并后 finish() 再按 combine 模式混入环境映射。

值得注意的是源码注释中的一处设计决策:setupOutgoingLight() 仍直接返回 diffuseColor.rgb,因为“lights 虽为 true,但我们希望出射光以漫反射颜色为基底”。此外 setupNormal() 返回 negateOnBackSide( normalViewGeometry ),表明基础材质不使用法线贴图参与光照。

五、实践要点与边界

综合文档与源码,使用 BasicLightingModel 相关能力时需注意:

  • 适用材质:它服务于节点版基础材质(MeshBasicNodeMaterial)等 unlit 场景,典型用途包括 UI 贴图层、无光照底图、纯烘焙光照场景,以及只需要环境映射“染色”的物体;
  • 没有直接光项:该模型未覆写 direct() / directRectArea(),场景中摆放 DirectionalLight、PointLight 等不会改变其结果,颜色由 diffuseColorlightMapaoMap 与环境贴图决定;
  • combine 仅支持三种操作MultiplyOperationMixOperationAddOperation,其他取值仅打印警告不生效;
  • 环境映射权重:混合强度由 materialSpecularStrengthmaterialReflectivity 相乘控制,调整这两个属性即可控制环境贴图对最终颜色的影响比例;
  • 光照贴图需配合 1/π 缩放BasicLightMapNode 已自动处理,自行在 TSL 中复现类似效果时应对齐这一约定。

延伸阅读

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