three.js TSL 中 AONode 详解:环境光遮蔽如何进入节点光照管线
本文基于 three.js 官方 API 文档 AONode 及其实现源码,讲解 AONode 的构造、属性、继承体系,以及它在 NodeMaterial 光照管线中注入环境光遮蔽(Ambient Occlusion,AO)的完整链路:从 LightingContextNode 中的 AO 插槽初始化,到 PhysicalLightingModel 等各光照模型对 AO 的实际消费方式。读完你可以理解如何用 material.aoNode 或 AONode 自定义 AO 来源(如 AO 贴图、自定义 TSL 表达式),以及 AO 值最终如何影响间接漫反射与间接镜面反射。
1. AONode 是什么
根据 API 文档 的定义,AONode 是一个通用节点类,供“为场景贡献环境光遮蔽”的节点使用,例如一个环境光遮蔽贴图节点(ambient occlusion map node)就可以作为它的输入,最终用于 NodeMaterial。其继承链为:
EventDispatcher → Node → LightingNode → AONode
从源码结构看,这条继承链的含义在 AONode.js 与 LightingNode.js 中都有体现:
LightingNode是所有“光照节点”的基类,其构造时传入输出类型'vec3',并带有isLightingNode标志,用于类型测试;AONode在此基础上只增加了语义——它代表“AO 贡献者”,而不是具体的光照模型。
AONode 通过 Nodes.js 统一导出,是节点系统(TSL)的一部分。
2. 构造函数与属性
new AONode( aoNode )
构造参数与属性说明(继承自 AONode 文档页):
| 名称 | 类型 | 说明 | 默认值 |
|---|---|---|---|
aoNode(构造参数) |
Node.<float> |
环境光遮蔽节点 | null |
.aoNode(属性) |
Node.<float> |
环境光遮蔽节点 | null |
实现见 AONode.js#L23-L41:
class AONode extends LightingNode {
static get type() {
return 'AONode';
}
constructor( aoNode = null ) {
super();
/**
* The ambient occlusion node.
* @type {?Node<float>}
* @default null
*/
this.aoNode = aoNode;
}
setup( builder ) {
builder.context.ambientOcclusion.mulAssign( this.aoNode );
}
}
关键点在 setup( builder ) 方法:它把自身的 aoNode 以乘法方式累积到光照上下文中的 ambientOcclusion 变量上。由于该变量初始值为 1(见下文第 3 节),单独一个 AONode 的效果等价于“AO = 自己的 aoNode”;多个 AONode 同时存在时,它们的效果会被相乘,这与 AO 的物理含义(多个遮蔽源叠加遮挡)一致。
3. AO 在光照上下文中的位置
builder.context.ambientOcclusion 并非凭空出现,它由 LightingContextNode.js#L95-L104 中的 getContext() 创建:
const context = {
radiance: vec3().toVar( 'radiance' ),
irradiance: vec3().toVar( 'irradiance' ),
iblIrradiance: vec3().toVar( 'iblIrradiance' ),
ambientOcclusion: float( 1 ).toVar( 'ambientOcclusion' ), // AO 初始值为 1,即“无遮蔽”
reflectedLight,
materialLightings,
backdrop: backdropNode,
backdropAlpha: backdropAlphaNode
};
ambientOcclusion 是一个初始化为 float( 1 ) 的变量节点——表示“完全不被遮蔽”。AONode 的 mulAssign 会在这个基础上把实际 AO 值乘进去。
那么 AONode 何时被加入光照上下文?从 NodeMaterial.js 的调用链看:
- 片段着色阶段,
setup()先调用setupAmbientOcclusion( builder ),将 AO 值写入上下文(见第 4 节); - 若材质参与场景光照,
setupMaterialLightings( builder )会在 NodeMaterial.js#L1012-L1016 处检查上下文:
if ( builder.context.ambientOcclusion ) {
materialLightsNode.push( new AONode( builder.context.ambientOcclusion ) );
}
也就是说,NodeMaterial 内部会自动用当前上下文里的 AO 构造一个 AONode,把它与 IrradianceNode(环境/光贴图)、环境节点等一起塞进 materialLightings 列表,交由 lightingContext 在光照求值时执行。AONode 因此更像管线内部的标准“AO 适配器”,而非必须手工 new 出来的类。
4. AO 值从哪里来:material.aoNode 与 aoMap 默认路径
NodeMaterial 提供了 aoNode 属性(NodeMaterial.js#L120-L132):节点材质的光照可受环境光遮蔽影响,默认 AO 由材质的 aoMap 与 aoMapIntensity 推导;该属性允许用一个自定义节点覆盖默认行为。若不覆盖只想修改现有值,文档建议使用 materialAO。
对应的装配逻辑在 NodeMaterial.js#L1028-L1052 的 setupAmbientOcclusion( builder ):
setupAmbientOcclusion( builder ) {
let aoNode = this.aoNode;
if ( aoNode === null && builder.material.aoMap ) {
aoNode = materialAO; // 默认路径:从 aoMap 推导
}
if ( builder.context.getAO ) {
aoNode = builder.context.getAO( aoNode, builder );
}
if ( aoNode !== null ) {
ambientOcclusion.assign( aoNode );
builder.context.ambientOcclusion = ambientOcclusion;
}
}
其中默认路径使用的 materialAO 定义在 MaterialNode.js,其取值公式由 MaterialNode.js#L401-L411 给出:
} else if ( scope === MaterialNode.AO ) {
if ( material.aoMap ) {
node = this.getTexture( scope ).r.sub( 1.0 ).mul( this.getFloat( 'aoMapIntensity' ) ).add( 1.0 );
} else {
node = float( 1.0 );
}
}
即 AO = (aoMap.r − 1) × aoMapIntensity + 1,无 aoMap 时为 1(不遮蔽)。这一公式解释了为什么 AO 贴图要求“白=无遮蔽、黑=完全遮蔽”,以及 aoMapIntensity > 1 时可以增强遮蔽效果。
综合起来,给一个节点材质施加 AO 有三种常见方式(均以仓库实际用法为准):
- 给材质赋值
aoMap+aoMapIntensity,走materialAO默认路径; - 直接设置
material.aoNode。仓库中 MaterialXSurfaceMappings.js#L363 将 MaterialX 的 occlusion 输入映射到material.aoNode,webgpu_compute_rasterizer_ibl.html#L1448 则用sampleMap( sourceMaterial.aoMap ).r这种自定义 TSL 采样赋值:
resolveShadedMaterial.aoNode = sampleMap( sourceMaterial.aoMap ).r;
- 在光照上下文层面直接使用
AONode(如new AONode( 你的AO节点 )),用于自定义光照流程。
5. 各光照模型如何消费 AO
AO 进入上下文后,由各 LightingModel 的 indirect 阶段消费(基类接口见 LightingModel.js#L73 的空实现 ambientOcclusion())。四种内置模型的行为差异值得注意:
- PhysicalLightingModel(PBR):PhysicalLightingModel.js#L867-L897 实现了最完整的 AO 项,包含 Fresnel 相关的视角落遮蔽衰减:
ambientOcclusion( builder ) {
const { ambientOcclusion, reflectedLight } = builder.context;
const dotNV = normalView.dot( positionViewDirection ).clamp();
const aoNV = dotNV.add( ambientOcclusion );
const aoExp = roughness.mul( - 16.0 ).oneMinus().negate().exp2();
const aoNode = ambientOcclusion.sub( aoNV.pow( aoExp ).oneMinus() ).clamp();
// clearcoat / sheen 的间接高光同样受 AO 调制
reflectedLight.indirectDiffuse.mulAssign( ambientOcclusion );
reflectedLight.indirectSpecular.mulAssign( aoNode );
}
可以看到:漫反射被 AO 直接调制,而镜面反射使用的是经过 dotNV/roughness 修正后的 aoNode——视线越接近垂直于法线、表面越粗糙,遮蔽作用越强,避免了对“擦边角”处的镜面高光做不合理遮蔽。
- PhongLightingModel / ToonLightingModel:二者在 PhongLightingModel.js#L87-L95(Toon 同构)中对间接漫反射做
irradiance × BRDF_Lambert后再mulAssign( ambientOcclusion ),只调制间接漫反射。 - BasicLightingModel:在 BasicLightingModel.js#L32-L56 中将光贴图等间接贡献统一乘以
context.ambientOcclusion做调制。
6. 小结与使用建议
AONode本身极轻:只有一个可选的Node<float>输入,核心逻辑是builder.context.ambientOcclusion.mulAssign( this.aoNode )(AONode.js#L37-L41)。- 对多数开发者而言,直接使用
material.aoNode(或aoMap+aoMapIntensity)即可;NodeMaterial会在装配阶段自动以AONode的形式把 AO 注入光照上下文(NodeMaterial.js#L1012-L1016)。 - 只有在编写自定义光照上下文/自定义光照模型时,才需要手动实例化
AONode,并理解“上下文 AO 初始为 1、多个贡献者相乘”这一约定。 - 相关实现文件:src/nodes/lighting/AONode.js、src/nodes/lighting/LightingNode.js、src/nodes/lighting/LightingContextNode.js、src/materials/nodes/NodeMaterial.js、src/nodes/accessors/MaterialNode.js、src/nodes/functions/PhysicalLightingModel.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 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