unity 3d物体描边效果_使用Render Feature做描边效果

cbacdd50a3ed3a4d5ae9f379ea127d41.png

43a11cb9abfd324e664a7c3ed2fe3570.png

在Unity轻量管线里实现多pass的shader一直是头疼的问题,两次还可以正常显示,但超过两次的就GG。

6731d9ce23d937b2464ef74ea1e32c3a.png

这次策划给了个需求,要求物体的描边在被遮挡的时候也要显示,原来的描边会被遮挡,不能满足需要,而且不能使用屏幕后期,那就得用上stencil了。可这样就得至少三次pass,第一次绘制主物体,第二次用stencil绘制遮挡物体,第三次扩大物体,绘制边缘。如果这么做一方面轻量管线不支持,需要通过SRP实现,另一方面控制起来会比较麻烦。正好之前看到Unity推文介绍Custom Forward Renderer功能,研究了下。

国内地址​www.bilibili.com

下面简单介绍下流程。

0x01 测试环境

Unity2019.3.4f1

URP 7.3.1

0x02 方案一:层方案

2.1 编写描边代码

Shader "Unlit / OutLine"
{
    Properties
    {
        _Color("outline color",color) = (1,1,1,1)
        _Width("outline width",range(0,1)) = 0.2
    }
    Subshader
    {
        Pass
        {
        Tags {"LightMode" = "LightweightForward" "RenderType" = "Opaque" "Queue" = "Geometry + 10"}
                //Tags可不添加,只是为了演示
 
        colormask 0 //不输出颜色
        ZWrite Off
        ZTest Off
 
        Stencil
        {
            Ref 1
            Comp Always
            Pass replace
        }
 
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
 
            #include "Packages/com.unity.render-pipelines.lightweight/ShaderLibrary/Core.hlsl"
 
            struct appdata
            {
                float4 vertex: POSITION;
            };
 
            struct v2f
            {
                float4 vertex: SV_POSITION;
            };
 
            v2f vert(appdata v)
            {
                v2f o;
                                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                return o;
            }
 
            half4 frag(v2f i) : SV_Target
            {
                return half4 (0.5h, 0.0h, 0.0h, 1.0h);
            }
            ENDHLSL
        }
 
        Pass
        {
                Tags {"RenderType" = "Opaque" "Queue" = "Geometry + 20"}
 
            ZTest off
 
            Stencil {
                Ref 1
                Comp notEqual
                Pass keep
            }
 
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
 
            #include "Packages/com.unity.render-pipelines.lightweight/ShaderLibrary/Core.hlsl"
 
            struct appdata
            {
                float4 vertex: POSITION;
                float3 normal:NORMAL;
            };
 
            struct v2f
            {
                float4 vertex: SV_POSITION;
            };
 
            half4 _Color;
            half _Width;
 
            v2f vert(appdata v)
            {
                v2f o;
                //v.vertex.xyz += _Width * normalize(v.vertex.xyz);
 
                v.vertex.xyz += _Width * normalize(v.normal);
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                return o;
            }
 
            half4 frag(v2f i) : SV_Target
            {
                return _Color;
            }
            ENDHLSL
        }
    }
}

2.2 创建材质球,命名为“Outline”,效果如图:

79e66dbbf5b844159dc1c7a03b4e12e1.png

这种描边会有断边和混合的问题,这里不展开,解决办法在此 硬边描边断边问题。

2.3 创建渲染层级

03ee24e0d8767d29db48a8d610620395.png

我这里添加了叫Outline的层级,用来处理满足需要的描边pass。

2.4 创建Custom Renderer

创建UniversalRenderAsset后会默认新建一个叫UniversalRenderAsset_Render的RenderList,这是控制渲染路径的资源。

e12b4915d0638ec8018ed832bb65d47c.png

Opaque Layer Mask:设置不透明物体渲染的层级,原视频里通过屏蔽“Character”图层,然后在Renderer Features里对“Character”图层单独控制。这里可以屏蔽outline层,然后再RenderObject里对这个层特殊处理。

Transparent Layer Mask:同理。

