前言
最近面临要制作大地图,于是开始捣腾大地图,会将学习总结记录于此。
SomeRecord
(大地图)可参考吃鸡
场景:分为地表(地形) 静态物件 动态物件(包括人物等) 光照烘焙等等。
地形处理:
- 地形切割
模型文件格式组织的研究 OBJ FBX MESH Terrain(也是可以转成相应模型文件)
对地形进行切割,再分块加载
地形无非就是 模型文件 然后也就是一些Mesh数据 或者Unity3d自带的Terrain
切分就是在进行这些数据的操作
在手机游戏中,一般很少用到Terrain,因为性能限制,一般会用Terrain制作好场景文件,然后转成Obj文件 美术进行减面操作,重新展UV等 再导入,所以一般切分就是一个模型文件。
- 地形加载 流式加载 Or 块加载(九宫加载,视野加载)(SceneManager)
可否考虑使用JobSystem(Unity自带的线程系统) 异步加载
参考资料
切分
切分模型Mesh数据:
https://zhuanlan.zhihu.com/p/34487275
https://github.com/DavidArayan/ezy-slice
Unity3D教程:储存模型网格资料:
https://gameinstitute.qq.com/community/detail/112830
Unity3D分割地形Terrain:
https://blog.csdn.net/qq_14903317/article/details/69835578?locationNum=13&fps=1
Unity3d Mesh2Obj: https://blog.csdn.net/tom_221x/article/details/61920213
基于Unity3D的大地形研究(1):Cluster Async Load:
https://www.zhihu.com/people/maxwellgeng
https://zhuanlan.zhihu.com/p/51291312
Unity官方EditorSceneManager文档
加载
切分代码
using UnityEngine;
using UnityEditor;
using EzySlice;
using System.Collections.Generic;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;
using System.Text;
using System.IO;
public class SplitTool : EditorWindow
{
//1.切GameObject(Obj or Fbx) 保存Mesh 保存Prefab 保存Scene
//2.... 保存成Object 保存Prefab 保存场景
//3.切Terrain 其他。
static Vector3 itemSize = new Vector3(3, 0 ,3);
[MenuItem("Assets/GenSubMesh", true)]
static bool IsCan()
{
if (PrefabUtility.GetPrefabAssetType(Selection.activeObject) == PrefabAssetType.Regular)
{
return true;
}
return false;
}
[MenuItem("Assets/GenSubMesh", false)]
static void GenSubMesh()
{
GameObject curGO = Selection.activeObject as GameObject;
//curGO.SliceInstantiate(Vector3.zero, -Vector3.forward);
curGO.transform.position = Vector3.zero;
curGO.transform.eulerAngles = Vector3.zero;
Vector3 curScale = curGO.transform.lossyScale;
Mesh curObjMesh = curGO.GetComponent<MeshFilter>().sharedMesh;
//Debug.Log($"X:{curObjMesh.bounds.center.x},Y:{curObjMesh.bounds.center.y},Z:{curObjMesh.bounds.center.z}");
Debug.Log($"X:{curObjMesh.bounds.min.x},Y:{curObjMesh.bounds.min.y},Z:{curObjMesh.bounds.min.z}");
//Debug.Log($"X:{curObjMesh.bounds.max.x},Y:{curObjMesh.bounds.max.y},Z:{curObjMesh.bounds.max.z}");
int column = (int)Mathf.Ceil(curObjMesh.bounds.size.x*curScale.x/itemSize.x);
int row = (int)Mathf.Ceil(curObjMesh.bounds.size.z * curScale.z / itemSize.z);
float columnInterval = itemSize.x / curScale.x;
float rowInterval = itemSize.z/curScale.z;
if (row == 1 && column == 1)
{
Debug.Log("不需要切分");
return;
}
Debug.Log("Row:" + row + ",Column:" + column);
List<Mesh> rowList = new List<Mesh>();
List<GameObject> rowGoList = new List<GameObject>();
Mesh[,] meshes = new Mesh[row,column];
SlicedHull h=null;
GameObject nextObj = curGO;
for(int i=1;i<=column-1;i++)
{
h = nextObj.Slice(curObjMesh.bounds.min*curScale.x + new Vector3(i*itemSize.x, 0, 0), Vector3.right);
nextObj = h.CreateUpperHull(nextObj);
rowGoList.Add(h.CreateLowerHull(nextObj));
rowList.Add(h.lowerHull);
}
rowGoList.Add(h.CreateUpperHull(nextObj));
rowList.Add(h.upperHull);
for (int i = 0; i < rowList.Count; i++)
{
SlicedHull h2 = null;
GameObject curMeshObj = rowGoList[i];
for (int j = 1; j <= row - 1; j++)
{
h2 = curMeshObj.Slice(curObjMesh.bounds.max* curScale.z - new Vector3(0, 0, j * itemSize.z), -Vector3.forward);
curMeshObj = h2.CreateUpperHull(curMeshObj);
meshes[j - 1,i] = h2.lowerHull;
}
meshes[row - 1, i] = h2.upperHull;
}
for (int i = 0; i < meshes.GetLength(0); i++)
{
for(int j=0;j<meshes.GetLength(1);j++)
{
AssetDatabase.CreateAsset(meshes[i,j], $"Assets/SubMesh/RowList{i}_{j}.asset");
}
}
for (int i = 0; i < row; i++)
{
for(int j=0;j<column;j++)
{
GameObject gameObj = new GameObject("Terrain" + i+"_"+j);
gameObj.transform.position = curGO.transform.position;
gameObj.transform.localScale = curGO.transform.localScale;
gameObj.transform.eulerAngles = curGO.transform.eulerAngles;
gameObj.AddComponent<MeshFilter>().mesh = AssetDatabase.LoadAssetAtPath<Mesh>($"Assets/SubMesh/RowList{i}_{j}.asset");
MeshRenderer renderer = gameObj.AddComponent<MeshRenderer>();
renderer.sharedMaterial = curGO.GetComponent<MeshRenderer>().sharedMaterial;
PrefabUtility.SaveAsPrefabAssetAndConnect(gameObj, $"Assets/SubMesh/Terrain{i}_{j}.prefab", InteractionMode.AutomatedAction);
//Scene s = SceneManager.CreateScene("Terrain" + i + "_" + j);
//Scene s= EditorSceneManager.NewScene("Terrain" + i + "_" + j);
Scene s= EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
s.name = $"Terrain{i}_{j}";
GameObject terrain = new GameObject("Terrain");
Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>($"Assets/SubMesh/Terrain{i}_{j}.prefab")).transform.SetParent(terrain.transform);
EditorSceneManager.MarkSceneDirty(s);
EditorSceneManager.SaveScene(s, $"Assets/Scenes/Terrain{i}_{j}.unity");
//AssetDatabase.CreateAsset(s,);
}
}
}
static Vector3 GetGameObjectSize(GameObject gameObject)
{
Mesh mesh = gameObject.GetComponent<MeshFilter>().mesh;
Vector3 scale = gameObject.transform.lossyScale;
return new Vector3(mesh.bounds.size.x * scale.x, mesh.bounds.size.y * scale.y, mesh.bounds.size.z * scale.z);
}
[MenuItem("Assets/GenObj")]
static void Test()
{
GameObject curObject = Selection.activeObject as GameObject;
GenObj(curObject);
}
//暂时还没完善
static void GenObj(GameObject meshGo)
{
using (StreamWriter streamWriter = new StreamWriter(string.Format("{0}{1}.obj", Application.dataPath+"/Obj/", meshGo.name)))
{
streamWriter.Write(MeshToString(meshGo.GetComponent<MeshFilter>(), new Vector3(-1f, 1f, 1f)));
streamWriter.Close();
}
AssetDatabase.Refresh();
}
static string MeshToString(MeshFilter mf,Vector3 scale)
{
Mesh mesh = mf.sharedMesh;
Material[] sharedMaterials= mf.GetComponent<Renderer>().sharedMaterials;
Vector2 textureOffset = mf.GetComponent<Renderer>().sharedMaterial.GetTextureOffset("_MainTex");
Vector2 textureScale = mf.GetComponent<Renderer>().sharedMaterial.GetTextureScale("_MainTex");
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append($"mtllib design.mtl\ng {mf.name}\n");
Vector3[] vertices = mesh.vertices;
for(int i=0;i<vertices.Length;i++)
{
Vector3 vector = vertices[i];
stringBuilder.Append($"v {vector.x * scale.x} {vector.y * scale.y} {vector.z * scale.z}\n");
}
stringBuilder.Append("\n");
Dictionary<int, int> dic = new Dictionary<int, int>();
if(mesh.subMeshCount>1)
{
int[] triangles = mesh.GetTriangles(1);
for(int j=0;j<triangles.Length;j+=3)
{
if(!dic.ContainsKey(triangles[j]))
{
dic.Add(triangles[j], 1);
}
if(!dic.ContainsKey(triangles[j+1]))
{
dic.Add(triangles[j + 1], 1);
}
if(!dic.ContainsKey(triangles[j+2]))
{
dic.Add(triangles[j + 2], 1);
}
}
}
for(int num=0;num!=mesh.uv.Length;num++)
{
Vector2 vector2 = Vector2.Scale(mesh.uv[num], textureScale) + textureOffset;
if(dic.ContainsKey(num))
{
stringBuilder.Append($"vt {mesh.uv[num].x} {mesh.uv[num].y}\n");
}
else
{
stringBuilder.Append($"vt {vector2.x} {vector2.y}\n");
}
}
for (int k = 0; k < mesh.subMeshCount; k++)
{
stringBuilder.Append("\n");
if (k == 0)
{
stringBuilder.Append("usemtl ").Append("Material_design").Append("\n");
}
if (k == 1)
{
stringBuilder.Append("usemtl ").Append("Material_logo").Append("\n");
}
int[] triangles2 = mesh.GetTriangles(k);
for (int l = 0; l < triangles2.Length; l += 3)
{
stringBuilder.Append(string.Format("f {0}/{0} {1}/{1} {2}/{2}\n", triangles2[l] + 1, triangles2[l + 2] + 1, triangles2[l + 1] + 1));
}
}
return stringBuilder.ToString();
}
//切分Terrain
public static string TerrainSavePath = "Assets/SubTerrain/";
//分割大小
public static int SLICING_SIZE = 4;
public static int subTerrainSize = 250;
[MenuItem("Assets/SliceTerrain",true)]
private static bool IsTerrain()
{
Object o = Selection.activeObject;
TerrainData tD = Selection.activeObject as TerrainData;
if (tD == null)
{
return false;
}
else
{
return true;
}
}
//开始分割地形
//[MenuItem("Terrain/Slicing")]
[MenuItem("Assets/SliceTerrain", false)]
private static void Slicing()
{
TerrainData tD = Selection.activeObject as TerrainData;
//Terrain terrain = null;
//Terrain terrain = GameObject.FindObjectOfType<Terrain>();
if (tD == null)
{
Debug.LogError("不是地形数据!");
return;
}
if (Directory.Exists(TerrainSavePath)) Directory.Delete(TerrainSavePath, true);
Directory.CreateDirectory(TerrainSavePath);
//TerrainData terrainData = terrain.terrainData;
TerrainData terrainData = tD;
//这里我分割的宽和长度是一样的.这里求出循环次数,TerrainLoad.SIZE要生成的地形宽度,长度相同
//高度地图的分辨率只能是2的N次幂加1,所以SLICING_SIZE必须为2的N次幂
SLICING_SIZE = (int)terrainData.size.x / subTerrainSize;
Vector3 oldSize = terrainData.size;
//得到新地图分辨率
int newHeightmapResolution = (terrainData.heightmapResolution - 1) / SLICING_SIZE;
int newAlphamapResolution = terrainData.alphamapResolution / SLICING_SIZE;
int newbaseMapResolution = terrainData.baseMapResolution / SLICING_SIZE;
TerrainLayer[] splatProtos = terrainData.terrainLayers;
//循环宽和长,生成小块地形
for (int x = 0; x < SLICING_SIZE; ++x)
{
for (int y = 0; y < SLICING_SIZE; ++y)
{
//创建资源
TerrainData newData = new TerrainData();
string terrainName = TerrainSavePath + terrainData.name + y + "_" + x + ".asset";
AssetDatabase.CreateAsset(newData, TerrainSavePath + terrainData.name + y + "_" + x + ".asset");
EditorUtility.DisplayProgressBar("正在分割地形", terrainName, (float)(x * SLICING_SIZE + y) / (float)(SLICING_SIZE * SLICING_SIZE));
//设置分辨率参数
newData.heightmapResolution = (terrainData.heightmapResolution - 1) / SLICING_SIZE;
newData.alphamapResolution = terrainData.alphamapResolution / SLICING_SIZE;
newData.baseMapResolution = terrainData.baseMapResolution / SLICING_SIZE;
//设置大小
newData.size = new Vector3(oldSize.x / SLICING_SIZE, oldSize.y, oldSize.z / SLICING_SIZE);
//设置地形原型
TerrainLayer[] newSplats = new TerrainLayer[splatProtos.Length];
for (int i = 0; i < splatProtos.Length; ++i)
{
newSplats[i] = new TerrainLayer();
newSplats[i].diffuseTexture = splatProtos[i].diffuseTexture;
newSplats[i].tileSize = splatProtos[i].tileSize;
float offsetX = (newData.size.x * x) % splatProtos[i].tileSize.x + splatProtos[i].tileOffset.x;
float offsetY = (newData.size.z * y) % splatProtos[i].tileSize.y + splatProtos[i].tileOffset.y;
newSplats[i].tileOffset = new Vector2(offsetX, offsetY);
}
newData.terrainLayers = newSplats;
//设置混合贴图
float[,,] alphamap = new float[newAlphamapResolution, newAlphamapResolution, splatProtos.Length];
alphamap = terrainData.GetAlphamaps(x * newData.alphamapWidth, y * newData.alphamapHeight, newData.alphamapWidth, newData.alphamapHeight);
newData.SetAlphamaps(0, 0, alphamap);
//设置高度
int xBase = terrainData.heightmapWidth / SLICING_SIZE;
int yBase = terrainData.heightmapHeight / SLICING_SIZE;
float[,] height = terrainData.GetHeights(xBase * x, yBase * y, xBase + 1, yBase + 1);
newData.SetHeights(0, 0, height);
}
}
EditorUtility.ClearProgressBar();
}
}