Unity中绘制线框(Wireframe)的几种方法

Unity GeometryShader(从一个线框渲染的例子开始)
Unity实用案例之——屏幕画线和线框渲染
UnityShader——Wireframe
Shader Compilation Target Levels

一、利用GL来绘制线框

该方法的思路是先获取mesh中的三角面,然后得到顶点数据组成线(line),然后利用GL直接绘制线得到线框,这里为提升性能可以先将数据获取然后序列化保存起来,避免运行时再去处理数据,c#代码如下:

using System.Collections;
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
[ExecuteInEditMode]
public class MeshWireframe : MonoBehaviour
{
    public Color lineColor = Color.white;
    public Material lineMat;
    [ReadOnly]
    [Tooltip("根据当前模型自动获取mesh")]
    public List<Mesh> meshs;
    [ReadOnly]
    [Tooltip("根据当前模型自动算计line")]
    public List<Vector3> lines;

    private void OnRenderObject()
    {
        lineMat.SetColor("_LineColor", lineColor);
        lineMat.SetPass(0);
        GL.PushMatrix();

        //转换到世界坐标
        GL.MultMatrix(transform.localToWorldMatrix);
        GL.Begin(GL.LINES);
        for (int i = 0; i < lines.Count / 3; i++)
        {
            GL.Vertex(lines[i * 3]);
            GL.Vertex(lines[i * 3 + 1]);
            GL.Vertex(lines[i * 3 + 1]);
            GL.Vertex(lines[i * 3 + 2]);
            GL.Vertex(lines[i * 3 + 2]);
            GL.Vertex(lines[i * 3]);
        }
        GL.End();
        GL.PopMatrix();
    }

    private void GenerateLines()
    {
        if (lines != null)
            lines.Clear();
        else
            lines = new List<Vector3>();
        foreach (var mesh in meshs)
        {
            var vertices = mesh.vertices;
            var triangles = mesh.triangles;
            for (int i = 0; i < triangles.Length / 3; i++)
            {
                lines.Add(vertices[triangles[i * 3]]);
                lines.Add(vertices[triangles[i * 3 + 1]]);
                lines.Add(vertices[triangles[i * 3 + 2]]);
            }
        }
    }

    [ContextMenu("根据MeshFilter组件生成Line数据")]
    private void GetMeshesData()
    {
#if UNITY_EDITOR
        GameObject o = UnityEditor.Selection.activeGameObject;
        if (o == null)
            return;
        if (meshs != null)
            meshs.Clear();
        else
            meshs = new List<Mesh>();
        MeshFilter[] meshFilters = o.GetComponentsInChildren<MeshFilter>(true);
        if (meshFilters != null && meshFilters.Length > 0)
        {
            foreach (var mf in meshFilters)
            {
                meshs.Add(mf.sharedMesh);
            }
            GenerateLines();
        }
        else
        {
            Debug.LogError("选中物体及子物体没有MeshFilter组件,请添加");
        }

        SkinnedMeshRenderer[] skinnedMeshRenderers = o.GetComponentsInChildren<SkinnedMeshRenderer>(true);
        if (skinnedMeshRenderers != null && skinnedMeshRenderers.Length > 0)
        {
            foreach (var sr in skinnedMeshRenderers)
            {
                meshs.Add(sr.sharedMesh);
            }
            GenerateLines();
        }
        else
        {
            Debug.LogError("选中物体及子物体没有SkinnedMeshRenderer组件,请添加");
        }
#endif
    }
}

材质可以使用一个只有颜色的shader,如下:

