Unity3D Shader系列之画虚线方式分析与总结

Unity3D Shader系列之画虚线方式分析与总结

2020年12月6日 0 作者 老王

1 引言

总结了一下几种画虚线的方式。
①使用LineRenderer
②代码生成网格画虚线
③使用片元着色器画虚线
④使用几何着色器画虚线
⑤使用UILineRenderer
⑥使用Vectrosity插件
6种方式都不是完美的,有利有弊,需要根据实际项目进行选择与调整。
本文我自己搞着玩儿总结的,如有不正确的地方欢迎批评指正。

2 LineRenderer画虚线

使用LineRenderer画虚线的方式实质是对虚线贴图进行UV采样。重点是Shader的选择,在3D场景时选择Legacy Shaders》Particles》Additive,在UI上显示为GUI 》Text Shader。
虚线
具体步骤如下:
①添加LineRenderer组件 Component 》Effects》LineRenderer
②创建一个材质球,选择Shader为Legacy Shaders》Particles》Additive(3D场景上使用)或者GUI 》Text Shader(UI上使用),并将虚线贴图指定给材质球
Paticles Additive

GUI Text Shader
③将②创建的材质球指定给LineRenderer,设置Positions并调节其他参数,这里就不细说了
使用LineRenderer有两个地方需要注意:
①3D场景中LineRenderer的Shader不能使用GUI 》Text Shader
为什么?因为选择GUI 》Text Shader,LineRenderer将会一直显示在所有不透明物体前面,效果如下。
LineRenderer使用GUI》Text Shader的效果
为什么会这样?
Unity会先渲染不透明物体(开启了深度测试与深度写入),然后再渲染半透明物体(一般会开启深度测试,但关闭深度写入)。
我找了很久GUI 》Text Shader的源码,但是没找到,我猜测它内部是关闭了深度测试 ZTest Off,也就是说当渲染LineRenderer时,不管这个像素点有没有被其他物体挡住,都将LineRenderer的颜色写入,这就导致了LineRenderer显示在最前面。
如果仍然想用Text Shader的话,可以稍微修改一下,只需去掉ZTest Off即可(去掉之后默认是ZTest LEqual)。

Shader "GUI/3D Text Shader" { 
    Properties {
        _MainTex ("Font Texture", 2D) = "white" {}
        _Color ("Text Color", Color) = (1,1,1,1)
    }

    SubShader {
        Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
        Lighting Off Cull Off ZWrite Off Fog { Mode Off }
        Blend SrcAlpha OneMinusSrcAlpha
        Pass {
            Color [_Color]
            SetTexture [_MainTex] {
                combine primary, texture * primary
            }
        }
    }
}

使用3D Text Shader的效果如下。可以看到,球体可以遮挡掉虚线了,效果正确。
使用3DText Shader

当然如果,不想自己写Shader,使用Legacy Shaders》Particles》Additive也是可以的。

②若想在UGUI上使用LineRenderer画虚线该怎么处理?

Camera的渲染模式一定要选择为Screen Space – Camera

Camera选择Screen Space - Camera模式

不能选择为Overlay模式,为什么?因为选择Overlay模式后,Unity会在除UI外的其他物体渲染完毕之后,最后去渲染UI,这样保证UI一直是在屏幕的最前面。这时无论LineRenderer选择什么Shader,无论如何调整LineRenderer的位置,其都是在UI的后面。
Canvas选择为Screen Space - Overlay模式LineRenderer一直在UI下面

但是Camera选择为Screen Space – Camera是不是就大功搞成啦?不是的,仍然会有新的问题,比如与其他UI元素的遮挡关系的问题。
举个例子。LineRenderer选择的Shader为GUI》Text Shader,其在Background与FrontGround两张图片中间。UI-LineRenderer视图如下
我们想要的效果:
我们想要的效果

实际的效果:
实际的效果

如果我们挪动一下FrontGround或者BackGround,会发现LineRenderer一会儿在前一会儿在后。
LineRenderer在UGUI上的层级问题

