关于 postprocessing
postprocessing 是一份指导在 Three.js 渲染流程中使用 postprocessing 库的技能文档,涵盖 HDR 配置、效果通道组合,以及透明物体光晕和着色器异常值导致的泛光闪屏排查。
主要功能
- 01配置 EffectComposer 效果通道
- 02设置环境光遮蔽与泛光
- 03配置 HDR 缓冲区和色调映射
- 04处理透明物体的 AO 瑕疵
- 05排查 NaN 和 Inf 引发的泛光闪屏
- 06介绍其他全屏效果
适用场景
- 01为场景添加接触阴影和柔和辉光
- 02配置 HDR 渲染与色调映射
- 03排查白屏闪光或 VFX 暗色光晕
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ drawcall-ai/ skills --skill postprocessing安装后在 Claude Code 中输入/postprocessing确认已加载。
详细介绍
通过 postprocessing 进行后期处理
即使几何体正确,渲染效果也可能显得不够完善、过于平淡和原始;没有最后的润色步骤,画面就会如此。适度的润色是精致游戏观感的一部分,而非可有可无的附加项。它包含三个部分:用环境光遮蔽让物体通过接触阴影扎根于场景中,用微妙的泛光让自发光和明亮物体发光,再通过色调映射和柔和的色彩分级营造氛围。等场景本身已经呈现正确后再添加,并保持克制——过强的泛光或过度压暗的色彩分级,比不加效果更难看。
后期处理引入了通道和效果的概念,用全屏图像处理工具扩展常见的渲染工作流。以下章节会介绍适度的润色步骤、它所依赖的 HDR 和色彩空间设置、两种会破坏效果的故障模式(透明 VFX 周围出现暗色光晕,以及单个异常片段被涂抹成全屏白闪),以及如何使用库中的其他效果。
适度的润色步骤
润色效果是在正确配置的渲染器和合成器上使用一个克制的 EffectPass。先设置渲染器属性,再连接合成器,最后根据需要添加柔和的色彩分级。
对于后期处理工作流,WebGLRenderer 无需屏幕上的深度或模板缓冲区,并且最好优先使用独立 GPU。这里关闭抗锯齿,因为它由管线内部处理。
import { WebGLRenderer } from 'three'
const renderer = new WebGLRenderer({
powerPreference: 'high-performance',
antialias: false,
stencil: false,
depth: false,
})
EffectComposer 负责管理和运行通道。使用 RenderPass 作为第一个通道来清除缓冲区并渲染场景,以供后续处理。然后通过 EffectPass 渲染全屏图像效果。一个 EffectPass 可以将任意数量的效果合并到一个着色器中,因此应将润色效果组合到一个通道里,而不是为每个效果各串联一个通道。
环境光遮蔽需要场景法线:添加一个 NormalPass,并将它的 .texture 传给 SSAOEffect。单独构造 new SSAOEffect(camera) 而不提供法线缓冲区,不会渲染任何内容——这是最常见的 AO 错误。色调映射必须放在最后;同一个 EffectPass 中的效果按数组顺序应用,因此它应作为最后一个参数。
import {
EffectComposer, EffectPass, RenderPass, NormalPass,
SSAOEffect, BloomEffect, ToneMappingEffect, ToneMappingMode,
} from 'postprocessing'
import { HalfFloatType } from 'three'
const composer = new EffectComposer(renderer, { frameBufferType: HalfFloatType })
composer.addPass(new RenderPass(scene, camera))
// Ambient occlusion needs scene normals: add a NormalPass and feed its texture to SSAOEffect.
// new SSAOEffect(camera) alone (no normal buffer) renders nothing — this is the usual AO mistake.
const normalPass = new NormalPass(scene, camera)
composer.addPass(normalPass) // but hide transparent VFX for this pass — see "SSAO and transparent objects" below
const ssao = new SSAOEffect(camera, normalPass.texture, { worldDistanceThreshold: 20, worldDistanceFalloff: 5, radius: 0.1, intensity: 2 })
const bloom = new BloomEffect({ intensity: 0.4, luminanceThreshold: 0.85 }) // subtle — see below
const toneMapping = new ToneMappingEffect({ mode: ToneMappingMode.ACES_FILMIC })
// Tone mapping goes LAST. Effects in one EffectPass apply in array order.
composer.addPass(new EffectPass(camera, ssao, bloom, toneMapping))
renderer.setAnimationLoop(() => composer.render())
如需柔和的色彩分级,可在同一个 EffectPass 中、色调映射之前添加分级效果:BrightnessContrastEffect、HueSaturationEffect,或用于电影 LUT 的 LUT3DEffect。色彩分级应保持克制——只调整少量对比度或饱和度,不要使用浓重的色调。
所需的 HDR 和色彩空间设置
精致的渲染流程假设使用 HDR 线性工作流:高精度缓冲区、sRGB 输出色彩空间,以及在管线内部而非由渲染器完成的色调映射。
新应用应采用线性工作流进行色彩管理,后期处理会自动支持这一点。将 WebGLRenderer.outputColorSpace 设为 SRGBColorSpace,后期处理便会随之匹配。
默认情况下,后期处理使用 UnsignedByteType sRGB 帧缓冲区存储中间结果——这是硬件支持、效率与质量之间的权衡,因为线性结果通常至少需要每个颜色通道 12 位才能避免画质下降。使用这些低精度 sRGB 缓冲区时,颜色会被限制在 [0.0, 1.0],信息损失会集中在较暗的色谱范围,从而导致暗场景中出现明显的色带。线性高精度 HalfFloatType 缓冲区不存在这些问题,是桌面设备上 HDR 类工作流的首选。
构造合成器时启用高精度帧缓冲区:
import { HalfFloatType } from 'three'
const composer = new EffectComposer(renderer, {
frameBufferType: HalfFloatType,
})
色调映射是将 HDR 颜色转换为 LDR 输出颜色的过程。使用后期处理时,渲染器的 toneMapping 必须设为 NoToneMapping(默认值),并且必须启用高精度帧缓冲区——否则,颜色会在管线一开始就被映射到 [0.0, 1.0],早于色调映射发挥作用的时机。若要启用色调映射,请在管线末尾使用 ToneMappingEffect。
关于清除颜色的说明:单独使用渲染器时,色调映射不会应用于清除颜色,因为清除过程不涉及着色器。后期处理会作用于完整输入图像,因此色调映射会均匀地应用于整张图像。因此,使用后期处理与不使用后期处理时,清除颜色背景经过色调映射后的效果会有所不同,而后期处理的做法才是正确的。
故障模式
有两种故障会破坏精致的渲染效果:SSAO 在透明 VFX 周围刻出深色光晕,以及单个 NaN 或 Inf 片段通过泛光效果引发覆盖整个屏幕的白色闪光。
SSAO 与透明对象(VFX 周围的深色光晕)
NormalPass 会使用覆盖材质将整个场景渲染到法线缓冲区中——包括透明对象。但透明 VFX——加法式枪口闪光、粒子、曳光弹、光束、UI 平面——使用 depthWrite: false 渲染,因此永远不会进入 SSAO 采样的深度缓冲区。于是,SSAO 会发现有法线却没有对应的深度,并在每个对象周围刻出遮蔽光晕:若隐若现的深色四边形会跟随粒子移动,射击时最为明显。
这与通过将阴影代码拼接到 VFX 着色器中,导致 CSM 将 VFX 渲染为纯色深色四边形的故障不同——请参阅 lights 技能。
解决方法是让透明对象不参与 NormalPass,以使其法线与深度缓冲区匹配。可靠且通用的做法是仅在该通道期间隐藏所有透明对象,之后再恢复它们——使用两个空操作通道将 NormalPass 前后夹住。TransparentToggle 扩展了 Pass 并设置 this.needsSwap = false;其隐藏分支会遍历场景,将可见的透明对象推入 hidden 并将其设为不可见;恢复分支则会重新显示这些对象并清空 hidden。
import { Pass } from 'postprocessing'
// Hides (or restores) every transparent object so the NormalPass sees only opaque geometry.
class TransparentToggle extends Pass {
constructor(scene, hidden, hide) { super('TransparentToggle'); this.scene = scene; this.hidden = hidden; this.hide = hide; this.needsSwap = false }
render() {
if (this.hide) this.scene.traverse((o) => { if (o.visible && o.material?.transparent) { this.hidden.push(o); o.visible = false } })
else { for (const o of this.hidden) o.visible = true; this.hidden.length = 0 }
}
}
const hidden = []
composer.addPass(new TransparentToggle(scene, hidden, true)) // hide for the normal pass
composer.addPass(normalPass)
composer.addPass(new TransparentToggle(null, hidden, false)) // restore before the effect pass draws the lit color buffer
不透明的 AO 表面不受影响——它们仍会以接触阴影为对象提供落地感。
通过泛光传播的着色器 NaN(全屏白色闪光)
一个片段将 NaN(或 Inf)写入 HDR 缓冲区后,运行泛光效果时就会变成全屏白色闪光。其原理是:泛光的 mipmap 下采样会将该片段平均到每个更粗的 mip 中,因此一个被污染的像素会扩散到整个泛光纹理,随后叠加到整帧画面上。
其特征很明显:关闭后期处理后,闪光会消失——没有泛光时,错误值只会留在少数几个源像素上,通常不可见;闪光边缘生硬、完全不透明,呈纯白色且不会显示背景,并且只会在特定摄像机角度或位置出现,因为其源头取决于视角。
常见源头是 GLSL pow():其底数在数学上非负,但经浮点舍入后略小于零。对于 x < 0,pow(x, y) 的行为未定义:在将其实现为 exp(y * log(x)) 的驱动上会返回 NaN,在其他驱动上则会返回 0——因此闪光取决于 GPU/驱动,可能无法在你的机器上复现。菲涅耳/边缘光项就是典型情况:正对表面时,两个归一化向量的 abs(dot(...)) 可能经舍入变成 ~1.0000001,导致底数为负。
float fres = pow(1.0 - abs(dot(normalize(n), normalize(v))), 1.4); // NaN risk: base can be < 0
float ndv = clamp(abs(dot(normalize(n), normalize(v))), 0.0, 1.0); // clamp the base ≥ 0 first
float fres = pow(1.0 - ndv, 1.4); // safe
为每个 pow 的底数添加 max(0.0, base)(或使用 clamp)保护,并为输入可能是零向量的任何 normalize() 添加保护。非 NaN 的溢出也会以相同方式扩散:高于 HalfFloat 最大值(约 65504)的值会在合成器缓冲区中变成 Inf,并以相同方式扩散,因此请限制发光/加法输出的范围。
如果无法在本地复现问题,可将场景渲染到 FloatType 渲染目标,并扫描 readRenderTargetPixels 中的 NaN/Inf/超大值,以查找源头。若要确认某个可疑材质,可将 NaN 写入它的一个颜色 uniform,并验证整个屏幕是否被白色覆盖。
使用其他效果
该库提供的效果远多于精选效果集,而且它们的接入方式都相同——但游戏的最终效果应由少数经过谨慎挑选的效果构成,而不是把所有可用效果都用上。
目录中包含景深、暗角、轮廓(用于选择/高亮)、上帝之光、色差、故障、像素化、冲击波等效果。接入方式都一样:创建效果并将其放入 EffectPass。请查看软件包的导出内容,了解当前提供的效果及其选项,而不要猜测名称。
这种方式也更有利于性能。EffectPass 会自动整理给定的任意效果组合,并将它们合并到一个着色器中,从而尽量减少渲染操作;因此,即使组合多种效果,也能避免传统通道串联带来的性能损耗。此外,每种效果都可以选择自己的混合函数。所有全屏渲染操作还会使用一个铺满屏幕的三角形——与四边形相比,它更符合现代 GPU 的光栅化模式,并能避免沿屏幕对角线进行不必要的片段计算,这对 GPGPU 通道和片段着色器复杂的效果尤其有益。