Unity 自定义Postprocess 景深和散景模糊

前言

本篇将介绍如何通过添加RenderFeature实现自定义的postprocess——散景模糊(Bokeh Blur)和景深

关于RenderFeature的基础可以看这篇https://www.cnblogs.com/chenglixue/p/17816447.html

景深

  • 定义:聚焦清晰的焦点前后可接受的清晰区域,也就是画面中景象清晰的范围
    如下图相机景深原理图所示,DOF为景深范围,S为被拍摄物体的距离,\(D_n\)为近点距离,\(D_F\)为远点距离,DOF左边(\(D_F\) - S)为后景深,DOF右边(S - \(D_n\))为前景深

    undefined

  • 但实际上,在shader可以无需模拟相机的成像,只需根据深度图来确定相机的模糊范围即可

散景模糊

  • 定义:落在景深以外的画面,会有逐渐产生松散模糊的效果

    下图中带有模糊的甜甜圈形状正是散景模糊的效果
    image-20231109135138396不过这一形状不是固定的,它主要受镜头的光圈叶片数的影响,所形成的光圈孔形状不同image-20231109135401908

  • 同样,我们无需模拟相机的成像,而是通过黄金角度、均匀排布来实现

  • 黄金分割(黄金比例):对一整体切一刀,形成一个较大长度a,较小长度b,黄金比例也就是\(\frac{a}{b} = \frac{b}{a + b} ≈ 0.618\)

  • 黄金角度:黄金角度也是使用的黄金比例的公式,只不过是对2\(\pi\)进行切割,a为较小角度,b为较大角度,求得a = 137.5°

    • 黄金角度有何用处?

      其实在大自然界,许多现象都与黄金角度有关:大部分花朵的花瓣以137.5°的角度旋转;植物的果实也以137.5°的角度进行生长

  • 双螺旋均匀分布

    假设现在最中心有一个像素,逐渐向外移动并以黄金角度进行旋转,就会形成如下图所示完美的双螺旋均匀分布
    image-20231109141554512

    奇特的是,就算角度有1°之差,双螺旋现象就会被打破
    image-20231109141752824

  • 实现思路:可以看到,上图所示的均匀排布的形状很像最开始所提到的甜甜圈形状,只是这里都是点。但对这些旋转后的均匀排布图像逐渐进行叠加,即可实现散景模糊

Shader

  • 实现

    Shader "Custom/BokehBlur"
    {
        Properties
        {
            [HideinInspector] _MainTex("Main Tex", 2D) = "white" {}
        }
        
        SubShader
        {
            Tags
            {
                "RenderPipeline" = "UniversalRenderPipeline"
                "RenderType" = "Transparent"
            }
            Cull Off 
            ZWrite OFF
            ZTest Always
            
            HLSLINCLUDE
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    
            #pragma vertex VS
            #pragma fragment PS
    
            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_TexelSize;
            CBUFFER_END
            
            half _FocusDistance;	//焦点
            half _FarBlurIntensity;	//远景模糊强度
            half _NearBlurIntensity;	// 近景模糊强度
            half _BlurLoop;		   // 散景模糊迭代次数,越大越模糊
            half _BlurRadius;	   // 采样半径,越大甜甜圈越大
            TEXTURE2D(_MainTex);   
            SAMPLER(sampler_MainTex);
            TEXTURE2D(_SourTex);   // 模糊前的纹理
            SAMPLER(sampler_SourTex);
            SAMPLER(_CameraDepthTexture);	// viewport的深度纹理
    
            struct VSInput
            {
                float4 positionL : POSITION;
                float2 uv : TEXCOORD0;
            };
    
            struct PSInput
            {
                float4 positionH : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            
            ENDHLSL
    
            Pass
            {
                NAME "Boken Blur"
                
                HLSLPROGRAM
                
                PSInput VS(VSInput vsInput)
                {
                    PSInput vsOutput;
    
                    vsOutput.positionH = TransformObjectToHClip(vsInput.positionL);
    
                    #ifdef UNITY_UV_STARTS_AT_TOP
                    if(_MainTex_TexelSize.y < 0)
                        vsInput.uv.y = 1 - vsInput.uv.y;
                    #endif
                    vsOutput.uv = vsInput.uv;
    
                    return vsOutput;
                }
    
                float4 PS(PSInput psInput) : SV_TARGET
                {
                    float4 outputColor;
    
                    float angle = 2.3398;   // 弧度制的黄金角度
                    float2x2 rotation = float2x2(cos(angle), -sin(angle), sin(angle), cos(angle));  // 旋转矩阵
                    float2 offsetUV = float2(_BlurRadius, 0);	// 每次旋转都需要进行偏移
                    float2 targetUV;
                    float r;
                    for(int i = 1; i < _BlurLoop; ++i)
                    {
                        // 甜甜圈的面积是均匀分布,因此r是线性增加的
                        r = sqrt(i);
                        offsetUV = mul(rotation, offsetUV);
                        targetUV = psInput.uv + _MainTex_TexelSize.xy * offsetUV * r;
                        // 暗处模糊不会很明显,但明处会
                        outputColor += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, targetUV);
                    }
    
                    return outputColor / (_BlurLoop - 1);	// 模糊基操
                }
                
                ENDHLSL
            }
    
            Pass
            {
                NAME "depth of field"
                HLSLPROGRAM
                PSInput VS(VSInput vsInput)
                {
                    PSInput vsOutput;
    
                    vsOutput.positionH = TransformObjectToHClip(vsInput.positionL);
    
                    #ifdef UNITY_UV_STARTS_AT_TOP
                    if(_MainTex_TexelSize.y < 0)
                        vsInput.uv.y = 1 - vsInput.uv.y;
                    #endif
                    vsOutput.uv = vsInput.uv;
    
                    return vsOutput;
                }
    
                float4 PS(PSInput psInput) : SV_TARGET
                {
                    
                    float4 blurTex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, psInput.uv);
                    float4 sourceTex = SAMPLE_TEXTURE2D(_SourTex, sampler_SourTex, psInput.uv);  // 未模糊的纹理
    
                    // 转换为线性深度,且由近到远,值从0到1
                    float depth = Linear01Depth(tex2D(_CameraDepthTexture, psInput.uv).r , _ZBufferParams);
                    float distance;
                    // 分为远处模糊和近处模糊
                    if(depth > _FocusDistance)
                    {
                        // 因为深度是线性的,但我们想要的是焦点处清晰,但前后逐渐模糊,所以这里采用二次函数
                        distance = saturate((depth - _FocusDistance) * (depth - _FocusDistance) * _FarBlurIntensity);
                    }
                    else
                    {
                        distance = saturate((depth - _FocusDistance) * (depth - _FocusDistance) * _NearBlurIntensity);
                    }
                    
                    // 最后模糊图和原图基于距离进行lerp
                    return lerp(sourceTex, blurTex, distance);
                }
                ENDHLSL
            }
        }
    }
    

