为什么有些效果一定要用feature实现?
-
当效果需要在"相机渲染流程的某个**++时机++**"拿到或改写++整张屏幕++的渲染结果时。
例如:
-
全屏后处理:模糊、调色、描边、像素化、镜头特效
-
依赖相机深度/法线的效果:SSAO、基于深度的雾、屏幕空间描边
-
需要先 渲染 一张额外全局纹理:角色遮罩、扭曲贴图、分层结果
-
必须控制执行顺序:在不透明物体后、透明物体前,或最终画面之后执行
-
但"全局数据"不一定都需要 Feature。比如某个全局 Shader 参数、全局贴图,只要脚本用 Shader.SetGlobal... 设置即可;材质在绘制时自然能读到它。
框架实现
需要打通 Camera → Feature → Pass → Shader → GPU 这条链路
ScriptableRendererFeature这个类的核心函数是:Create 和 AddRenderPasses,++负责feature自己的构造函数,以及将某些自定义pass加入到++ ++渲染++ ++队列中++。
-
Create:同步new一个Pass,并把feature的配置数据给它
-
AddRenderPasses:将Pass加入队列
既然要new pass与配置,就需要在该Feature中,创建两个 ++类++ :Settings和 ScriptableRenderPass
-
Settings:这个配置内容会显示在 UniversalRenderData 的 Inspector 的对应 feature 上
-
ScriptableRenderPass:要有自己的构造函数,以及执行内容
- Execute:写 CommandBuffer
cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace MiniRenderer.Rendering
{
public class FullscreenTintRendererFeature : ScriptableRendererFeature
{
// 1.
// 会显示在 Renderer Feature 的 Inspector 中,供你调参。
// 这里只是声明了一个 Serializable 的类,真正让这个Feature有该序列化数据,还需要下面new一个
[System.Serializable]
public class Settings
{
public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
public Color tintColor = Color.cyan;
[Range(0f, 1f)] public float intensity = 0.2f;
}
[SerializeField] private Settings settings = new Settings();
// 2.
// 这是该feature持有的一个pass
// 这个类将来会录制"全屏滤镜"的 GPU 命令。
// 这个pass会做什么,都写在Execute中
private class TintPass : ScriptableRenderPass
{
private readonly Settings settings;
public TintPass(Settings settings)
{
this.settings = settings;
}
public override void Execute(
ScriptableRenderContext context,
ref RenderingData renderingData)
{
// 在这里写 CommandBuffer。
}
}
// 3.
// feature调度pass加到渲染队列中
private TintPass tintPass;
public override void Create()
{
// Feature 负责注册;真正干活的是 Pass。
// 所以在feature的构造函数中,用我们的配置setting,new一个要干活的Pass
tintPass = new TintPass(settings) // 存下来,不用每帧创建
{
// 把 Inspector 选的渲染时机交给 URP。
renderPassEvent = settings.renderPassEvent
};
}
public override void AddRenderPasses(
ScriptableRenderer renderer,
ref RenderingData renderingData)
{
// 每次相机渲染时,把 Pass 加进 URP 队列。
renderer.EnqueuePass(tintPass);
}
}
}
临时 Render Target 的必要性
必要性在于:读取相机颜色 A → 处理 → 立刻写回同一张 A 的操作,如果GPU同时读写同一张纹理 不安全,结果可能不确定,所以正确的行为是要"乒乓"一次:
相机颜色 A → 临时纹理 B → 处理后的结果写回 A
cs
// 1. 创建一张与来源画面兼容的临时纹理 B。
var temporary = AllocateCompatibleTarget(source);
// 2. 第一次 Blit:把来源画面 A 复制到 B。
cmd.Blit(source, temporary);
// 3. 第二次 Blit:读取 B,执行材质/Shader,并写入管线指定的输出目标。
cmd.Blit(temporary, destination, material);
// 4. 释放 B。
Release(temporary);
- commandBuffer.Blit(src, dest, mat, pass)
URP框架提供的封装写法
在 URP 的 ScriptableRenderPass 中,如果不需要手动管理临时 RT,可以调用它提供的 Blit 封装。传入效果材质后,URP 会在内部选择相机颜色的前后 Buffer,并在渲染完成后交换它们。
这行代码的内部逻辑仍是"读取当前颜色 Buffer → 写入另一颜色 Buffer → 交换 Buffer"。区别是临时 Buffer 的选择、写入目标和交换操作都由 URP 管理,因此不需要手动创建和释放临时 RT。
cs
// 注意:这是 ScriptableRenderPass 的 Blit,
// 不是 commandBuffer.Blit。
Blit(commandBuffer, ref renderingData, tintMaterial);
