首页
/ three.js AmbientLightDataNode 详解:动态光照模式下环境光批处理数据节点的原理与用法

three.js AmbientLightDataNode 详解:动态光照模式下环境光批处理数据节点的原理与用法

2026-09-05 12:47:31作者:彭桢灵Jeremy

本文基于 three.js 官方 API 文档 AmbientLightDataNode 展开,讲解这个"动态光照模式下的环境光批处理数据节点"的继承关系、内部实现(颜色累加、逐帧 uniform 更新、irradiance 注入),以及它在 DynamicLightsNodeDynamicLighting 中的调用链,帮助读者理解 three.js 如何做到"增删环境光不触发着色器重编译"。

1. AmbientLightDataNode 是什么

官方文档的定义非常精炼:

Batched data node for ambient lights in dynamic lighting mode.(动态光照模式下,用于环境光的批处理数据节点。)

继承链为:

EventDispatcher → Node → AmbientLightDataNode

它的源码位于 examples/jsm/tsl/lighting/data/AmbientLightDataNode.js,仅 61 行。理解它的最佳方式是把它放进一个具体场景:默认情况下,LightsNode 会为场景中的每盏灯生成一个独立的解析光节点(如 AmbientLightNode)。由于 light 的"种类与数量"会参与着色器缓存键(见 LightsNode.customCacheKey),运行时增删灯光往往导致材质着色器重新编译。而动态光照模式(DynamicLighting)的思路是把支持批处理的光汇总成统一数据:环境光本身没有方向、没有衰减,多盏环境光的作用等价于"颜色×强度"的向量相加,因此只需要**一个固定大小的 uniform(一个 vec3 颜色)**即可表达任意数量的环境光。AmbientLightDataNode 就是承载这个"求和结果"的节点。

2. 源码逐段解析

2.1 构造器:一个 uniform 颜色 + RENDER 级更新

构造函数源码

constructor() {

    super();

    this._color = new Color();
    this._lights = [];

    this.colorNode = uniform( this._color ).setGroup( renderGroup );
    this.updateType = NodeUpdateType.RENDER;

}

几个关键设计:

  • _colorcolorNode:内部持有一个 Color 实例,并包成 uniform 节点。setGroup( renderGroup ) 表示该 uniform 归属于渲染阶段的更新分组。由于 uniform 的大小在着色器里是编译期固定的,这与"光数量可变但着色器不变"的目标完全吻合。
  • updateType = NodeUpdateType.RENDER:声明该节点需要在每一帧渲染前执行 update()。这是"动态"二字的底层保障——灯光的 color/intensity 每帧可能被业务代码修改,而节点只需刷新 CPU 端数据,GPU 着色器纹丝不动。

构造函数没有参数(官方文档中的 new AmbientLightDataNode() 也是无参构造)。这是它与其他几个批处理数据节点(点光、聚光等需要 maxCount 来分配 uniform 数组)的本质区别:环境光无论多少盏,求和后都是一个 vec3,不需要按数量预留数组容量。这一点在 DynamicLightsNode.setupLightsNode 中有明确体现:_lightTypeToMaxProp 里只有 Directional/Point/Spot/Hemisphere 四种光型,没有 AmbientLight,因此代码走到 new DataNodeClass()(无参)分支。

2.2 setLights:绑定一批环境光

setLights( lights ) {

    this._lights = lights;

    return this;

}

setLights 只做一件事:保存当前帧应参与求和的环境光数组引用,并以 this 结尾支持链式调用。注意它不立即计算颜色——计算被推迟到渲染管线统一调度的 update() 中,这样多个数据节点可以在同一时机、按同一顺序刷新。

2.3 update:逐帧颜色累加

update() {

    this._color.setScalar( 0 );

    for ( let i = 0; i < this._lights.length; i ++ ) {

        const light = this._lights[ i ];

        this._color.r += light.color.r * light.intensity;
        this._color.g += light.color.g * light.intensity;
        this._color.b += light.color.b * light.intensity;

    }

}