Shader "Custom/LineColor" {

    Properties{
        _LineColor ("Line Color", Color) = (1.0, 1.0, 1.0, 1.0)
    }

    SubShader {
        Pass {
            Tags { "RenderType"="Opaque"}
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct v2f
            {
                half4 pos : SV_POSITION;
            };

            fixed4 _LineColor;

            v2f vert(appdata_base  v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : COLOR
            {
                return _LineColor;
            }

            ENDCG
        }
    }
}

效果如下:


显示模型的线框

只显示线框

二、利用Geomotry Shader(几何着色器)来绘制线框

在开始之前,需要先对几何着色器阶段有个初步了解,大概需要知道需要下面几两个概念:

1.图元(graphics primitive)

几乎所有的图形渲染入门书籍里,都要提到这个概念,我们知道,所有的几何模型都是有点,线,三角形等基本单元组成的(这里以三角形为例),每个图元又是由若干个顶点构成。在渲染管线的开始,GPU处理的是每一个顶点,但是GPU是知道每一个顶点是属于哪个三角形的。所有顶点经过顶点着色器处理后输出的结果会经过一个图元装配(Primitive Assembly)的阶段,这个阶段就是把这些处理后的顶点组装成成一个个三角形。为什么这么做呢?因为之后的无论是光栅化和顶点信息插值过程,以及视椎体的裁剪,都是以图元为单位进行的(如果你对这个过程不是非常了解,可以查查资料,或者去看一下刘鹏翻译的《计算机图形学—基于3D图形开发技术》),经过上述的这些阶段后再到达我们熟悉的片元着色阶段,也就离最终渲染结果不远了。

2.几何着色器(Geometry Shader)

对于VS,FS我们都比较熟悉,那GS出现在哪呢?从下面这种图中我们可以看到GS是位于VS和FS之间的。并且是虚线连接,即是可选的。GeometryShader所接收的实际是对VS输出的图元进行添加,删除,或修改,然后输出新的图元信息。再之后的流程就和之前的一样了。

渲染流程

3.Assets Store里面有一个免费的Wireframe Shader,直接搜索UCLA Wireframe Shader就可以下载了,这里只是简单展示一下,具体原理可以参考文章开头的引用第一篇文章Unity GeometryShader(从一个线框渲染的例子开始),说的很详细了。


不过要注意的是,Geomotry Shader是OpenGL 3.2中引入的特性。需要 #pragma target 4.0 ,也就是要OpenGL ES3.2 或 OpenGL ES3.1+AEP,另外iOS的metal据说为了效率,不支持Geomotry Shader,也就是说在iOS设备上没戏,更多的可以参考Unity官方文档Shader Compilation Target Levels.

三、利用Geomotry Shader(几何着色器)来绘制四边形(Quad)

先上图:


四边形

再上代码:

##  Wireframe Function.cginc  ###

#ifndef Unity_EANGULEE_WIREFRAME
#define Unity_EANGULEE_WIREFRAME
// Wireframe shader based on the the following
// http://developer.download.nvidia.com/SDK/10/direct3d/Source/SolidWireframe/Doc/SolidWireframe.pdf

#include "UnityCG.cginc"

float _WireThickness;
half4 _LineColor;

struct appdata
{
    float4 vertex : POSITION;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2g
{
    float4 projectionSpaceVertex : SV_POSITION;
    float4 vertexPos : TEXCOORD2;
    UNITY_VERTEX_OUTPUT_STEREO
};

struct g2f
{
    float4 projectionSpaceVertex : SV_POSITION;
    float4 dist : TEXCOORD1;
    int maxIndex : TEXCOORD2;
    UNITY_VERTEX_OUTPUT_STEREO
};

v2g vert (appdata v)
{
    v2g o;
    UNITY_SETUP_INSTANCE_ID(v);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
    o.projectionSpaceVertex = UnityObjectToClipPos(v.vertex);
    o.vertexPos = v.vertex;
    return o;
}

[maxvertexcount(3)]
void geom(triangle v2g i[3], inout TriangleStream<g2f> triangleStream)
{
    float2 _p0 = i[0].projectionSpaceVertex.xy / i[0].projectionSpaceVertex.w;
    float2 _p1 = i[1].projectionSpaceVertex.xy / i[1].projectionSpaceVertex.w;
    float2 _p2 = i[2].projectionSpaceVertex.xy / i[2].projectionSpaceVertex.w;

    float3 p0 = i[0].vertexPos;
    float3 p1 = i[1].vertexPos;
    float3 p2 = i[2].vertexPos;

    float2 edge0 = _p2 - _p1;
    float2 edge1 = _p2 - _p0;
    float2 edge2 = _p1 - _p0;

    float s0 = length(p2 - p1);
    float s1 = length(p2 - p0);
    float s2 = length(p1 - p0);

    // To find the distance to the opposite edge, we take the
    // formula for finding the area of a triangle Area = Base/2 * Height, 
    // and solve for the Height = (Area * 2)/Base.
    // We can get the area of a triangle by taking its cross product
    // divided by 2.  However we can avoid dividing our area/base by 2
    // since our cross product will already be double our area.
    float area = abs(edge1.x * edge2.y - edge1.y * edge2.x);
    float wireThickness = 800 - _WireThickness;//经验系数(不清楚怎么来的,可能是配出来的)
    int maxIndex = 0;

    #if ENABLE_DRAWQUAD
    if(s1 > s0)
    {
        if(s1 > s2)
            maxIndex = 1;
        else
            maxIndex = 2;
    }
    else if(s2 > s0)
    {
        maxIndex = 2;
    }
    #endif
    g2f o;

    o.projectionSpaceVertex = i[0].projectionSpaceVertex;
    o.dist.xyz = float3( (area / length(edge0)), 0.0, 0.0) * wireThickness * o.projectionSpaceVertex.w;
    o.dist.w = 1.0 / o.projectionSpaceVertex.w;
    o.maxIndex = maxIndex;
    UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(i[0], o);
    triangleStream.Append(o);

    o.projectionSpaceVertex = i[1].projectionSpaceVertex;
    o.dist.xyz = float3(0.0, (area / length(edge1)), 0.0) * wireThickness * o.projectionSpaceVertex.w;
    o.dist.w = 1.0 / o.projectionSpaceVertex.w;
    o.maxIndex = maxIndex;
    UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(i[1], o);
    triangleStream.Append(o);

    o.projectionSpaceVertex = i[2].projectionSpaceVertex;
    o.dist.xyz = float3(0.0, 0.0, (area / length(edge2))) * wireThickness * o.projectionSpaceVertex.w;
    o.dist.w = 1.0 / o.projectionSpaceVertex.w;
    o.maxIndex = maxIndex;
    UNITY_TRANSFER_VERTEX_OUTPUT_STEREO(i[2], o);
    triangleStream.Append(o);
}

fixed4 frag (g2f i) : SV_Target
{               
    float minDistanceToEdge;
    #if ENABLE_DRAWQUAD
    if(i.maxIndex == 0)
        minDistanceToEdge = min(i.dist.y, i.dist.z);
    else if(i.maxIndex == 1)
        minDistanceToEdge = min(i.dist.x, i.dist.z);
    else 
        minDistanceToEdge = min(i.dist.x, i.dist.y);
    #else
        minDistanceToEdge = min(i.dist.x, min(i.dist.y, i.dist.z)) * i.dist.w;
    #endif
    // Early out if we know we are not on a line segment.
    if(minDistanceToEdge > 0.9)
    {
        return fixed4(0,0,0,0);
    }

    // Smooth our line out
    float t = exp2(-2 * minDistanceToEdge * minDistanceToEdge);
    fixed4 wireColor = _LineColor;

    fixed4 finalColor = lerp(float4(0,0,0,0), wireColor, t);
    finalColor.a = t;

    return finalColor;
}
#endif
Shader "Custom/Wireframe"
{
    Properties
    {
        [HDR]_LineColor("Line Color", Color) = (1,1,1,1)
        _WireThickness ("Wire Thickness", RANGE(0, 800)) = 100
        [Toggle(ENABLE_DRAWQUAD)]_DrawQuad("Draw Quad", Float) = 0
    }

    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha 
            ZWrite Off
            Cull Front
            LOD 200
            
            CGPROGRAM
            #pragma target 4.0
            #pragma multi_compile __ ENABLE_DRAWQUAD
            #include "UnityCG.cginc"
            #include "Wireframe Function.cginc"
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag
            
            ENDCG
        }

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha 
            ZWrite Off
            Cull Back
            LOD 200
            
            CGPROGRAM
            #pragma target 4.0
            #pragma multi_compile __ ENABLE_DRAWQUAD
            #include "UnityCG.cginc"
            #include "Wireframe Function.cginc"
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag
            
            ENDCG
        }
    }
}

默认是三角线框


三角线框

四边形需要沟上Draw Quad


四边线框和三边线框类似,主要区别是需要处理一下对角线,思路是在geometry函数中计算得到最长的边的索引,在frag函数中根据索引进行判断,有些部分笔者也没看明白,不过确实可以用的,那就先用着吧,回头研究明白了再来补充吧。
本着对读者负责任的态度,笔者将三种方法都整理到一个项目中,方便各位学习,因为笔者深深的感受到,光说理论,没有代码的痛苦,都是泪。
地址如下:UnityWireframe

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容