第二节 绘制mesh网格平面,增加随机高度绘制随机地形


__original_drawn_by_tinker_bell__sample-228ee9a27dfc2401d9542d907e82d1b3.jpg
上一篇:


3.新的目标:构建更多方块,组成平面

上一阶段咱们的MeshGenerator脚本里面实现了两个方法,一个是创建存储顶点形状信息,CreateShape,另一个是UpdateMesh方法,将顶点和三角面信息载入mesh网格里让咱们的meshrenderer进行渲染。
接下来目标我们通过构建更多的方块,组成平面,最后抬升平面一些顶点,完成基础地形构建,因此,我们清空CreateShape方法,开始思考需要重新构建网格平面,思考需要创建的顶点数量
顶点数量跟平面尺寸有关,因此我们开头新加一些变量:

public int xSize =20;
public int zSize =20;

通过【总顶点数构图】分析,
总顶点数分析.png

如果我们x方向有N个方块,则顶点数为N+1;
如果z方向方块数为M,则z方向顶点数为M+1;
因此我们根据平面尺寸,得到总顶点数公式:

vertex count = (xSize+1)*(zSize+1);

代码里进行书写:

vertices = new Vector3[xSize+1)*(zSize+1)];

这样我们就构建好所有顶点存储数组空间了,接下来将遍历所有顶点,将每个顶点在咱们的平面网格的坐标位置和顶点一一对应存储即可。
按从左到右顺序(0,0),(1,0),(2,0),(3,0),(0,1),(1,1),(2,1),(3,1),代码如下:

int i=0;
for (int z=0;z<=zSize;z++)//因为达到最边界zSize也要进行存储计算,所以中间需要计算等于的时候
{
    for(int x=0;x<=xSize;x++)
    {
        vertices[i]=new Vector3(x,0,z);
        i++;
        //这里需要遍历xSize内元素,引入i这个变量,从而我们可以在开头加入int i=0;由于i在咱们这个z,x循环里使用,我们可以将int i=0;条件放入for(int z=0;这里使得代码更加简练理解,最后代码是
        for (int i =0 ,z=0;z<=zSize;z++)
    }
}

顶点都存储完毕了,但是我们实际渲染后是看不见这些顶点的,要想渲染出来顶点信息,我们需要调用unity的 OnDrawGizmos的方法,来画出咱们的顶点

