URP | 后处理-描边
那个人真狗
编辑于 2022年12月24日 22:25
收录于文集
共43篇

内容偏多

使用软件 Unity 2021.3.15 后处理实现描边效果

目的

  • 后处理实现描边效果

  • 这样做有什么需要注意的地方

效果

深度描边

Renderer Feature

我们准备一个默认的 Renderer Feature 在只需要把Shader渲染出来就可以

管线准备

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



public class Outlint : ScriptableRendererFeature
{
    [System.Serializable] 
    public class Settings
    {
        public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;   
        public Shader shader;
    }
    public Settings settings = new Settings();

    OutlintPass outlintPass;           // 定义我们创建出Pass


    public override void Create()
    {
        this.name = "Bilt";    // 模糊渲染的名字
        outlintPass = new OutlintPass(RenderPassEvent.BeforeRenderingPostProcessing, settings.shader);    // 初始化 我们的渲染层级和Shader

    }
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(outlintPass);
    }
}


public class OutlintPass : ScriptableRenderPass
{
    static readonly string RenderTag = "Post Effects";                         // 设置渲染标签

    OutlintVolume outlintvolume;                                                            // 定义组件类型
    Material biltmaterial;                                                      // 后处理材质 


    public OutlintPass(RenderPassEvent evt, Shader biltshader)
    {
        renderPassEvent = evt;
        var shader = biltshader;  

        if (shader == null)
        {
            Debug.LogError("没有指定Shader");
            return;
        }
        biltmaterial = CoreUtils.CreateEngineMaterial(biltshader);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (biltmaterial == null)
        {
            Debug.LogError("材质初始化失败");
            return;
        }

        if (!renderingData.cameraData.postProcessEnabled)
        {
            return;
        }

        var stack = VolumeManager.instance.stack;                          // 传入 volume 
        outlintvolume = stack.GetComponent<OutlintVolume>();                     // 获取到后处理组件

        var cmd = CommandBufferPool.Get(RenderTag);    // 渲染标签 

        Render(cmd, ref renderingData);                 // 调用渲染函数  

        context.ExecuteCommandBuffer(cmd);              // 执行函数,回收。
        CommandBufferPool.Release(cmd);

    }

    void Render(CommandBuffer cmd, ref RenderingData renderingData)
    {
        RenderTargetIdentifier source = renderingData.cameraData.renderer.cameraColorTarget;                 // 定义RT
        RenderTextureDescriptor inRTDesc = renderingData.cameraData.cameraTargetDescriptor;
        inRTDesc.depthBufferBits = 0;                                                                          // 清除深度

				// 增加控制Shader的属性

        int destination = Shader.PropertyToID("Temp1");

        // 获取一张临时RT
        cmd.GetTemporaryRT(destination, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //申请一个临时图像,并设置相机rt的参数进去                                                                 

        cmd.Blit(source, destination);                            // 设置后处理

        cmd.Blit(destination, source, biltmaterial, 0);                            //  第二个Pass  
    }
}
复制成功

URP | 后处理-自定义后处理 - 哔哩哔哩 (bilibili.com)​

cut-off

Shader

上面定好后处理管线了,那我们开始制作Shader, 核心算法是在Shader中。

提供默认Shader模板

代码块
JavaScript
自动换行
复制代码
Shader "Hidden/Outlint"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline"}
        LOD 100

        HLSLINCLUDE

        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"


        CBUFFER_START(UnityPerMaterial)
        float4 _MainTex_ST;

        CBUFFER_END

            struct appdata
            {
                float4 positionOS : POSITION;
                float2 texcoord : TEXCOORD0;
            };

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

            TEXTURE2D(_MainTex);                          SAMPLER(sampler_MainTex);
        ENDHLSL


        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            v2f vert (appdata v)
            {
                v2f o;
                VertexPositionInputs  PositionInputs = GetVertexPositionInputs(v.positionOS.xyz);
                o.positionCS = PositionInputs.positionCS;   
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

                return o;
            }


