超小白向URP Scriptable Render Feature写法——实现老式CRT电视效果!
仓鼠的摆烂日常
2022年07月12日 16:13
收录于文集
共8篇

  上一篇大概解释了一下render feature每个函数都用来干啥的,这篇就以一个简单的例子手把手教大伙儿写render feature!

  灵感来源是在知乎上看到了这位大佬用shadergraph写的图片的crt效果,俺就想把它改成屏幕后处理的方式:

(https://zhuanlan.zhihu.com/p/232450616)

  呃呃呃b站怎么引用不了外链库鲁西......

浅浅的上一下效果

cut-off

重头戏是render feature部分,俺一个一个带着大家顺下来捏。

首先在render feature类里写一个公有的类setting,这个类主要是用于传递暴露在面板上的参数,我们控制shader的参数都需要写在这个setting里。(当然也可以不写,就是setup函数里传参数的时候要一个一个传,有的时候参数十几个就要写十几个变量非常麻烦意思是)

代码块
C#
自动换行
复制代码
 [System.Serializable]
    public class Setting//暴露在面板上的参数
    {
        public Vector2 curvature;//一大堆需要的参数
        public Vector2 resolution;
        public Vector4 pixelScanBright;
        public float pixelScanSpeed;
        public Texture2D mask;
        public float scanTexSpeed;
        public float scanTexOffset;
        public float scanTexBrightness;
        public float rgbOffset;
        public float vignetteBright;
        public float vignettePow;
        public Color vignetteCol;
        public Texture2D noise;
        public float noiseIntensity;
        [ColorUsage(true,true)]//使用hdr颜色
        public Color basecolor;
        public float uvnoise;
    }
    public Setting setting = new Setting();
复制成功

接下来就可以开始写我们的render pass了,先在render pass的类里把我们需要的变量再写一遍(要有参数接收外面传进来的变量捏),然后我们需要设置一个pass的名字,方便我们在frame debugger中查看这个pass的渲染结果。我们还需要声明两张rt,一张是source,也就是我们相机渲染的图片,另一张为临时的rt,用来储存经过后处理后的图像,跟build in的destination差不多。

代码块
C#
自动换行
复制代码
 string passName = "CRT";//pass名字

        Vector2 curvature;//需要的变量
        Vector2 resulotion;
        Vector4 pixelScanBright;
        float pixelScanSpeed;
        Texture2D mask;
        float scanTexSpeed;
        float scanTexOffset;
        public float scanTexBrightness;
        float rgbOffset;
        float vignetteBright;
        float vignettePow;
        Color vignetteCol;
        Texture2D noise;
        float noiseIntensity;
        Color basecolor;
        float uvnoise;

        Material crtM;//需要的材质
        RenderTargetIdentifier source;//source图片
        RenderTargetHandle tempTex;//临时rt 跟destination差不多
        RenderTextureDescriptor descriptor;//用来描述rt属性的参数
复制成功

接下来实现构造函数,我通常在这个函数里创建材质和shader,也可以做一些初始化的操作。

代码块
C#
自动换行
复制代码
 public crtPass()
        {
            var shader = Shader.Find("Custom/CRT");//找到shader
            if (shader == null)
            {
                Debug.LogError("shader not found");
                return;
            }
            crtM = CoreUtils.CreateEngineMaterial(shader);//创建材质
            
        }
复制成功

在camerasetup函数中我对rt的具体属性进行了设置

代码块
C#
自动换行
复制代码
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {
            descriptor = renderingData.cameraData.cameraTargetDescriptor;//将属性设置和相机参数相同
            descriptor.depthBufferBits = 0;
        }
复制成功

接下来实现setup函数,这个函数主要就是传递参数,把feature类的参数传进来,同时传入我们的相机渲染的source图片

代码块
C#
自动换行
复制代码
 public void Setup(Setting setting,RenderTargetIdentifier source)
        {
            this.curvature = setting.curvature;
            this.resulotion = setting.resolution;
            this.pixelScanBright = setting.pixelScanBright;
            this.pixelScanSpeed = setting.pixelScanSpeed;
            this.mask = setting.mask;
            this.scanTexSpeed = setting.scanTexSpeed;
            this.scanTexOffset = setting.scanTexOffset;
            this.scanTexBrightness = setting.scanTexBrightness;
            this.rgbOffset = setting.rgbOffset;
            this.vignetteBright = setting.vignetteBright;
            this.vignetteCol = setting.vignetteCol;
            this.vignettePow = setting.vignettePow;
            this.noise = setting.noise;
            this.noiseIntensity = setting.noiseIntensity;
            this.basecolor = setting.basecolor;
            this.uvnoise = setting.uvnoise;
            this.source = source;
        }
复制成功

接下来是最重要的excute函数,我们在这个函数里实现blit的具体过程。首先要声明一个commandbuffer,blit操作需要依赖commandbuffer命令来进行。然后创建我们的临时rt并将一大堆参数传入shader,接着进行blit操作,最后释放我们创建的commandbuffer。

划重点:这里的blit操作与build in不同,需要blit两次,将source图片blit到临时rt上进行后处理效果,之后还要blit回source才会有效果。

代码块
C#
自动换行
复制代码
 public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get(passName);//获取到当前pass
            cmd.GetTemporaryRT(tempTex.id, descriptor);//创建临时rt

            crtM.SetVector("_Curvature", curvature);//设置材质参数
            crtM.SetVector("_Resolution", resulotion);
            crtM.SetVector("_pixelScanlineBrightness", pixelScanBright);
            crtM.SetFloat("_pixelScanSpeed", pixelScanSpeed);
            crtM.SetTexture("_mask", mask);
            crtM.SetFloat("_scanTexSpeed", scanTexSpeed);
            crtM.SetFloat("_scanTexOffset", scanTexOffset);
            crtM.SetFloat("_scanTexBrightness", scanTexBrightness);
            crtM.SetFloat("_rgbOffset", rgbOffset);
            crtM.SetColor("_vignetteCol", vignetteCol);
            crtM.SetFloat("_vignetteBright", vignetteBright);
            crtM.SetFloat("_vignettePow", vignettePow);
            crtM.SetTexture("_noise", noise);
            crtM.SetFloat("_noiseIntensity", noiseIntensity);
            crtM.SetColor("_baseColor", basecolor);
            crtM.SetFloat("_UVnoiseBright", uvnoise);

            cmd.Blit(source, tempTex.Identifier(), crtM);//跟buildin一样的blit操作
            cmd.Blit(tempTex.Identifier(), source);

            context.ExecuteCommandBuffer(cmd);//释放资源
            CommandBufferPool.Release(cmd);
        }