Overrides:覆盖选中的层。

  • Stencil:覆盖Default层的设置,特殊需求时使用。

2.5 设置Custom Renderer

点击AddRenderFeature按钮,新建两个RenderObject。之所以是两个,是因为上面描边的shader用了两个pass,第一个pass用来处理mask,第二个pass用来发现外扩填充描边。

2bfa05f4ac2a975115a9b143f70cc742.png
  • Name:此List的名称
  • Event:设置渲染时机
  • Queue:渲染队列
  • Layer Mask:设置需要渲染的图层
  • Shader Passes:选择当前物体渲染Pass的回调位置(Callback),后面讲这个选项
  • Material:需要覆盖的材质球,
  • Pass Index:选择相应的pass,实现不同效果
  • Depth:覆盖原深度,官方视频就是用这个来处理遮挡效果
  • Stencil:覆盖原模板缓冲,因此原shader中的模板缓冲只用来检查,还需要这里配置正确
  • Camera:覆盖原相机投影(这个有意思,官方提供的例子是FPS游戏中的枪和场景的FOV不同)

上图是我对描边做的设置,我将对Outline做特殊处理,注意List的顺序就是渲染的顺序,顺序不对效果也会有偏差。

2.6 设置Universal Render Pipeline Asset

e0034748a4adc05879990f29215de069.png

设置好Custom Renderer Data,得有入口才能做渲染。URP下默认帮你设置好默认渲染的list,我们之前也是对这个RenderObject做的处理,理解这个功能后也可以自行添加不同的UniversalRenderAsset实现特殊效果。

2.7 查看效果

52664c657bd609934e86e6b3cb870cd2.png

首先创建测试物体,设置层级为Default。

f957262f9b3036dc6af9fa36b06d89e3.png

将他们的层级设置为outline后的效果。

实现逻辑是先按照RenderList里的设置参数跑一遍默认的效果,然后对指定层级的物体做后续的自定义渲染。

2.8 性能测试

fb17c9b961c8b8605f140fe094647f9e.png

7b8df1779cf2ff35309c48e3deb6eaf9.png

16e2ae85345e2403bfd17c97dd251c9a.png

更新Unity后真躺着优化,SRP Batcher没有因为描边pass而打断,性能开销方面主要是描边本身,后续优化描边算法,让其只是srp batcher看看能不能解决合批问题。

0x02 PassName方案

3.1 编写描边代码