update 实现 的逻辑是标准的"基色 × 强度"逐通道累加:先把 _color 清零,再对 _lights 中每盏 AmbientLightcolor * intensity 的向量和。这正好是 PBR/Phong 光照模型中环境项(irradiance)的物理含义——多盏环境光线性叠加等效于单盏"合成环境光"。

2.4 setup:注入 irradiance 上下文

setup( builder ) {

    builder.context.irradiance.addAssign( this.colorNode );

}

setup 实现 只有三行:向 builder.context.irradiance 累加赋值为 colorNode。也就是说,当 TSL 构建该节点时,它的求和颜色会被加入当前光照上下文的辐照度项,随后由光照模型(lighting model)统一用于计算间接漫反射分量。从源码结构看,AmbientLightDataNode 并不直接输出最终颜色,而是"向共享上下文贡献一份 irradiance",这与单个 AmbientLightNode 在光照模型中参与 diffuse 计算的职责一致,只是数据来源从"每灯一个节点"变成了"每类型一个节点"。

3. 在 DynamicLightsNode 中的调用链

AmbientLightDataNode 的宿主是 DynamicLightsNode。理解整条链路才能理解文档中 "in dynamic lighting mode" 的确切含义。

3.1 光型到数据节点的映射

const _lightTypeToDataNode = {
    AmbientLight: AmbientLightDataNode,
    DirectionalLight: DirectionalLightDataNode,
    PointLight: PointLightDataNode,
    SpotLight: SpotLightDataNode,
    HemisphereLight: HemisphereLightDataNode
};

映射表 将 5 种内置解析光映射到 5 个批处理数据节点类。能否进入批处理通道由 canBatchLight 决定:

const canBatchLight = ( light ) => {

    return light.isNode !== true &&
        light.castShadow !== true &&
        isSpecialSpotLight( light ) === false &&
        _lightTypeToDataNode[ light.constructor.name ] !== undefined;

};

条件拆开看:不是自定义 Node 光、不投影(castShadow === false)、不是带投影贴图/自定义颜色节点的"特殊聚光灯"、且构造器名在映射表内。标准的 AmbientLight 几乎总是满足全部条件,因此默认走 AmbientLightDataNode 通道;只有当用户显式给环境光设置 castShadow = true 这类异常状态时,才会退回默认的逐灯节点路径。

3.2 数据节点的创建与复用

setupLightsNode 中,同类型光先按 id 排序后分组(sortLights 保证顺序稳定),再按类型取出或创建数据节点:

let dataNode = this._dataNodes.get( typeName );

if ( dataNode === undefined ) {

    const DataNodeClass = _lightTypeToDataNode[ typeName ];
    const maxProp = _lightTypeToMaxProp[ typeName ];
    const maxCount = maxProp !== undefined ? this[ maxProp ] : undefined;

    dataNode = maxCount !== undefined ? new DataNodeClass( maxCount ) : new DataNodeClass();

    this._dataNodes.set( typeName, dataNode );

}

dataNode.setLights( typeLights );
lightNodes.push( dataNode );

AmbientLight 而言 maxPropundefined,所以永远是 new AmbientLightDataNode()。注意 _dataNodes 是一个 Map,数据节点创建一次后长期复用,后续帧只调 setLights 更新灯光数组——这正是"不重编译"的机制来源之一。

另外,第 222–231 行 有一个容易被忽略的细节:如果某类光上一帧存在、本帧全部被移除(lightsByType 中没有该类型),代码会执行 dataNode.setLights( [] )仍然把该数据节点推入 lightNodes。此时 update() 累加空数组,_color 保持 0,即环境光贡献自然归零,而着色器结构保持不变。

3.3 缓存键:为什么增删环境光不触发重编译

DynamicLightsNode.customCacheKey 的哈希策略是动态模式的灵魂:

  • 可批处理的光:只把光型名(如 AmbientLight)加入类型集合,参与最终哈希——不记录具体数量
  • 不可批处理的光(node 光、投影光、特殊聚光灯):逐灯记录 light.idcastShadow、聚光贴图/颜色节点等细粒度数据。

