效果展示:
可直接使用的完整代码在本文最后
工具介绍:
- Auto Cutting属性,勾选该属性后,Each Terrain Size属性失效,脚本会根据Terrain Size属性自动划分网格,单个网格大小不会大于256*256.
- Height Map属性用来存放灰度图
- Terrain Height、Terrain Size 用来指定mesh的高度和大小
- 在Auto Cutting属性未勾选的情况下, Each Terrain Size属性可以用来指定每块地图的大小,属性要求必须能被TerrainSize整除
- Terrain Mat规定了地形的材质
- Origin属性为Mesh的父对象,可为空,默认为脚本所在对象
- 勾选Create Prefab属性,会将动态生成的Mesh文件保存为Prefab,在Assets文件夹下
Mesh基础知识:
https://catlikecoding.com/unity/tutorials/procedural-grid/
根据灰度图生成Mesh
在读取图像之前,务必勾选Read/Write Enable属性
单张Mesh的话,我们需要做的只是对它顶点坐标的Y值做一些手脚
vertices.Add(new Vector3(i, HeightMap.GetPixel(X,Y).grayscale * TerrainHeight,j));
HeightMap.GetPixel((int)a,(int)b).grayscale;可以对图像灰度值进行采样
对Mesh进行切割
由于Mesh的限制,单张Mesh大小不能大于256*256,而且,我们通常不会把整个地图全部加载出来,地图分块是必须的
所谓切割,其实就是分批对灰度图采样
vertices.Add(new Vector3(TX*EachTerrainSize+i, HeightMap.GetPixel(X,Y).grayscale * TerrainHeight, TY*EachTerrainSize+j));
需要注意,无论TerrainSize值为多少,整张灰度图都会被利用到,就是说,在采样之前有一个缩放的过程
int X =(int)((float)TX * (float)EachTerrainSize *((float)MapWidth/(float)TerrainSize) + (float)i * ((float)MapWidth / (float)TerrainSize));
int Y = (int)((float)TY * (float)EachTerrainSize *((float)MapHeight / (float)TerrainSize) + (float)j * ((float)MapHeight / (float)TerrainSize));
Mesh切割之后有缝隙问题
这是因为,两个相邻的Mesh中间的连线并没有连接起来
我的做法是,规定255*255为最大单片Mesh大小,剩下的预留,用来补间,排除不需要补间的情况
List<Vector3> vertices = new List<Vector3>();
List<int> triangles = new List<int>();
//只有在单片Mesh(不切割)的情况下,EachTerrainSize才有可能==256
int EachTerrainSize_1 = EachTerrainSize == 256 ? 256 : EachTerrainSize + 1;
for (int i = 0; i < EachTerrainSize_1; i++)
{
for (int j = 0; j < EachTerrainSize_1; j++)
{
if (j == EachTerrainSize_1 && TX >= MAXXY-1)
{
break;
}
if (i == EachTerrainSize_1 && TY >= MAXXY - 1)
{
break;
}
int X =(int)((float)TX * (float)EachTerrainSize *((float)MapWidth/(float)TerrainSize) + (float)i * ((float)MapWidth / (float)TerrainSize));
int Y = (int)((float)TY * (float)EachTerrainSize *((float)MapHeight / (float)TerrainSize) + (float)j * ((float)MapHeight / (float)TerrainSize));
vertices.Add(new Vector3(TX*EachTerrainSize+i, HeightMap.GetPixel(X,Y).grayscale * TerrainHeight, TY*EachTerrainSize+j));
if (i == 0 || j == 0) continue;
triangles.Add(EachTerrainSize_1 * i + j);
triangles.Add(EachTerrainSize_1 * i + j - 1);
triangles.Add(EachTerrainSize_1 * (i - 1) + j);
triangles.Add(EachTerrainSize_1 * (i - 1) + j);
triangles.Add(EachTerrainSize_1 * i + j - 1);
triangles.Add(EachTerrainSize_1 * (i - 1) + j - 1);
}
}
将Mesh保存
按照文件格式,将文件保存为预设体
if (createPrefab)
{
MeshToFile(terraints[ii].GetComponent<MeshFilter>(), terraints[ii].name+string.Format("_{0}",ii));
}
public static string MeshToString(MeshFilter mf)
{
Mesh m = mf.mesh;
Material[] mats = mf.GetComponent<Renderer>().sharedMaterials;
StringBuilder sb = new StringBuilder();
//写入文件名
sb.Append("g ").Append(mf.name).Append("\n");
//写入顶点,法线,uv,material,三角面
foreach (Vector3 v in m.vertices)
{
sb.Append(string.Format("v {0} {1} {2}\n", v.x, v.y, v.z));
}
sb.Append("\n");
foreach (Vector3 v in m.normals)
{
sb.Append(string.Format("vn {0} {1} {2}\n", v.x, v.y, v.z));
}
sb.Append("\n");
foreach (Vector3 v in m.uv)
{
sb.Append(string.Format("vt {0} {1}\n", v.x, v.y));
}
for (int material = 0; material < m.subMeshCount; material++)
{
sb.Append("\n");
sb.Append("usemtl ").Append(mats[material].name).Append("\n");
sb.Append("usemap ").Append(mats[material].name).Append("\n");
int[] triangles = m.GetTriangles(material);
for (int i = 0; i < triangles.Length; i += 3)
{
sb.Append(string.Format("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\n",
triangles[i] + 1, triangles[i + 1] + 1, triangles[i + 2] + 1));
}
}
return sb.ToString();
}
public static void MeshToFile(MeshFilter mf, string filename)
{
using (StreamWriter sw = new StreamWriter("Assets\\" + filename + ".obj"))
{
sw.Write(MeshToString(mf));
}
}
}
全部代码
该脚本文件可以直接使用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Text;
public class MeshTerrain : MonoBehaviour {
//选择自动切割之后,EachTerrainSize失效
public bool AutoCutting;
public Texture2D HeightMap;
[Range(-300f, 300f)]
public float TerrainHeight;
[Range(1,1024)]
public long TerrainSize;
[Range(16, 256)]
public int EachTerrainSize;
public Material TerrainMat;
public GameObject Origin;
private int MapHeight;
private int MapWidth;
public bool createPrefab;
void Start () {
if (TerrainMat == null || HeightMap == null)
{
Debug.Log("Missing necessary parameters!");
return;
}
try
{
float test = HeightMap.GetPixel(1, 1).grayscale;
}catch
{
Debug.Log("Check HeightMap's readable properties");
return;
}
if (EachTerrainSize>TerrainSize)
{
Debug.Log("EachTerrainSize can not bigger than TerrainSize");
return;
}
if(!AutoCutting&&TerrainSize%EachTerrainSize!=0)
{
Debug.Log("TerrainSize%EachTerrainSize!=0");
return;
}
MapHeight = HeightMap.height;
MapWidth = HeightMap.width;
if (Origin == null)
{
Origin = this.gameObject;
}
CreateTerrain();
}
private void CreateTerrain()
{
GameObject[] terraints;
int MAXXY;
int TX=0;
int TY = 0;
if (AutoCutting)
{
if (TerrainSize >= 256)
{
int i = 256;
long TT = TerrainSize * TerrainSize;
while (true)
{
if (TT % (i * i) == 0) break;
i--;
}
EachTerrainSize = i-1;
terraints = new GameObject[(int)Mathf.Pow(TerrainSize / EachTerrainSize, 2)];
MAXXY = (int)TerrainSize / i;
}
else
{
terraints = new GameObject[1];
EachTerrainSize = (int)TerrainSize;
MAXXY = 0;
}
}
else
{
MAXXY = (int)TerrainSize / EachTerrainSize;
if (MAXXY > 1) EachTerrainSize--;
terraints = new GameObject[MAXXY * MAXXY];
}
for(int ii=0;ii<terraints.Length;ii++)
{
terraints[ii] = new GameObject();
terraints[ii].name = string.Format("TerrainPieces_{0}", ii);
List<Vector3> vertices = new List<Vector3>();
List<int> triangles = new List<int>();
int EachTerrainSize_1 = EachTerrainSize == 256 ? 256 : EachTerrainSize + 1;
for (int i = 0; i < EachTerrainSize_1; i++)
{
for (int j = 0; j < EachTerrainSize_1; j++)
{
if (j == EachTerrainSize_1 && TX >= MAXXY-1)
{
break;
}
if (i == EachTerrainSize_1 && TY >= MAXXY - 1)
{
break;
}
int X =(int)((float)TX * (float)EachTerrainSize *((float)MapWidth/(float)TerrainSize) + (float)i * ((float)MapWidth / (float)TerrainSize));
int Y = (int)((float)TY * (float)EachTerrainSize *((float)MapHeight / (float)TerrainSize) + (float)j * ((float)MapHeight / (float)TerrainSize));
vertices.Add(new Vector3(TX*EachTerrainSize+i, HeightMap.GetPixel(X,Y).grayscale * TerrainHeight, TY*EachTerrainSize+j));
if (i == 0 || j == 0) continue;
triangles.Add(EachTerrainSize_1 * i + j);
triangles.Add(EachTerrainSize_1 * i + j - 1);
triangles.Add(EachTerrainSize_1 * (i - 1) + j);
triangles.Add(EachTerrainSize_1 * (i - 1) + j);
triangles.Add(EachTerrainSize_1 * i + j - 1);
triangles.Add(EachTerrainSize_1 * (i - 1) + j - 1);
}
}
Vector2[] uvs = new Vector2[vertices.Count];
for (var i = 0; i < uvs.Length; i++)
{
uvs[i] = new Vector2(vertices[i].x, vertices[i].z);
}
terraints[ii].transform.parent = Origin.transform;
terraints[ii].AddComponent<MeshFilter>();
MeshRenderer renderer = terraints[ii].AddComponent<MeshRenderer>();
renderer.sharedMaterial = TerrainMat;
Mesh groundMesh = new Mesh();
groundMesh.vertices = vertices.ToArray();
groundMesh.uv = uvs;
groundMesh.triangles = triangles.ToArray();
groundMesh.RecalculateNormals();//生成法线
terraints[ii].GetComponent<MeshFilter>().mesh = groundMesh;
terraints[ii].AddComponent<MeshCollider>();
if (createPrefab)
{
MeshToFile(terraints[ii].GetComponent<MeshFilter>(), terraints[ii].name+string.Format("_{0}",ii));
}
vertices.Clear();
triangles.Clear();
uvs = null;
TX++;
if (TX >= MAXXY)
{
TX = 0;
TY++;
}
}
}
public static string MeshToString(MeshFilter mf)
{
Mesh m = mf.mesh;
Material[] mats = mf.GetComponent<Renderer>().sharedMaterials;
StringBuilder sb = new StringBuilder();
//写入文件名
sb.Append("g ").Append(mf.name).Append("\n");
//写入顶点,法线,uv,material,三角面
foreach (Vector3 v in m.vertices)
{
sb.Append(string.Format("v {0} {1} {2}\n", v.x, v.y, v.z));
}
sb.Append("\n");
foreach (Vector3 v in m.normals)
{
sb.Append(string.Format("vn {0} {1} {2}\n", v.x, v.y, v.z));
}
sb.Append("\n");
foreach (Vector3 v in m.uv)
{
sb.Append(string.Format("vt {0} {1}\n", v.x, v.y));
}
for (int material = 0; material < m.subMeshCount; material++)
{
sb.Append("\n");
sb.Append("usemtl ").Append(mats[material].name).Append("\n");
sb.Append("usemap ").Append(mats[material].name).Append("\n");
int[] triangles = m.GetTriangles(material);
for (int i = 0; i < triangles.Length; i += 3)
{
sb.Append(string.Format("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\n",
triangles[i] + 1, triangles[i + 1] + 1, triangles[i + 2] + 1));
}
}
return sb.ToString();
}
public static void MeshToFile(MeshFilter mf, string filename)
{
using (StreamWriter sw = new StreamWriter("Assets\\" + filename + ".obj"))
{
sw.Write(MeshToString(mf));
}
}
}