首页
/ three.js TSL 深度解析:AnalyticLightNode 解析光节点基类与阴影、逐帧更新机制

three.js TSL 深度解析:AnalyticLightNode 解析光节点基类与阴影、逐帧更新机制

2026-09-05 22:04:57作者:冯梦姬Eddie

在 three.js 的节点着色语言(TSL,Three Shading Language)体系中,AnalyticLightNode 是所有“解析光”(方向光、点光、聚光灯、矩形区域光等可计算光照方向的真实光源)节点的统一基类。它负责把场景中的 Light 对象桥接到节点系统:逐帧同步颜色与强度、在构建期把光照方向与衰减数据注册进当前 LightingModel,并自动接管阴影的接入与释放。读完本文,你将理解 TSL 光照管线中“光源对象 → 光节点 → 光照模型 → 输出节点”的完整链路,以及阴影节点如何被挂载到光颜色上参与最终计算。

继承链与职责定位

AnalyticLightNode 的完整继承链为:

EventDispatcher → Node → LightingNode → AnalyticLightNode
  • LightingNode.js 是最底层的抽象,构造函数中调用 super('vec3') 声明输出类型并设置只读标志 isLightingNode,本身没有任何行为逻辑;
  • AnalyticLightNode.js 在其之上补齐了真实光源才需要的能力:颜色 uniform、阴影接入、逐帧 uniform 更新。

需要区分的是,“解析光”特指能够求出“从当前像素指向光源方向”这类方向向量的光源。从源码目录结构看,src/nodes/lighting/ 下的 DirectionalLightNodePointLightNodeSpotLightNodeRectAreaLightNodeIESSpotLightNodeProjectorLightNode 都继承自 AnalyticLightNode;而 AmbientLightNodeHemisphereLightNodeLightProbeNode 这类环境/间接光节点则直接继承 LightingNode,不走阴影与方向向量逻辑。

构造函数与核心属性

构造函数签名与文档一致:

new AnalyticLightNode( light = null )

结合 src/nodes/lighting/AnalyticLightNode.js 的构造函数,各属性含义如下:

属性 类型 默认值 说明
.light Light null 引用的光源对象,后续 getLightVectorgetHashupdate 都依赖它
.color Color new Color() 光的颜色值,每帧由 light.color * light.intensity 覆盖
.colorNode Node 光颜色的节点。若光源自身已设置 colorNode 则直接复用;否则基于 .color 创建 uniform 节点
.baseColorNode Node null 保存 colorNode 的原始引用;启用阴影后 .colorNode 会被替换为“颜色 × 阴影”的组合节点,baseColorNode 用于在阴影关闭或释放时还原
.shadowNode ShadowNode null 表示该光阴影的节点,由 setupShadowNode() 创建
.shadowColorNode Node null 表示阴影调制后的最终光颜色节点
.isAnalyticLightNode boolean(只读) true 类型测试标志
.updateType string 'frame' 覆写自 LightingNode,因为解析光节点需要逐帧更新

