Shader 学习 21:自定义 Render Feature

为什么有些效果一定要用feature实现?

  • 当效果需要在"相机渲染流程的某个**++时机++**"拿到或改写++整张屏幕++的渲染结果时。

    例如:

    • 全屏后处理:模糊、调色、描边、像素化、镜头特效

    • 依赖相机深度/法线的效果:SSAO、基于深度的雾、屏幕空间描边

    • 需要 渲染 一张额外全局纹理:角色遮罩、扭曲贴图、分层结果

    • 必须控制执行顺序:在不透明物体后、透明物体前,或最终画面之后执行

但"全局数据"不一定都需要 Feature。比如某个全局 Shader 参数、全局贴图,只要脚本用 Shader.SetGlobal... 设置即可;材质在绘制时自然能读到它。


框架实现

需要打通 Camera → Feature → Pass → Shader → GPU 这条链路

ScriptableRendererFeature这个类的核心函数是:CreateAddRenderPasses,++负责feature自己的构造函数,以及将某些自定义pass加入到++ ++渲染++ ++队列中++。

  • Create:同步new一个Pass,并把feature的配置数据给它

  • AddRenderPasses:将Pass加入队列

既然要new pass与配置,就需要在该Feature中,创建两个 ++++ :SettingsScriptableRenderPass

  • 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);
相关推荐
网络研究院1 小时前
一款利用人工智能将电子游戏翻译成任何语言的桌面应用程序
人工智能·游戏·工具·平台·翻译·软件
脚踏实地皮皮晨2 小时前
003006001_WrapPanel控件
开发语言·c#·.net·wpf·visual studio
码农小韩2 小时前
AIAgent应用开发——大模型理论基础与应用(七)
python·学习·ai·大模型·agent
流云鹤3 小时前
05Java学习day(5)
java·学习
脚踏实地皮皮晨3 小时前
003005002_WPF StackPanel 基类官方类定义逐行深度解析
开发语言·windows·算法·c#·wpf·visual studio
only-lucky3 小时前
QML深入学习五(Controls模块)
前端·javascript·学习
用户298698530143 小时前
Excel 转 PDF 免费在线工具,格式完美保留
人工智能·c#·excel
Yolanda_20223 小时前
Python学习-第九部分-错误处理与异常处理
开发语言·python·学习
库拉库拉里4 小时前
深入理解 C# 异步编程:同步、Task.Wait () 与 await 的本质区别及实践指南
开发语言·c#