1. 程式人生 > >untiy 2D 曲面地圖動態生成

untiy 2D 曲面地圖動態生成

      前面說要做個2D曲面,因為一直在找工作沒時間,如果大家有機會可以幫小弟推薦推薦,小弟不勝感激。好言歸正傳了,開始講今天的東西了,先截個圖看看效果。圖如下:



第一張是在scence檢視下的圖,可以很清楚的看到這個地形的分佈,第二張則是一個球在場景中的執行圖,也就是我們的主角了。如果我們需要製作出一個曲面,毫無疑問的是我們得動態構造網格,然後給我們的網格貼uv。一個曲面是有很多個三角形繪製,所以我們知道如何繪製出一個三角形的話,然後間接的就可以繪製出一個四邊形,乃至整個曲面網格了。如果不會繪製一個三角形的可以參考前面的部落格了。如果我們先繪製出一個長方形的網格,然後通過變幻把直的變成彎的(如果我們能變魔術就好了,把直的變成彎的)。

我們先把直的繪製出來,上面就是一個方的地形了(逗比劉真是菜,一個方的地形還這樣,逗比劉就只會裝b了 哈哈)我們發現方的地形我們用了很多點來繪製其實是有必要的,這裡便於我們理解,我們發現如果把上面的地形用sin曲線或者cos曲線做的話,那不是就是變成曲線了麼。好我們先繪製出直的來,然後演變成曲的。

那麼首先需要建一個類了。我們定義其為BKCurveEntity,

public class BKCurveEntity
{

}

我們先寫個初始化方法,用來給定我們用什麼材質,方形網格的高度,整個網物體的其實位置。

    public Vector3 P1;
    public Vector3 P2;
    public Vector3 P3;
    public Vector3 P4;

    public Vector3 OriginVector3;

    private Material _bodyMeshMaterial;
    private Material _terrianMaterial;
    private float _factor;

    public void IntilizedBkEntity(List<Vector3> keyPoints, Material tm, Material bm, float factor)
    {
        P1 = keyPoints[0]; P2 = keyPoints[1];
        P3 = keyPoints[2]; P4 = keyPoints[3];
        _bodyMeshMaterial = bm;
        _terrianMaterial = tm;
        _factor = factor;
    }
p2,p3表示我們繪製網格的左上座標和右上座標。這裡p1,p4的用處我們先不講,tm代表網格黑色山體的材質,而bm代表的網格灰色山體上面草的材質了。後面的引數先不講了。接下來我們新增一個主方法。GameObject GenerateMeshGameObject(Vector3 originPos, float startuv)用來生成一個gameobject。
  public GameObject GenerateMeshGameObject(Vector3 originPos, float startuv)
    {     
        var bk = new GameObject("BK");
        bk.transform.position = originPos;
        OriginVector3 = originPos;   
       
       
        return bk;
    }

我們還是先對一些引數進行賦值,比如生成網格他的初始位置,起始uv等資訊。然後就是我們需要新增網格的點了,這裡我們網格左下到右下的點我們可以根據左上到右上的點來進行計算,所以我們需要先得到左上到右上這之間的點了。

<span style="background-color: rgb(255, 153, 0);">_tempVector3S = new List<Vector3>();</span>
var bk = new GameObject("BK");
        OriginVector3 = originPos;      

       <span style="background-color: rgb(255, 153, 0);"> float right = P3.x;
        float width = 8f;
        _tempVector3S.Add(P2);
        for (float i = P2.x; i < right; )
        {
            i += 0.5f;
            if (i > right)
                i = right;
            var leftTopPoint = P2+new Vector3(i,0,0);
            _tempVector3S.Add(leftTopPoint);
        }</span>
這個地方我們採用的是線型取樣點。把它存在一個臨時陣列中。接下來我們就生成山體和山體上面的草了。同時我們同樣需要得到山體網格的頂點資訊和頂點繪製順序及頂點的uv。
       OriginVector3 = originPos;