其中 colorNode 的初始化逻辑值得注意(见 AnalyticLightNode.js#L54):

this.colorNode = ( light && light.colorNode ) || uniform( this.color ).setGroup( renderGroup );

这意味着如果用户在光源上直接挂载了一个 TSL 颜色节点(例如程序化颜色或投射纹理),光节点会尊重用户节点;否则回退为标准的 uniform 节点,并归入 renderGroup 更新组,保证 uniform 数据按渲染帧同步。

updateType 被覆写为 NodeUpdateType.FRAME(即字符串 'frame')。四种更新频率定义在 src/nodes/core/constants.js#L20-L25NONE(不更新)、FRAME(每帧更新)、RENDER(每次 render 调用更新,比 frame 更细)、OBJECT(每个使用该节点的 Object3D 更新一次)。解析光选择 frame,是因为光颜色/强度、位置等属性通常按动画帧变化,逐帧同步一次 uniform 即可。

此外还有一个未在文档属性表中列出、但对渲染缓存至关重要的方法 getHash():它直接返回 this.light.uuid(见 AnalyticLightNode.js#L148-L152)。这个哈希会参与 LightsNode.jsgetHash(),用于判断同一组光源的着色器是否可以直接复用缓存。

setup:把光注册进光照模型

文档特别强调了一点:光照节点在 setup 中不返回输出节点,这与大多数普通 Node 不同。光照节点的核心职责是配置当前 LightingModel 并调用其接口方法。setup(builder) 的完整流程(见 AnalyticLightNode.js#L255-L290)可以拆解为四步:

setup( builder ) {

  this.colorNode = this.baseColorNode || this.colorNode; // 1. 每帧还原基础颜色节点

  if ( this.light.castShadow ) {

    if ( builder.object.receiveShadow ) {

      this.setupShadow( builder );                        // 2. 条件接入阴影

    }

  } else if ( this.shadowNode !== null ) {

    this.shadowNode.dispose();                            // 2'. 光不再投射阴影时主动释放
    this.shadowNode = null;
    this.shadowColorNode = null;

  }

  const directLightData = this.setupDirect( builder );    // 3. 抽象方法:子类给出颜色与方向
  const directRectAreaLightData = this.setupDirectRectArea( builder );

  if ( directLightData ) {

    builder.lightsNode.setupDirectLight( builder, this, directLightData );

  }

  if ( directRectAreaLightData ) {

    builder.lightsNode.setupDirectRectAreaLight( builder, this, directRectAreaLightData );

  }

}
  1. 还原颜色节点:阴影接入会改写 colorNode,所以每次 setup 开头先取回 baseColorNode(若存在),保证基础状态干净;
  2. 阴影条件接入:只有当 light.castShadow === true 且当前构建对象 builder.object.receiveShadow 为真时才调用 setupShadow(builder);反之如果上一帧还存在阴影节点,则立即 dispose() 释放——阴影资源的生命周期完全由光节点的 setup 自动管理;
  3. 抽象的直接光照数据setupDirect(builder)setupDirectRectArea(builder) 在基类中是空实现(标记 @abstract),必须由具体光类覆写,返回 { lightColor, lightDirection } 或矩形区域光数据;
  4. 交给 LightsNode 注册:拿到数据后调用 builder.lightsNode.setupDirectLight(...),最终触发 LightingModel.direct(...) 完成 PBR 等模型的具体光照累加(见 LightsNode.js#L303-L332)。

阴影管线:setupShadow、setupShadowNode 与 disposeShadow

setupShadow(builder) 是阴影与光照计算的汇合点(见 AnalyticLightNode.js#L204-L246),其内部行为:

  • 首先检查 renderer.shadowMap.enabled === false 则直接返回——全局关闭阴影时不做任何阴影构建;
  • 若尚未创建 shadowColorNode,则优先使用光源自定义的 light.shadow.shadowNode(经 nodeObject() 包装),否则调用 this.setupShadowNode() 创建默认节点;
  • 关键一行:this.shadowColorNode = this.colorNode.mul( shadowNode )。也就是说阴影结果被乘进光的颜色节点,之后整个光照模型对该光的任何使用(漫反射、高光、环境反射等)都自动带上阴影衰减,无需逐处修改;
  • 记录 this.baseColorNode = this.colorNode,保证 setupShadow 的幂等(第二次进入不会把“颜色×阴影”再乘一次阴影);
  • 若构建上下文提供了 builder.context.getShadow 钩子,则用它覆盖默认的阴影颜色节点,这为自定义光照系统(如体积光模型)提供了扩展点。

setupShadowNode() 的默认实现直接返回 TSL 函数 shadow( this.light ) 创建的 ShadowNode。该方法是刻意为子类保留的工厂方法——例如 PointLightNode.js#L83-L87 覆写为 pointShadow( this.light ),以便点光使用立方体贴图阴影。ShadowNode 内部完成了阴影贴图渲染目标创建、PCF/VSM 滤波选择、shadowCoord 计算与 updateBefore 中的阴影贴图按需重渲染等完整逻辑(见 ShadowNode.js#L595-L631ShadowNode.js#L792-L826)。

资源回收由两条路径共同保证:

  1. 光被销毁时:构造函数中如果检测到 light.shadow,会向 light 注册一个 'dispose' 监听器,触发 disposeShadow()dispose() 中会先移除该监听器(见 AnalyticLightNode.js#L99-L123);
  2. 阴影被关闭时setup()castShadow 为假且存在旧 shadowNode 时主动释放。

disposeShadow() 本身(见 AnalyticLightNode.js#L128-L146)依次释放 shadowNode、清空 shadowColorNode,并把 colorNode 还原为 baseColorNode,恢复为无阴影状态。

getLightVector:视图空间中的光照方向

getLightVector(builder) 返回一个 vec3 节点,语义是“从当前像素的视图空间位置指向光源视图空间位置的方向向量”:

getLightVector( builder ) {

  return lightViewPosition( this.light ).sub( builder.context.positionView || positionView );

}

AnalyticLightNode.js#L161-L165。这里 lightViewPosition( this.light )src/nodes/accessors/Lights.js 提供的 TSL 访问器;builder.context.positionView 允许特定构建上下文注入不同的观察位置,缺省时回退到标准 positionView 节点。子类正是基于这个向量计算距离与方向:例如 PointLightNode.setupDirect 先取 lightVector.normalize() 作为方向、.length() 作为距离,再经 getDistanceAttenuation 计算物理衰减(见 PointLightNode.js#L89-L98);SpotLightNode.setupDirect 则用它与目标方向的点积求出锥角余弦,配合 smoothstep 生成 penumbra 平滑衰减(见 SpotLightNode.js#L119-L164)。

逐帧更新:update(frame) 与具体光类的扩展

基类的 update(frame) 只有一行核心逻辑(见 AnalyticLightNode.js#L299-L305):

update( /*frame*/ ) {

  const { light } = this;

  this.color.copy( light.color ).multiplyScalar( light.intensity );

}

即每帧把 light.color × light.intensity 写入 this.color,进而经由 uniform 节点进入着色器。具体光类在此基础上覆写 update 补充各自特有的 uniform,例如:

  • PointLightNode.js#L67-L76:同步 cutoffDistanceNode(来自 light.distance)与 decayExponentNode(来自 light.decay);
  • SpotLightNode.js#L73-L85:额外同步 coneCosNodepenumbraCosNode,注意二者都预先转换为余弦值,着色器侧因此只需一次比较/插值即可判定锥角范围。

这种“基类管通用颜色/强度、子类管特有参数”的拆分,配合 updateType = 'frame',使得光源参数在动画中逐帧平滑更新而无需手动刷新着色器。

与 LightsNode 的协作:节点如何被创建与缓存

AnalyticLightNode 并不是用户手动创建的——它由 LightsNode.jssetupLightsNode(builder)(见 LightsNode.js#L236-L294)自动实例化:

  1. 将材质声明的光(materialLightings)与场景内置光(getBuiltinLights())合并后按 id 排序,保证构建顺序稳定;
  2. 对每个非节点化光源,通过 builder.renderer.library.getLightNodeClass( light.constructor )NodeLibrary.js 查询对应的光节点类(内部维护 Light.constructor → AnalyticLightNode.constructor 的映射,见 NodeLibrary.js#L128-L141),查不到则警告并跳过;
  3. 用模块级 WeakMap_lightsNodeRef)按光源对象缓存已创建的光节点实例,同一光源在多个材质间共享同一个节点;
  4. 复用判断时通过 getLightNodeById 匹配,而该函数正是依赖 lightNode.isAnalyticLightNode 标志做类型测试(见 LightsNode.js#L48-L62)——这也是文档中 isAnalyticLightNode 属性存在的直接原因。

LightsNode.customCacheKey()(见 LightsNode.js#L147-L175)进一步把每盏灯的 idcastShadow 状态编入着色器缓存键,聚光灯还会纳入 mapcolorNode 的哈希。从源码结构看,这意味着光源集合或其阴影状态发生变化时,TSL 会触发相应着色器重新生成;而仅改变光的颜色、强度、位置这类由 uniform 承载的参数,则只走 update 的逐帧同步,不产生重编译。

具体子类一览

以下光节点均以 AnalyticLightNode 为基类,源码全部位于 src/nodes/lighting/

节点类 来源文件 覆写的抽象方法 要点
DirectionalLightNode DirectionalLightNode.js setupDirect 方向取 lightTargetDirection( light ),无距离衰减,适合平行光
PointLightNode PointLightNode.js setupDirectsetupShadowNode 距离衰减 + 立方体阴影 pointShadow
SpotLightNode SpotLightNode.js setupDirect 锥角/半影 smoothstep 衰减 + 距离衰减,支持 light.map 或自定义 light.colorNode 投射
RectAreaLightNode RectAreaLightNode.js setupDirectRectArea 矩形区域光,走 lightingModel.directRectArea 分支
IESSpotLightNode IESSpotLightNode.js setupDirect 在聚光灯基础上叠加 IES 配光曲线采样
ProjectorLightNode ProjectorLightNode.js setupDirect 投影光,复用点光的距离衰减结构

其中 DirectionalLightNode.setupDirect(见 DirectionalLightNode.js#L28-L35)是最简实现:

setupDirect() {

  const lightColor = this.colorNode;
  const lightDirection = lightTargetDirection( this.light );

  return { lightDirection, lightColor };

}

返回的 { lightColor, lightDirection } 正是基类 setup 中传给 builder.lightsNode.setupDirectLight 的“直接光数据”,随后由当前 LightingModel(如 PBR 模型)完成 BRDF 累加并汇入 totalDiffuseNode / totalSpecularNode(见 LightsNode.js#L373-L436LightsNode.setuplightingModel.start / finish 的调用)。

小结与延伸阅读

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