            half4 frag (v2f i) : SV_Target
            {

                half4 col = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.uv);

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

为了计算轮廓,计算相邻像素进行采样比较两个像素的值,如果数值不同,就判断是边缘绘制一条线。

  • 使用深度计算我们的边缘,在深度缓存中以X形状进行采样,定义我们的渲染储存大小 _MainTex_TexelSize,_Scale

代码块
JavaScript
自动换行
复制代码
float halfScaleFloor = floor(_Scale * 0.5);
float halfScaleCeil = ceil(_Scale * 0.5);

float2 bottomLeftUV  = i.uv - float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleFloor;
float2 topRightUV    = i.uv + float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleCeil;  
float2 bottomRightUV = i.uv + float2(_MainTex_TexelSize.x * halfScaleCeil, -_MainTex_TexelSize.y * halfScaleFloor);
float2 topLeftUV     = i.uv + float2(-_MainTex_TexelSize.x * halfScaleFloor, _MainTex_TexelSize.y * halfScaleCeil);
复制成功

我们首先计算两个值,然后.这两个值将随着增加而递增 1。通过以这种方式缩放UV,我们能够一次仅增加一个像素的边缘宽度 - 实现最大可能的粒度 - 同时仍然保持坐标的中心。

扩展_MainTex_TexelSize和_MainTex_ST 的区别?

  • _MainTex_TexelSize 是贴图 _MainTex 的像素尺寸大小,值: Vector4(1 / width, 1 / height, width, height)half2 offs = _MainTex_TexelSize.xy * half2(1,0) *  _BlurSize;

  • _MainTex_ST 是贴图_MainTex的tiling和offset的四元数_MainTex_ST.xy 是tiling的值_MainTex_ST.zw 是offset的值// Transforms 2D UV by scale/bias property #define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)

注意:这里前到 后处理和管线中增加 控制 Scale 属性

  • 后处理

  • 管线中增加控制Shader

  • 这样后续才能显示正确

cut-off

采样深度

URP | Depth 深度 - 哔哩哔哩 (bilibili.com)​

使用我们计算出来的UV对深度进行采样。

  • 增加深度

  • 因为我们是4个方向,所以使用4个方向采样深度。

代码块
JavaScript
自动换行
复制代码
float depth0 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, bottomLeftUV).r;
float depth1 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, topRightUV).r;
float depth2 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, bottomRightUV).r;
float depth3 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, topLeftUV).r;
复制成功
  • 我们输出一个深度查看一下效果,

  • 效果

  • 使用减法来比较像素之间的不同深度。

由于差值可以是正数或负数,因此我们在返回结果之前取其绝对值。由于附近深度值之间的差异可能非常小(因此很难在屏幕上看到),我们将差异乘以 100 以使其更容易看到。

  • 效果

这个是检测边缘的一半,depthFiniteDifference1 是另一半。

  • 我们现在需要把这俩个合并成一个,

代码块
JavaScript
自动换行
复制代码
float edgeDepth = sqrt(pow(depthFiniteDifference0, 2) + pow(depthFiniteDifference1, 2)) * 100;
复制成功
  • 效果

  • 我们看到表面还是有很多灰色区域,我们希望是只有黑和白。

  • 增加一个变量控制,黑白

代码块
JavaScript
自动换行
复制代码
float edgeDepth = sqrt(pow(depthFiniteDifference0, 2) + pow(depthFiniteDifference1, 2)) * 100;
edgeDepth = edgeDepth > _DepthThreshold ? 1 : 0;
复制成功
  • 到后处理中增加控制变量的方法,

  • 并且增加到 Render里

  • 效果

  • 出现大面积的白色区域,这些区域不是我想要的,我们对表面深度进行调整。

  • 效果

使用深度法线

我们要获取深度法线,我们在管线中增加一个SSAO ,SSAO自带深度法线

  • SSAO

