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);
相关推荐
想要入门的程序猿7 小时前
回调函数学习
java·网络·学习
码上上班8 小时前
iptables学习; firewalld学习;ufw学习
学习
RisunJan8 小时前
丝连族谱 0452 — 功能拆解与使用手册
学习
老王爱玩车8 小时前
深入理解指针2
c语言·开发语言·学习
那年窗外下的雪.9 小时前
AIDC 学习日志|第 22 天|All-Active/Single-Active 故障演练设计
服务器·网络·学习·算法·哈希算法
思盛iOS签名上架9 小时前
ipa企业签名闪退是什么原因?
游戏·ios·testfight
龚寿生9 小时前
23-敏捷开发实战:从Scrum到看板的团队协作方法论
android·学习·scrum·敏捷流程
cc_yy_zh10 小时前
学习使用kali抓包
服务器·学习·php
动词ing10 小时前
【学习笔记】数据结构栈与队列(栈先进后出+队列先进先出+括号匹配+消消乐模型)
数据结构·笔记·学习