第六节 地形Mesh网格生成器实现


__misawa_maho_dengeki_moeou_and_etc_drawn_by_tinker_bell__sample-6fe9d90784722f7a95b47aa872b38f19.jpg
上一篇:


上一节,我们已经生成了一张图片显示彩色分区域图和黑白高度图,这一节,我们将生成一个plane,来构造地形的mesh网格基础,通过对不同区域mesh网格顶点坐标设置不同的height值,来形成真实地形基本构造
我们在第一节的时候提到过x,y轴尺寸和顶点数,三角面以及矩形方块计算的数学式子关系,这里再温习一下:
再次查阅我们的【总顶点数构图】
总顶点数分析.png
从左下角原点开始,顺时针方向,第一个三角形是 (0,5,1),第二个为(1,5,6),接下来是(1,6,2),(2,6,7)依次类推
顶点数 vertices=width*height,而三角形顶点总数则为 (width-1)*(height-1)*2*3//方块数=》三角形个数=》顶点总数。
这也是记录我们三角形顶点总数数组所需长度Length的式子。
温习了这些数学关系之后,我们新建MeshGenerator脚本
这个也不需要其他脚本进行修改,改成static类,不需要在unity世界直接展示,去掉:MonoBehaviour继承

public static void GenerateTerrainMesh(float[,] heightMap){
    int width =heightMap.GetLength (0);////获取数组第一维元素长度值,赋值给width
    int height = heightMap.GetLength(1);////获取数组第二维元素长度值,赋值给height

    for (int y=0; y<height; y++){
        for (int x =0; x<width; x++){
        }
    }
}

为了方便记录我们的网格数据,在当前类最外面,后面新建一个网格数据类,方便后面的数据保存调用:

public class MeshData
{
public Vector3[] vertices;
public int[] triangles;
int triangleIndex;//构建AddTriangle方法时,在此申明一个int变量作为三角面索引值
public MeshData (int meshWidth, int meshHeight)
{  //网格数据存储顶点和三角面信息
    vertices = new Vector3[meshWidth * meshHeight];
    triangles = new int[(meshWidth - 1) * (meshHeight - 1) * 6];
}
public void AddTriangle(int a, int b, int c)
{ //新建一个添加三角面的方法,a,b,c代替,简化后面的表达式。
    triangles[triangleIndex] = a;
    triangles[triangleIndex + 1] = b;
    triangles[triangleIndex + 2] = c;
    triangleIndex += 3;
}
}

完成了MeshData类的构建,我们可以继续完善GenerateTerrainMesh方法了:

