后处理链中的一切 — TAA、泛光、GTAO、DDGI、天空 LUT — 都是一个
ComputeEffect,通过你会为自己使用的同一个公开 API
注册。没有特权内置路径,这意味着你写的特效拥有
与内置特效完全一样的能力。
四个成员。Name 用于诊断,
Phase 决定它何时运行,
Initialize 创建资源,
Record 派发。可选的
OnResize 重建任何按窗口尺寸分配的东西。
using Season.Rendering;
sealed class EdgeEffect : ComputeEffect
{
public const string TextureName = "compute://edges";
public override string Name => "edges";
public override ComputePhase Phase => ComputePhase.AfterScene;
ComputeKernel? _kernel;
int _width, _height;
public override bool Initialize(IGraphics graphics)
{
_width = graphics.Width;
_height = graphics.Height;
_kernel = graphics.CreateComputeKernel(new ComputeKernelDesc
{
Name = "edges",
Source = new ShaderSourceSet { Hlsl = Hlsl, EntryPoint = "CSMain" },
Bindings = /* ... */,
WorkgroupX = 8, WorkgroupY = 8, WorkgroupZ = 1
});
if (_kernel == null) return false;
return graphics.CreateComputeTexture(TextureName, _width, _height);
}
public override void Record(IGraphics graphics)
{
graphics.DispatchCompute(new ComputeDispatchArgs
{
Kernel = _kernel!,
Resources = /* ... */,
GroupsX = (_width + 7) / 8,
GroupsY = (_height + 7) / 8,
GroupsZ = 1
});
}
}
Initialize 返回 false 不是错误条件,它是
优雅降级路径:特效被丢弃,它创建的任何东西都不留下,
帧在没有它的情况下继续运行。用它处理任何你在启动时发现的事情 — 这台设备
不支持 compute、这个后端没有着色器源码、用户关掉的某个模式。
每个内置特效都这样做,这就是为什么同一个应用能跑在一台完全没有
compute 的机器上。
ShaderSourceSet 有一个
Hlsl、Glsl、
Msl 和 Wgsl 槽,加上一个
EntryPoint,默认为
CSMain。空槽意味着该后端无法运行这个内核,并且
在那里注册会干净地失败,而不是拖到绘制时。
| 类型 | 用途 |
|---|---|
ComputeKernelDesc | 名称、源码集、绑定列表、工作组大小。ValidateWorkgroupSize() 抓出没有后端接受的大小。 |
ComputeBindingDesc | 一个槽:它的 ComputeBindingType,对存储纹理来说还有它的 ComputeStorageFormat。 |
ComputeResourceRef | 你在派发时绑定的东西 — 一个纹理名、一个 StorageBuffer 或一个渲染目标。隐式转换意味着你通常只需传字符串。 |
ComputeDispatchArgs | 一个 ref struct:内核、推式常量、资源引用和组数。每帧无需分配任何东西。 |
工作组大小默认为 8×8×1,这是几乎每个全屏内核
想要的形状。让描述符里的值与
Record 中的除数保持一致 — 没人替你检查这一点,
症状是右边缘和下边缘少了一条像素。
在 base.Create() 之后注册,那时图形设备已存在。
同一阶段的特效按注册顺序记录,所以注册顺序就是
执行顺序。
void RegisterEffects()
{
var g = Season.Basic.Graphics.Instance;
// Returns false when the effect declined to initialize. Skip the
// debug control that would display its output and carry on.
if (FrameSchedule.RegisterCompute(g, new EdgeEffect()))
AddControl(new Sprite2D { Name = EdgeEffect.TextureName, /* ... */ });
}
两者都住在 AfterScene,而泛光必须消费
已解析的图像。顺序反过来注册,泛光就会放大 TAA 本来要
消除的闪烁,结果是高光上的可见闪烁,而不是细微的
差别。参考应用按 plasma、场景拷贝、TAA、泛光的顺序注册,
原因写在注释里。
UnregisterCompute 释放特效创建的一切;
ResizeCompute 在窗口变化时替你调用并转发到
OnResize。你永远不调用
Execute — 帧会调用。
| 阶段 | 何时 | 典型用途 |
|---|---|---|
FrameStart | 场景绘制之前。 | 场景将要读取的一切:天空与透射率 LUT、云噪声、程序化纹理、SDF 切片视图。 |
AfterScene | 场景颜色、深度和速度可用时。 | 后处理:解析、色调映射输入、AO、泛光、调试可视化。 |
Record 只可以派发
不开启或关闭通道,不绘制调用,不屏障。同步在
后端的 DispatchCompute 内部处理,这既防止一个
特效破坏另一个的资源,也让四个后端能实现同一个
特效列表。如果一个内核需要前一个内核的输出,就在同一个
Record 里按顺序派发它们 — GTAO 对它的
两次模糊通道正是这么做的。
一个特效在
compute:// 名下创建它的存储纹理,并把有趣的那张发布到
FrameSchedule 上,让帧的其余部分无需知道类型
就能找到它。GtaoEffect 创建
compute://gtao/ao 并把它赋给
FrameSchedule.AoTexture;光照通道读取那个字段。
| 特效 | 阶段 | 发布 |
|---|---|---|
SkyAtmosphereEffect | FrameStart | SkyViewTexture、CloudNoiseTexture、AerialLutTexture |
PlasmaEffect | FrameStart | compute://plasma — compute 基线冒烟测试 |
Sdf3DViewEffect | FrameStart | compute://sdf3dview |
TaaEffect | AfterScene | compute://taa0 / taa1 乒乓,TaaActive |
BloomEffect | AfterScene | BloomTexture,加上它的 compute://bloom/ mip 链 |
GtaoEffect | AfterScene | AoTexture |
DdgiEffect | AfterScene | 探针和 SDF 体积 |
DepthView、VelocityView、SceneColorCopy | AfterScene | compute://depthview、compute://velocityview、compute://scenecopy |
FrameSchedule 也暴露通道输出本身 —
SceneColor、SceneDepth、
SceneVelocity、ShadowMap、
PostColor — 以及说明阴影和
后处理通道是否运行过的标志。在 Initialize 中读取它们,以决定
你的特效能否工作。
因为输出是有名字的纹理,而
Sprite2D 按名字绘制纹理,把任何中间阶段
放上屏幕只需三行。这就是参考应用调试模式的
整个实现。
AddControl(new Sprite2D
{
Name = Season.Rendering.Effects.GtaoEffect.TextureName,
Color = Colors.White,
PosX = 20, PosY = 580, Width = 240
});
调试模式
深度、运动矢量、GTAO、泛光 mip 链、TAA 历史和场景 SDF 都能
这样看到。唯一的例外是 3D 输出:它们用
compute3d:// 前缀,而精灵无法显示体积,所以
可视化一个体积需要一个切片内核 —
Sdf3DViewEffect 就是那个内核,如果你
自己写体积特效,它也是一个合理的模板。
后处理与 Compute 覆盖这些特效所接入的帧图 和逐后端支持矩阵。 全局光照与 AO 走读 两个最大的例子。
指南到此结束。再往后的参考是源码,而
Apps/Engine 里的参考应用是这里每一页
的完整示例。