复制成功

最后就是释放我们声明的临时rt的frame clean up函数

代码块
C#
自动换行
复制代码
 public override void FrameCleanup(CommandBuffer cmd)
        {
            cmd.ReleaseTemporaryRT(tempTex.id);
        }
复制成功

render pass的类基本到这就结束了捏

最后就是补充完整renderFeature的那两个函数,把参数传进pass

代码块
C#
自动换行
复制代码
crtPass m_ScriptablePass;

    /// <inheritdoc/>
    public override void Create()
    {
        m_ScriptablePass = new crtPass();//初始化pass
        m_ScriptablePass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;//设置插入位置
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        m_ScriptablePass.Setup(setting, renderer.cameraColorTarget);//传入参数
        renderer.EnqueuePass(m_ScriptablePass);//执行插入pass
    }
复制成功

以上就是一个最简单最基本的renderfeature的写法捏!下面是renderFeature的源码ww

代码块
C#
自动换行
复制代码
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class crtFeature : ScriptableRendererFeature
{
    [System.Serializable]
    public class Setting//暴露在面板上的参数
    {
        public Vector2 curvature;//一大堆需要的参数
        public Vector2 resolution;
        public Vector4 pixelScanBright;
        public float pixelScanSpeed;
        public Texture2D mask;
        public float scanTexSpeed;
        public float scanTexOffset;
        public float scanTexBrightness;
        public float rgbOffset;
        public float vignetteBright;
        public float vignettePow;
        public Color vignetteCol;
        public Texture2D noise;
        public float noiseIntensity;
        [ColorUsage(true,true)]
        public Color basecolor;
        public float uvnoise;
    }
    public Setting setting = new Setting();

    class crtPass : ScriptableRenderPass
    {
        string passName = "CRT";//pass名字

        Vector2 curvature;//需要的变量
        Vector2 resulotion;
        Vector4 pixelScanBright;
        float pixelScanSpeed;
        Texture2D mask;
        float scanTexSpeed;
        float scanTexOffset;
        public float scanTexBrightness;
        float rgbOffset;
        float vignetteBright;
        float vignettePow;
        Color vignetteCol;
        Texture2D noise;
        float noiseIntensity;
        Color basecolor;
        float uvnoise;

        Material crtM;//需要的材质
        RenderTargetIdentifier source;//source图片
        RenderTargetHandle tempTex;//临时rt 跟destination差不多
        RenderTextureDescriptor descriptor;//用来描述rt属性的参数
        public crtPass()
        {
            var shader = Shader.Find("Custom/CRT");
            if (shader == null)
            {
                Debug.LogError("shader not found");
                return;
            }
            crtM = CoreUtils.CreateEngineMaterial(shader);
            
        }
        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {
            descriptor = renderingData.cameraData.cameraTargetDescriptor;//将属性设置和相机参数相同
            descriptor.depthBufferBits = 0;
        }
        public void Setup(Setting setting,RenderTargetIdentifier source)
        {
            this.curvature = setting.curvature;
            this.resulotion = setting.resolution;
            this.pixelScanBright = setting.pixelScanBright;
            this.pixelScanSpeed = setting.pixelScanSpeed;
            this.mask = setting.mask;
            this.scanTexSpeed = setting.scanTexSpeed;
            this.scanTexOffset = setting.scanTexOffset;
            this.scanTexBrightness = setting.scanTexBrightness;
            this.rgbOffset = setting.rgbOffset;
            this.vignetteBright = setting.vignetteBright;
            this.vignetteCol = setting.vignetteCol;
            this.vignettePow = setting.vignettePow;
            this.noise = setting.noise;
            this.noiseIntensity = setting.noiseIntensity;
            this.basecolor = setting.basecolor;
            this.uvnoise = setting.uvnoise;
            this.source = source;
        }
       
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get(passName);//获取到当前pass
            cmd.GetTemporaryRT(tempTex.id, descriptor);//创建临时rt

            crtM.SetVector("_Curvature", curvature);//设置材质参数
            crtM.SetVector("_Resolution", resulotion);
            crtM.SetVector("_pixelScanlineBrightness", pixelScanBright);
            crtM.SetFloat("_pixelScanSpeed", pixelScanSpeed);
            crtM.SetTexture("_mask", mask);
            crtM.SetFloat("_scanTexSpeed", scanTexSpeed);
            crtM.SetFloat("_scanTexOffset", scanTexOffset);
            crtM.SetFloat("_scanTexBrightness", scanTexBrightness);
            crtM.SetFloat("_rgbOffset", rgbOffset);
            crtM.SetColor("_vignetteCol", vignetteCol);
            crtM.SetFloat("_vignetteBright", vignetteBright);
            crtM.SetFloat("_vignettePow", vignettePow);
            crtM.SetTexture("_noise", noise);
            crtM.SetFloat("_noiseIntensity", noiseIntensity);
            crtM.SetColor("_baseColor", basecolor);
            crtM.SetFloat("_UVnoiseBright", uvnoise);

            cmd.Blit(source, tempTex.Identifier(), crtM);//跟buildin一样的blit操作
            cmd.Blit(tempTex.Identifier(), source);

            context.ExecuteCommandBuffer(cmd);//释放资源
            CommandBufferPool.Release(cmd);
        }

        public override void FrameCleanup(CommandBuffer cmd)
        {
            cmd.ReleaseTemporaryRT(tempTex.id);
        }
    }

    crtPass m_ScriptablePass;

    /// <inheritdoc/>
    public override void Create()
    {
        m_ScriptablePass = new crtPass();//初始化pass
        m_ScriptablePass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;//设置插入位置
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        m_ScriptablePass.Setup(setting, renderer.cameraColorTarget);//传入参数
        renderer.EnqueuePass(m_ScriptablePass);//执行插入pass
    }
}
复制成功