为什么会出现这种现象,不是说GUI 》Text Shader一直显示在最上层吗?
要回答这个问题,我们需要先知道两个知识点。
一是UI没什么特别的,在本质上都是网格Mesh,UGUI的Canvas在进行渲染前会进行合批(Batch),就是将这个Canvas下的满足合批规则的UI的网格合并为一个大的网格,然后再提交给GPU渲染。(合批的作用是减少Draw Call)
UI的本质仍是网格
我们怎么去看两个UI元素是否合批了呢?
咱们可以通过Windows》Analysis》Profiler工具进行分析。
Profiler工具的打开
合批效果

二是Unity渲染物体是有顺序的,这个顺序叫渲染顺序。
具体渲染顺序是这样的,先渲染所有不透明物体,然后把所有半透明物体按它们距离摄像机的远近进行排序,再按照从后往前的顺序渲染透明物体。
但是这个将物体“按它们距离摄像机的远近”就有点模糊了,一个物体那么大(呈现在屏幕上会是一个区域),该以物体的哪一个点来计算呢,是物体上用距离相机的最远点、最近点、还是中点还是其他什么点?答案是用哪一个点来计算都是不合适的。

半透明物体循环重叠效果

在上面这两种情况中,无论是左边的三个半透明物体还是右边的两个半透明物体都是无法渲染正确的。
这里可以参考冯乐乐《Unity Shader入门精要》163页8.1节为什么渲染顺序很重要,看完应该就明白了。
而我们这里出现上面的结果我猜测很可能是这个原因。
等等,上面不是说GUI》Text Shader都渲染在不透明物体的最上面的吗,这里怎么会跑到Frontground和Background后面去了?
因为两个Image也是半透明物体,所以移动的Background的时候,合并后的网格一会儿排序在LineRenderer前面,一会儿排序在LineRenderer后面。
这里有个问题,一个物体是不是半透明物体到底怎么确定?
需要根据Shader来确定,Frontground和Background两个Image用的Shader都是默认的UI/Default,渲染队列是Transparent,此队列针对半透明物体的。
咱们可以去材质调节面板查看,渲染队列(Render Queue)大于3000的都是半透明物体。

UI/Default Shader材质面板

ps:Unity提前定义的5个渲染队列如下

名称 队列索引号 描述
Background 1000 在其他任何渲染队列之前被渲染,正如名称一样,一般用来渲染背景物体
Geometry 2000 默认的渲染队列,不透明物体使用此渲染队列
AplhaTest 2450 需要进行透明度测试的物体使用此物体,从Geometry抽离出来,原因是在所有不透明物体渲染完成后再渲染它们更高效
Transparent 3000 此队列的物体会在前面三个渲染队列的物体渲染后,按照物体从后往前的顺序进行渲染。任何使用了透明度混合的物体都应该使用这个渲染队列

说到这里,我想使用LineRenderer在UGUI上画虚线虽然能实现效果,但是难以控制与其他UI的层级关系,所以不建议使用LineRenderer在UGUI上画虚线,在3D场景中可以用用。
当然为了解决LineRenderer在UGUI上的层级问题,有人写了一个新控件叫UILineRenderer。
长成这样。源码在这里。
UILineRenderer

3 代码生成网格画虚线

用这个就比较简单啦,直接看代码。
Mesh这个类该怎么使用,可以参考我之前写的《Unity3D C#数学系列之创建圆柱体》。
但是在UGUI上使用时,上面LineRenderer出现的层级问题,这种方式依然会出现,所以也不是一种好的方法。

using System.Collections.Generic;
using UnityEngine;

public class MeshDotLine : MonoBehaviour
{
    private void Start()
    {
        MeshFilter mf = gameObject.GetComponent();
        if (mf == null)
        {
            mf = gameObject.AddComponent();
        }
        MeshRenderer mr = gameObject.GetComponent();
        if (mr == null)
        {
            mr = gameObject.AddComponent();
        }
        Mesh mesh = new Mesh();
        mf.mesh = mesh;
        mr.material = new Material(Shader.Find("Unlit/Color"));
        mr.material.SetColor("_Color", Color.yellow);
        CreateDotLineMesh(mesh, 100, 0.5f, 20);
    }

