three.js DirectionalLightNode 深度解析:TSL 节点化平行光的实现与用法
导读
DirectionalLightNode 是 three.js 节点化光照系统中的平行光(Directional Light)表示模块,它将传统场景中的 DirectionalLight 光源对象翻译为可供 WebGPU 节点渲染管线与 NodeMaterial(TSL,Three Shading Language)消费的着色节点。通过阅读本文,你将掌握 DirectionalLightNode 的继承关系、构造方式、平行光方向与颜色在源码中如何计算,以及该节点在渲染器中的自动注册与实例化机制,从而在 Node 材质工作流中更准确地使用平行光。
模块定位与继承链
在 three.js 的节点系统中,每个灯光类型都对应一个“灯光节点”(lighting node),DirectionalLightNode 即平行光的专属节点。它的完整继承链为:
EventDispatcher → Node → LightingNode → AnalyticLightNode → DirectionalLightNode
- LightingNode:所有照明节点的基类,继承自
Node,声明了isLightingNode标志,构造时以'vec3'作为节点输出类型。 - AnalyticLightNode:解析式(点光、平行光、聚光等)灯光的通用基类,负责把光源对象的颜色、强度同步进节点统一变量,并内置阴影接入逻辑。
DirectionalLightNode:在基类之上实现“平行光”特有的一步——直接光照数据{ lightDirection, lightColor }的推导。
该模块的实现位于 src/nodes/lighting/DirectionalLightNode.js,并经由 src/nodes/Nodes.js 统一导出,供各渲染器节点库引用。
构造函数与参数说明
依据官方 API 文档(docs/pages/DirectionalLightNode.html.md),其构造函数签名如下:
new DirectionalLightNode( light : DirectionalLight )
| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
light |
DirectionalLight |
null |
要被节点化的平行光源对象 |
对应源码实现位于 src/nodes/lighting/DirectionalLightNode.js#L22-L26:
constructor( light = null ) {
super( light );
}
构造参数直接透传给父类 AnalyticLightNode。参数缺省时默认 null,这也意味着允许构造一个尚未绑定光源的空节点。
传入 light 后发生了什么(继承自 AnalyticLightNode):
this.light = light保存光源引用;- 构造颜色节点
colorNode:若光源本身配置了colorNode则直接复用,否则创建一个基于this.color的 uniform 节点,并归入renderGroup统一变量组:this.colorNode = ( light && light.colorNode ) || uniform( this.color ).setGroup( renderGroup ); this.color会在每一帧从light.color * light.intensity刷新(见下文“逐帧更新”);- 若光源带阴影(
light.shadow存在),还会注册dispose监听器,光源释放时同步释放关联的ShadowNode。
此外,节点类声明了静态 type 访问器返回 'DirectionalLightNode',用于按类型名索引或调试。
谁在创建它:节点库注册与自动实例化
DirectionalLightNode 通常不需要用户手动 new,而是由渲染器内部的节点库(Node Library)在把灯光接入节点着色器时自动创建。
在 WebGPU 节点渲染器中,两套内置节点库都完成了平行光到节点类的注册:
- src/renderers/webgpu/nodes/BasicNodeLibrary.js#L46:
this.addLight( DirectionalLightNode, DirectionalLight ); - src/renderers/webgpu/nodes/StandardNodeLibrary.js#L79:同样注册了一对映射。
注册背后的机制位于 src/renderers/common/nodes/NodeLibrary.js:
getLightNodeClass( light ):根据光源对象的构造函数(light.constructor)在lightNodes弱映射中反查对应的灯光节点类;addLight( lightNodeClass, lightClass ):把DirectionalLightNode ↔ DirectionalLight这对关系写入映射。
真正实例化的位置在 src/nodes/lighting/LightsNode.js 的 setupLightsNode( builder ):遍历场景收集到的灯光,通过 nodeLibrary.getLightNodeClass( light.constructor ) 取到类后调用 new lightNodeClass( light ),并用 WeakMap 按光源对象缓存实例(_lightsNodeRef)。每帧该结果经 getHash()(以 light.uuid 为哈希)做节点哈希缓存,若灯光集合未变化则直接复用既有构建产物。
因此,把 DirectionalLight 加入场景后,平行光的全部计算会自动经由 DirectionalLightNode 汇入节点的着色管线。
核心实现:setupDirect 如何产出“平行光”
DirectionalLightNode 自身只覆写了一个抽象方法 setupDirect(),源码见 src/nodes/lighting/DirectionalLightNode.js#L28-L35:
setupDirect() {
const lightColor = this.colorNode;
const lightDirection = lightTargetDirection( this.light );
return { lightDirection, lightColor };
}
它返回一个描述“直接光照”的对象,包含两项:
lightDirection:平行光照射方向(视图空间下的单位方向向量);lightColor:灯光颜色节点,供当前光照模型(LightingModel)做漫反射/镜面反射计算。
平行光方向的计算链
lightDirection 来自 TSL 辅助函数 lightTargetDirection( light ),定义于 src/nodes/accessors/Lights.js#L139:
export const lightTargetDirection = ( light ) =>
cameraViewMatrix.transformDirection( lightPosition( light ).sub( lightTargetPosition( light ) ) );
其语义完全对齐 DirectionalLight 的“目标定向光(Target Direct Light)”模型:
lightPosition( light ):每帧从light.matrixWorld取世界坐标作为 uniform(见 src/nodes/accessors/Lights.js#L84-L90);lightTargetPosition( light ):每帧从light.target.matrixWorld取目标点世界坐标(见 src/nodes/accessors/Lights.js#L100-L106);- 两向量相减得到“光源→目标”的方向向量;
- 再经
cameraViewMatrix.transformDirection(...)变换进视图空间,得到 shader 中可直接使用的光照方向。
换言之,DirectionalLightNode 并不像 PointLightNode 那样按“表面位置→光源位置”逐像素推导方向,而是依赖 position 与 target 两点一次性确定,这正是“无限远、光线彼此平行”这一物理抽象的实现证据。相关实现佐证可从光源类源码 src/lights/DirectionalLight.js 看到:该光源注释明确指出“旋转对它无效”,方向始终由 position → target 决定,且默认位置取自 Object3D.DEFAULT_UP(即正上方 (0,1,0))。
一个侧面佐证:同样使用 lightTargetDirection 的是 SpotLightNode——聚光灯用它计算 lightDirection 与圆锥轴方向的点积,从而形成光锥衰减;而平行光直接以它为最终照明方向。
颜色与逐帧刷新
平行光颜色的物理值在 AnalyticLightNode.update() 中每帧刷新(src/nodes/lighting/AnalyticLightNode.js#L299-L305):
update( /*frame*/ ) {
const { light } = this;
this.color.copy( light.color ).multiplyScalar( light.intensity );
}
因为 updateType = NodeUpdateType.FRAME(src/nodes/lighting/AnalyticLightNode.js#L97),上述 uniform 节点在每帧渲染前被刷新,从而保证运行时修改 light.color / light.intensity 能立即反映到最终画面上。
节点化的光照与阴影协同
作为 AnalyticLightNode 的子类,DirectionalLightNode 自动继承了一套完整的“构建—接入”流程,见父类 setup( builder )(src/nodes/lighting/AnalyticLightNode.js#L255-L290):
- 若
light.castShadow为真且当前物体receiveShadow为真,则调用setupShadow( builder ),把阴影并入颜色计算(this.shadowColorNode = this.colorNode.mul( shadowNode ),原颜色节点备份到baseColorNode); - 调用
setupDirect( builder )得到DirectionalLightNode的直接光照数据; - 通过
builder.lightsNode.setupDirectLight( builder, this, directLightData )把数据交给当前LightingModel的接口方法(见 src/nodes/lighting/LightsNode.js#L303)。
阴影开关同样受全局渲染器配置约束:renderer.shadowMap.enabled === false 时跳过阴影计算(src/nodes/lighting/AnalyticLightNode.js#L208)。平行光对应的阴影对象类型为 DirectionalLightShadow(见 src/lights/DirectionalLight.js#L77),因此使用普通 shadowMap 工作流时,DirectionalLight.castShadow = true 与 renderer.shadowMap.enabled = true 的既有经验在节点渲染管线中依然成立。
在场景中的实际用法
由于 DirectionalLightNode 由节点库自动接入,开发者只需像往常一样操作光源对象,在 WebGPURenderer + 节点材质(NodeMaterial / TSL)场景下即可获得节点化光照,例如:
import * as THREE from 'three';
import { WebGPURenderer } from 'three/webgpu';
const renderer = new WebGPURenderer();
renderer.shadowMap.enabled = true;
await renderer.init();
const scene = new THREE.Scene();
// 平行光位置决定“光的来向”:指向其 target
const directionalLight = new THREE.DirectionalLight( 0xffffff, 1 );
directionalLight.position.set( 5, 10, 7 );
scene.add( directionalLight );
// 如需移动照射目标,必须把 target 加入场景
scene.add( directionalLight.target );
// 需要阴影时开启(shadow.camera 为 DirectionalLightShadow 默认配置)
directionalLight.castShadow = true;
注意事项:
- 方向不由旋转决定:平行光的方向仅由
position与target决定,旋转该光源对象不会改变照射方向; - 目标需要入场景:若要让
target跟随或指向某个物体,需先将target加入场景后再设置坐标(见 src/lights/DirectionalLight.js#L59-L70 的注释说明); - 默认朝向:未显式设置位置时,光源默认位于世界坐标
(0,1,0)(Object3D.DEFAULT_UP),方向指向原点方向的目标点。
对于需要自行组织节点图的高级用法(例如把平行光方向接入自定义计算),DirectionalLightNode 可从 src/nodes/Nodes.js 中按名称导入,再以 new DirectionalLightNode( light ) 手动创建;一般情况下并不建议这样做,因为 LightsNode 会在构建阶段为场景内每个 DirectionalLight 自动完成该节点的创建、缓存与哈希复用。
小结与延伸阅读
DirectionalLightNode 是理解 three.js“灯光节点化”的极佳入口:它体量虽小(仅覆写 setupDirect),却串联起光源类、节点库注册、逐帧 uniform 刷新、阴影接入与光照模型调用等整条 TSL 管线。对它的解读同样适用于 PointLightNode、SpotLightNode、AmbientLightNode 等同类节点(均位于 src/nodes/lighting/)。
可继续在仓库中深入验证的路径:
- 官方 API 页:docs/pages/DirectionalLightNode.html.md
- 节点实现:src/nodes/lighting/DirectionalLightNode.js
- 父类与统一变量逻辑:src/nodes/lighting/AnalyticLightNode.js
- 灯光方向/位置 TSL 辅助函数:src/nodes/accessors/Lights.js
- 光源对象本体:src/lights/DirectionalLight.js
- 灯光节点收集与实例化:src/nodes/lighting/LightsNode.js
- 节点库注册示例:src/renderers/webgpu/nodes/BasicNodeLibrary.js、src/renderers/common/nodes/NodeLibrary.js
- 光源单元测试:test/unit/src/lights/DirectionalLight.tests.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 StartedRust0627
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