cut-off

shader部分不会做太多说明,原文的大佬已经解释的非常清楚了,跟着大佬写就okok!!有几处改动的地方俺会挑出来说说ww

·第一个注意的点是原文大佬在做屏幕边缘扭曲的时候使用的函数会造成整体的图片有点缩小,边缘拉伸的情况,大佬的解决方法是直接把拉伸的区域给clip掉,但是由于我们是做屏幕后处理没办法直接对屏幕图片clip,所以俺简单修改了一下函数让它边缘不会出现拉伸也可以实现扭曲效果。大概就是下面这样

代码块
C#
自动换行
复制代码
float2 remapUV = i.uv * 2 - 1;//重映射uv
float x = remapUV.x - (1-pow((abs(remapUV.x)),3))* (remapUV.x* _Curvature.x);//修改了一下函数
float y = remapUV.y - (1- pow((abs(remapUV.y)), 3))*(remapUV.y* _Curvature.y);
float finalX = x * 0.5 + 0.5;//再映射回来
float finalY = y * 0.5 + 0.5;
复制成功

(呃呃呃怎么感觉添加代码块没办法复制的样子不会吧阿b你好烂....)

这个函数仅代表个人尝试结果,感觉效果还行,不代表最完美的情况,家人们也可以自己改改看

·第二个修改的点是噪声闪烁这部分,原文是使用噪声生成器,俺这里改成了使用了一个随机数函数,同时还增加了雪花屏的效果,也是使用2d的随机数函数实现。

