urp管线的自学hlsl之路 第二十五篇 Render Feature制作特定模型外描边
雪风carel
2020年07月12日 01:49
收录于文集
共38篇

      本文是记录本人自学urp的一个月写的最后一篇专栏,最近因为urp这个新的Renderer eFeature花了很多精力看官方源码,自己也是一边啃一边问,很多细节的东西自己也没有摸清楚,本文仅供读者参考,实际情况请以读者的实际项目进行调整。本效果的支持如下:

  • 支持颜色修改;

  • 支持队列的过滤;

  • 支持层级的过滤;

  • 支持pass的插入位置;

  • 支持模糊的半径;

  • 支持模糊的迭代数;

  • 支持2种颜色的表现类型,如下图。

左为带内描边,右为不带

        基本原理如下:1.使用DrawRenderers对目标过滤的模型进行渲染,渲染使用材质球里的第一个pass,将得到的图像存到名为temp的RT里;2.然后在将temp的RT在材质的第二三个pass里进行kawase升降采样模糊,把模糊后的图存在_BlurTex的RT里;3.再把屏幕图像sour,_BlurTex的RT,temp的RT在材质的第4个pass里合并计算,根据不同的计算方式可以得到2种描边效果(如上图)。

    先说明shader,shader比较简单,带了4个pass,其部分内容也是之前在专栏里提到过的内容。

       上纯色pass,灰常简单,就是给模型画一下纯色。

        双重模糊DownPass和 双重模糊upPass,在以前的专栏已经细致讲过,这里不再重复,跳转链接https://www.bilibili.com/read/cv6597443​

        合并图像pass,分2种情况进行处理。

        然后就是Renderer Feature部分,rendererFeature里有2个CustomRenderPass,第一个CustomRenderPass插在绘制不透明物体之前,仅仅是给指定模型上纯色画在一张rt上;第一个pass是在画完不透明物体之后(或者半透明物体之后,具体插入位置由读者指定),进行模糊绘制,最后合并所有图像。

        第一个绘制纯色的CustomRenderPass里,设置好过滤的队列和层级,指定渲染目标,使用DrawRenderers进行渲染即可,渲染后并不释放rt(这张图我们还需要继续使用,不能丢回pool里),并且把对应的id存到外面,方便第二个CustomRenderPass使用这张图。

第一个绘制纯色的CustomRenderPass

绘制得到的纯色图和对应的位置

                第二个CustomRenderPass里,先对第一个CustomRenderPass的图像进行模糊计算,计算之前对纯色图做个备份,因为我们后续会用到它。下图为模糊后的效果,使用了3次循环,3次降采样3次升高采样,模糊算子使用kawase算子进行升降,具体的原理请移步该专栏https://www.bilibili.com/read/cv6597443​

双重kawase模糊后的效果

        我们将这张模糊的图像减去之前的纯色图会得到什么结果?模糊的部分只绘制在了外面,貌似一切正常。

        但是若再叠加上屏幕原本的颜色,就发现模型内部的颜色并不对有些“反色”的效果,原因是模型内部的颜色为负数,叠加后就是反色的效果了。

处理方法很简单,直接把为负数的部分直接舍弃掉即可,这里我们使用saturate函数。

舍弃掉负数部分后的效果

当然,我们把负数的部分变成正数会如何?把saturate函数换成abs函数取绝对值试试。看起来效果还不错。

使用abs函数

为了保留这2种效果,本人拿出multi_compile_local做shader变体,然后在RenderFeature外部去控制选择。

         计算完毕后,我们再把所有的rt丢回pool里,包括第一个CustomRenderPass保留的纯色rt。

         这样,我们就得到了我们想要的效果了,更多的细节建议读者在源码里去查看,如果有疑问欢迎留言讨论。

SHADER源码

Shader "WX/URP/Post/SelectOutline&#​34;