使用深度法线来绘制,不是深度,我们最后把两者结合起来,

  • Shader中增加法线深度

  • 使用同样的方法调用

代码块
JavaScript
自动换行
复制代码
// 深度法线
float3 normal0 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, bottomLeftUV).rgb;
float3 normal1 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, topRightUV).rgb;
float3 normal2 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, bottomRightUV).rgb;
float3 normal3 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, topLeftUV).rgb;

float3 normalFiniteDifference0 = normal1 - normal0;
float3 normalFiniteDifference1 = normal3 - normal2;

float edgeNormal = sqrt(dot(normalFiniteDifference0, normalFiniteDifference0) + dot(normalFiniteDifference1, normalFiniteDifference1));
edgeNormal = edgeNormal > _NormalThreshold ? 1 : 0;

return edgeNormal;
复制成功

我们前输出看一下深度法线是否起作用,

  • 效果

显示这样的效果就是正确的,如果是黑色的还是没有获取到深度法线。

  • 现在输出 edgeNormal 法线计算的边缘

我们可以看到法线产生了一些新的边缘,原来的边缘效果有一些消失了,

  • 我们把这俩种方法结合起来。

  • 效果

  • 我们看到平面有时候一片白色的区域

  • 可以调大 深度处理来处理,但是会出现法线边缘不完整的情况。

为什么出现白色边缘?

白色区域

表面的斜率越大,相邻像素深度之间的差异就越大。沿着这些表面的这种大深度增量导致我们的算法检测它们上的“边缘”

为了实现这一点,我们需要每个表面的法线,以及从相机到表面的视角方向。

我们计算视角方向,

  • 我们使用发法线深度是在屏幕空间中,所以我们摄像机视角方向也需要在屏幕空间才可以计算,

  • 我们需要视角空间转换到屏幕空间,我们需要反向投影矩阵。

  • 后处理脚本中我们计算视角方向

  • Shader中获取后处理传入的View

代码块
JavaScript
自动换行
复制代码
float4x4 _ClipToView;
复制成功
  • 我们Shader计算屏幕空间中的视角方向。

代码块
JavaScript
自动换行
复制代码
v2f vert (appdata v)
            {
                v2f o;
                VertexPositionInputs  PositionInputs = GetVertexPositionInputs(v.positionOS.xyz);
                o.positionCS = PositionInputs.positionCS;   
                
                // uv
                o.uv  = TransformTriangleVertexToUV(o.positionCS.xy);
                #if UNITY_UV_STARTS_AT_TOP
				    o.uv = o.uv * float2(1.0, -1.0) + float2(0.0, 1.0);
			    #endif
	            o.texcoordStereo = TransformStereoScreenSpaceTex(o.uv, 1.0);      
                
                float4 vertex = float4(o.positionCS.xy, 0.0, -1.0);
                o.viewSpaceDir = mul(_ClipToView, vertex).xyz;

                return o;
            }
复制成功
  • 片元着色器阶段来处理计算法线和视角关系

代码块
JavaScript
自动换行
复制代码
float3 viewNormal = normal0 * 2 - 1;
				float NdotV = 1 - dot(viewNormal, -i.viewSpaceDir);

                float normalThreshold01 = saturate((NdotV - _DepthNormalThreshold) / (1 - _DepthNormalThreshold));
                float normalThreshold = normalThreshold01 * _DepthNormalThresholdScale + 1;

                float depthThreshold = _DepthThreshold * depth0 * normalThreshold;

                float depthFiniteDifference0 = depth1 - depth0;
                float depthFiniteDifference1 = depth3 - depth2;

                float edgeDepth = sqrt(pow(depthFiniteDifference0, 2) + pow(depthFiniteDifference1, 2)) * 100;
                edgeDepth = edgeDepth > depthThreshold ? 1 : 0;
复制成功
  • 我们在合并输出,一个是边缘的颜色,一个原图颜色。