public static void GenerateTerrainMesh(float[,] heightMap){
    int width =heightMap.GetLength (0);
    int height = heightMap.GetLength(1);
    //在这继续输入:
    MeshData meshData =new MeshData (width,height);
    int vertexIndex=0;
    //再将遍历循环内加入
    meshData.vertices[vertextIndex]=new Vector3(x,heightMap[x,y],y);//将高度图信息存储在(x,z)平面上
    vertexIndex ++;

接下来,我们如果想让mesh网格顶点居中显示,如下图效果的话
Mesh网格居中显示.png
需要对居中计算公式做一下推导:

假设有一排三个像素点,我们想要中间的点居中显示,则中间点的x轴坐标值设为0,那么左边的点x轴的值就是-1,右边则为1.那么计算最左边的像素点的坐标值公式可以是:首先整个x轴总像素点数width-1得到对应轴的长度,然后对半分/2,就获得左右两半对应的长度值,对应就是最右侧的点的坐标值:x=(width-1)/2
居中计算公式验证:
我们可以验证一下,总共9个点的话,9-1拿掉最中间的5号,再除以2得到左右两侧平分为4,最右侧坐标4,这是符合我们预期的。
当然,如果总像素点是偶数的时候,比如8,计算得到最右侧坐标值3.5,也是ok的。
我们在

public static void GenerateTerrainMesh(float[,] heightMap){

方法中继续完善我们的mesh网格居中计算,需要用到两个新变量定义最右侧的x坐标值以及最上端Z轴坐标值

float maxX=(width-1)/2f;
float maxZ=(height-1)/2f;

接下来修正我们的meshData,vertices[vertexIndex]存储位置,使其居中:

meshData.vertices[vertextIndex]=new Vector3(x,heightMap[x,y],y);

改为:

meshData.vertices[vertexIndex]=new Vector3(x-maxX,heightMap[x,y],y-maxZ);

接下来我们查看演示:
三角形.png
用0号点管理第一个方块中2个三角形,1号管理对应两个,最右侧坐标点留空不计算,最后y=height时上方边界也留空不进行计算。
我们再思考下,顶点index索引我们是设为i开始遍历,
索引值和宽度关系.png
我们将index和之前的三角形构建公式结合起来,我们比方说第一排中间任意一个顶点索引值i,右侧类推就是i+1,i+2,那么一排遍历完毕,往上第二排同列位置就应该是i+width,往右侧类推就是i+width+1,i+width+2;
那么我们第一个三角形记录下来就是(i,i+width,i+1),第二个就是(i+1,i+width,i+width+1),以此类推,等于是再次温习下之前提到的三角形数组记录:(i+1,i+width+1,i+2),(i+2,i+width+1,i+width+2)
分析完毕,我们在vertexIndex++;之前,构建我们的三角形网格:

 if (x < width -1&& y < height -1)
  {//最右侧像素点和上方边界不进行绘制
       meshData.AddTriangle(vertexIndex, vertexIndex + width, vertexIndex + 1);
       meshData.AddTriangle(vertexIndex + 1, vertexIndex + width, vertexIndex + width + 1);
       meshData.AddTriangle(vertexIndex, vertexIndex + width, vertexIndex + 1);
  }
  vertexIndex++;

这样,我们网格绘制基础方块就完成了
接下来我们思考下UV地图绘制,有了UV贴图,就等于告诉unity我们的二维图片该以何种顺序和方向布局到我们的三维物体表面,这样我们就可以将材质贴到我们的mesh网格上了,这里用到的是plane,所以uv构建是比较简单的,以后接触三维模型制作的时候,uv就需要专门的软件进行自动或者手动引导设置一块块uv布局了,unity就难以胜任复杂模型快速方便地标记uv贴图了。
好了,接下来,我们去MeshData类里面新申明一个uv 变量:

public Vector2[] uvs;

MeshData构造里面新增:

uvs =new Vector2[meshWidth*meshHeight];

然后回到GenerateTerrainMesh方法里面meshData.vertices顶点绘制语句后,补充uv绘制:

meshData.uvs[vertexIndex]=new Vector2(x/(float)width,y/(float)height);

平面的三维模型uv贴图绘制还是非常简单的,这样就ok了
接下来,我们构建一个mesh绘制方法,对我们刚才已经完毕的meshData进行加工,绘制出Mesh网格
在MeshData类里面新增一个公开方法:

public Mesh CreateMesh(){
    Mesh mesh = new Mesh();
    mesh.vertices=vertices;
    mesh.triangles=triangles;
    mesh.uv=uvs;
    mesh.RecalculateNormals();//重新计算法线,使得所有三角面方向正常显示,从而顺利计算光线反射。
    return mesh;//最后传出绘制好顶点,三角面,uv贴图和正确法线分部的mesh信息。

然后回到我们的静态方法GenerateTerrainMesh,我们现在已经构建完毕MeshData类,获得MeshData数据了,那么此方法就可以传出MeshData数据了,将无传出的void改为MeshData,并在此方法最后补上

return meshData;

有人会问为什么我们不直接传递出来mesh信息,而是传递整个meshData类干嘛
这是因为后面我们创建多个mesh地图块调用多线程计算的时候,不能直接在每个线程里面直接创建或者调用mesh信息,只能将整个类数据传回unity主线程,然后在主线程里进行unity组件类型的调用,所以传出整个meshData类,方便后期主线程调用获取内部的mesh信息。

目前MeshGenerator里面基本就是这样了,我们回到MapGenerator脚本里面新增一个DrawMode:Mesh

public enum DrawMode{NoiseMap,ColourMap,Mesh};//新增Draw Mesh枚举内容;

接下来在GenerateMap()方法中新增MapDisplay渲染,如果drawMode不是NoiseMap,ColourMap的话,加入判断:

else if (drawMode==DrawMode.Mesh){
    display.DrawMesh(MeshGenerator.GenerateTerrainMesh(noiseMap),TextureGenerator.TextureFromColourMap(colourMap,mapWidth,mapHeight));//DrawMesh方法我们还没有构建,先写在这里好了。

我们去MapDisplay脚本,把DrawMesh方法构建出来
申明两个变量:

public MeshFilter meshFilter;//引导mesh信息传递
public MeshRenderer meshRenderer;//mesh信息导入后进行渲染
public void DrawMesh(MeshData meshData , Texture2D texture){  //开始构建绘制mesh方法
    meshFilter.sharedMesh=meshData.CreateMesh();
    meshRenderer.sharedMaterial.mainTexture=texture;//由于我们可能会在脚本之外会调用修改网格贴图材质,所以这里都采用共用网格和材质类型

回到unity,我们新建一个空物体,命名为Mesh,物体新增两个组件:meshFilter和meshRenderer
Assets里面,Materials文件夹下新建一个材质球,命名Mash Mat;
材质球smooth度调成0,让边缘清晰,点Mesh物体,将Mesh Renderer组件下的材质栏打开,拖放我们新建的材质球到这里,然后点击Hierarchy面板的Map Generator物体,挂载的Map Display脚本,我们新建的Mesh Filter和Mesh Renderer字段,都挂载上我们的Mesh物体,这样我们就可以在Inspector面板选择脚本里面的DrawMode模式为Mesh绘制,应该Scene场景就可以看见我们绘制出来的网格了
之后我们对这个mesh网格进行高度拉升就可以实现基本的三维地形绘制了,现在我们先把Mesh的Scale改成10,10,10好匹配我们的地图
新加一个光源,让效果更舒适,这里目前有一个小问题,我们绘制出来的mesh网格贴图和单独的彩色材质图是180度倒置的,造成此问题的原因目前我的能力无法找到,我们可以通过将MeshGenerator脚本里面

meshData.vertices[vertexIndex]=new Vector3(x-maxX,heightMap[x,y],y-maxZ);

顶点坐标进行x,y轴翻转,可以得到正确的结果:

meshData.vertices[vertexIndex]=new Vector3(maxX-x,heightMap[x,y],maxZ-y);

希望有大佬指点下原因,非常感谢(☆ω☆)。
好了,下一节我们再讲解如何拉升mesh网格高度,目前来说,我们基本的mesh绘制已经完成,当然对于无限生成的大地形目标来说,目前这个受到unity自身65000个顶点单地形生成限制,如果我们在Inspector面板Map Generator脚本上修改Map Width和Map Height 超过256,则会出现一些鬼畜的问题
这在后面的分享中我们会通过分割mesh地图块来实现无限地图生成。这节到此结束。
下一篇:

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