<span style="background-color: rgb(255, 153, 0);">        _terrainBodyV = new List<Vector3>();
        _terrainBodyT = new List<int>();
        _terrainBodyU = new List<Vector2>();

        _terrainSurfaceV = new List<Vector3>();
        _terrainSurfaceT = new List<int>();
        _terrainSurfaceU = new List<Vector2>();
</span>
        float right = P3.x;
上面分別定義2種網格所需要的基本資訊:網格頂點資訊,頂點繪製順序,頂點uv資訊。
      for (float i = P2.x; i < right; )
        {
            i += 0.5f;
            if (i > right)
                i = right;           
            var leftTopPoint = P2+new Vector3(i,0,0);
            _tempVector3S.Add(leftTopPoint);
        }
       <span style="background-color: rgb(255, 153, 0);"> for (int i = 0; i < _tempVector3S.Count - 1; i++)
        {
            _terrainBodyV.Add(_tempVector3S[i]);
            _terrainBodyV.Add(new Vector3(_tempVector3S[i].x, 0, 0));
            _terrainBodyV.Add(_tempVector3S[i + 1]);
            _terrainBodyV.Add(new Vector3(_tempVector3S[i + 1].x, 0, 0));

            _terrainSurfaceV.Add(_tempVector3S[i] + new Vector3(0, 0.5f, 0f));
            _terrainSurfaceV.Add(_tempVector3S[i] - new Vector3(0, 0.2f, 0f));
            _terrainSurfaceV.Add(_tempVector3S[i + 1] + new Vector3(0, 0.5f, 0f));
            _terrainSurfaceV.Add(_tempVector3S[i + 1] - new Vector3(0, 0.2f, 0f));

            _terrainBodyT.Add(0); _terrainBodyT.Add(3); _terrainBodyT.Add(1);
            _terrainBodyT.Add(3); _terrainBodyT.Add(0); _terrainBodyT.Add(2);

            _terrainSurfaceT.Add(0); _terrainSurfaceT.Add(3); _terrainSurfaceT.Add(1);
            _terrainSurfaceT.Add(3); _terrainSurfaceT.Add(0); _terrainSurfaceT.Add(2);        

            var terrainBodymesh = new Mesh();
            var terrainFaceMesh = new Mesh();

            terrainBodymesh.vertices = _terrainBodyV.ToArray();
            terrainBodymesh.uv = _terrainBodyU.ToArray();
            terrainBodymesh.triangles = _terrainBodyT.ToArray();

            terrainFaceMesh.vertices = _terrainSurfaceV.ToArray();
            terrainFaceMesh.uv = _terrainSurfaceU.ToArray();
            terrainFaceMesh.triangles = _terrainSurfaceT.ToArray();

            var ob = new GameObject("meshobject");
            var surface = new GameObject("Surface Gameobject");

            ob.transform.parent = bk.transform;
            ob.transform.localPosition = Vector3.zero;

            surface.transform.parent = bk.transform;
            surface.transform.localPosition = Vector3.zero;
                  
            _terrainBodyV.Clear();
            _terrainBodyT.Clear();
            _terrainBodyU.Clear();

            _terrainSurfaceV.Clear();
            _terrainSurfaceT.Clear();
            _terrainSurfaceU.Clear();
        }</span>
這裡我們只是對2種網格的頂點資訊和頂點繪製順序進行了賦值。還沒有對他們uv賦值了。這裡我們畫一張圖給大家理解一下。


我們新增點的順序依次是左上,左下,右上,右下,所以我們我們這個繪製順序陣列就可以很輕鬆得出來,分別0,3,1和0,2,3.最後就是新增uv和碰撞器及材質了。
           surface.transform.localPosition = Vector3.zero;

           <span style="background-color: rgb(255, 153, 0);"> AddColliderToChildMesh(_tempVector3S[i], _tempVector3S[i + 1], bk.transform);

            var ren = ob.AddComponent<MeshRenderer>();
            var filter = ob.AddComponent<MeshFilter>();
            ren.material = _bodyMeshMaterial;
            filter.mesh = terrainBodymesh;

            var ren1 = surface.AddComponent<MeshRenderer>();
            var filter1 = surface.AddComponent<MeshFilter>();
            ren1.material = _terrianMaterial;
            filter1.mesh = terrainFaceMesh;</span>

            _terrainBodyV.Clear();
