

本文是记录本人自学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"
{
Properties
{
[HideInInspector]_MainTex("MainTex",2D)="white"{}
[HideInInspector]_SoildColor("SoildColor",Color)=(1,1,1,1)
[HideInInspector]_Blur("Blur",float)=1
}
SubShader
{
Tags{
"RenderPipeline"="UniversalRenderPipeline"
}
Cull Off ZWrite Off ZTest Always
HLSLINCLUDE
#include "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"
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"
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");//只有在这个标签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");
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",mysetting.color);
CommandBuffer cmd=CommandBufferPool.Get("提取固有色pass");
//绘制设定
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"+t),
up = Shader.PropertyToID("_BlurMipUp"+t)
};
}
if(mysetting.ColorType== TYPE.INcolorON)
{
mysetting.mymat.EnableKeyword("_INCOLORON");
mysetting.mymat.DisableKeyword("_INCOLOROFF");
}
else
{
mysetting.mymat.EnableKeyword("_INCOLOROFF");
mysetting.mymat.DisableKeyword("_INCOLORON");
}
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
CommandBuffer cmd=CommandBufferPool.Get("颜色计算");
RenderTextureDescriptor desc=renderingData.cameraData.cameraTargetDescriptor;
int SourID=Shader.PropertyToID("_SourTex");
cmd.GetTemporaryRT(SourID,desc);
cmd.CopyTexture(sour,SourID);
//计算双重kawase模糊
int BlurID=Shader.PropertyToID("_BlurTex");
cmd.GetTemporaryRT(BlurID,desc);
mysetting.mymat.SetFloat("_Blur",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("材质球丢失!请设置材质球");
}
}
}