根据平面的多边形点的数据,生成简单的立体网格
最近需要一个功能,就是根据给定的多边形顶点(按照顺时针或者逆时针这样的顺序),然后生成一个具有高度,投影与多边形一样的一个Mesh。
效果如下,比如给定以下的一些顶点的位置,
生成一个这样的Mesh。
1.分析
首先肯定就是要生成mesh的这样一个问题。mesh的主要内容包括顶点、三角形、法线(uv的话我用不上,所以就不考虑了,而且感觉不好弄)。
顶点,这个很简单,地面的一圈顶点是给定好的,然后上层的顶点只要加上一个高度就能算出来。
三角形,这个尤为不好弄,如果是一个规规矩矩的矩形,三角形的关系很容易就算出来了。但是问题现在给的多边形可以是任意的。我是真的想不到什么方法。然后经过一番查找,终于找到了我想要的东西——多边形三角化(wiki)。
里面提到了很多方法,然后在github上找了一下,有同志已经把耳切法的算法实现了,并且也是在Unity里面——耳切法(github)。那我就不客气了。虽然耳切法不适用于带孔的多边形,但是能够处理凹凸多边形对我来说已经足够了。
法线,法线其实也还好,直接用向量差乘算出来就好了。
2.用耳切法把多边形三角化
具体算法我暂时没有去深究,现在的先试着用前面yiwei151写好的代码生成一下。主要的东西就是在Triangulation.cs
中。
仿照他原来的测试代码我也写了一个生成mesh的代码。
这个函数是传入一个多边形顶点的List (按照我的需求多边形顶点给的是逆时针为顺序,并且是一个位于xOz平面上的多边形,所以比较轴我也设置的Y轴),然后返回对应三角形的索引。
如果不把三角形反转的话,是会算的z轴的反向为正向,与我的需求不服,所以我这里默认反转以下三角形的顺序。
public static int[] PolygonToTriangles(List<Vector3> polygonVertex, bool filpTriangle = true)
{
// 存结果的列表
var resultVertexes = new List<Vector3>();
// 转化器
var triangulation = new Triangulation(polygonVertex);
// 设置比较轴
triangulation.SetCompareAxle(CompareAxle.Y);
// 取得三角形
var triangles = triangulation.GetTriangles();
// 反转三角形
int tempInt;
if (filpTriangle)
{
for (int i = 0; i < triangles.Length; i+=3)
{
// 交换两个顶点的顺序令其三角形的顺序相反
tempInt = triangles[i + 1];
triangles[i + 1] = triangles[i + 2];
triangles[i + 2] = tempInt;
}
}
return triangles;
}
再调用上面的算法,顺便把mesh也一并生成了
public static Mesh GenPolyMesh(List<Vector3> polyVerts)
{
var resMesh = new Mesh();
// 把多边形数据转换为三角形
var triangles = PolygonToTriangles(polyVerts);
// 设置顶点
resMesh.vertices = polyVerts.ToArray();
// 设置三角形
resMesh.triangles = triangles;
// 计算法线
resMesh.RecalculateNormals();
return resMesh;
}
然后也学着yiwei151用物体的坐标计算几个顶点位置,方便调试。
写了个测试脚本。
using System.Collections.Generic;
using UnityEngine;
using SimpleObject;
using UnityEngine.PlayerLoop;
namespace Test
{
public class TestGenerateSimpleMesh : MonoBehaviour
{
// 是否实时更新mesh
public bool updateMesh = false;
// 生成物体的材质
public Material mat;
// 生成的高度
public float height = 3;
// 用物体的坐标来代替点
public List<Transform> tList;
private GameObject _targetObj;
private MeshFilter _targetMeshFilter;
private List<Vector3> posList = new List<Vector3>();
private void Start()
{
// 获取多边形顶点
for (int i = 0; i < tList.Count; i++)
posList.Add(tList[i].position);
// 创建物体
if(_targetObj) Destroy(_targetObj);
_targetObj = new GameObject("Target");
// 网格
_targetMeshFilter = _targetObj.AddComponent<MeshFilter>();
_targetObj.AddComponent<MeshRenderer>();
_targetMeshFilter.mesh = MeshGenerator.GenPolyMesh(posList);
//_targetMeshFilter.mesh = MeshGenerator.GenBuildingMesh(posList,height);
// 材质
if(mat)
_targetObj.GetComponent<Renderer>().material = mat;
else
_targetObj.GetComponent<Renderer>().material = new Material(Shader.Find("Diffuse"));
}
private void FixedUpdate()
{
if (updateMesh)
{
// 获取多边形顶点
posList.Clear();
for (int i = 0; i < tList.Count; i++)
posList.Add(tList[i].position);
_targetMeshFilter.mesh = MeshGenerator.GenPolyMesh(posList);
//_targetMeshFilter.mesh = MeshGenerator.GenBuildingMesh(posList,height);
}
}
}
}
场景里稍微弄成这样,运行就能看见结果了。
测试得这个用法没问题。
3.构造墙壁
这边先说一下我的思路,假设给的是一个四边形的话(四边形比较简单,边数更多的也是一样的道理)。四边形的顶点编号分别是0、1、2、3,然后根据设定的高度,算出顶层的顶点4、5、6、7。然后应该如下图所示。
现在我需要做的就是以此把垂直的四个面构造出来,构造一个四边形至少需要两个三角形(这个三角形怎么分就看你怎么写了,其实都是可以的)。所以这四个墙壁应该要有8个三角面组成。
我打算按照如下顺序遍历一圈依次生成四个面。
遍历顶点0、1、2、3,在到点0
的时候计算面0154
,点1
的时候计算面1265
,点2
的时候计算面2376
,点3
的时候计算面3047
。
在计算一个面的时候,把这个面当作是面向自己的。然后三角形的顺序是顺时针为正向。
如果想要这个面为正面,需要存的数据应该是ADCACB(字母要换成顶点对应的索引),这样子才是一个面向自己的面。
法线的计算的话,这里同一个面的四个顶点的法线都是相同方向的,所以只要算一次就好了,算法就用一个向量的叉乘就好了,根据右手定则可以算出垂直于两个向量的向量。
如用向量12
叉乘向量15
就可以得到点1、2、5、6的法线了。
实现成具体代码如下。
public static Mesh GenBuildingMesh(List<Vector3> polyVerts, float height)
{
// 结果Mesh
var resMesh = new Mesh();
// 所有顶点的集合
var allVerts = new List<Vector3>();
var upperVerts = new List<Vector3>();
// 所有法线的集合
var allNormals = new List<Vector3>();
var upperNormals = new List<Vector3>();
// 所有三角形的集合
var allTriangles = new List<int>();
var upperTriangles = new List<int>();
// 计算顶部的顶点位置
for (var i = 0; i < polyVerts.Count; i++)
upperVerts.Add(polyVerts[i] + Vector3.up * height);
// 计算墙壁的顶点、法线、三角
var wallVerts = new List<Vector3>();
var wallNormals = new List<Vector3>();
var wallTriangles = new List<int>();
// 遍历一边多边形的所有顶点
var counter = 0;
for (var i = 0; i < polyVerts.Count; i++)
{
// 先添加这个面的四个顶点(顺时针)
wallVerts.Add(polyVerts[i]);
wallVerts.Add(upperVerts[i]);
wallVerts.Add(upperVerts[(i + 1) % polyVerts.Count]);
wallVerts.Add(polyVerts[(i + 1) % polyVerts.Count]);
// 利用两个向量差乘计算法线
var normal = Vector3.Cross(upperVerts[i] - polyVerts[i], polyVerts[(i + 1) % polyVerts.Count] - polyVerts[i]).normalized;
wallNormals.Add(normal);
wallNormals.Add(normal);
wallNormals.Add(normal);
wallNormals.Add(normal);
// 计算三角
// 第一个三角
wallTriangles.Add(counter);
wallTriangles.Add(counter + 1);
wallTriangles.Add(counter + 2);
// 第二个三角
wallTriangles.Add(counter);
wallTriangles.Add(counter + 2);
wallTriangles.Add(counter + 3);
// 自增
counter += 4;
}
// 计算顶部的顶点、法线、三角
// 法线
for (var i = 0; i < upperVerts.Count; i++)
upperNormals.Add(Vector3.up);
// 三角
upperTriangles = PolygonToTriangles(upperVerts).ToList();
// 延后三角的索引
for (var i = 0; i < upperTriangles.Count; i++)
upperTriangles[i] += wallVerts.Count;
// 合并数据
allVerts.AddRange(wallVerts);
allVerts.AddRange(upperVerts);
allNormals.AddRange(wallNormals);
allNormals.AddRange(upperNormals);
allTriangles.AddRange(wallTriangles);
allTriangles.AddRange(upperTriangles);
// 设置顶点
resMesh.vertices = allVerts.ToArray();
// 设置三角形
resMesh.triangles = allTriangles.ToArray();
// 计算法线
resMesh.normals = allNormals.ToArray();
return resMesh;
}
(因为我不需要底面的,所以就没有生成底面需要信息,要生成的话按照顶面照猫画虎的生成一遍,记得反转三角形就是了。)
有一个需要注意的点,就是三角形存的是顶点的索引,而我这里的顶面和墙面是分开计算三角形的(也就是他们都是以0为开始的索引)。这样子直接把两个三角形的列表加起来肯定是错误的结果。
因为我是把顶部的顶点加载墙面顶点的后面,所以顶部的三角形的所有索引值都应该加上墙面顶点的个数,这就是代码里面// 延后三角的索引
所做的事情。
是否有人有疑问,为什么每一个四边面创建三角形的时候都要新建四个新的顶点呢?这样子在同一个位置不就有好几个重复的顶点了吗?
这是因为我需要的是一个硬边的mesh,而这样就需要每个点的法线都是垂直于这个面的。
然而一个顶点只有一个法线,如果每个角都公用一个顶点的话,那在渲染的时候他周围的法线都会被插值计算为一个平滑的法线。所以如果共用顶点的话,光照看起来会很奇怪,如下图。
下图才是我想要的效果。
而且unity自带的cube也是24个顶点,6个面中每个面都是四个顶点,不是8个。虽然这样子在三个面的交点会有三个位置一样的点,但是他们的法线各不相同。
在blender里面弄了个立方体也是24个顶点。
之前就是没考虑到这一个,先写了一个生成方法是共用顶点的,但是看了效果才知道不行。
但是我还是把代码留着了,毕竟这个mesh用来做碰撞体也许会更省性能?(我猜的,毕竟顶点更少,但是我不知道)计算共用顶点的mesh代码如下。
public static Mesh GenBuildingMeshSimple(List<Vector3> polyVerts, float height)
{
var resMesh = new Mesh();
// verts作为存放最终顶点的容器
var verts = polyVerts;
// 计算顶层顶点
var upperVerts = new List<Vector3>();
for (var i = 0; i < polyVerts.Count; i++)
{
upperVerts.Add(polyVerts[i] + Vector3.up * height);
}
// 计算顶层三角形
var upperTriangle = PolygonToTriangles(upperVerts);
// 给每一个顶点索引加上长度
/* 因为加上顶层顶点之后,相应的索引应该延后 */
for (var i = 0; i < upperTriangle.Length; i++)
{
upperTriangle[i] += polyVerts.Count;
}
// 计算墙壁的三角形
var wallTriangle = new List<int>();
int j;
for (var i = 0; i < verts.Count; i++)
{
// 四边形中第一个三角形
wallTriangle.Add(i);
wallTriangle.Add(i + verts.Count);
wallTriangle.Add(i + 1);
// 计算四边形中第二个三角形
j = (i + 1) % verts.Count;
wallTriangle.Add(j);
wallTriangle.Add(j + verts.Count - 1);
wallTriangle.Add(j + verts.Count);
}
// 将顶点相加
verts.AddRange(upperVerts);
// 将三角相加
var triangleList = wallTriangle.ToList();
triangleList.AddRange(upperTriangle);
// 设置顶点
resMesh.vertices = verts.ToArray();
// 设置三角形
resMesh.triangles = triangleList.ToArray();
// 计算法线
resMesh.RecalculateNormals();
return resMesh;
}
把前面测试生成多边面的代码中的mesh生成改成用GenBuildingMesh
生成,效果就如下了。
手机上播放可能看到的视频不对,可以到原视频观看。
(当输入的多边形是一个不正确的形状的时候,三角计算会为空,然后后面就会报错,加一个检测其实就行了)