前言

1、前段时间工作,需要给模型描边,由于对Shader不熟悉,就直接网上找了描边Shader文件,无奈项目发布环境是WebGL,WebGL对Shader的需求比较特殊,故无法使用。

2、因为项目需要描边的物体并不多,所以萌生出,动态生成整个模型所有的边(线条),给各个边附上需要的材质球即可。(当然,也可以直接请美术在模型上描边,但这样不能实现泛光之类的效果)

3、现写下三种实现模型描边的方法,方便日后查看学习与使用。


实现

1、GL描边

unity text描边效果 unity 模型描边_d3


原理比较简单,就是获取到模型的所有顶点,然后使用GL连线.

以下是GL描边代码,在项目的GLSingleWireFrameScene场景中有演示.

/// <summary>
/// 描绘单个模型线框.
/// </summary>
public class GLSingleWireFrame : MonoBehaviour
{
    public Material lineMaterial;

    private Mesh m_mesh;
    private Vector3[] m_vertices;
    private int[] m_triangles;
    private Transform m_transform;

    void Awake()
    {
        m_mesh = gameObject.GetComponent<MeshFilter>().mesh;
        m_vertices = m_mesh.vertices;
        m_triangles = m_mesh.triangles;
        m_transform = transform;
    }

    public void OnRenderObject()
    {
        lineMaterial.SetPass(0);    //GLSingleWireFrame材质球Shader是 Unlit/Color.

        GL.PushMatrix();
        GL.MultMatrix(m_transform.localToWorldMatrix);

        GL.Begin(GL.LINES);

        for (int cnt = 0; cnt < m_triangles.Length; cnt += 3)
        {
            GL.Vertex(m_vertices[m_triangles[cnt]]);
            GL.Vertex(m_vertices[m_triangles[cnt + 1]]);
            GL.Vertex(m_vertices[m_triangles[cnt + 1]]);
            GL.Vertex(m_vertices[m_triangles[cnt + 2]]);
            GL.Vertex(m_vertices[m_triangles[cnt + 2]]);
            GL.Vertex(m_vertices[m_triangles[cnt]]);
        }

        GL.End();
        GL.PopMatrix();
    }
}


2、Shader描边

unity text描边效果 unity 模型描边_unity text描边效果_02


Shader描边实现的方法也有很多,上图的是法线外拓方法。使用两个pass,第一个pass让顶点沿着法线方向延伸出去,使得模型变大一圈。第二个pass正常渲染,让正常渲染的模型挡在第一个pass之上,这样就会露出去的部分就是描绘的边。

网上有很多描边的例子,这里不详细介绍. 例如unity官方问答stackoverflow

以下是Shader代码,在项目的UtilizeShaderScene场景中有演示.

Shader "Shaders/ToonBound"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1, 1, 1, 1)
        _MainTex ("Main Tex", 2D) = "white" {}
        _Ramp ("Ramp Texture", 2D) = "white" {}                  //控制漫反射色调的渐变纹理
        _Outline ("Outline", Range(0, 1)) = 0.1                  //控制轮廓线宽度
        _OutlineColor ("Outline Color", Color) = (1, 0, 0, 1) //轮廓线颜色
        _Specular ("Specular", Color) = (1, 1, 1, 1)          //高光反色颜色
        _SpecularScale ("Specular Scale", Range(0, 0.1)) = 0.01 //高光反射系数阈值
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry"}
        LOD 100

        Pass
        {
            //命名Pass块,以便复用
            NAME "OUTLINE"
            //剔除正面
            Cull Front

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            //#pragma multi_compile_fog
            
            #include "UnityCG.cginc"

            float _Outline;
            fixed4 _OutlineColor;

            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            }; 
            
            struct v2f {
                float4 pos : SV_POSITION;
            };

            v2f vert (a2v v) {
                v2f o;
                //让描边在观察空间达到最好的效果
                float4 pos = mul(UNITY_MATRIX_MV, v.vertex); 
                float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);  
                normal.z = -0.5;
                pos = pos + float4(normalize(normal), 0) * _Outline;
                //将顶点从视角空间变换到裁剪空间
                o.pos = mul(UNITY_MATRIX_P, pos);
                
                return o;
            }
            
            float4 frag(v2f i) : SV_Target { 
                //轮廓线颜色渲染整个背面
                return float4(_OutlineColor.rgb, 1);               
            }

            ENDCG
        }
        Pass 
        {
            Tags { "LightMode"="ForwardBase" }
            //渲染正面
            Cull Back
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase
        
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
            #include "UnityShaderVariables.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _Ramp;
            fixed4 _Specular;
            fixed _SpecularScale;
        
            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 texcoord : TEXCOORD0;
                float4 tangent : TANGENT;
            }; 
        
            struct v2f {
                float4 pos : POSITION;
                float2 uv : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 worldPos : TEXCOORD2;
                SHADOW_COORDS(3)
            };

            v2f vert (a2v v) {
                v2f o;
                
                o.pos = UnityObjectToClipPos( v.vertex);
                o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
                o.worldNormal  = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                
                TRANSFER_SHADOW(o);
                
                return o;
            }
            
            float4 frag(v2f i) : SV_Target { 
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir);
                
                fixed4 c = tex2D (_MainTex, i.uv);
                fixed3 albedo = c.rgb * _Color.rgb; //计算材质反射率
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; //计算环境光
                
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); //计算当前世界坐标下的阴影值
                
                fixed diff =  dot(worldNormal, worldLightDir);
                //计算半兰伯特漫反射系数
                diff = (diff * 0.5 + 0.5) * atten;
                //对渐变纹理_Ramp进行采样
                fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;
                
                fixed spec = dot(worldNormal, worldHalfDir);
                fixed w = fwidth(spec) * 2.0; //抗锯齿处理
                fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(0.0001, _SpecularScale);
                
                return fixed4(ambient + diffuse + specular, 1.0);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}


3、代码生成描边物体

①动态生成:
在项目DynamicDrawWireFrameScene场景中有演示.

unity text描边效果 unity 模型描边_#include_03


②使用编辑器拓展生成:
在项目EditorDrawWireFrameScene场景中有演示.

unity text描边效果 unity 模型描边_#include_04


unity text描边效果 unity 模型描边_d3_05


unity text描边效果 unity 模型描边_#pragma_06


unity text描边效果 unity 模型描边_#pragma_07


unity text描边效果 unity 模型描边_#pragma_08


unity text描边效果 unity 模型描边_unity3d_09


unity text描边效果 unity 模型描边_#pragma_10


③原理分析:

原理比较简单,就是获取该模型所有的顶点位置,使用LineRenderer连接两个点,即生成一个边。如果全部连接,就会生成跟GL描边一样的效果。

(题外话:连点成面,连面成网,模型网格是由许多个三角面构成的,而两个三角面即形成一个四边形,至于如何构造三角面形成网格,可以看我之前的文章)

如何剔除掉模型一个面不需要的描边,我们可以使用叉乘,分别获得两个相邻三角面的法线向量,然后对两个法线向量点乘,获得角度,如果两个三角面平行,即它们相交的边可以剔除掉。

unity text描边效果 unity 模型描边_unity3d_11

unity text描边效果 unity 模型描边_#pragma_12


配合上方图片,得出以下核心代码:

/// <summary>
/// 退化四边形.
/// </summary>
[System.Serializable]
public struct DegradedRectangle  	//(v1、v2、v3_1)(v1、v2、v3_2)为相同一条边的两个三角面,两个三角面即为一个退化四边形.   v3_2为-1,即该四边形相同边是“边界边缘”.
{
    public int vertex1;             //构成边的顶点1的索引
    public int vertex2;             //构成边的顶点2的索引
    public int triangle1_vertex3;   //边所在三角面1的顶点3索引
    public int triangle2_vertex3;   //边所在三角面2的顶点3索引 
}

/// <summary>
/// 绘制模型的网格线框.(直接放在模型身上,初始化时创建)
/// </summary>
public class DynamicDrawWireFrame : MonoBehaviour
{
    private Transform m_Transform;
    private MeshFilter m_MeshFilter;
    private Transform m_drawWireFrameParent;            //描绘物体线框的线 父物体.
    
    [Header("退化四边形资源文件")]
    [SerializeField]
    private DegradedRectangles m_DegradedRectangles;    //退化四边形资源文件,就是DegradedRectangled数组. 

    [Header("LineRenderer预制体")]
    [SerializeField]
    private GameObject m_Prefab_Line;                   //LineRender预制体.

    void Start()
    {
        if (m_DegradedRectangles == null)
        {
            Debug.LogError("没有赋值退化四边形.");
            return;
        }

        //查找初始化.
        m_Transform = gameObject.GetComponent<Transform>();
        m_MeshFilter = gameObject.GetComponent<MeshFilter>();
        Mesh mesh = m_MeshFilter.sharedMesh;
        m_drawWireFrameParent = m_Transform.Find("DrawWireFrameParent");
        if (m_drawWireFrameParent == null) 
        { 
            m_drawWireFrameParent = new GameObject("DrawWireFrameParent").transform;
            m_drawWireFrameParent.SetParent(m_Transform, false); 
        }
            
        //临时变量.
        Vector3 v1;
        Vector3 v2;
        Vector3 v3_1;
        Vector3 v3_2;
        Vector3 vv1;
        Vector3 vv2;
        Vector3 vv3;
        Vector3 face1Normal;
        Vector3 face2Normal;
        float angle;
        List<Vector3> drawList = new List<Vector3>();

        //循环退化四边形,通过计算,得出网格的边缘线.
        for (int i = 0; i < m_DegradedRectangles.degraded_rectangles.Count; i++)
        {
            //获取退化四边形对应的网格顶点坐标.
            v1 = mesh.vertices[m_DegradedRectangles.degraded_rectangles[i].vertex1];    
            v2 = mesh.vertices[m_DegradedRectangles.degraded_rectangles[i].vertex2];
            v3_1 = mesh.vertices[m_DegradedRectangles.degraded_rectangles[i].triangle1_vertex3];

            if (m_DegradedRectangles.degraded_rectangles[i].triangle2_vertex3 > 0)    //如果是边界边缘,该值为-1.
            {
                v3_2 = mesh.vertices[m_DegradedRectangles.degraded_rectangles[i].triangle2_vertex3]; //获取退化四边形对应的网格顶点坐标.

                //计算出两个相邻三角面的法线向量.
                vv1 = v2 - v1;      
                vv2 = v3_1 - v1;
                vv3 = v3_2 - v1;
                face1Normal = Vector3.Cross(vv1, vv2).normalized;
                face2Normal = Vector3.Cross(vv3, vv1).normalized;

                //点积,计算两个三角面是否平行.
                angle = Mathf.Acos(Vector3.Dot(face1Normal, face2Normal)) * Mathf.Rad2Deg;  //两条法线相交的角度.
                //angle = Vector3.Angle(face1Normal, face2Normal);      //只能算到 [0,180] 度.

                if (angle < -2f || angle > 2f)  //小于或大于.    两个面不平行,该线不是中间线.
                {
                    Debug.Log("边缘");
                    drawList.Add(v1);
                    drawList.Add(v2);
                }
                else    //两个面平行.    不画中间的线.
                {

                }
            }
            else    //边界边缘.
            {
                Debug.Log("边界边");
                Debug.Log(m_DegradedRectangles.degraded_rectangles[i]);
                drawList.Add(v1);
                drawList.Add(v2);
            }
        }

        //循环相框集合,每两个点,生成一根线.
        GameObject line;
        LineRenderer line_LineRenderer;
        for (int i = 0; i < drawList.Count; i += 2)
        {
            line = GameObject.Instantiate<GameObject>(m_Prefab_Line, m_drawWireFrameParent);
            line.name = "Line_" + i;
            line_LineRenderer = line.GetComponent<LineRenderer>();
            line_LineRenderer.positionCount = 2;
            line_LineRenderer.SetPosition(0, drawList[i]);
            line_LineRenderer.SetPosition(1, drawList[i + 1]);
        }

    }

}


项目链接Github项目链接

这是以前写下的东西,当时查阅了不少文档,如有雷同,侵删,完毕.