代码块
JavaScript
自动换行
复制代码
float edge = max(edgeDepth, edgeNormal);
// 边缘颜色
float4 edgeColor = float4(_Color.rgb, _Color.a * edge);

float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);         
return alphaBlend(edgeColor, color);
复制成功

注意:记得在Volume增加颜色控制。

  • 效果

cut-off

Volume

后处理组件,我们在后处理组件中定义我们刚刚Shader中创建的变量属性,

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

public class OutlintVolume : VolumeComponent, IPostProcessComponent
{
    [Tooltip("边缘颜色")]
    public ColorParameter  OutlintColor = new ColorParameter(Color.white);
    [Tooltip("边缘检测大小")]
    public ClampedFloatParameter Scale = new ClampedFloatParameter(1f, 0f, 10f);
    [Tooltip("深度")]
    public ClampedFloatParameter DepthThreshold = new ClampedFloatParameter(0.2f, 0f, 10f);

    [Tooltip("法线深度")]
    public ClampedFloatParameter NormalThreshold = new ClampedFloatParameter(0.4f, 0f, 1f);
    public ClampedFloatParameter DepthNormalThreshold = new ClampedFloatParameter(0.5f, 0f, 1f);
    public ClampedFloatParameter DepthNormalThresholdScale = new ClampedFloatParameter(7f, 0f, 10f);


    public bool IsActive() => Scale.value > 0;

    public bool IsTileCompatible() => false;

}
复制成功
  • 效果

代码

Shader