Shader "OutLine_type2"
{
    Properties
    {
        _Matcap("Matcap", 2D) = "white" {}
 
        _Color("outline color",color) = (1,1,1,1)
        _Width("outline width",range(0,1)) = 0.2
 
        [Space][Header(Ztest State)]
        [Enum(UnityEngine.Rendering.CompareFunction)]_ZTest("Ztest", Float) = 0.0
    }
 
    Subshader
    {
        Tags{"RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "Queue" = "Geometry" "IgnoreProjector" = "True"}
 
        LOD 300
 
        Cull Back
        HLSLINCLUDE
        #pragma target 2.0
        ENDHLSL
 
 
        Pass
        {
        //新增matcap效果
            Name "Forward"
            Tags { "LightMode" = "UniversalForward" }
 
            ZWrite On
            ZTest LEqual
            Offset 0 , 0
 
            HLSLPROGRAM
            #pragma multi_compile_instancing
            #define ASE_SRP_VERSION 999999
 
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
 
            #pragma vertex vert
            #pragma fragment frag
 
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderGraphFunctions.hlsl"
 
            #if ASE_SRP_VERSION <= 70108
            #define REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR
            #endif
 
            #define ASE_NEEDS_VERT_NORMAL
 
 
            struct VertexInput
            {
                float4 vertex : POSITION;
                float3 ase_normal : NORMAL;
 
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };
 
            struct VertexOutput
            {
                float4 clipPos : SV_POSITION;
                #if defined(ASE_NEEDS_FRAG_WORLD_POSITION)
                float3 worldPos : TEXCOORD0;
                #endif
                #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) && defined(ASE_NEEDS_FRAG_SHADOWCOORDS)
                float4 shadowCoord : TEXCOORD1;
                #endif
                #ifdef ASE_FOG
                float fogFactor : TEXCOORD2;
                #endif
                float4 ase_texcoord3 : TEXCOORD3;
                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };
 
            sampler2D _Matcap;
 
 
 
            VertexOutput vert(VertexInput v)
            {
                VertexOutput o = (VertexOutput)0;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
 
                half3 ase_worldNormal = TransformObjectToWorldNormal(v.ase_normal);
                o.ase_texcoord3.xyz = ase_worldNormal;
 
 
                //setting value to unused interpolator channels and avoid initialization warnings
                o.ase_texcoord3.w = 0;
                #ifdef ASE_ABSOLUTE_VERTEX_POS
                    float3 defaultVertexValue = v.vertex.xyz;
                #else
                    float3 defaultVertexValue = float3(0, 0, 0);
                #endif
                float3 vertexValue = defaultVertexValue;
                #ifdef ASE_ABSOLUTE_VERTEX_POS
                    v.vertex.xyz = vertexValue;
                #else
                    v.vertex.xyz += vertexValue;
                #endif
                v.ase_normal = v.ase_normal;
 
                float3 positionWS = TransformObjectToWorld(v.vertex.xyz);
                float4 positionCS = TransformWorldToHClip(positionWS);
 
                #if defined(ASE_NEEDS_FRAG_WORLD_POSITION)
                o.worldPos = positionWS;
                #endif
                #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) && defined(ASE_NEEDS_FRAG_SHADOWCOORDS)
                VertexPositionInputs vertexInput = (VertexPositionInputs)0;
                vertexInput.positionWS = positionWS;
                vertexInput.positionCS = positionCS;
                o.shadowCoord = GetShadowCoord(vertexInput);
                #endif
                #ifdef ASE_FOG
                o.fogFactor = ComputeFogFactor(positionCS.z);
                #endif
                o.clipPos = positionCS;
                return o;
            }
 
            half4 frag(VertexOutput IN) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(IN);
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(IN);
 
                #if defined(ASE_NEEDS_FRAG_WORLD_POSITION)
                float3 WorldPosition = IN.worldPos;
                #endif
                float4 ShadowCoords = float4(0, 0, 0, 0);
 
                #if defined(ASE_NEEDS_FRAG_SHADOWCOORDS)
                    #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
                        ShadowCoords = IN.shadowCoord;
                    #elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)
                        ShadowCoords = TransformWorldToShadowCoord(WorldPosition);
                    #endif
                #endif
                half3 ase_worldNormal = IN.ase_texcoord3.xyz;
 
                float3 BakedAlbedo = 0;
                float3 BakedEmission = 0;
                float3 Color = tex2D(_Matcap, ((mul(UNITY_MATRIX_V, half4(ase_worldNormal , 0.0)).xyz * 0.5) + 0.5).xy).rgb;
                float Alpha = 1;
                float AlphaClipThreshold = 0.5;
 
                #ifdef _ALPHATEST_ON
                    clip(Alpha - AlphaClipThreshold);
                #endif
 
                #ifdef ASE_FOG
                    Color = MixFog(Color, IN.fogFactor);
                #endif
 
                #ifdef LOD_FADE_CROSSFADE
                    LODDitheringTransition(IN.clipPos.xyz, unity_LODFade.x);
                #endif
 
                return half4(Color, Alpha);
            }
 
            ENDHLSL
        }
 
 
    Pass
        {
        Name "Mask"
        Tags {
        "LightMode" = "Mask"    //切记这里灯光模式为Mask
        "RenderType" = "Transparent"
        "Queue" = "Transparent + 10"
        }
 
        Cull off
        colormask 0
        ZWrite Off
        ZTest always
 
        Stencil
        {
            Ref 1
            Comp Always
            Pass replace
        }
 
        }
 
        Pass
        {
                Name "Fill"
                Tags {
                "LightMode" = "Fill"    //切记这里灯光模式为Fill
                "RenderType" = "Transparent"
                "Queue" = "Transparent + 20"
                "DisableBatching" = "True"
            }
 
 
                Cull Off
                ZWrite on
                ZTest [_ZTest]
 
                Blend SrcAlpha OneMinusSrcAlpha
                ColorMask RGB
 
            Stencil {
                Ref 1
                Comp notEqual
                Pass keep
            }
 
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
 
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
 
            struct appdata
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float3 smoothNormal : TEXCOORD3;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };
 
            struct v2f
            {
                float4 vertex: SV_POSITION;
                float4 color : COLOR;
                UNITY_VERTEX_OUTPUT_STEREO
            };
 
            half4 _Color;
            half _Width;
 
            v2f vert(appdata v)
            {
                v2f o;
                 
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
 
                VertexPositionInputs input = GetVertexPositionInputs(v.vertex.xyz);
 
                //float3 normal = any(v.smoothNormal) ? v.smoothNormal : v.normal;
                float3 normal = v.normal;
                float3 viewPosition = input.positionVS;
                float3 viewNormal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, normal));
 
                o.vertex = mul(UNITY_MATRIX_P, float4(viewPosition + viewNormal * -viewPosition.z * _Width / 100.0, 1.0));
                o.color = _Color;
 
                return o;
            }
 
            half4 frag(v2f i) : SV_Target
            {
                return i.color;
            }
            ENDHLSL
        }
    }
}

