three.js GodraysNode 深度指南:基于 TSL 的屏幕空间光线步进体积光后处理节点
GodraysNode 是 three.js 中一个基于 TSL(Three Shading Language)的后处理节点,用于为场景添加**屏幕空间光线步进(screen-space raymarching)体积光(godrays)**效果——即光线从光源发散穿过尘埃/雾气时形成的可见光束。本文围绕该节点的核心文档,结合其源码 GodraysNode.js 与其配套的 bilateralBlur()、depthAwareBlend() 工具,系统讲解它的工作原理、参数语义、完整接入流程、限制条件与调参实战,帮助你在自己的 WebGPU 渲染管线中快速落地这一特效。
本文主题对应的可运行参考示例位于 webgpu_postprocessing_godrays.html。
什么是 GodraysNode:后处理链中的定位
在 three.js 的 TSL 后处理体系中,GodraysNode 是一个 Post-Processing Node,它继承自 EventDispatcher → Node → TempNode。它的作用是以屏幕空间光线步进的方式,基于一张场景深度纹理计算并渲染体积光,输出一张独立的 Godrays 结果纹理,供后续与主场景(beauty pass)合成。
它的典型后处理链路如下(源码注释与本节点顶部文档一致):
const godraysPass = godrays( scenePassDepth, camera, light );
const blurPass = bilateralBlur( godraysPassColor ); // 可选模糊
const outputBlurred = depthAwareBlend( scenePassColor, blurPassColor, scenePassDepth, camera, { blendColor, edgeRadius, edgeStrength } ); // 合成
三个环节各司其职:
- godrays():计算体积光本身,输出原始(含噪点/步进伪影)的光照纹理;
- bilateralBlur():文档明确指出,在体积光计算完成后强烈建议对其结果做一次双边模糊,以缓解光线步进与噪声伪影(双边模糊相较高斯模糊能在平滑的同时保持锐利边缘,相关实现见 BilateralBlurNode.js);
- depthAwareBlend():与场景 pass 的合成理想情况下使用该函数,它能通过深度感知的采样偏移缓解锯齿与漏光(light leaking)问题,实现见 depthAwareBlend.js。
GodraysNode 是一个 addon(附加组件),不属于 three.js 核心库,必须显式导入。其来源自社区方案 three-good-godrays,已被官方收纳于 examples/jsm/tsl/display/ 目录下。
导入与快速开始
由于 GodraysNode 是 addon,需像其他附加模块一样显式引入:
import { godrays } from 'three/addons/tsl/display/GodraysNode.js';
在 WebGPU 渲染管线中,一个最小可用的接入流程(从示例 webgpu_postprocessing_godrays.html 提炼)为:
import * as THREE from 'three/webgpu';
import { pass } from 'three/tsl';
import { godrays } from 'three/addons/tsl/display/GodraysNode.js';
import { bilateralBlur } from 'three/addons/tsl/display/BilateralBlurNode.js';
import { depthAwareBlend } from 'three/addons/tsl/display/depthAwareBlend.js';
// 渲染器与阴影
const renderer = new THREE.WebGPURenderer();
renderer.shadowMap.enabled = true;
// 主场景 pass
const scenePass = pass( scene, camera );
const scenePassColor = scenePass.getTextureNode( 'output' );
const scenePassDepth = scenePass.getTextureNode( 'depth' );
// 体积光
const godraysPass = godrays( scenePassDepth, camera, pointLight );
const godraysPassColor = godraysPass.getTextureNode();
// (可选)双边模糊
const blurPass = bilateralBlur( godraysPassColor );
const blurPassColor = blurPass.getTextureNode();
// 深度感知合成
const outputBlurred = depthAwareBlend( scenePassColor, blurPassColor, scenePassDepth, camera, {
blendColor: uniform( color( 0xf6287d ) ),
edgeRadius: uniform( int( 2 ) ),
edgeStrength: uniform( float( 2 ) ),
} );
renderPipeline.outputNode = outputBlurred;
构造函数与参数语义
new GodraysNode( depthNode, camera, light )
- depthNode : TextureNode —— 表示场景深度的纹理节点(即上例中的
scenePassDepth)。 - camera : Camera —— 用于渲染场景的相机,节点在每帧
updateBefore()中需要它的矩阵、近远平面等参数(源码第 133–173 行读取了相机的matrixWorld、projectionMatrixInverse、near、far)。 - light : DirectionalLight | PointLight —— 为之生成体积光的光源,目前仅支持方向光与点光源(两者阴影处理分支不同,见下文源码分析)。
工厂函数签名与构造函数完全一致:
export const godrays = ( depthNode, camera, light ) => new GodraysNode( depthNode, camera, light );
构造器内部的私有初始化
查看源码 GodraysNode.js 可发现,除公共属性外,构造器还预分配了:
- 相机世界矩阵、相机逆投影矩阵、相机世界位置等 uniform;
- 相机与阴影相机的
near/far引用节点(reference( 'near', 'float', light.shadow.camera )); - 6 组用于构建光源阴影包围盒平面(
_fNormals/_fConstants)的 uniform 数组; - 一个 关闭了 depth buffer 的内部
RenderTarget(名称为'Godrays'),其纹理经passTexture( this, this._godraysRenderTarget.texture )包装成PassTextureNode; - 一个名为
'Godrays'的NodeMaterial。
这些细节说明:体积光效果需要一个额外的全屏 pass 渲染到一个中间 render target,再以纹理形式供后续节点采样。
可调属性(Uniform)详解
节点对外暴露的可调参数全部是 UniformNode,因此既可直接赋值,也可在运行时通过 GUI 实时修改(示例中即通过 godraysPass.raymarchSteps.value 等方式绑定控制面板)。
.density : UniformNode<float>(默认 0.7)
光线累积速率。源码注释描述为“Higher values roughly equate to more humid air/denser fog”——值越高,等效于空气越湿润/雾气越浓,体积光在步进采样中的累积就越快、越明显。调参区间示例中为 0–1。
.depthNode : TextureNode
保存 beauty pass 深度纹理的节点引用(构造参数),供 setup() 中逐像素重建世界坐标与阴影判断使用。
.distanceAttenuation : UniformNode<float>(默认 2)
距离衰减系数。更高的值会加快体积光随距离(远离光源)衰减的速度,使光束更早收敛、影响范围更短。从源码第 567 行可见它直接参与光照累积的幂次衰减计算:
pow( shadowInfo.y.div( this._shadowCameraFar ).oneMinus(), this.distanceAttenuation )
其中 shadowInfo.y 是当前采样点到光源的深度(视空间),因此该项使远处样本的贡献按指数级缩小。示例中 GUI 调参范围取 0–5。
.maxDensity : UniformNode<float>(默认 0.5)
最大密度/亮度上限。限制体积光累积后的最大亮度,防止过曝。从输出公式看,它作为 clamp( ..., 0, this.maxDensity ) 的上界发挥作用(源码第 573 行)。示例 GUI 范围 0–1。
.raymarchSteps : UniformNode<uint>(默认 60)
光线步进采样步数。步数越多,体积光质量越高、条带伪影越少,但性能开销越大。实际步数并非固定值——源码中引入了 交织渐变噪声(interleavedGradientNoise)进行时域抖动,让每像素的采样数在 raymarchSteps ± (raymarchSteps/8 + 2) 范围内波动,以打散条带伪影(第 557–559 行)。示例 GUI 范围 24–120,推荐从 60 起步按画质/性能权衡调整。
.resolutionScale : number(默认 0.5)
内部渲染分辨率缩放系数。setSize() 在把绘制缓冲尺寸应用到内部 RenderTarget 前,会先乘上该系数并取整:
width = Math.round( this.resolutionScale * width );
height = Math.round( this.resolutionScale * height );
this._godraysRenderTarget.setSize( width, height );
默认 0.5 意味着体积光以半分辨率计算,这是性价比很高的默认值——体积光属低频光照信号,降采样后配合模糊几乎无损观感,却能显著节省填充率开销。
.updateBeforeType : string(默认 'frame')
该节点在每帧 updateBefore() 中渲染自身效果,因此设为 NodeUpdateType.FRAME。它覆写了 TempNode.updateBeforeType,并由节点帧循环驱动。
生命周期方法
.getTextureNode() : PassTextureNode
返回效果结果的纹理节点(即内部 RenderTarget 纹理的 PassTextureNode 包装),用于把体积光结果接入后续模糊或合成节点。
.setSize( width, height )
设置效果的渲染尺寸。如前所述,实际写入内部 RenderTarget 的尺寸是传入值乘以 resolutionScale 后的结果。它在 updateBefore() 中每帧根据 renderer.getDrawingBufferSize() 自动调用(源码第 278–279 行),一般无需手动调用。
.updateBefore( frame : NodeFrame )
每帧渲染效果一次,是节点的“渲染主循环”,流程为(源码第 270–303 行):
- 通过
RendererUtils.resetRendererState()保存并重置渲染器状态; - 取当前绘制缓冲尺寸并调用
setSize()同步内部 RenderTarget; - 用
QuadMesh(命名为'Godrays')配合内部 NodeMaterial 执行一次全屏渲染; - 渲染前设置
_updateLightParams()更新光源阴影参数,并把相机世界位置同步到 uniform; - 用白色清除后,将渲染目标切到内部 Godrays RenderTarget 并渲染四边网格;
RendererUtils.restoreRendererState()恢复渲染器状态。
.setup( builder : NodeBuilder ) : PassTextureNode
构建节点 TSL 代码的核心方法(源码第 352–588 行),它把 godrays 的 Fn 程序挂到内部 material 的 fragmentNode,并复用 builder.getSharedContext() 作为材质上下文,最后返回 _textureNode。具体做了:
- 深度重建与坐标换算:
getViewPosition()由 UV 与深度重建视空间坐标,再乘相机世界矩阵得世界坐标;若渲染器开启了对数深度缓冲(logarithmicDepthBuffer),会先通过logarithmicDepthToViewZ()与viewZToPerspectiveDepth()换算(第 361–366 行); - 光源包围盒裁剪:对方向光与点光源分别用 6 个平面(点光源为以光源为中心、半径取阴影相机 far 的立方体面)构造包围盒。射线起点在盒内时从起点步进;起点在盒外时,通过求交把起点/终点夹到盒内“最近点”,避免在无光空间空走(第 452–551 行);
- 阴影采样判断(inShadow):
- 点光源:取世界点到光源方向向量各分量绝对值的最大值构造视空间深度,与立方体阴影纹理深度比较(
cubeTexture( ... ).compare( depth )); - 方向光:用
lightShadowMatrix()变换世界坐标得到阴影 UV(注意翻转 Y),先做 [0,1] 视锥测试,再与 2D 阴影深度纹理比较; - 其他光源类型直接抛错
'THREE.GodraysNode: Unsupported light type.'——这与文档中“仅支持点光源与方向光”的限制一一对应。
- 点光源:取世界点到光源方向向量各分量绝对值的最大值构造视空间深度,与立方体阴影纹理深度比较(
- 步进累积:沿起点到终点以
mix()插值采样,每步调用inShadow(),将“处于阴影外”的贡献乘以密度与距离衰减叠加到illum(第 561–571 行); - 输出公式:最终
illum归一化后经exp(-illum)求补再clamp到[0, maxDensity],作为 vec3 输出,alpha 通道写入深度值以支持后续深度感知合成(第 573 行)。
.dispose()
释放内部资源(Godrays RenderTarget 与 NodeMaterial)。当效果不再需要时应调用,避免 GPU 资源泄漏。覆写了 TempNode.dispose。
完整集成范例:组合 blur + depthAwareBlend
真正落地时,推荐把体积光节点、模糊节点与深度感知合成节点串成一条完整后处理链。以下是官方示例 webgpu_postprocessing_godrays.html 中的核心集成代码:
renderPipeline = new THREE.RenderPipeline( renderer );
// beauty
const scenePass = pass( scene, camera );
const scenePassColor = scenePass.getTextureNode( 'output' );
const scenePassDepth = scenePass.getTextureNode( 'depth' );
// godrays
const godraysPass = godrays( scenePassDepth, camera, pointLight );
const godraysPassColor = godraysPass.getTextureNode();
// blur
const blurPass = bilateralBlur( godraysPassColor );
const blurPassColor = blurPass.getTextureNode();
// composite
const blendColor = uniform( color( 0xf6287d ) );
const edgeRadius = uniform( int( 2 ) );
const edgeStrength = uniform( float( 2 ) );
const outputBlurred = depthAwareBlend( scenePassColor, blurPassColor, scenePassDepth, camera, { blendColor, edgeRadius, edgeStrength } );
const outputRaw = depthAwareBlend( scenePassColor, godraysPassColor, scenePassDepth, camera, { blendColor, edgeRadius, edgeStrength } );
renderPipeline.outputNode = outputBlurred;
合成参数
depthAwareBlend( baseNode, blendNode, depthNode, camera, options )(见 depthAwareBlend.js)通过 8 点泊松盘采样检测当前像素邻域内的深度不连续(即物体边缘)。一旦发现边缘,就把采样坐标沿远离边缘的方向“推开”,从而抑制体积光在物体轮廓处产生的光晕/漏光:
| 参数 | 默认值 | 语义 |
|---|---|---|
blendColor |
Color(0xffffff) |
应用到体积光结果的染色颜色,通常取光源颜色(示例中为粉色 0xf6287d) |
edgeRadius |
2(int) |
检测深度边缘的搜索半径(像素),示例 GUI 范围 0–5 |
edgeStrength |
2(float) |
检测到边缘后把采样 UV 推开多远,示例 GUI 范围 0–5 |
baseNode |
— | 主场景/beauty pass 纹理 |
blendNode |
— | 待混合的效果(Godrays/Bloom 等)纹理 |
depthNode |
— | 场景深度纹理 |
camera |
— | 场景相机,用于深度线性化 |
双边模糊
bilateralBlur( textureNode, directionNode = null, sigma = 4, sigmaColor = 0.1 ) 定义在 BilateralBlurNode.js。相比高斯模糊对一切一视同仁地柔化,双边模糊会分析邻近像素的颜色/强度差异,差异过大(视为边缘)的像素会被排除在模糊之外,因此能在去除步进噪声的同时保住几何边缘。通过 sigma(空间核半径)与 sigmaColor(强度域阈值)可控制“抹平多少、保留多少”。
调试实时调参
官方示例还演示了如何在运行时用 GUI 绑定额外节点属性,便于直观观察参数作用(webgpu_postprocessing_godrays.html):
const godraysFolder = gui.addFolder( 'Godrays' );
godraysFolder.add( godraysPass.raymarchSteps, 'value', 24, 120 ).step( 1 ).name( 'raymarch steps' );
godraysFolder.add( godraysPass.density, 'value', 0, 1 ).name( 'density' );
godraysFolder.add( godraysPass.maxDensity, 'value', 0, 1 ).name( 'max density' );
godraysFolder.add( godraysPass.distanceAttenuation, 'value', 0, 5 ).name( 'distance attenuation' );
同时可在“开/关模糊”之间切换输出节点,对比 raw 与 blurred 两种合成效果:
if ( value === true ) {
renderPipeline.outputNode = outputBlurred;
} else {
renderPipeline.outputNode = outputRaw;
}
renderPipeline.needsUpdate = true;
限制与适用前提
使用前必须清醒认识以下限制(文档与源码双重确认):
- 仅支持点光源(PointLight)与方向光(DirectionalLight)。
setup()中_updateLightParams()与inShadow()均为这两种光源写了不同分支,其他光源类型会直接抛出THREE.GodraysNode: Unsupported light type.错误; - 依赖完整的阴影配置:渲染器必须开启阴影(
renderer.shadowMap.enabled = true),3D 物体必须既 castShadow 又 receiveShadow,主光源本身必须 castShadow。这是因为体积光的核心逻辑就是沿射线在光源阴影贴图里做遮挡采样——没有阴影贴图就没有“被遮挡/未被遮挡”的信息可供累积。示例中通过scene.traverse统一开启所有网格的投射/接收,并把光源设castShadow = true、微调shadow.bias = -0.00001、将 shadow map 提到 2048×2048(webgpu_postprocessing_godrays.html); - 该节点基于 WebGPU + TSL 后处理体系运行,需使用
three/webgpu与three/tsl入口及RenderPipeline(或等价节点渲染流程)。
示例中的灯光配置可作模板:用一个不投阴影的白球体 Mesh 标示光源视觉位置,实际发光的 PointLight 紧贴其后并投射阴影,场景周围以黑墙围成“暗室”以突出光束对比。
附:示例场景与源码路径速查
- 效果演示示例:examples/webgpu_postprocessing_godrays.html(WebGPU 管线)与 examples/webgl_postprocessing_godrays.html(WebGL 对应版本)
- 节点实现:examples/jsm/tsl/display/GodraysNode.js
- 双边模糊实现:examples/jsm/tsl/display/BilateralBlurNode.js
- 深度感知合成实现:examples/jsm/tsl/display/depthAwareBlend.js
- 演示场景模型:示例 HTML 中通过
GLTFLoader加载models/gltf/godrays_demo.glb(见 webgpu_postprocessing_godrays.html,模型文件位于 examples/models/gltf 目录下)
结合本文对参数语义、内部渲染流程与阴影依赖的梳理,你可以直接在现有 WebGPU 后处理管线中接入 GodraysNode,并依据画质与帧率目标依次调节 raymarchSteps(画质)、resolutionScale(性能)、density/maxDensity(亮度形态)与合成端的 edgeRadius/edgeStrength(边缘干净度)。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00