    /// 
    /// 生成虚线网格.
    /// 
    /// 网格
    /// 总长度
    /// 虚线段数
    private void CreateDotLineMesh(Mesh mesh, float length, float ratio, int cnt)
    {
        List vertices = new List();

        List indices = new List();
        List uvs = new List();

        float deltaLength = length / cnt;
        float solidLength = deltaLength * ratio;
        float deltaUv = 1.0f / cnt;
        float solidUv = deltaUv * ratio;
        for (int i = 0; i < cnt; i++)
        {
            float start = i * deltaLength;
            vertices.Add(new Vector3(start, 0, 0));
            vertices.Add(new Vector3(start + solidLength, 0, 0));
            indices.Add(2*i);
            indices.Add(2*i+1);

            float startUv = i * deltaUv;
            uvs.Add(new Vector2(startUv, 0));
            uvs.Add(new Vector2(startUv + solidUv, 0));
        }
        mesh.SetVertices(vertices);
        mesh.SetIndices(indices, MeshTopology.Lines, 0);
    }
}

效果如下。
生成网格画虚线

4 使用片元着色器画虚线

4.1 源码

还是直接上码。

Shader "Custom/UI/DotLine"
{
    Properties
    {
        _Color ("Tint", Color) = (1,1,1,1)
        _Cnt ("Cnt", float) = 100
        _Ratio ("Ratio", Range(0, 1.0)) = 0.5
        [Toggle(VERTICAL)] _Y ("Y?", float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend One OneMinusSrcAlpha

        Pass
        {
            Name "Default"
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0
            #pragma multi_compile __ VERTICAL

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                float2 texcoord  : TEXCOORD0;
            };

            fixed4 _Color;
            float _Cnt;
            fixed _Ratio;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                OUT.vertex = UnityObjectToClipPos(v.vertex);
                OUT.texcoord = v.texcoord;
                return OUT;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 color = _Color;

                #if VERTICAL

                float y = IN.texcoord.y * _Cnt;
                int intY = int(y);
                color.a *= step(y-intY, _Ratio);

                #else

                float x = IN.texcoord.x * _Cnt;
                int intX = int(x);
                color.a *= step(x-intX, _Ratio);

                #endif

                color.rgb *= color.a;
                return color;
            }
        ENDCG
        }
    }
}

效果如下:
片元着色器画虚线
逻辑也很简单,比如水平方向的虚线核心逻辑如下。

float x = IN.texcoord.x * _Cnt;
int intX = int(x);
color.a *= step(x-intX, _Ratio);

假设_Cnt为10,_Ratio为0.5,则相当于将整个uv划分为了10段,每段大小为1,其中0~0.5为实线,0.5 ~1为虚线。
片元着色器画虚线描述

4.2 MaterialPropertyDrawer

上面代码中有个地方需要注意,咱们在Properties中定义了一个属性_Y用来让用户选择是x方向还是y方向。

[Toggle(VERTICAL)] _Y ("Y?", float) = 0

属性前面加了个[Toggle(VERTICAL)],这个叫MaterialPropertyDrawer可以用来自定义材质面板。比如使用Toggle的材质面板就长这样。
添加Toggle属性的材质面板
Toggle括号中的VERTICAL被称为keyword,表示在材质面板上勾选Y?就会在Shader中定义VERTICAL这个keyword。当然我们也可以直接使用[Toggle],此时勾选上后Unity会默认定义一个名为 属性名_ON 的keyword,比如这里我们如果这样用

// 勾选后会默认定义 _Y_ON这个keyword
[Toggle] _Y ("Y?", float) = 0

就会定义一个_Y_ON的keyword。
其次要注意的是,Toggle修饰的属性的类型为float,选中时为1.0,未选中时为0。
我们想在代码中使用这个keyword前,还需要调用#pragma multi_compile来申明这个keyword。

#pragma multi_compile __ VERTICAL

#pragma multi_compile可以实现只写一份代码但可由用户选择不同功能的目的。Untiy其实是在背后帮我们生成了几份不同的Shader,这写不同的Shader称为Shader变种(shader variants)。咱们可以通过选中Shader并打开Compiled code查看此Shader有几种Shader变种。
比如我们上面的Shader中,就有两个变种,一个是不包含任何keyword的,一个是包含VERTICAL的。
其中不包含任何keyword是由两个下划线__表示的。
在这里插入图片描述
当然我们也可以使用#pragma shader_feature来替换#pragma multi_compile,它们两者在大多数情况下是等价的。
两者的区别如下。

pragma shader_feature和#pragma multi_compile的区别

那么#pragma shader_feature和#pragma multi_compile有什么区别呢?实际上,#pragma shader_feature是#pragma multi_compile的子集,#pragma shader_feature生成的变种一个是不包含任何keyword的,一个是包含某个keyword的。我们可以使用#pragma multi_compile来实现同样的目的,只需要使用一个全是下划线的名字来表示不使用任何keyword即可。下面的两句话是在大多数情况下等价的:

#pragma shader_feature ENABLE_FANCY
#pragma multi_compile __ ENABLE_FANCY

但区别在于,使用multi_compile来定义keyword的话Unity是一定会生成所有变种的,。而如果使用的是shader_feature的话,如果有些keyword我们实际上并没有使用到,Unity也不会为这些生成shader变种。
因此(非常重要!!!),shader_feature适用于那些会在材质面板中直接设置的情况,而如果需要在脚本里通过DisableKeyword和EnableKeyword来开启或关闭keyword的话就应该使用multi_compile。(栽过坑啊!!!)并且不要在#pragma后面写注释!!!如果要在脚本里通过Shader.DisableKeyword来控制keyword的开关的话,不要在Properties里写KeywordEnum,这样可能会导致脚本里的设置失效(可以重新建一个材质解决)。但如果是使用material.DisableKeyword来设置的话,就不会有这个问题,原因暂时不明。

实际上,Unity的surface shader能够有那样强大的功能也是依靠了这样的机制。也包括我们在通过使用#pragma
multi_compile_fwdbase或multi_compile_fwdadd这样的代码时,我们之所以需要使用这样的语句就是因为Unity为forward
pass等内置了一些shader
keyword,而我们通过这些语句来让unity为不同的光照pass生成不同的shader变种,否则的话我们的shader就一种类型,只能包含特定的任务,无法为多类型的光源等进行不同的服务。

当然,我们可以独立使用#pragma shader_feature和#pragma
multi_compile,而不必一定要和MaterialPropertyDrawer配合使用。我们可以直接在代码里通过Material.EnableKeyword和Material.DisableKeyword来局部开启某些keyword,也可以通过Shader.EnableKeyword和Shader.DisableKeyword来全局开启。

以上引用部分来自于冯乐乐的文章《【Unity Shader】自定义材质面板的小技巧》。

4.3 unity_GUIZTestMode

细心的读者可能会发现,在上面这个Shader中在使用深度测试时咱们没有明确指定ZTest是开还是关,而是使用了unity_GUIZTestMode。那这玩意儿到底表示是啥?
unity_GUIZTestMode会由Canvas自动设置。
如果Canvas的Render Mode为 Screen Space - Overlay模式时,unity_GUIZTestMode将设置为Always,当为其他模式时,将设置为LEqual。这也就解释了,为什么Canvas选择为Screen Space - Overlay模式时,UI会一直显示在最上层
ps: ZTest 可取值为:Greater , GEqual , Less , LEqual , Equal , NotEqual , Always , Never , Off,默认是 LEqual,ZTest Off 等同于 ZTest Always。
What is the value of shader ZTest mode unity_GUIZTestMode

可参考这篇博客

5 使用几何着色器画虚线

几何着色器位于顶点着色器与片元着色器中间,是可选的,其可以用来修改模型的顶点。
目前几何着色器只支持Shader Model 4.0及以上,OpenGL ES 2.0/3.0/3.1是不支持的,所以不能用在移动端。
那些版本支持Shader Model4.0可以从Unity官网查看。
效果如下。
几何着色器虚线效果
这个版本只能实现两点之间的虚线。

using UnityEngine;