RenderFeature

  • 都是些老朋友,这里放重要部分

  • PassSetting

    [System.Serializable]
    public class PassSetting
    {
        [Tooltip("profiler tag will show up in frame debugger")]
        public readonly string m_ProfilerTag = "Dual Blur Pass";
        
        [Tooltip("Pass insert position")]
        public RenderPassEvent m_passEvent = RenderPassEvent.AfterRenderingTransparents;
        
        [Tooltip("resolution ratio of sample")]
        [Range(1, 10)] 
        public int m_Downsample = 1;
    
        [Tooltip("Loop of sample")]
        [Range(2, 7)] 
        public int m_PassLoop = 2;
    
        [Tooltip("Radius of sample")] 
        [Range(0, 10)]
        public float m_BlurRadius = 1;
    
        [Tooltip("Near Blur Intensity of DOF")] 
        [Range(0, 10)]
        public float m_NearBlurIntensity = 1;
    
        [Tooltip("Far Blur Intensity of DOF")] 
        [Range(0,10)]
        public float m_FarBlurIntensity = 1;
    
        [Tooltip("Camera Focus Point")] 
        [Range(0, 1)]
        public float m_FocusDistance = 1;
    
    }
    
  • 全部实现

    public class BokehBlurRenderFeature : ScriptableRendererFeature
    {
        // render feature 显示内容
        [System.Serializable]
        public class PassSetting
        {
            [Tooltip("profiler tag will show up in frame debugger")]
            public readonly string m_ProfilerTag = "Dual Blur Pass";
            
            [Tooltip("Pass insert position")]
            public RenderPassEvent m_passEvent = RenderPassEvent.AfterRenderingTransparents;
            
            [Tooltip("resolution ratio of sample")]
            [Range(1, 10)] 
            public int m_Downsample = 1;
    
            [Tooltip("Loop of sample")]
            [Range(2, 7)] 
            public int m_PassLoop = 2;
    
            [Tooltip("Radius of sample")] 
            [Range(0, 10)]
            public float m_BlurRadius = 1;
    
            [Tooltip("Near Blur Intensity of DOF")] 
            [Range(0, 10)]
            public float m_NearBlurIntensity = 1;
    
            [Tooltip("Far Blur Intensity of DOF")] 
            [Range(0,10)]
            public float m_FarBlurIntensity = 1;
    
            [Tooltip("Camera Focus Point")] 
            [Range(0, 1)]
            public float m_FocusDistance = 1;
    
        }
        
        public PassSetting m_Setting = new PassSetting();
        BokehBlurRenderPass m_BokehBlurPass;
        
        // 初始化
        public override void Create()
        {
            m_BokehBlurPass = new BokehBlurRenderPass(m_Setting);
        }
    
        // Here you can inject one or multiple render passes in the renderer.
        // This method is called when setting up the renderer once per-camera.
        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            // can queue up multiple passes after each other
            renderer.EnqueuePass(m_BokehBlurPass);
        }
    }
    

