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