public class GeoDotLineDemo : MonoBehaviour
{
    void Start()
    {
        MeshFilter mf = gameObject.GetComponent();
        if (mf == null)
        {
            mf = gameObject.AddComponent();
        }
        MeshRenderer mr = gameObject.GetComponent();
        if (mr == null)
        {
            mr = gameObject.AddComponent();
        }
        Mesh mesh = new Mesh();
        mf.mesh = mesh;
        mr.material = new Material(Shader.Find("Custom/Geometry/GeoDotLine"));

        mesh.vertices = new Vector3[]
        {
            new Vector3(0, 0, 0),
            new Vector3(0, 100, 0),
        };
        mesh.SetIndices(new int[]{0, 1}, MeshTopology.LineStrip, 0);
        mesh.uv = new Vector2[]
        {
            Vector2.zero,
            new Vector2(1, 1),
        };
    }

}
Shader "Custom/Geometry/GeoDotLine"
{
    Properties
    {
        _MainTex ("Main Tex", 2D) = "white" {}
        _LineColor ("Line Color", color) = (0, 1, 0, 1)
        _LineWidth ("Line Width", float) = 2
        _Cnt ("Cnt", float) = 30
        _Ratio ("Ratio", Range(0, 1.0)) = 0.5
    }

SubShader
{
    Cull Off
    Blend SrcAlpha OneMinusSrcAlpha
    Tags {"Queue" = "Transparent"}

    LOD 100
    Pass
    {
        CGPROGRAM
        #pragma target 4.0
        #pragma vertex vert
        #pragma geometry geo
        #pragma fragment frag

        #include "UnityCG.cginc"

        struct appdata
        {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
        };

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

        sampler2D _MainTex;
        float4 _MainTex_ST;
        fixed4 _LineColor;
        float _LineWidth;
        float _Cnt;
        float _Ratio;

        v2f vert (appdata v)
        {
            v2f o;
            o.vertex = v.vertex;
            o.uv = v.uv;
            return o;
        }

        //产生最大的顶点数,append到triStream的次数应该小于此。最大好像是64
        [maxvertexcount(6)]
        void geo(line appdata l[2], inout TriangleStream triStream)
        {
            v2f pIn;
            v2f v[4];                               //  先生成四个顶点,构造两个三角形

            float4 a = l[0].vertex;
            float4 b = l[1].vertex;

            float halfLineWidth =  0.5 * _LineWidth;

            v[0].vertex = UnityObjectToClipPos(a + float4(halfLineWidth, 0, 0, 0));
            v[0].uv = float2(0.0f, 1.0f);
            v[1].vertex = UnityObjectToClipPos(b + float4(halfLineWidth, 0, 0, 0));
            v[1].uv = float2(1.0f, 1.0f);
            v[2].vertex = UnityObjectToClipPos(b + float4(-halfLineWidth, 0, 0, 0));
            v[2].uv = float2(1.0f, 0.0f);
            v[3].vertex = UnityObjectToClipPos(a + float4(-halfLineWidth, 0, 0, 0));
            v[3].uv = float2(0.0f, 0.0f);

            triStream.Append(v[0]);
            triStream.Append(v[1]);
            triStream.Append(v[2]);
            triStream.RestartStrip();           // 重置三角形计数,提交三角形
            triStream.Append(v[2]);
            triStream.Append(v[3]);
            triStream.Append(v[0]);
            triStream.RestartStrip();
        }

        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 color = _LineColor;
            float x = i.uv.x * _Cnt;
            int intX = int(x);
            color.a *= step(x-intX, _Ratio);
            return color;
        }
        ENDCG
        }
    }
}

6 其他方式

⑤使用UILineRenderer
⑥使用Vectrosity插件
这两种是直接使用别人写好的插件,这里就不多说啦。

UILineRenderer来源于unity-ui-extensions,里面包含了很多其他控件,可以参考学习。
链接:https://pan.baidu.com/s/1d5926rAPC6onFPYFKkWB6Q
提取码:jc1q

Vectrosity插件
链接:https://pan.baidu.com/s/1WhvzF3WyU0Tcr6f2DDpk-w
提取码:3ff2

7 完整项目

完整项目放到这儿了。
链接:https://pan.baidu.com/s/1Uleo41bni7e_a7f0JDAqHQ
提取码:g8bj