第六节 地形Mesh网格生成器实现
上一篇:
上一节,我们已经生成了一张图片显示彩色分区域图和黑白高度图,这一节,我们将生成一个plane,来构造地形的mesh网格基础,通过对不同区域mesh网格顶点坐标设置不同的height值,来形成真实地形基本构造
我们在第一节的时候提到过x,y轴尺寸和顶点数,三角面以及矩形方块计算的数学式子关系,这里再温习一下:
再次查阅我们的【总顶点数构图】
从左下角原点开始,顺时针方向,第一个三角形是 (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网格顶点居中显示,如下图效果的话
需要对居中计算公式做一下推导:
假设有一排三个像素点,我们想要中间的点居中显示,则中间点的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);
接下来我们查看演示:
用0号点管理第一个方块中2个三角形,1号管理对应两个,最右侧坐标点留空不计算,最后y=height时上方边界也留空不进行计算。
我们再思考下,顶点index索引我们是设为i开始遍历,
我们将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地图块来实现无限地图生成。这节到此结束。
下一篇: