后处理
Estella 通过 ping-pong 帧缓冲运行全屏后处理。加一个 PostProcessVolume 组件,启用你要的
效果,并调节它们的参数——在编辑器里或用代码。效果共享 ping-pong 缓冲,所以多个可以在一帧里
叠加,除各遍(pass)本身外没有额外的目标开销。
| 效果 | 作用 | 关键 uniform |
|---|---|---|
bloom |
亮部辉光 | threshold、intensity、radius |
tonemap |
ACES filmic 色调映射 | exposure |
colorGrade |
曝光 / 对比度 / 饱和度 / 白平衡 | exposure、contrast、saturation、temperature、tint |
lutGrade |
通过查找表纹理(LUT)调色 | u_lut(纹理)、intensity |
vignette |
屏幕边缘变暗 | intensity、softness |
chromaticAberration |
RGB 色散 | intensity |
blur |
高斯模糊 | intensity |
fxaa |
快速抗锯齿 | intensity |
lensDistortion |
桶形 / 枕形畸变 | strength、zoom |
pixelate |
块状量化 | pixelSize |
outline |
墨线描边(Sobel 边缘压暗) | intensity、threshold、thickness |
grayscale |
去色 | intensity |
上面是友好标签;在代码里它们带 u_ 前缀(u_intensity、u_exposure……)。多数效果
在默认值下是恒等(colorGrade、blur、chromaticAberration、fxaa、lensDistortion、
lutGrade),加进去不调参不改画面。少数一加就可见——grayscale(完全去色)、vignette、
bloom、outline、tonemap(ACES 曲线恒生效)——这些只在你需要时才启用。
HDR(线性色彩空间)
Section titled “HDR(线性色彩空间)”当项目以线性色彩空间渲染时(见光照),只要浮点渲染目标
可用——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
Section titled “使用 PostProcessVolume”加一个 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)。 |
纹理参数(LUT 与遮罩)
Section titled “纹理参数(LUT 与遮罩)”有些效果吃的是纹理而不只是标量。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。 - 用体积做局部(某区域的氛围),用
PostProcessAPI 做固定的相机管线。 - 过渡时用
weight淡化体积,而不是硬开关。 - 低端/移动端让栈保持简短——每一遍都是一次全屏绘制。