跳转到内容

后处理

Estella 通过 ping-pong 帧缓冲运行全屏后处理。加一个 PostProcessVolume 组件,启用你要的 效果,并调节它们的参数——在编辑器里或用代码。效果共享 ping-pong 缓冲,所以多个可以在一帧里 叠加,除各遍(pass)本身外没有额外的目标开销。

效果 作用 关键 uniform
bloom 亮部辉光 thresholdintensityradius
tonemap ACES filmic 色调映射 exposure
colorGrade 曝光 / 对比度 / 饱和度 / 白平衡 exposurecontrastsaturationtemperaturetint
lutGrade 通过查找表纹理(LUT)调色 u_lut(纹理)、intensity
vignette 屏幕边缘变暗 intensitysoftness
chromaticAberration RGB 色散 intensity
blur 高斯模糊 intensity
fxaa 快速抗锯齿 intensity
lensDistortion 桶形 / 枕形畸变 strengthzoom
pixelate 块状量化 pixelSize
outline 墨线描边(Sobel 边缘压暗) intensitythresholdthickness
grayscale 去色 intensity

上面是友好标签;在代码里它们带 u_ 前缀(u_intensityu_exposure……)。多数效果 在默认值下是恒等(colorGradeblurchromaticAberrationfxaalensDistortionlutGrade),加进去不调参不改画面。少数一加就可见——grayscale(完全去色)、vignettebloomoutlinetonemap(ACES 曲线恒生效)——这些只在你需要时才启用。

当项目以线性色彩空间渲染时(见光照),只要浮点渲染目标 可用——WebGPU 恒可用,WebGL2 上是几乎无处不在的 EXT_color_buffer_float——效果链就跑在 半浮点 HDR 上。超过 1.0 的光照累积(intensity 大于 1 的 Light2D)会存活进效果链, 而不是在 8 位存储处被压平:

  • threshold 大于 1 的 bloom 只让超范围能量泛光——亮光发辉光,普通 LDR 美术保持锐利。 这就是经典的“自发光辉光”配方:拉高灯的 intensity,把 threshold 设在约 1.2–1.5。
  • tonemap 收到真正的 HDR 输入,ACES 曲线得以收卷高光,而不是对已压平的值做整形。
  • 不加 tonemap 时,超范围值在最终的线性→sRGB 编码处直接截断——轻微辉光没问题,大面积过曝会显得生硬。

伽马模式(或无浮点目标能力)下效果链仍是 LDR,行为与从前一致;此时 threshold 超过约 0.75 就永远不会触发。

加一个 PostProcessVolume,启用效果,设置参数。体积在重叠处混合,所以你可以按区域驱动 效果(比如一个“水下”体积),或在过渡中淡入。

属性 类型 默认 说明
effects list [] 每个效果的 { type, enabled, uniforms }
isGlobal boolean true 全局应用 vs 仅在体积形状内。
shape 'box' | 'sphere' 'box' 局部体积的边界形状。
size Vec2 {5, 5} 体积范围(局部体积)。
priority number 0 重叠处优先级高者胜。
weight number 1 混合权重,0..1(淡化整个体积)。
blendDistance number 0 体积边缘的衰减距离。

固定的相机管线用 PostProcess 资源。栈初始为空——每个效果加一个 pass。用 getEffectDef(type).factory() 从注册表拿到效果的着色器,再设它带 u_ 前缀的 uniform, 把栈绑到相机:

import { defineSystem, Query, Res, Camera, PostProcess, getEffectDef } from 'esengine';
const setup = defineSystem([Query(Camera), Res(PostProcess)], (q, pp) => {
const grade = getEffectDef('colorGrade')!;
for (const [camera] of q) {
const stack = pp.createStack();
stack.addPass('colorGrade', grade.factory())
.setUniform('colorGrade', 'u_saturation', 1.3)
.setUniform('colorGrade', 'u_contrast', 1.1);
pp.bind(camera, stack); // pp.unbind(camera) to remove
}
});

多 pass 的效果(如 bloom)带 def.multiPass;逐个 sub-pass 加进去 (for (const p of grade.multiPass!) stack.addPass(p.name, p.factory()))。固定管线之外, PostProcessVolume 更省事——它会替你解析多 pass 与纹理参数。

方法 说明
PostProcess.createStack() 创建一个空效果栈。
stack.addPass(name, shader) 加一个效果 pass;shader 来自 getEffectDef(type).factory()(可链式)。
stack.setUniform(pass, uniform, value) 设一个带 u_ 前缀的参数(可链式)。
stack.setTexture(pass, uniform, handle) 绑定一个纹理参数,比如 LUT(可链式)。
stack.setEnabled(name, on) 开关一个已加入的 pass(可链式)。
PostProcess.bind(camera, stack) / unbind(camera) 附加 / 移除相机的栈。
getEffectDef(type) / getAllEffectDefs() / getEffectTypes() 查效果定义(着色器工厂 + uniform)。

有些效果吃的是纹理而不只是标量。lutGrade 采样一张颜色查找表——一张 2D 打包的 1024×32 PNG(32³ 色立方切成 32 片)——于是你可以在图像编辑器里做好整套调色再丢进来。 先加 pass,再用 setTexture 绑定纹理(或在 PostProcessVolume 上,把它列在该效果的 textures: { u_lut: <ref> } 下):

stack.addPass('lutGrade', getEffectDef('lutGrade')!.factory())
.setTexture('lutGrade', 'u_lut', lutHandle)
.setUniform('lutGrade', 'u_intensity', 1);
  • 顺序有讲究——pass 按你加入的顺序运行(或在体积上按其 effects[] 的顺序)并共享缓冲, 合理的链是 bloom → tonemap → 调色 → vignette。
  • 用体积做局部(某区域的氛围),用 PostProcess API 做固定的相机管线。
  • 过渡时weight 淡化体积,而不是硬开关。
  • 低端/移动端让栈保持简短——每一遍都是一次全屏绘制。