与用层的方案不同,用层的方案时可以将初始效果和描边效果分开写两个shader,但用Passes Name的方案就得将初始效果和描边效果写在同一个shader里,同时切记LightMode的名字需要唯一,底层没做for循环。

3.2 创建材质球

11a5cc104731cf9ac3a0d0acae03dcd9.png

3.3 设置Custom Renderer

这时候就要讲上面说的Shader Passes Name参数。我这里用了两种方法并且对比了其效果。

2e8aabb54e5aabfe7f0fabe514853d0e.png

8e94c80eafa39a0de671899139eab401.png

第一种方法只用一个RenderObject,但在ShaderPassesName的地方设置了两个pass的名字,会发现当有多个子物体时,描边效果会穿插。原因是只有一个RenderLoop,会对每个子物体单独绘制描边,效果就会出现堆叠,哪怕在ZTest上下功夫也会有瑕疵。

75b9ab5b40133adf1f68a9cf057d1c86.png

453966fd9660ca06b8a03f1b280db2e3.png

第二种方法使用两个RenderObject,分别处理两个功能Pass,得到两个RenderLoop,避免描边堆叠的问题。对于增加的RenderLoop开销问题也问了引擎,理论上开销不大。(这里有个unity的bug,ShaderPassesName列表居然不能删除,Rua鸡)

0x03 遇到的问题

使用第一种方法需要消耗一个层来做特殊效果,对于正常游戏项目,层经常吃紧,而且不同效果需要不同层,会让层更紧张。

对于描边效果而言,需求往往是针对某个物体有描边,并不希望对整体有描边效果,因此用第二种方案就需要写两个shader,一个普通效果,一个普通效果加描边效果,这会增加shader变体数量,对变体数量敏感的项目而言会不太友好。如果需求变成”我要在展示界面有妲己毛发效果,游戏中不要”,那就好办了,直接通过代码控制RenderObject开关就行。代码如下:

public class ChangePipline : MonoBehaviour
{
    public ForwardRendererData forwardRenderData;
    private List<ScriptableRendererFeature> _features;
 
    private void Start()
    {
        _features = forwardRenderData.rendererFeatures;
    }
 
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Z))
        {
            _features[1].SetActive(false);
        }
        if (Input.GetKeyDown(KeyCode.X))
        {
            _features[1].SetActive(true);
        }
    }
}

0x04 结论

Unity2019新增的Custom Forward Renderer简单实用,不需要代码就可实现在轻量管线下的多Pass功能,而且只要通过层级管理就可以。Unity提供了32个层,怕不够用??还有PassName的方案,另外还有屏幕后期这条路,这里不介绍。妲己毛发是不是感觉有救了?

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值