首页
/ three.js TSL 中 AONode 详解:环境光遮蔽如何进入节点光照管线

three.js TSL 中 AONode 详解:环境光遮蔽如何进入节点光照管线

2026-09-06 21:05:09作者:袁立春Spencer

本文基于 three.js 官方 API 文档 AONode 及其实现源码,讲解 AONode 的构造、属性、继承体系,以及它在 NodeMaterial 光照管线中注入环境光遮蔽(Ambient Occlusion,AO)的完整链路:从 LightingContextNode 中的 AO 插槽初始化,到 PhysicalLightingModel 等各光照模型对 AO 的实际消费方式。读完你可以理解如何用 material.aoNodeAONode 自定义 AO 来源(如 AO 贴图、自定义 TSL 表达式),以及 AO 值最终如何影响间接漫反射与间接镜面反射。

1. AONode 是什么

根据 API 文档 的定义,AONode 是一个通用节点类,供“为场景贡献环境光遮蔽”的节点使用,例如一个环境光遮蔽贴图节点(ambient occlusion map node)就可以作为它的输入,最终用于 NodeMaterial。其继承链为:

EventDispatcher → Node → LightingNode → AONode

从源码结构看,这条继承链的含义在 AONode.jsLightingNode.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 ) 的变量节点——表示“完全不被遮蔽”。AONodemulAssign 会在这个基础上把实际 AO 值乘进去。

那么 AONode 何时被加入光照上下文?从 NodeMaterial.js 的调用链看:

  1. 片段着色阶段,setup() 先调用 setupAmbientOcclusion( builder ),将 AO 值写入上下文(见第 4 节);
  2. 若材质参与场景光照,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 由材质的 aoMapaoMapIntensity 推导;该属性允许用一个自定义节点覆盖默认行为。若不覆盖只想修改现有值,文档建议使用 materialAO

对应的装配逻辑在 NodeMaterial.js#L1028-L1052setupAmbientOcclusion( 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 有三种常见方式(均以仓库实际用法为准):

  1. 给材质赋值 aoMap + aoMapIntensity,走 materialAO 默认路径;
  2. 直接设置 material.aoNode。仓库中 MaterialXSurfaceMappings.js#L363 将 MaterialX 的 occlusion 输入映射到 material.aoNodewebgpu_compute_rasterizer_ibl.html#L1448 则用 sampleMap( sourceMaterial.aoMap ).r 这种自定义 TSL 采样赋值:
resolveShadedMaterial.aoNode = sampleMap( sourceMaterial.aoMap ).r;
  1. 在光照上下文层面直接使用 AONode(如 new AONode( 你的AO节点 )),用于自定义光照流程。

5. 各光照模型如何消费 AO

AO 进入上下文后,由各 LightingModelindirect 阶段消费(基类接口见 LightingModel.js#L73 的空实现 ambientOcclusion())。四种内置模型的行为差异值得注意:

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. 小结与使用建议

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