這樣一個方的網格就出來了。這裡我們建一個類測試一下。
public class CreateTerrian : MonoBehaviour
{
}
先定義一個在控制面板上可以拖拽的2個材質。
    <span style="background-color: rgb(255, 153, 0);">private BKCurveEntity _bkCurveEntity;
    [SerializeField] private Material bm;
    [SerializeField] private Material tm;</span>
然後再start方法中我們生成4段方的網格。
  [SerializeField] private Material tm;

  <span style="background-color: rgb(255, 153, 0);">  private float _length;
    private float _originx;
    private float _x1, _x2, _x3, _x4;

    private List<float> _posFloats;
    private List<GameObject> _bkGameObjects; 
	void Start () 
    {
        _posFloats=new List<float>();
	    _bkCurveEntity=new BKCurveEntity();
        List<Vector3> points=new List<Vector3>();
        _bkGameObjects=new List<GameObject>();
	_originx = -10;//起始位置的x值為-10
	 _length = 0;
        _x1 = 0; _x2 = 2; _x3 = 6; _x4 = 5;
	    for (int i = 0; i < 3; i++)
	    {
            points.Add(new Vector3(-10, _x1, 0));
            points.Add(new Vector3(0, _x2, 0));
            points.Add(new Vector3(10, _x3, 0));
            points.Add(new Vector3(20, _x4, 0));


            _bkCurveEntity.IntilizedBkEntity(points, bm, tm, 0.5f);
            var ob=  _bkCurveEntity.GenerateMeshGameObject(new Vector3(_originx+_length, -5, 0), 0);
            _bkGameObjects.Add(ob);

            _length= _bkCurveEntity.GetBkLength();
            _originx=_bkCurveEntity.OriginVector3.x;
            _posFloats.Add(_originx);
           
            
            _x1 = _bkCurveEntity.P2.y; 
            _x2 = _bkCurveEntity.P3.y; 
            _x3 = _bkCurveEntity.P4.y;
            _x4 = GetRandomValue(2,8,_x3);
            points.Clear();
	    }
    }</span>
這裡第二段網格的最後一個點的y值我們用隨機函式生成。
   private float GetRandomValue(float min, float max, float referce)
    {
        while (true)
        {
            float endvalue = Random.Range(min, max);
            if (Mathf.Abs(endvalue - referce) > 2)
            {
                return endvalue;
            }
        }
    }
第二段網格的起始點就是第一段網格的終點。這樣就輕鬆的搞定了一個方形的網格了,那麼接下來我們想辦法把一個方形的網格變成一個曲的網格了。這裡我們可以用最常見的sin和cos函式來試試吧。
       for (float i = P2.x; i < right; )
        {
            i += 0.5f;
            if (i > right)
                i = right;        
       <span style="background-color: rgb(255, 153, 0);">     var leftTopPoint = P2+new Vector3(i,Mathf.Sin(i),0);</span><span style="background-color: rgb(255, 204, 102);">
</span>            _tempVector3S.Add(leftTopPoint);
        }




這就是效果,很顯然我覺得很糟糕,因為2段之間沒有連線起來,當然想讓他們無縫連線起來也很簡單。只需保證所有網格的起始點和終點他們的y保持一致就行了。這樣雖然保證了網格是曲面,發現整個網格是有規則的沒有隨機性可言。所以我們只有想別的辦法然他保證隨機性和曲面了。(逗比劉這都不知道麼,我來告訴你吧,用取樣曲線啊,這你都不知道啊。)哦 我們可以用取樣曲線,首先讓我想到的就是貝塞爾曲線,那我們來看看如果想讓2條貝塞爾曲線連線起來,2段曲線有什麼關係呢?從下圖來分析: 上圖中有2段貝塞爾曲線分別為p0,c0,p1和p1,c2,p2。如果想讓2段2次貝爾賽曲線連線起來,那麼滿足第一段貝爾賽曲線的終點和第二段盃賽曲線的起始點為同一個點,同樣p1要在c0和c2線段上,我們在做完第一段貝塞爾曲線的時候,開始連線第二段貝塞爾曲線的時候,我們先通過p1c0向量求得c1控制點,然後在隨機p2點就可以做出曲線了,思路已經很清晰了,我們可以試試,這裡我就不試了,這樣雖然可以做出曲線,但是對於y的取值是個費勁的問題,y的取值超出攝像機的視野範圍,這對於我們來講可以來說很簡單同樣也不簡單。所以這裡我們就不用這個曲線了,我們採用catmull rom曲線,不知道的朋友可以在wiki上檢視https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline。接下來我們返回到BKCurveEntity這個類中,新增如下方法:
   private Vector3 CatmullRom(Vector3 P0, Vector3 P1, Vector3 P2, Vector3 P3, float t)
    {
        Vector3 c0 = P1;
        Vector3 c1 = (P2 - P0) * _factor;
        Vector3 c2 = (P2 - P1) * 3.0f - (P3 - P1) * _factor - (P2 - P0) * 2.0f * _factor;
        Vector3 c3 = (P2 - P1) * -2.0f + (P3 - P1) * _factor + (P2 - P0) * _factor;

        Vector3 curvePoint = c3 * t * t * t + c2 * t * t + c1 * t + c0;
        return curvePoint;
    }
取樣曲線前面2個點就相當於我們的控制點,factor為曲線平滑因子範圍是[0,1]。  t的範圍是[0,1]為0時 該曲線取得p1。為1時該曲線取得p2。這樣我們想讓我們2段catmull曲線連線起來只需要,第一段曲線的p1,p2,p3等於第二段曲線的p0,p1,p2即可。這樣給我們的曲線構建帶來很大的方便。這裡我們就將他改為該曲線線,效果就如最上面的圖了。
       for (float i = P2.x; i < right; )
        {
            i += 0.5f;
            if (i > right)
                i = right;
           <span style="background-color: rgb(255, 153, 0);"> var t = GetCatmullRomT(P1, P2, P3, P4, i);
            var leftTopPoint = CatmullRom(P1, P2, P3, P4, t);</span>
            _tempVector3S.Add(leftTopPoint);
        }
新增方法如下:
   private float GetCatmullRomT(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4, float cur)
    {
        return 2 * (cur - p2.x) / (p3.x - p1.x);
    }
</pre>最後給草地新增碰撞器。<pre name="code" class="plain"> private void AddColliderToChildMesh(Vector3 originVector3, Vector3 endVector3, Transform parent)
    {
        var ob = new GameObject("Collider");
        ob.tag = "Ground";
        ob.transform.parent = parent;

        var length = Vector3.Distance(originVector3, endVector3);
        var midX = (endVector3.x + originVector3.x) / 2f;
        var midY = (originVector3.y + endVector3.y) / 2f;

        var offset = (endVector3 - originVector3);
        var eulerZ = Mathf.Atan2(offset.y, offset.x) * Mathf.Rad2Deg;
        ob.transform.localPosition = new Vector3(midX, midY, 0f);
        ob.transform.rotation = Quaternion.Euler(0, 0, eulerZ);

        var col = ob.AddComponent<BoxCollider2D>();
        col.offset = new Vector2(0, -0.03f);
        col.size = new Vector2(length, 0.06f);
    }
這裡給點建議,如果我們有時不是很特別想突出表現我們的山體,我們可以給曲線的點讓他們分散的稀疏點,給我們的草地上的點新增密一點。如果有紕漏的話,大家可以把這個資原始檔下載下來,如果有什麼不懂的 可以加qq:185076149,大家可以一起討論。資源連結:http://pan.baidu.com/s/1mh66kDq