代码块
JavaScript
自动换行
复制代码
Shader "Hidden/Outlint"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline"}
        LOD 100

        HLSLINCLUDE

        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"


        CBUFFER_START(UnityPerMaterial)
        float4 _MainTex_ST;
        float4 _MainTex_TexelSize;
        float4x4 _ClipToView;
        float4 _Color;

        float _Scale;
        float _DepthThreshold;
        float _NormalThreshold;

        float _DepthNormalThreshold;
        float _DepthNormalThresholdScale;

        CBUFFER_END

            struct appdata
            {
                float4 positionOS : POSITION;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 viewSpaceDir : TEXCOORD1;
	            float2 texcoordStereo : TEXCOORD2;

            };

            TEXTURE2D(_MainTex);                         SAMPLER(sampler_MainTex);
			TEXTURE2D(_CameraDepthTexture);              SAMPLER(sampler_CameraDepthTexture);
            TEXTURE2D(_CameraNormalsTexture);            SAMPLER(sampler_CameraNormalsTexture);

        ENDHLSL


        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            float _RenderViewportScaleFactor;

            float2 TransformStereoScreenSpaceTex(float2 uv, float w)
            {

                float4 scaleOffset = unity_StereoScaleOffset[unity_StereoEyeIndex];
                scaleOffset.xy *= _RenderViewportScaleFactor;
                return uv.xy * scaleOffset.xy + scaleOffset.zw * w;
            }

            float2 TransformTriangleVertexToUV(float2 vertex)
            {
                float2 uv = (vertex + 1.0) * 0.5;
                return uv;
            }

			float4 alphaBlend(float4 top, float4 bottom)
			{
				float3 color = (top.rgb * top.a) + (bottom.rgb * (1 - top.a));
				float alpha = top.a + bottom.a * (1 - top.a);

				return float4(color, alpha);
			}

            v2f vert (appdata v)
            {
                v2f o;
                VertexPositionInputs  PositionInputs = GetVertexPositionInputs(v.positionOS.xyz);
                o.positionCS = PositionInputs.positionCS;   
                
                // uv
                o.uv  = TransformTriangleVertexToUV(o.positionCS.xy);
                #if UNITY_UV_STARTS_AT_TOP
				    o.uv = o.uv * float2(1.0, -1.0) + float2(0.0, 1.0);
			    #endif
	            o.texcoordStereo = TransformStereoScreenSpaceTex(o.uv, 1.0);      
                
                float4 vertex = float4(o.positionCS.xy, 0.0, -1.0);
                o.viewSpaceDir = mul(_ClipToView, vertex).xyz;

                return o;
            }


            half4 frag (v2f i) : SV_Target
            {

                float halfScaleFloor = floor(_Scale * 0.5);
                float halfScaleCeil = ceil(_Scale * 0.5);

                float2 bottomLeftUV  = i.uv - float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleFloor;
                float2 topRightUV    = i.uv + float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleCeil;  
                float2 bottomRightUV = i.uv + float2(_MainTex_TexelSize.x * halfScaleCeil, -_MainTex_TexelSize.y * halfScaleFloor);
                float2 topLeftUV     = i.uv + float2(-_MainTex_TexelSize.x * halfScaleFloor, _MainTex_TexelSize.y * halfScaleCeil);

                // 深度
                
                float depth0 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, bottomLeftUV).r;
                float depth1 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, topRightUV).r;
                float depth2 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, bottomRightUV).r;
                float depth3 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, topLeftUV).r;
       

                // 深度法线
                float3 normal0 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, bottomLeftUV).rgb;
                float3 normal1 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, topRightUV).rgb;
                float3 normal2 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, bottomRightUV).rgb;
                float3 normal3 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, topLeftUV).rgb;


                float3 viewNormal = normal0 * 2 - 1;
				float NdotV = 1 - dot(viewNormal, -i.viewSpaceDir);

                float normalThreshold01 = saturate((NdotV - _DepthNormalThreshold) / (1 - _DepthNormalThreshold));
                float normalThreshold = normalThreshold01 * _DepthNormalThresholdScale + 1;

                float depthThreshold = _DepthThreshold * depth0 * normalThreshold;

                float depthFiniteDifference0 = depth1 - depth0;
                float depthFiniteDifference1 = depth3 - depth2;

                float edgeDepth = sqrt(pow(depthFiniteDifference0, 2) + pow(depthFiniteDifference1, 2)) * 100;
                edgeDepth = edgeDepth > depthThreshold ? 1 : 0;	



                // 法线深度

                float3 normalFiniteDifference0 = normal1 - normal0;
                float3 normalFiniteDifference1 = normal3 - normal2;

                float edgeNormal = sqrt(dot(normalFiniteDifference0, normalFiniteDifference0) + dot(normalFiniteDifference1, normalFiniteDifference1));
                edgeNormal = edgeNormal > _NormalThreshold ? 1 : 0;

                // 合并输出

                float edge = max(edgeDepth, edgeNormal);
                // 边缘颜色
				float4 edgeColor = float4(_Color.rgb, _Color.a * edge);

				float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);         
                return alphaBlend(edgeColor, color);
            }
            ENDHLSL
        }
    }
}
复制成功

Render

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



public class Outlint : ScriptableRendererFeature
{
    [System.Serializable] 
    public class Settings
    {
        public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;   
        public Shader shader;
    }
    public Settings settings = new Settings();

    OutlintPass outlintPass;           // 定义我们创建出Pass


    public override void Create()
    {
        this.name = "Outlint";    // 模糊渲染的名字
        outlintPass = new OutlintPass(RenderPassEvent.BeforeRenderingPostProcessing, settings.shader);    // 初始化 我们的渲染层级和Shader

    }
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(outlintPass);
    }
}


public class OutlintPass : ScriptableRenderPass
{
    static readonly string RenderTag = "Post Effects";                         // 设置渲染标签

    OutlintVolume outlintvolume;                                                            // 定义组件类型
    Material biltmaterial;                                                      // 后处理材质 


    public OutlintPass(RenderPassEvent evt, Shader biltshader)
    {
        renderPassEvent = evt;
        var shader = biltshader;  

        if (shader == null)
        {
            Debug.LogError("没有指定Shader");
            return;
        }
        biltmaterial = CoreUtils.CreateEngineMaterial(biltshader);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (biltmaterial == null)
        {
            Debug.LogError("材质初始化失败");
            return;
        }

        if (!renderingData.cameraData.postProcessEnabled)
        {
            return;
        }

        var stack = VolumeManager.instance.stack;                          // 传入 volume 
        outlintvolume = stack.GetComponent<OutlintVolume>();                     // 获取到后处理组件

        var cmd = CommandBufferPool.Get(RenderTag);    // 渲染标签 

        Render(cmd, ref renderingData);                 // 调用渲染函数  

        context.ExecuteCommandBuffer(cmd);              // 执行函数,回收。
        CommandBufferPool.Release(cmd);

    }

    void Render(CommandBuffer cmd, ref RenderingData renderingData)
    {
        RenderTargetIdentifier source = renderingData.cameraData.renderer.cameraColorTarget;                 // 定义RT
        RenderTextureDescriptor inRTDesc = renderingData.cameraData.cameraTargetDescriptor;
        inRTDesc.depthBufferBits = 0;                                                                          // 清除深度

        var camera = renderingData.cameraData.camera;                         // 传入摄像机
        Matrix4x4 clipToView = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true).inverse;

        biltmaterial.SetColor("_Color", outlintvolume.OutlintColor.value);   // 获取value 组件的颜色

        biltmaterial.SetMatrix("_ClipToView", clipToView);   // 反向输出到Shader

        biltmaterial.SetFloat("_Scale", outlintvolume.Scale.value);
        biltmaterial.SetFloat("_DepthThreshold", outlintvolume.DepthThreshold.value);
        biltmaterial.SetFloat("_NormalThreshold", outlintvolume.NormalThreshold.value);

        biltmaterial.SetFloat("_DepthNormalThreshold", outlintvolume.DepthNormalThreshold.value);
        biltmaterial.SetFloat("_DepthNormalThresholdScale", outlintvolume.DepthNormalThresholdScale.value);

        int destination = Shader.PropertyToID("Temp1");

        // 获取一张临时RT
        cmd.GetTemporaryRT(destination, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //申请一个临时图像,并设置相机rt的参数进去                                                                 

        cmd.Blit(source, destination);                            // 设置后处理


        cmd.Blit(destination, source, biltmaterial, 0);                            //  第二个Pass  
    }
}
复制成功

Volume

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

public class OutlintVolume : VolumeComponent, IPostProcessComponent
{
    [Tooltip("边缘颜色")]
    public ColorParameter  OutlintColor = new ColorParameter(Color.white);
    [Tooltip("边缘检测大小")]
    public ClampedFloatParameter Scale = new ClampedFloatParameter(1f, 0f, 10f);
    [Tooltip("深度")]
    public ClampedFloatParameter DepthThreshold = new ClampedFloatParameter(0.2f, 0f, 10f);

    [Tooltip("法线深度")]
    public ClampedFloatParameter NormalThreshold = new ClampedFloatParameter(0.4f, 0f, 1f);
    public ClampedFloatParameter DepthNormalThreshold = new ClampedFloatParameter(0.5f, 0f, 1f);
    public ClampedFloatParameter DepthNormalThresholdScale = new ClampedFloatParameter(7f, 0f, 10f);


    public bool IsActive() => Scale.value > 0;

    public bool IsTileCompatible() => false;

}
复制成功

总结

  • 实现后处理描边,比较复杂的地方是在Shader阶段,需要的数据很对,主要是获取深度和深度法线的计算方式,

    • 深度获取记得开始管线

  • 深度法线,我们使用SSAO系统给我们提供好的,

  • 计算视角,需要把视角方向转换到屏幕空间进行计算,那一部分比较复杂,主要是 一个是处理UV,一个是处理使用顶点转换到屏幕空间。

资料

卡通渲染之描边技术的实现(URP) - 知乎 (zhihu.com)

URP/LWRP Shader实现描边效果_danad的博客-CSDN博客_urp 描边

Unity Outline Shader Tutorial - Roystan