RenderPass

  • ShaderIDs

    static class ShaderIDs
    {
        // int 相较于 string可以获得更好的性能,因为这是预处理的
        internal static readonly int  m_BlurRadiusProperty = Shader.PropertyToID("_BlurRadius");
        internal static readonly int  m_NearBlurIntensityProperty = Shader.PropertyToID("_NearBlurIntensity");
        internal static readonly int  m_FarBlurIntensityProperty = Shader.PropertyToID("_FarBlurIntensity");
        internal static readonly int  m_PassLoopProperty = Shader.PropertyToID("_BlurLoop");
        internal static readonly int m_FocusDistanceProperty = Shader.PropertyToID("_FocusDistance");
    
        // Blur RT and source RT
        internal static readonly int m_BlurRTProperty = Shader.PropertyToID("_BufferRT1");
        internal static readonly int m_SourRTProperty = Shader.PropertyToID("_SourTex");
    }
    
  • 构造函数

    // 用于设置material 属性
    public BokehBlurRenderPass(BokehBlurRenderFeature.PassSetting passSetting)
    {
        this.m_passSetting = passSetting;
    
        renderPassEvent = m_passSetting.m_passEvent;
    
        if (m_Material == null) m_Material = CoreUtils.CreateEngineMaterial("Custom/BokehBlur");
        
        // 基于pass setting设置material Properties
        m_Material.SetFloat(ShaderIDs.m_BlurRadiusProperty, m_passSetting.m_BlurRadius);
        m_Material.SetFloat(ShaderIDs.m_NearBlurIntensityProperty, m_passSetting.m_NearBlurIntensity);
        m_Material.SetFloat(ShaderIDs.m_FarBlurIntensityProperty, m_passSetting.m_FarBlurIntensity);
        m_Material.SetFloat(ShaderIDs.m_FocusDistanceProperty, m_passSetting.m_FocusDistance);
        m_Material.SetInt(ShaderIDs.m_PassLoopProperty, m_passSetting.m_PassLoop);
    }
    
  • OnCameraSetup

    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        // Grab the color buffer from the renderer camera color target
        m_TargetBuffer = renderingData.cameraData.renderer.cameraColorTarget;
        
        // camera target descriptor will be used when creating a temporary render texture
        RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
        // 设置 temporary render texture的depth buffer的精度
        descriptor.depthBufferBits = 0;
        // 注意!因为申请RT后,RT的分辨率不可改变,而该Source RT在后续由于camera render targt会复制一份给他,所以这里不能用降采样的分辨率
        cmd.GetTemporaryRT(ShaderIDs.m_SourRTProperty, descriptor, FilterMode.Bilinear);
        // 降采样
        descriptor.width /= m_passSetting.m_Downsample;
        descriptor.height /= m_passSetting.m_Downsample;
        cmd.GetTemporaryRT(ShaderIDs.m_BlurRTProperty, descriptor, FilterMode.Bilinear);
    }
    
  • Execute

    // The actual execution of the pass. This is where custom rendering occurs
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        // Grab a command buffer. We put the actual execution of the pass inside of a profiling scope
        CommandBuffer cmd = CommandBufferPool.Get();
    
        using (new ProfilingScope(cmd, new ProfilingSampler(m_passSetting.m_ProfilerTag)))
        {
            // 将camera texture复制给sour RT,同时发送给shader中的_SourTex
            cmd.CopyTexture(m_TargetBuffer, ShaderIDs.m_SourRTProperty);
            // 进行 bokenh blur并复制给Blur RT
            cmd.Blit(m_TargetBuffer, ShaderIDs.m_BlurRTProperty, m_Material, 0);
            // 将blur后且进行景深计算的RT复制给输出的RT
            cmd.Blit(ShaderIDs.m_BlurRTProperty, m_TargetBuffer, m_Material, 1);
        }
        
        // Execute the command buffer and release it
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }
    
  • OnCameraCleanUp

    // Called when the camera has finished rendering
    // release/cleanup any allocated resources that were created by this pass
    public override void OnCameraCleanup(CommandBuffer cmd)
    {
        if(cmd == null) throw new ArgumentNullException("cmd");
        
        cmd.ReleaseTemporaryRT(ShaderIDs.m_BlurRTProperty);
        cmd.ReleaseTemporaryRT(ShaderIDs.m_SourRTProperty);
    }
    

效果

image-20231109212709491

reference

https://zhuanlan.zhihu.com/p/67389489

https://zhuanlan.zhihu.com/p/125744132

https://zh.wikipedia.org/wiki/黄金分割率

https://zhuanlan.zhihu.com/p/64288140

https://zhuanlan.zhihu.com/p/394821497

https://zhuanlan.zhihu.com/p/387107536

posted @ 2023-11-09 21:30  爱莉希雅  阅读(110)  评论(0编辑  收藏  举报