mcpField
P
postprocessing

postprocessing

github.com/drawcall-ai创意与媒体125 次安装
代码仓库

关于 postprocessing

postprocessing 是一份指导在 Three.js 渲染流程中使用 postprocessing 库的技能文档,涵盖 HDR 配置、效果通道组合,以及透明物体光晕和着色器异常值导致的泛光闪屏排查。

主要功能

  1. 01配置 EffectComposer 效果通道
  2. 02设置环境光遮蔽与泛光
  3. 03配置 HDR 缓冲区和色调映射
  4. 04处理透明物体的 AO 瑕疵
  5. 05排查 NaN 和 Inf 引发的泛光闪屏
  6. 06介绍其他全屏效果

适用场景

  1. 01为场景添加接触阴影和柔和辉光
  2. 02配置 HDR 渲染与色调映射
  3. 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 通道和片段着色器复杂的效果尤其有益。

评论

支持 Markdown