因此,运行时新增/移除一盏 AmbientLight,类型集合不变,customCacheKey 不变,TSL 判定着色器结构未变,不会重新编译;变化的只是 update() 里累加进 uniform 的数值。作为对照,默认 LightsNode.customCacheKey 会把每一盏灯的 id 都压入哈希,灯光数量一变缓存键就变——两套机制的差异即动态光照模式的价值所在。

运行时替换灯光数组的路径是 DynamicLightsNode.setLights_updateDataNodeLights:按类型重新分组,对每个已存在的数据节点调用 dataNode.setLights( ... )。同时 hasLights 被重写为 super.hasLights || this._dataNodes.size > 0,保证"场景里只剩环境光(数据节点通道)"时,系统依然认为场景有光。

4. 实战:启用动态光照模式

AmbientLightDataNode 一般不直接手动构造,而是通过 DynamicLighting 接入渲染器。DynamicLighting 的构造参数为:

参数 默认值 说明
maxDirectionalLights 8 批处理的平行光上限(决定 uniform 数组容量)
maxPointLights 16 批处理的点光上限
maxSpotLights 16 批处理的聚光灯上限
maxHemisphereLights 4 批处理的半球光上限

环境光不受数量上限约束(求和结果恒为单个 vec3)。官方示例 examples/webgpu_lights_dynamic.html 展示了完整用法:

import * as THREE from 'three/webgpu';
import { DynamicLighting } from 'three/addons/lighting/DynamicLighting.js';

const renderer = new THREE.WebGPURenderer( { antialias: true } );

// 一行开启动态光照模式,内部会为每个场景创建 DynamicLightsNode
renderer.lighting = new DynamicLighting();

// 场景中的 AmbientLight 将被 AmbientLightDataNode 批处理
scene.add( new THREE.AmbientLight( 0x404040, 0.5 ) );

该示例场景中摆放了 100 个使用 50 种 PBR 材质的网格,通过 Inspector 面板在运行时反复增删点光与 AmbientLight,官方页面描述为 "Opt-in DynamicLighting avoids shader recompilation when adding/removing supported lights"(选用的 DynamicLighting 在增删受支持灯光时避免着色器重编译)。渲染效果如下:

three.js webgpu_lights_dynamic 示例:动态光照模式下运行时增删灯光的渲染场景

运行前提:需要 WebGPU 渲染路径(WebGPURenderer + three/webgpu 构建),DynamicLighting 通过 renderer.lighting 属性挂载,内部经 getNode 按场景缓存 DynamicLightsNode 实例(QuadMesh 场景走默认 LightsNode)。

5. 小结:设计要点与适用边界

结合源码可以归纳出 AmbientLightDataNode 的三条设计要点:

  1. 数据结构恒定:用单个 Color uniform 表达任意多盏环境光的叠加,着色器里没有"循环 N 盏环境光"的展开,光数量变化不改变着色器结构;
  2. CPU 端逐帧求和NodeUpdateType.RENDER + update() 每帧重新累加,灯光参数修改零成本生效;
  3. 上下文注入而非独立输出setup 中向 builder.context.irradianceaddAssign,与光照模型的其他间接分量汇合,行为与单个 AmbientLightNode 在渲染结果上保持等价。

适用边界同样清晰:它只服务于 DynamicLighting/DynamicLightsNode 这条动态通道;投影光、自定义 Node 光、带投影贴图的聚光灯会被 canBatchLight 拦截,退回 LightsNode 的逐灯节点路径(该路径下灯光数量变化仍会触发重编译)。如果你需要为每盏环境光保留独立行为(例如各自使用 Node 定义颜色),则应直接使用 AmbientLightNode 或自定义光照节点,而不是这条批处理通道。

参考路径汇总:AmbientLightDataNode 源码DynamicLightsNode 源码DynamicLighting 源码LightsNode 基类官方示例页面API 文档页

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