private void OnDrawGizmos(){
if (vertices==null)
return;
for (int i=0 ;i<vertices.Length;i++)
{
    Gizmos.DrawSphere(vertices[i],.1f);
}

保存代码,进入unity运行,发现在我们的Scene界面,可以看见完整的顶点铺满一个平面,从我们的(0,0,0)原点。我们还可以改变inspector面板咱们的xSize和zSize大小,查看改变,顶点信息构建完毕。
下一个目标
继续构建我们的三角形以便完成我们的网格平面
在CreateShape方法中,我们继续完善第二个功能,

triangles =new int[3];
triangles[0]=0;
triangles[1]=xSize+1;
triangles[2]=1;

这样,我们第一个三角形就构建完毕了,保存查看unity,Scene面板和game面板都ok
接下来构建第二个三角形,将triangles数组大小改成6
继续加新顶点:

triangles[3]=1;
triangles[4]=xSize+1;
triangles[5]=xSize+2;

保存后unity里运行发现第一个方块构建完毕了
接下来如果要构建后面的方块,我们只需要重复这一步,同时做偏移量,进行循环即可

for (int x=0;x<xSize;x++)
{//将之前6个顶点信息全部剪切进来
triangles =new int[6];
triangles[0]=0;
triangles[1]=xSize+1;
triangles[2]=1;
triangles[3]=1;
triangles[4]=xSize+1;
triangles[5]=xSize+2;
}

为了对顶点和三角形进行叠加计数,我们在咱们的三角形计算功能前申明两个变量:

int vert =0;
int tris =0;

在for循环中对咱们的遍历进行重复计数,做一些小修改即可实现:

    for (int x=0;x<xSize;x++)
    {
    triangles =new int[6];
    triangles[tris +0]=vert+0;
    triangles[tris +1]=vert+xSize+1;
    triangles[tris +2]=vert+1;
    triangles[tris +3]=vert+1;
    triangles[tris +4]=vert+xSize+1;
    triangles[tris +5]=vert+xSize+2;
  
    vert++;
    tris +=6;
    }//第一遍,第一个方块,,tris=0,vert=0,构建完毕后vert+1,第二次循环开始,因为vert=1,此刻进行的是第二个方块顶点存储了。
//同时咱们三角形存储数组大小扩大6格,方便进行第二个方块六个顶点信息存储,
//我们不需要每次循环重复构建存储三角形顶点信息的数组,所以还需要将
    triangles =new int[6];放到for循环外面,在开始并修改元素总数大小为:
    triangles =new int[xSize*zSize*6];

这样我们通过tris +=6实现每次存储一个方块的6格顶点信息,循环后偏移量+1,达成所有x轴方向的方块顶点信息存储。
如果想看到每个三角形如何建成的顺序,我们可以构建一个协程,
如果对协程不太熟悉的,我这里简单解释下:
Coroutine:协程是让程序员自己作为调度员,告知系统等待某个操作指令完成后再执行后面的代码
一般用来控制运动或者地图生成或者一些剧情触发
协程关键调度指令就是yield这个中断指令,yield返回的值决定了这个协程等待多久恢复执行,
我们新建一个Coroutine Test脚本做个简单测试和说明:

void start(){
Debug.Log("start1");
StartCoroutine
(Test());
Debug.Log("start2");
}

IEnumerator Test(){
Debug.Log("test1");
yield return null;
Debug.Log("test2");
}

保存,运行,结果应该是start1,test1,start2,test2
这startCoroutine调用和普通函数方法调用类似,然后执行Ienumerator,方法类型就不是普通的函数类型了,需要特定的IEnumerator类型
执行test1输出后,yield断点,返回空,就立马回到start方法后面的指令,这边暂停,输出start2后,我们的主程序一般都是立马返回的,直接返回到我们yield断点后继续执行协程后面的test2输出,这就是协程的工作流程
进阶分析:
如果我们return一个等待时间,结果又是如何呢?
修改 yield return null;yield return new WaitForSeconds(3);
保存运行,我们可以发现,test2会在start2输出后三秒才显示,这就是return的vaule对暂停等待时间的控制效果
我们再分析下,协程套协程是如何运行的:
修改 yield return new WaitForSeconds(3);
yield return StartCoroutine(AnotherTest());
没错,我们协程里返回开始另一个协程,我们构建下这个新协程:

IEnumerator AnotherTest(){
    Debug.Log("AnotherTest1");
    yield return null;
    Debug.Log("AnotherTest2");
    }

保存后执行,结果应该是start1,test1,AnotherTest1,start2,AnotherTest2,test2
这里注意,第一个协程会等待第二个协程中所有代码执行完毕后再继续执行,第一个yield resturn告诉第一个主程StartCoroutine我这边先暂停了,你去继续执行StartCoroutine(AnotherTest),我在这等着你完事 的消息,就会再回来执行我后面的test2,于是我们第一个StartCoroutine就跑去就执行AnotherTest先,这个AnotherTest1就紧接着输出了,之后遇到yield return null,这个AnotherTest协程就告诉我们的StartCoroutine,行了,我这也先暂停了,你继续执行,你完毕后告诉我,我就执行我后面的AnotherTest2输出,于是咱们的StartCoroutine又跑回来,执行完毕自己剩下的start2输出,然后告诉我们AnotherTest协程,我好了,你可以继续yield return null后面的内容了,于是AnotherTest2输出,告诉第一个StartCoroutine我执行完毕了,你继续,于是第一个StartCoroutine检查了下,我还与test协程说好了,等着AnotherTest协程结束,他就可以继续干活了,最后告诉我们的Test协程你久等了,那个垃圾AnotherTest协程终于完事了,你继续,于是test2就输出执行了
协程这东西差不多基础的原理了解这么多就够用了,我们回到这节的内容上来,刚才说到
如果想看到每个三角形如何建成的顺序,我们可以构建一个协程,那么在协程里
对每一次构建插入一个yield等待时间,就可以放缓构建过程了,这就需要用到IEnmulator和StartCoroutine了,我们将CreateShape方法改为IEnumerator
在三角形信息存储for循环里,最后返回

yield return new WaitForSeconds (.1f);

由于方法改成了coroutine协程了,我们在start方法里调用CreateShape也要修改成

StartCoroutine(CreateShape());

现在要重复执行createShape方法,完成咱们mesh整个绘制过程,我们需要将UpdateMesh()方法移到Update方法里每一帧多次执行,这样才可以做到查看每个三角形构建过程:

private void Update(){
UpdateMesh();
}

保存后,这样我们就可以看到一排三角形创建的顺序了
继续对z轴方向剩下的方块进行绘制

for (int z=0;z<zSize;z++){
    将刚才的x轴for循环体全部剪贴进来
    }

保存测试,发现确实可以绘制了,但是有个问题,两侧边界的三角面渲染有些不对劲,翻到底部还可以看到每绘制一行新的方块,边界总跟随一条斜线绘制在底部。
这是因为我们每次x轴一行绘制完毕了,并没有将每行的vertex顶点进行切断,而是将x轴最右侧的顶点和z轴+1后新的一行顶点直接相连,导致在咱们网格平面背面进行跨行绘制,导致两侧边界三角形出现正反两个方向的面,从而光线计算出现异常,同时绘制出底部飞线效果。
要修改这个bug,只需要将x轴for循环外面,在后面加一个

vert++;

将最后一个顶点跳过,z轴+1后,将左侧新的顶点作为新的一行的初始顶点开始新的绘制即可。

保存后测试一切ok
最后目标:通过修改一些顶点的高度值获得高低起伏地形效果。
createShape方法里

for(int x=0;x<=xSize;x++)
{
    vertices[i]=new Vector3(x,0,z);
    i++;

之前循环遍历存储每个顶点信息的时候,y轴储存的都是0高度,这里我们修改下
vertices[i]=new Vector3(x,0,z);前增加:

float y =Mathf.PerlinNoise(x*.3f,z*.3f)*2f;//2f使得柏林噪音起伏高低效果更加显著,.3f是形成更广阔的柏林噪音分布,使得地形变化更加多样。

修改 vertices[i]=new Vector3(x,0,z);vertices[i]=new Vector3(x,y,z);使得y轴高度产生柏林噪音高低起伏存储进来。
yield return new WaitForSeconds (0.01f);可以修改更快的时间间隔,使得生成的图形更快。
最后调试完毕,将IEmulator改成普通void方法,将Update里面updateMesh方法移到start方法里使得生成更加快速,同时修改startcorutine(createShape())为普通的createShape()方法调用,最后Ctrl +K+C注释掉
private void OnDrawGizmos()方法,不再需要测试绘制顶点了,保存测试,一张随机全新平面地图快速生成完毕,我们的基本地形生成演示到此完成。
下一篇:

Last modification:August 6, 2020
If you think my article is useful to you, please feel free to appreciate