Unity3D繪制地形的實現(xiàn)方法
項目中肯定會遇到需要用戶自己繪制地形的需求,然后根據(jù)地形自動生成房間。下面說說我在繪制地形的實現(xiàn)方法。
我們百度可以看到很多關(guān)于自己創(chuàng)建mesh的博客,mesh的生成需要三角面頂點坐標(biāo)以及頂點序列。所以,想要創(chuàng)建我們想要的mesh,首先要獲取到繪制mesh的頂點。我們用戶在繪制自己想創(chuàng)建的地形時會有很大的自由性。他是隨心所欲想怎么畫就怎么畫。這也造就了很大的錯誤風(fēng)險性,要求程序更加智能。好了,下面說下我們給自己程序設(shè)定的一些規(guī)則。
首先我們設(shè)置在繪制的時候攝像頭的forward朝向Y軸向上,即我們可以俯視到的就是xz軸組成的平面。其次我們要使用linerenderer來畫線,linerenderer組合起來必須是一個封閉的區(qū)間,否則有無限種可能。
而畫房間不僅有最外層的墻,還有內(nèi)部一個個小房間,他們也組成了閉合區(qū)間。而我們在畫地形時需要抓取的是最外層閉合區(qū)間的頂點。本文畫復(fù)雜多邊形使用的算法是耳切法分割多邊形,所以我們選擇操作的方向是逆時針方向。算法鏈接。下面就說下在做項目時遇到的核心問題處理。還是老套路,先放效果圖吸睛。
一、選取第一個處理的頂點
如上我們指定的規(guī)則是攝像機沿Y軸向下俯視。所以我們獲取所有頂點,然后選取這些定點中x值最小的,選取出X值最小的點,有可能有一個,也有可能有多個,所以我們要接著篩選。在獲取的這些點中我們設(shè)置篩選的條件是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; } }
二、獲取逆時針方向的第二個點
我們獲取到所有與第一個點連線的線段的斜率,因為是閉合區(qū)間,所以至少會有兩條線段與第一個點連接,由于第一個點為凸點且x值為所有點里最小,所以我們比較與第一個點連接的線段斜率。會有如下兩種情況:斜率存在和斜率不存在。當(dāng)斜率存在時,我們可以想象,k為最小值時即為逆時針的第二個點,k為最大值時線段連接的另一個端點為逆時針方向的最后一個點。當(dāng)斜率不存在時即線段是平行于x軸的,所以我們要比較線段的斜率最小值是否小于0,如果小于0則這個線段連接的另一個端點為第二個點。如果斜率大于0,則這條斜率不存在的線段連接的端點為第二個點。同理可獲取最后一個端點。
/// <summary> /// 返回第二個頂點坐標(biāo) /// </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軸狀態(tài) { 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; } }
三、處理其他頂點
處理其他頂點我們就比較復(fù)雜,因為一個頂點會有很多線段與之相連,而我們要獲取的是最外圍的頂點。所以我們在獲取到第二個頂點以及與第二個頂點連接的線段后(去除連接第一個頂點和第二個頂點的線段),如下圖:三條線段OA,OB,OC.OD.
我們自己分析會知道我們要得到OD,但是程序沒有我們直觀的分析能力。程序只能依靠計算來作為“視覺”依靠。所以接下來就是我們的處理。首先我們要判斷凹凸角。因為毋庸置疑凹角肯定是最外層的閉合回路。如圖角EOD.所以接下來我們要進行計算篩選。首先我們要計算各個閉合回路的點是凸角還是凹角。如判斷角EOA,角EOB,角EOB,角EOC,角EOD。判斷的方法就是向量的叉乘。
3.1判斷凹凸角
我們知道第一個點為凸角,所以我們先根據(jù)第一個頂點的兩條邊叉乘得到凸角的方向。即向量o2o1xo1E,這里我們一定要記住判斷凹凸角的向量叉乘一定要選取同一走向的向量,即都沿著逆時針方向或者都順時針方向。而unity的坐標(biāo)系是右手坐標(biāo)系,所以叉乘的結(jié)果和我們右手定則得到的方向相反。即沿Y軸向下。我們得到標(biāo)準(zhǔn)凸角的叉乘方向,在用其他角的叉乘結(jié)果和標(biāo)準(zhǔn)方向比較。如果同向即為凸角,否則為凹角。代碼如下:
private float crossValue(Vector3 v1,Vector3 v2) { v1 = new Vector3(v1.x,0,v1.z);//把頂點坐標(biāo)處理下 v2 = new Vector3(v2.x,0,v2.z);//把頂點坐標(biāo)處理下 return Vector3.Dot(Vector3.up, Vector3.Cross(v1.normalized, v2.normalized)); }
根據(jù)float值判斷,當(dāng)為負值即超Y軸向下,為凸角。當(dāng)為正值時朝Y軸向上,為凹角。
判斷結(jié)果一般會出現(xiàn)如下三種情況:1.全是凸角2.全是凹角3既有凸角也有凹角。在程序中我們需要加入if判斷。第一種情況全是凸角:我們就需要計算組成角的兩邊向量點積,點積越小,夾角越大,也就是最外圍線段。第二種情況和第三種情況處理情況相同,篩選出來凹角,然后根向量點積公式,點積越大,夾角越大。即可求出最外圍線段。代碼如下:
private void dealOtherPoint(Vector3 sv) { int num = lrlist.Count; int _addIndex; //后續(xù)還要添加 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("死循環(huán)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>();//記錄凹角個數(shù) 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; } }
好了以上我們就可以篩選出最外圍頂點了并把他們添加到數(shù)組中。
四、劃分三角形
耳切法分割三角形算法。點擊打開鏈接。按照文章的講解就可以明白解決方法。然后將自己想法用程序表達出來。
五、創(chuàng)建mesh
接下來也是最后一步,我們根據(jù)頂點來創(chuàng)建mesh。我們在分割多邊形時會得到多個三角形以及對應(yīng)三角形的頂點索引,在創(chuàng)建mesh時將頂點以及對應(yīng)的索引數(shù)組賦值給mesh.vertices和mesh.triangles。代碼如下:
//處理下得到的list數(shù)組 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;
里面的處理代碼很繁瑣,要不斷判斷凹凸角的問題以及最大夾角。重要的是理解耳切法算法原理以及他的一些判斷標(biāo)準(zhǔn),就能很好的理解以及完成我們的需求了。希望本博客對你有幫助。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
程序中兩個Double類型相加出現(xiàn)誤差的解決辦法
本篇文章介紹了,程序中兩個Double類型相加出現(xiàn)誤差的解決辦法。需要的朋友參考下2013-04-04