Unity3D绘制地形的实现方法
项目中肯定会遇到需要用户自己绘制地形的需求,然后根据地形自动生成房间。下面说说我在绘制地形的实现方法。
我们百度可以看到很多关于自己创建mesh的博客,mesh的生成需要三角面顶点坐标以及顶点序列。所以,想要创建我们想要的mesh,首先要获取到绘制mesh的顶点。我们用户在绘制自己想创建的地形时会有很大的*性。他是随心所欲想怎么画就怎么画。这也造就了很大的错误风险性,要求程序更加智能。好了,下面说下我们给自己程序设定的一些规则。
首先我们设置在绘制的时候摄像头的forward朝向y轴向上,即我们可以俯视到的就是xz轴组成的平面。其次我们要使用linerenderer来画线,linerenderer组合起来必须是一个封闭的区间,否则有无限种可能。
而画房间不仅有最外层的墙,还有内部一个个小房间,他们也组成了闭合区间。而我们在画地形时需要抓取的是最外层闭合区间的顶点。本文画复杂多边形使用的算法是耳切法分割多边形,所以我们选择操作的方向是逆时针方向。。下面就说下在做项目时遇到的核心问题处理。还是老套路,先放效果图吸睛。
一、选取第一个处理的顶点
如上我们指定的规则是摄像机沿y轴向下俯视。所以我们获取所有顶点,然后选取这些定点中x值最小的,选取出x值最小的点,有可能有一个,也有可能有多个,所以我们要接着筛选。在获取的这些点中我们设置筛选的条件是z值最小,这样就能获取到唯一的一个点。此时,该点即为凸点。代码如下:
vector3 firstvalue=vector3.zero; for (int i=0;i<plist.count;i++) { if (plist[i].position.x<= firstvalue.x) { if (plist[i].position.x == firstvalue.x) { if (plist[i].position.z > firstvalue.z) { return; } } firstvalue = plist[i].transform.position; } }
二、获取逆时针方向的第二个点
我们获取到所有与第一个点连线的线段的斜率,因为是闭合区间,所以至少会有两条线段与第一个点连接,由于第一个点为凸点且x值为所有点里最小,所以我们比较与第一个点连接的线段斜率。会有如下两种情况:斜率存在和斜率不存在。当斜率存在时,我们可以想象,k为最小值时即为逆时针的第二个点,k为最大值时线段连接的另一个端点为逆时针方向的最后一个点。当斜率不存在时即线段是平行于x轴的,所以我们要比较线段的斜率最小值是否小于0,如果小于0则这个线段连接的另一个端点为第二个点。如果斜率大于0,则这条斜率不存在的线段连接的端点为第二个点。同理可获取最后一个端点。
/// <summary> /// 返回第二个顶点坐标 /// </summary> /// <param name="v"></param> /// <returns></returns> private vector3 returnsecondvalue(vector3 v) { //debug.log("v+" + v); list<linerendererstruct> lrst = new list<linerendererstruct>(); for (int i=0;i<lrlist.count;i++) { if (vector3.distance(lrlist[i].getposition(0),v)<0.1f) { lrst.add(new linerendererstruct(0, lrlist[i])); } else if (vector3.distance(lrlist[i].getposition(1), v) < 0.1f) { lrst.add(new linerendererstruct(1, lrlist[i])); } } //debug.log("lrst.count+"+lrst.count); if (lrst.count >= 2) { float k1 = 0; //斜率最大 float k2 = 0; //斜率最小 vector3 v1=vector3.zero; vector3 v2 = vector3.zero; linerenderer llrr=new linerenderer(); for (int i=0;i< lrst.count;i++)//选取斜率最大和最小的两个点 { vector3 vvv= lrst[i]._lr.getposition(lrst[i]._index == 0 ? 1 : 0); if (vvv.x-v.x==0)//此处斜率不存在 就是平行x轴状态 { if (k1 <= 0) { v1 = vvv; llrr = lrst[i]._lr; lastlinerenderer = lrst[i]._lr; continue; } if (k2 >= 0) { v2 = vvv; llrr = lrst[i]._lr; continue; } } float k= (vvv.z - v.z) / (vvv.x - v.x); if (i == 0) { k1 = k; v1 = vvv; lastlinerenderer = lrst[i]._lr; k2 = k; v2 = vvv; llrr = lrst[i]._lr; } else { if (k1 < k) { k1 = k; v1 = vvv; lastlinerenderer = lrst[i]._lr; } if (k2 > k) { k2 = k; v2 = vvv; llrr = lrst[i]._lr; } } } vertexlist.add(new vertexstruct(1,v2)); lrlist.remove(llrr); debug.log("vertexlist[1]._vec+" + vertexlist[1]._vec); return vertexlist[1]._vec; } else { debug.logerror("此处有错误"); iscontinue = false; if (lrst.count < 2) { _warning.setactive(true); startcoroutine(globle.invokedelay(()=> { _warning.setactive(false); }, fadetime)); } return vector3.zero; } }
三、处理其他顶点
处理其他顶点我们就比较复杂,因为一个顶点会有很多线段与之相连,而我们要获取的是最外围的顶点。所以我们在获取到第二个顶点以及与第二个顶点连接的线段后(去除连接第一个顶点和第二个顶点的线段),如下图:三条线段oa,ob,oc.od.
我们自己分析会知道我们要得到od,但是程序没有我们直观的分析能力。程序只能依靠计算来作为“视觉”依靠。所以接下来就是我们的处理。首先我们要判断凹凸角。因为毋庸置疑凹角肯定是最外层的闭合回路。如图角eod.所以接下来我们要进行计算筛选。首先我们要计算各个闭合回路的点是凸角还是凹角。如判断角eoa,角eob,角eob,角eoc,角eod。判断的方法就是向量的叉乘。
3.1判断凹凸角
我们知道第一个点为凸角,所以我们先根据第一个顶点的两条边叉乘得到凸角的方向。即向量o2o1xo1e,这里我们一定要记住判断凹凸角的向量叉乘一定要选取同一走向的向量,即都沿着逆时针方向或者都顺时针方向。而unity的坐标系是右手坐标系,所以叉乘的结果和我们右手定则得到的方向相反。即沿y轴向下。我们得到标准凸角的叉乘方向,在用其他角的叉乘结果和标准方向比较。如果同向即为凸角,否则为凹角。代码如下:
private float crossvalue(vector3 v1,vector3 v2) { v1 = new vector3(v1.x,0,v1.z);//把顶点坐标处理下 v2 = new vector3(v2.x,0,v2.z);//把顶点坐标处理下 return vector3.dot(vector3.up, vector3.cross(v1.normalized, v2.normalized)); }
根据float值判断,当为负值即超y轴向下,为凸角。当为正值时朝y轴向上,为凹角。
判断结果一般会出现如下三种情况:1.全是凸角2.全是凹角3既有凸角也有凹角。在程序中我们需要加入if判断。第一种情况全是凸角:我们就需要计算组成角的两边向量点积,点积越小,夹角越大,也就是最外围线段。第二种情况和第三种情况处理情况相同,筛选出来凹角,然后根向量点积公式,点积越大,夹角越大。即可求出最外围线段。代码如下:
private void dealotherpoint(vector3 sv) { int num = lrlist.count; int _addindex; //后续还要添加 list<vertexstruct> temporarylist; while (true) { temporarylist = new list<vertexstruct>(); num--; if (num<-1) { iscontinue = false; debug.log("重新智能处理,若处理不了,则警告用户重新操作"); _warning.setactive(true); startcoroutine(globle.invokedelay(() => { _warning.setactive(false); }, fadetime)); //debug.logerror("死循环1"); return; } //debug.log("sv+" + sv); //在剩下的所有定点中找按顺序排列的下一个顶点 for (int i = 0; i < lrlist.count; i++) { if (vector3.distance(lrlist[i].getposition(0),sv)<0.1f) { if (lastlinerenderer == lrlist[i]) { //debug.log("lastkinerenderer1"); return; } _addindex = vertexlist.count; temporarylist.add(new vertexstruct(i, lrlist[i].getposition(1))); continue; } else if (vector3.distance(lrlist[i].getposition(1), sv) < 0.1f) { if (lastlinerenderer == lrlist[i]) { debug.log("lastkinerenderer2"); debug.log(lrlist.count); return; } _addindex = vertexlist.count; temporarylist.add(new vertexstruct(i, lrlist[i].getposition(0))); continue; } } _addindex = vertexlist.count; if (temporarylist.count== 1)//一个顶点只有两个linerenderer连接时 { vertexlist.add(new vertexstruct(_addindex, temporarylist[0]._vec)); lrlist.removeat(temporarylist[0]._num); } else if (temporarylist.count>1) { list<int> aolist =new list<int>();//记录凹角个数 for (int i = 0; i < temporarylist.count; i++) { if (!istuangle(sv, temporarylist[i]._vec)) aolist.add(i); } //初始边向量 vector3 vc = sv - vertexlist[vertexlist.count - 1]._vec; //全是凸角 if (aolist.count == 0) { float dotvalue=1; int dotvalueindex = 0; for (int i=0;i< temporarylist.count; i++) { vector3 vm = temporarylist[i]._vec - sv; dotvalue = dotvalue > getdotvalue(vc, vm) ? getdotvalue(vc, vm) : dotvalue;//取余弦值最小值 dotvalueindex = dotvalue > getdotvalue(vc, vm) ? i : dotvalueindex; } vertexlist.add(new vertexstruct(_addindex, temporarylist[dotvalueindex]._vec)); } //全是凹角 else //if (aolist.count == 1) { float dotvalue = 1; int dotvalueindex = 0; for (int i = 0; i < temporarylist.count; i++) { vector3 vm = temporarylist[aolist[i]]._vec - sv; dotvalue = dotvalue < getdotvalue(vc, vm) ? getdotvalue(vc, vm) : dotvalue;//取余弦值最大值 dotvalueindex = dotvalue < getdotvalue(vc, vm) ? i : dotvalueindex; } vertexlist.add(new vertexstruct(_addindex, temporarylist[dotvalueindex]._vec)); } list<linerenderer> temporarylrlist = new list<linerenderer>(); for (int i=0;i< temporarylist.count;i++) { temporarylrlist.add(lrlist[temporarylist[i]._num]); } for (int i=0;i< temporarylrlist.count;i++) { if (lrlist.contains(temporarylrlist[i])) { lrlist.remove(temporarylrlist[i]); } else debug.log("有错误"); } } sv = vertexlist[vertexlist.count - 1]._vec; } }
好了以上我们就可以筛选出最外围顶点了并把他们添加到数组中。
四、划分三角形
耳切法分割三角形算法。点击打开链接。按照文章的讲解就可以明白解决方法。然后将自己想法用程序表达出来。
五、创建mesh
接下来也是最后一步,我们根据顶点来创建mesh。我们在分割多边形时会得到多个三角形以及对应三角形的顶点索引,在创建mesh时将顶点以及对应的索引数组赋值给mesh.vertices和mesh.triangles。代码如下:
//处理下得到的list数组 int[] ints = new int[verticelist.count * 3]; for (int i = 0; i < verticelist.count; i++) { ints[3 * i + 0] = verticelist[i][0]; ints[3 * i + 1] = verticelist[i][2]; ints[3 * i + 2] = verticelist[i][1]; } gameobject g = new gameobject("myplane"); g.addcomponent<meshrenderer>().material= myplanematerial; g.transform.tag = "house"; g.transform.setparent(_house.transform); mesh mesh = new mesh(); mesh.vertices = vecs; mesh.triangles = ints; g.addcomponent<meshfilter>().mesh = mesh; g.addcomponent<meshcollider>().sharedmesh=mesh;
里面的处理代码很繁琐,要不断判断凹凸角的问题以及最大夹角。重要的是理解耳切法算法原理以及他的一些判断标准,就能很好的理解以及完成我们的需求了。希望本博客对你有帮助。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: C#基础语法:as 运算符使用实例
下一篇: C#基础语法:可空类型详解
推荐阅读