{

    Properties

    {

      [HideInInspector]_MainTex("MainTex&#​34;,2D)="white&#​34;{}

      [HideInInspector]_SoildColor("SoildColor&#​34;,Color)=(1,1,1,1)

      [HideInInspector]_Blur("Blur&#​34;,float)=1

    }

    SubShader

    {

        Tags{

        "RenderPipeline&#​34;="UniversalRenderPipeline&#​34;

        }

        Cull Off ZWrite Off ZTest Always

        HLSLINCLUDE

        #include &#​34;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

        CBUFFER_START(UnityPerMaterial)

        float4 _MainTex_TexelSize;

        real4 _SoildColor;

        float _Blur;

        CBUFFER_END

        TEXTURE2D(_MainTex);

        SAMPLER(sampler_MainTex);

        TEXTURE2D(_BlurTex);

        SAMPLER(sampler_BlurTex);

        TEXTURE2D(_SourTex);

        SAMPLER(sampler_SourTex);

         struct a2v

         {

             float4 positionOS:POSITION;

             float2 texcoord:TEXCOORD;

         };

         struct v2f

         {

             float4 positionCS:SV_POSITION;

             float4 texcoord[4]:TEXCOORD;

         };

        ENDHLSL

        pass//上纯色

        {

            HLSLPROGRAM

            #pragma vertex VERT

            #pragma fragment FRAG

            v2f VERT(a2v i)

            {

                v2f o;

                o.positionCS=TransformObjectToHClip(i.positionOS.xyz);

                return o;

            }

            half4 FRAG(v2f i):SV_TARGET

            {

                return _SoildColor;

            }

            ENDHLSL

        }

        pass//双重模糊DownPass

        {

            NAME"Down&#​34;

            HLSLPROGRAM

            #pragma vertex VERT

            #pragma fragment FRAG

            v2f VERT(a2v i)

            {

                v2f o;

                o.positionCS=TransformObjectToHClip(i.positionOS.xyz);

                o.texcoord[2].xy=i.texcoord;

                o.texcoord[0].xy=i.texcoord+float2(1,1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                o.texcoord[0].zw=i.texcoord+float2(-1,1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                o.texcoord[1].xy=i.texcoord+float2(1,-1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                o.texcoord[1].zw=i.texcoord+float2(-1,-1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                return o;

            }

            half4 FRAG(v2f i):SV_TARGET

            {

                half4 tex=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[2].xy)*0.5;

                for(int t=0;t<2;t++)

                {

                    tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[t].xy)*0.125;

                    tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[t].zw)*0.125;

                }

                return tex;

            }

            ENDHLSL

        }

        pass//双重模糊upPass

        {

            NAME"Up&#​34;

            HLSLPROGRAM

            #pragma vertex VERT

            #pragma fragment FRAG

            v2f VERT(a2v i)

            {

                v2f o;

                o.positionCS=TransformObjectToHClip(i.positionOS.xyz);

                

                o.texcoord[0].xy=i.texcoord+float2(1,1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                o.texcoord[0].zw=i.texcoord+float2(-1,1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                o.texcoord[1].xy=i.texcoord+float2(1,-1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                o.texcoord[1].zw=i.texcoord+float2(-1,-1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                o.texcoord[2].xy=i.texcoord+float2(0,2)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                o.texcoord[2].zw=i.texcoord+float2(0,-2)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                o.texcoord[3].xy=i.texcoord+float2(-2,0)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                o.texcoord[3].zw=i.texcoord+float2(2,0)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;

                return o;

            }

            half4 FRAG(v2f i):SV_TARGET

            {

                half4 tex=0;

                for(int t=0;t<2;t++)

                {

                    tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[t].xy)/6;

                    tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[t].zw)/6;

                }

                for(int k=2;k<4;k++)

                {

                    tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[k].xy)/12;

                    tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[k].zw)/12;

                }

              

                return tex;

            }

            ENDHLSL

        }

        pass//合并所有图像

        {

            HLSLPROGRAM

            #pragma vertex VERT

            #pragma fragment FRAG

            #pragma multi_compile_local _INCOLORON _INCOLOROFF

            v2f VERT(a2v i)

            {

                v2f o;

                o.positionCS=TransformObjectToHClip(i.positionOS.xyz);

                o.texcoord[0].xy=i.texcoord.xy;

                return o;

            }

            half4 FRAG(v2f i):SV_TARGET

            {

                real4 blur=SAMPLE_TEXTURE2D(_BlurTex,sampler_BlurTex,i.texcoord[0].xy);

                real4 sour=SAMPLE_TEXTURE2D(_SourTex,sampler_SourTex,i.texcoord[0].xy);

                real4 soild=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[0].xy);

                real4 color;

                #ifdef _INCOLORON

                color= abs(blur-soild)+sour;

                #elif _INCOLOROFF

                color=saturate(blur-soild)+sour;

                 #endif

                return color;

                

            }

            ENDHLSL

        }

        

    }

}

RendererFeature源码

using UnityEngine;

using UnityEngine.Rendering;

using UnityEngine.Rendering.Universal;

[ExecuteInEditMode]

public class SelectOutline : ScriptableRendererFeature

{

    public enum TYPE

        {

            INcolorON,INcolorOFF

        }

    [System.Serializable]public class setting

    {

        public Material mymat;

        public Color color=Color.blue;

        [Range(1000,5000)]public int QueueMin=2000;

        [Range(1000,5000)]public int QueueMax=2500;

        public LayerMask layer;

        public RenderPassEvent passEvent=RenderPassEvent.AfterRenderingSkybox;

       [Range(0.0f,3.0f)] public float blur=1.0f;

        [Range(1,5)]public int passloop=3;

        

        public TYPE ColorType=TYPE.INcolorON;

    }

    public setting mysetting=new setting();

    int solidcolorID;

    //第一个pass 绘制纯色的图像

    class DrawSoildColorPass : ScriptableRenderPass

    {

        setting mysetting=null;

        SelectOutline SelectOutline=null;

        ShaderTagId shaderTag = new ShaderTagId("DepthOnly&#​34;);//只有在这个标签LightMode对应的shader才会被绘制

        FilteringSettings filter;

        public DrawSoildColorPass(setting setting,SelectOutline render)

        {

            mysetting=setting;

            SelectOutline=render;

            //过滤设定

            RenderQueueRange queue=new RenderQueueRange();

            queue.lowerBound=Mathf.Min(setting.QueueMax,setting.QueueMin);

            queue.upperBound=Mathf.Max(setting.QueueMax,setting.QueueMin);

            filter=new FilteringSettings(queue,setting.layer);

        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)

        {

            int temp=Shader.PropertyToID("_MyTempColor1&#​34;);

            RenderTextureDescriptor desc=cameraTextureDescriptor;

            cmd.GetTemporaryRT(temp,desc);

            SelectOutline.solidcolorID=temp;

            ConfigureTarget(temp);

            ConfigureClear(ClearFlag.All,Color.black);

        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)

        {

            mysetting.mymat.SetColor("_SoildColor&#​34;,mysetting.color);

            CommandBuffer cmd=CommandBufferPool.Get("提取固有色pass&#​34;);

            //绘制设定

            var draw=CreateDrawingSettings(shaderTag,ref renderingData,renderingData.cameraData.defaultOpaqueSortFlags);

            draw.overrideMaterial=mysetting.mymat;

            draw.overrideMaterialPassIndex=0;

            //开始绘制(准备好了绘制设定和过滤设定)

            context.DrawRenderers(renderingData.cullResults,ref draw,ref filter);

            context.ExecuteCommandBuffer(cmd);

            CommandBufferPool.Release(cmd);

        }

    }

    //第二个pass 计算颜色

    class Calculate : ScriptableRenderPass

    {

        setting mysetting=null;

        SelectOutline SelectOutline=null;

        struct LEVEL//

        {

            public int down;

            public int up;

        };

        LEVEL[] my_level;

        int maxLevel=16;

        RenderTargetIdentifier sour;

        public Calculate(setting setting,SelectOutline render,RenderTargetIdentifier source)

        {

            mysetting=setting;

            SelectOutline=render;

            sour=source;

            my_level = new LEVEL[maxLevel];

            for (int t = 0; t < maxLevel; t++)//申请32个ID的,up和down各16个,用这个id去代替临时RT来使用

            {

                my_level[t] = new LEVEL

                {

                    down = Shader.PropertyToID("_BlurMipDown&#​34;+t),

                    up = Shader.PropertyToID("_BlurMipUp&#​34;+t)

                };

            }

        if(mysetting.ColorType== TYPE.INcolorON)

            {

            mysetting.mymat.EnableKeyword("_INCOLORON&#​34;);

            mysetting.mymat.DisableKeyword("_INCOLOROFF&#​34;);  

            }

        else

            {

            mysetting.mymat.EnableKeyword("_INCOLOROFF&#​34;);

            mysetting.mymat.DisableKeyword("_INCOLORON&#​34;);

            }

        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)

        {

            CommandBuffer cmd=CommandBufferPool.Get("颜色计算&#​34;);

            RenderTextureDescriptor desc=renderingData.cameraData.cameraTargetDescriptor;

            int SourID=Shader.PropertyToID("_SourTex&#​34;);

            cmd.GetTemporaryRT(SourID,desc);

            cmd.CopyTexture(sour,SourID);

            //计算双重kawase模糊

            int BlurID=Shader.PropertyToID("_BlurTex&#​34;);

            cmd.GetTemporaryRT(BlurID,desc);

            mysetting.mymat.SetFloat("_Blur&#​34;,mysetting.blur);

            int width=desc.width/2;

            int height=desc.height/2;

            int LastDown=SelectOutline.solidcolorID;

            for(int t=0;t<mysetting.passloop;t++)

            {

                int midDown=my_level[t].down;//middle down ,即间接计算down的工具人ID

                int midUp=my_level[t].up; //middle Up ,即间接计算的up工具人ID

                cmd.GetTemporaryRT(midDown,width,height,0,FilterMode.Bilinear,RenderTextureFormat.ARGB32);//对指定高宽申请RT,每个循环的指定RT都会变小为原来一半

                cmd.GetTemporaryRT(midUp,width,height,0,FilterMode.Bilinear,RenderTextureFormat.ARGB32);//同上,但是这里申请了并未计算,先把位置霸占了,这样在UP的循环里就不用申请RT了

                cmd.Blit(LastDown,midDown,mysetting.mymat,1);//计算down的pass

                LastDown=midDown;//工具人辛苦了

                width=Mathf.Max(width/2,1);//每次循环都降尺寸

                height=Mathf.Max(height/2,1);

            }

            //up

            int lastUp=my_level[mysetting.passloop-1].down;//把down的最后一次图像当成up的第一张图去计算up

            for(int j=mysetting.passloop-2;j>=0;j--)//这里减2是因为第一次已经有了要减去1,但是第一次是直接复制的,所以循环完后还得补一次up

            {

                int midUp=my_level[j].up;

                cmd.Blit(lastUp,midUp,mysetting.mymat,2);

                lastUp=midUp;

            }

            cmd.Blit(lastUp,BlurID,mysetting.mymat,2);//补一个up,顺便在模糊一下

            cmd.Blit(SelectOutline.solidcolorID,sour,mysetting.mymat,3);//在第4个pass里合并所有图像

            context.ExecuteCommandBuffer(cmd);

            //回收

            for(int k=0;k<mysetting.passloop;k++)

            {

                cmd.ReleaseTemporaryRT(my_level[k].up);

                cmd.ReleaseTemporaryRT(my_level[k].down);

            }

            cmd.ReleaseTemporaryRT(BlurID);

            cmd.ReleaseTemporaryRT(SourID);

            cmd.ReleaseTemporaryRT(SelectOutline.solidcolorID);

            CommandBufferPool.Release(cmd);

        }

    }

    DrawSoildColorPass m_DrawSoildColorPass;

    Calculate m_Calculate;

    public override void Create()

    {

        m_DrawSoildColorPass = new DrawSoildColorPass(mysetting,this);

        m_DrawSoildColorPass.renderPassEvent = RenderPassEvent.AfterRenderingPrePasses;

    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)

    {

        if(mysetting.mymat!=null)

        {

        RenderTargetIdentifier sour= renderer.cameraColorTarget;

        renderer.EnqueuePass(m_DrawSoildColorPass);

        m_Calculate=new Calculate(mysetting,this,sour);

        m_Calculate.renderPassEvent=mysetting.passEvent;

        renderer.EnqueuePass(m_Calculate);

        }

        else

        {

            Debug.LogError("材质球丢失!请设置材质球&#​34;);

        }

    }

}