代码块
C#
自动换行
复制代码
//两个随机函数
float Radom(float x) {
        return frac(sin(x + 0.546) * 143758.5694);//随机数函数
}
float UVnoise(float2 uv) {
   return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);//雪花屏随机数函数
}
复制成功
代码块
C#
自动换行
复制代码
//噪声部分代码
float noise = tex2D(_noise, float2(i.uv.y,i.uv.y)).r;
float noiseTime = step(Radom(_Time.y*0.005), 0.05);//闪烁时间噪声
finalX += noise * _noiseIntensity * noiseTime;
float uvNoise = saturate(UVnoise(i.uv * _Time.x)* _UVnoiseBright);//雪花屏效果 _UVnoiseBright变量控制雪花屏颜色深浅
复制成功

修改的大概就是这些,下面是shader源代码ww

代码块
C#
自动换行
复制代码
Shader "Custom/CRT"
{
    Properties
    {
       _MainTex ("mainTex", 2D) = "white" {}
       _mask("mask",2D)="white"{}
       _noise("noise",2D) = "white"{}
    }
        SubShader
    {
        Tags { "RenderPipeline" = "UniversalPipeline" }
        Pass{
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        sampler2D _MainTex;
        sampler2D _mask;//扫描线的那个贴图
        sampler2D _noise;//噪声图
        float2 _Curvature;
        float2 _Resolution;
        float4 _pixelScanlineBrightness;
        float _pixelScanSpeed;
        float _scanTexSpeed;
        float _scanTexOffset;
        float _scanTexBrightness;
        float _rgbOffset;
        float _vignetteBright;
        float _vignettePow;
        half4 _vignetteCol;
        float _noiseIntensity;
        half4 _baseColor;
        float _UVnoiseBright;

         struct a2v {
        float4 vertex:POSITION;
        float2 texcoord:TEXCOORD0;
    };

    struct v2f {
        float2 uv:TEXCOORD0;
        float4 pos:SV_POSITION;
    };

    v2f vert(a2v v) {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.uv = v.texcoord;
        return o;
    }

    float Radom(float x) {
        return frac(sin(x + 0.546) * 143758.5694);//随机数函数
    }

    float UVnoise(float2 uv) {
        return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);//雪花屏随机数函数
    }

    float4 frag(v2f i) :SV_Target{
        float2 remapUV = i.uv * 2 - 1;//重映射uv
        float x = remapUV.x - (1-pow((abs(remapUV.x)),3))* (remapUV.x* _Curvature.x);//修改了一下函数
        float y = remapUV.y - (1- pow((abs(remapUV.y)), 3))*(remapUV.y* _Curvature.y);
        float finalX = x * 0.5 + 0.5;//再映射回来
        float finalY = y * 0.5 + 0.5;

        float noise = tex2D(_noise, float2(i.uv.y,i.uv.y)).r;
        float noiseTime = step(Radom(_Time.y*0.005), 0.05);//闪烁时间噪声
        finalX += noise * _noiseIntensity * noiseTime;
        float uvNoise = saturate(UVnoise(i.uv * _Time.x)* _UVnoiseBright);//雪花屏效果 _UVnoiseBright变量控制雪花屏颜色深浅

        float pixelLineX = sin((i.uv.x) * _Resolution.x) * _pixelScanlineBrightness.x + _pixelScanlineBrightness.y;//像素化的线
        float pixelLineY = sin((i.uv.y+ _pixelScanSpeed*_Time.y) * _Resolution.y) * _pixelScanlineBrightness.z + _pixelScanlineBrightness.w;
        float pixelLine = pixelLineX * pixelLineY;

        float2 scanTexUV = float2(i.uv.x, i.uv.y + _Time.y * _scanTexSpeed);//屏幕扫描线
        float scanTexCol00 = tex2D(_mask, scanTexUV).r;
        float scanTexCol01 = tex2D(_mask, float2(scanTexUV.x, scanTexUV.y + _scanTexOffset)).r;
        float scanTex = scanTexCol00 * scanTexCol01;


        float colR = tex2D(_MainTex, float2(finalX + _rgbOffset, finalY + _rgbOffset)).r;//rgb通道分离
        float colG = tex2D(_MainTex, float2(finalX, finalY)).g;
        float colB = tex2D(_MainTex, float2(finalX - _rgbOffset, finalY - _rgbOffset)).b;
        float4 col = lerp(float4(colR, colG, colB, 1) * pixelLine, float4(scanTex, scanTex, scanTex, 1), _scanTexBrightness);

        float vignette = pow(i.uv.x * i.uv.y * (1 - i.uv.x) * (1 - i.uv.y) * _vignetteBright, _vignettePow);//边缘黑框
        col = lerp(_vignetteCol, col, vignette) * _baseColor* uvNoise;

        return col;
    }
        ENDCG
        }
    }
}
复制成功

tips:urp把RenderPipeline的tag改成UniversalPipeline似乎也是可以用cg的!塔诺西!