修改了保存预制体的bug

This commit is contained in:
ZombieKitty 2025-06-16 17:35:44 +08:00
parent 5419e9b08f
commit a826fdaafd
2 changed files with 266 additions and 81 deletions

View File

@ -197,68 +197,142 @@ namespace MindPowerSdk.Editor
Debug.Log($"map size:({map.Width},{map.Height}) | section size:({map.SectionWidth},{map.SectionHeight})");
// 调用生成 Terrain 的方法,传入 mapName 作为标识
TerrainGenerator.GenTerrain(map, chunkSize, terrainMaterial, parent.transform, entry.mapName,
entry.isSmoothing);
var textureList = TerrainGenerator.GenTerrain(map, chunkSize, terrainMaterial, parent.transform,
entry.mapName,
entry.isSmoothing);
Debug.Log($"生成地图:{entry.mapName}\n数据路径{mapDataPath}\n资源路径{resourceDataPath}");
CreateMapObject(resourceDataPath, map, parent.transform);
// 创建场景对象并关联到地形
CreateMapObjectsWithTerrain(resourceDataPath, map, parent.transform, entry.mapName, chunkSize);
}
}
void CreateMapObject(string objPath, MPMap map, Transform parent)
/// <summary>
/// 创建场景对象并智能地关联到地形系统
/// </summary>
/// <param name="objPath">场景对象文件路径</param>
/// <param name="map">地图数据</param>
/// <param name="parent">父物体</param>
/// <param name="mapName">地图名称</param>
/// <param name="chunkSize">区块大小</param>
void CreateMapObjectsWithTerrain(string objPath, MPMap map, Transform parent, string mapName, int chunkSize)
{
// 加载场景对象数据
CSceneObjSet sceneObjSet = new CSceneObjSet();
sceneObjSet.LoadBin("Assets/Resources/sceneobjinfo.bin");
SceneObjFile sceneObjFile = new SceneObjFile();
sceneObjFile.Load(objPath);
string objName = Path.GetFileNameWithoutExtension(objPath);
GameObject root = new GameObject($"{objName}_SceneAssetsRoot");
root.transform.SetParent(parent);
// 找到地形根节点
Transform terrainRoot = parent.Find(mapName);
if (terrainRoot == null)
{
Debug.LogError($"找不到地形根节点: {mapName}");
return;
}
// 创建场景对象根节点
string objName = Path.GetFileNameWithoutExtension(objPath);
GameObject sceneRoot = new GameObject($"{objName}_SceneObjects");
sceneRoot.transform.SetParent(parent);
// 收集所有地形组件,用于场景对象关联
Dictionary<Vector2Int, Terrain> terrainMap = new Dictionary<Vector2Int, Terrain>();
CollectTerrainComponents(terrainRoot, terrainMap, chunkSize);
// 遍历所有场景对象
for (int y = 0; y < sceneObjFile.FileHeader.SectionCntY; y++)
{
for (int x = 0; x < sceneObjFile.FileHeader.SectionCntX; x++)
{
var list = sceneObjFile.objInfos[x, y];
if (list is null) continue;
var objList = sceneObjFile.objInfos[x, y];
if (objList == null) continue;
foreach (var sceneObjInfo in list)
foreach (var sceneObjInfo in objList)
{
if (sceneObjInfo.GetTypeId() == 1)
continue;
int id = sceneObjInfo.GetID();
if (sceneObjInfo.GetTypeId() == 1) continue; // 跳过特效物件
int id = sceneObjInfo.GetID();
CSceneObjInfo modeInfo = sceneObjSet.Get(id);
Vector3 pos = new Vector3(sceneObjInfo.X / 100f, sceneObjInfo.HeightOff / 100f,
sceneObjInfo.Y / 100f * -1f);
float mapHeight = map.GetHeight(pos.x, (sceneObjInfo.Y / 100f));
pos.y += mapHeight;
// 计算世界坐标,确保与地形坐标系一致
// 地图坐标系转换X不变Z轴翻转以匹配地形位置计算
float objX = sceneObjInfo.X / 100f;
float objY = sceneObjInfo.HeightOff / 100f;
float objZ = map.Height - (sceneObjInfo.Y / 100f); // 与地形位置计算保持一致
//Quaternion rot = Quaternion.AngleAxis(sceneObjInfo.YawAngle, Vector3.up);
string modePath = "Assets/Resources/Model/Scene/" +
Path.GetFileNameWithoutExtension(modeInfo.szDataName) + ".lmo.obj";
Object mode = AssetDatabase.LoadAssetAtPath<Object>(modePath);
if (mode)
Vector3 worldPos = new Vector3(objX, objY, objZ);
// 获取地形高度
float mapHeight = map.GetHeight(worldPos.x, sceneObjInfo.Y / 100f);
worldPos.y += mapHeight;
// 加载模型资源
string modelPath = "Assets/Resources/Model/Scene/" +
Path.GetFileNameWithoutExtension(modeInfo.szDataName) + ".lmo.obj";
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(modelPath);
if (prefab == null)
{
GameObject goModel = (GameObject)GameObject.Instantiate(mode);
goModel.transform.position = pos;
goModel.transform.localScale = new Vector3(1, 1, -1);
Vector3 e = goModel.transform.eulerAngles;
goModel.transform.rotation = Quaternion.Euler(e.x, sceneObjInfo.YawAngle, e.z);
goModel.name = $"[{id}]{mode.name}";
goModel.transform.SetParent(root.transform);
// Debug.Log(goModel);
Debug.LogError($"模型未找到: {modelPath}");
continue;
}
else
// 简化处理:统一作为场景对象处理,确保正确的地形贴合
GameObject instance = UnityEngine.Object.Instantiate(prefab);
instance.transform.position = worldPos;
instance.transform.localScale = new Vector3(1, 1, -1);
Vector3 euler = instance.transform.eulerAngles;
instance.transform.rotation = Quaternion.Euler(euler.x, sceneObjInfo.YawAngle, euler.z);
instance.name = $"[{id}]{prefab.name}";
instance.transform.SetParent(sceneRoot.transform);
// 添加地形关联信息(可选)
// 计算该对象属于哪个地形块
int terrainX = Mathf.FloorToInt(worldPos.x / chunkSize);
// 因为我们的地形Z位置是 map.Height - startY - resY所以需要反向计算
int originalMapY = (int)(map.Height - worldPos.z);
int terrainY = Mathf.FloorToInt(originalMapY / chunkSize);
Vector2Int terrainCoord = new Vector2Int(terrainX, terrainY);
if (terrainMap.TryGetValue(terrainCoord, out Terrain associatedTerrain))
{
Debug.LogError($"mode not found: {modePath}");
// 可以在这里添加对象与地形的关联逻辑
// 例如:给对象添加一个组件来记录关联的地形
Debug.Log($"对象 {instance.name} 关联到地形: {associatedTerrain.name}");
}
}
}
}
Debug.Log($"场景对象处理完成 - 总对象数: {sceneRoot.transform.childCount}");
}
/// <summary>
/// 收集所有地形组件
/// </summary>
void CollectTerrainComponents(Transform terrainRoot, Dictionary<Vector2Int, Terrain> terrainMap, int chunkSize)
{
foreach (Transform child in terrainRoot)
{
Terrain terrain = child.GetComponent<Terrain>();
if (terrain != null)
{
// 从地形名称解析坐标 (terrain_i_j)
string[] parts = child.name.Split('_');
if (parts.Length >= 3 &&
int.TryParse(parts[1], out int i) &&
int.TryParse(parts[2], out int j))
{
// 存储地形块的索引坐标和对应的Terrain组件
Vector2Int coord = new Vector2Int(j, i); // j是x索引, i是y索引
terrainMap[coord] = terrain;
Debug.Log($"收集地形块: {child.name} -> 坐标({j}, {i}) -> 世界位置{terrain.transform.position}");
}
}
}
}
@ -295,11 +369,12 @@ namespace MindPowerSdk.Editor
// 遍历每个地图生成的子对象
foreach (Transform mapTransform in parent.transform)
{
if (mapTransform.name.Contains("SceneAssetsRoot"))
if (mapTransform.name.Contains("SceneObjects") || mapTransform.name.Contains("SceneAssetsRoot"))
{
continue;
}
string folder = EditorUtility.OpenFolderPanel("选择预制体路径", "", "") + $"/{mapTransform.name}/";
string prefabPath = folder + mapTransform.name + ".prefab";
@ -311,6 +386,7 @@ namespace MindPowerSdk.Editor
}
string materialDir = Path.GetDirectoryName(prefabPath) + "/Material";
if (!Directory.Exists(materialDir))
{
Directory.CreateDirectory(materialDir);
@ -328,24 +404,28 @@ namespace MindPowerSdk.Editor
var terrain = child.GetComponent<Terrain>();
var terrainCollider = child.GetComponent<TerrainCollider>();
Debug.Log(child.name);
if (terrain != null)
{
materialDir = materialDir.Replace(@"\", "/");
materialDir = materialDir.Replace(Application.dataPath, "Assets");
materialDir = $"{materialDir}/{mapTransform.name}.mat";
var materialFiled = $"{materialDir}/{mapTransform.name}_{child.name}.mat";
var terrainDataPath = directory.Replace(@"\", "/");
terrainDataPath = terrainDataPath.Replace(Application.dataPath, "Assets");
terrainDataPath = $"{terrainDataPath}/{mapTransform.name}.asset";
if (File.Exists(materialDir))
AssetDatabase.DeleteAsset(materialDir);
terrainDataPath = $"{terrainDataPath}/{mapTransform.name}_{child.name}.asset";
if (File.Exists(materialFiled))
AssetDatabase.DeleteAsset(materialFiled);
if (File.Exists(terrainDataPath))
AssetDatabase.DeleteAsset(terrainDataPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
AssetDatabase.CreateAsset(terrain.materialTemplate, materialDir);
// 保存材质和地形数据
AssetDatabase.CreateAsset(terrain.materialTemplate, materialFiled);
AssetDatabase.CreateAsset(terrainCollider.terrainData, terrainDataPath);
// 确保地形数据的引用正确
terrain.terrainData = terrainCollider.terrainData;
// 把 terrain.materialTemplate 中所有的图片保存下来
foreach (var texturePropertyName in terrain.materialTemplate.GetTexturePropertyNames())
{
@ -355,7 +435,7 @@ namespace MindPowerSdk.Editor
Debug.Log($"保存材质贴图:{texturePropertyName} | {texture.name}");
string texturePath = texturesDir.Replace(@"\", "/");
texturePath = texturePath.Replace(Application.dataPath, "Assets");
var textName = texturePropertyName;
var textName = $"{texturePropertyName}_{child.name}";
texturePath = $"{texturePath}/{textName}.png";
// 检查纹理是否已经是项目中的资产
@ -377,7 +457,8 @@ namespace MindPowerSdk.Editor
byte[] bytes = texture2D.EncodeToPNG();
if (bytes != null)
{
File.WriteAllBytes(texturePath.Replace("Assets", Application.dataPath), bytes);
File.WriteAllBytes(texturePath.Replace("Assets", Application.dataPath),
bytes);
AssetDatabase.ImportAsset(texturePath);
Debug.Log($"导出材质贴图:{texturePath}");
}
@ -402,7 +483,7 @@ namespace MindPowerSdk.Editor
}
/// <summary>
/// 根据传入的类型名称(如“草”或“树”)进行模糊匹配,
/// 根据传入的类型名称(如"草"或"树")进行模糊匹配,
/// 遍历材质映射配置,返回第一个匹配的目标材质,未匹配返回 null。
/// </summary>
public Material GetMappedMaterial(string typeName)

View File

@ -13,8 +13,8 @@ public static class TerrainGenerator
/// <param name="chunkSize">每块区域的瓦片数(例如 64</param>
/// <param name="terrainMaterial">基于自定义Shader例如 Custom/TerrainUVBlendShader的材质</param>
/// <param name="parent">生成的 Terrain 父节点</param>
/// <param name="isSmoothing"></param>
/// <param name="entryMapName"></param>
/// <param name="isSmoothing"></param>
public static List<Texture> GenTerrain(MPMap map, int chunkSize, Material terrainMaterial, Transform parent,
string entryMapName, bool isSmoothing = true)
{
@ -26,95 +26,199 @@ public static class TerrainGenerator
int yCnt = Mathf.CeilToInt((float)map.Height / chunkSize);
var terrainMap = new GameObject(entryMapName);
terrainMap.transform.parent = parent;
Debug.Log($"开始生成地形: 地图尺寸({map.Width}, {map.Height}), 块大小={chunkSize}, 将生成{xCnt}x{yCnt}={xCnt*yCnt}个地形块");
for (int i = 0; i < yCnt; i++)
{
for (int j = 0; j < xCnt; j++)
{
int startX = j * chunkSize;
int startY = i * chunkSize;
// 为保证边界内不越界右侧和上侧减1
int endX = Mathf.Min(startX + chunkSize, map.Width - 1);
int endY = Mathf.Min(startY + chunkSize, map.Height - 1);
int resX = (endX - startX) + 1; // 横向瓦片数
int resY = (endY - startY) + 1; // 纵向瓦片数
// 修复:正确计算边界,确保最后一块包含所有剩余像素
int endX = Mathf.Min(startX + chunkSize, map.Width);
int endY = Mathf.Min(startY + chunkSize, map.Height);
int resX = endX - startX; // 横向瓦片数
int resY = endY - startY; // 纵向瓦片数
// 为了保证生成的 Terrain 与原始地图高度一致,并且位置正确,需要使用固定分辨率
int newRes = 4097;
Debug.Log($"地形块[{i},{j}]: startX={startX}, startY={startY}, endX={endX}, endY={endY}, resX={resX}, resY={resY}");
// 优化:使用合理的分辨率,避免过高的分辨率导致性能和兼容性问题
// 根据chunk大小动态计算分辨率确保每个tile至少有4个像素
int newRes = Mathf.NextPowerOfTwo(Mathf.Max(resX, resY) * 4) + 1;
newRes = Mathf.Clamp(newRes, 129, 1025); // 限制在合理范围内
// 创建 TerrainData设置高度图和混合图分辨率以及尺寸
TerrainData terrainData = new TerrainData();
terrainData.heightmapResolution = newRes;
terrainData.alphamapResolution = newRes;
terrainData.size = new Vector3((resX - 1), globalMax - globalMin, (resY - 1));
// 设置地形尺寸,确保与实际数据对应
float terrainWidth = resX;
float terrainLength = resY;
float terrainHeight = globalMax - globalMin;
terrainData.size = new Vector3(terrainWidth, terrainHeight, terrainLength);
// 设置地形的边界处理模式,确保能被正确拆分
#if UNITY_EDITOR
// 这些设置有助于地形被第三方工具正确处理
terrainData.wavingGrassStrength = 0.5f;
terrainData.wavingGrassAmount = 0.5f;
terrainData.wavingGrassSpeed = 0.5f;
terrainData.wavingGrassTint = Color.white;
#endif
// 生成高度图(归一化到 [0,1]
float[,] heights = GenerateHeightMap(map, startX, startY, resX, resY, newRes, globalMin, globalMax,
isSmoothing);
terrainData.SetHeights(0, 0, heights);
// ---------------------- 新增部分 -------------------------
// 生成贴图编号和遮罩贴图(调用你已有的 GenTxtNoTexture 方法)
Debug.LogWarning($"{startX} {startY} {chunkSize}");
(Texture2D texNo, Texture2D maskNo) = map.GenTxtNoTexture((short)startX, (short)startY, chunkSize);
// ---------------------- 贴图设置部分 -------------------------
// 生成贴图编号和遮罩贴图
// 注意贴图采样使用原始的resX, resY (不包含+1边界)
int texResX = endX - startX;
int texResY = endY - startY;
Debug.LogWarning($"生成地形块: startX={startX}, startY={startY}, texResX={texResX}, texResY={texResY}");
(Texture2D texNo, Texture2D maskNo) = map.GenTxtNoTexture((short)startX, (short)startY, texResX);
textureList.Add(texNo);
textureList.Add(maskNo);
// 设置每个 tile 在辅助 baked UV 贴图中希望的像素尺寸(建议不小于 4
int cellPixelSize = 7;
if (terrainMaterial== null)
{
Debug.LogError("地形材质未设置,请提供一个自定义材质或使用内置材质系统。");
return null;
}
// 创建材质实例,并设置贴图
Material matInstance = new Material(terrainMaterial);
matInstance.SetTexture("_TexNo", texNo);
matInstance.SetTexture("_MaskNo", maskNo);
// 计算每个 tile 在 UV 空间内的尺寸(通常为 1/chunkSize
Vector2 mainTileScale = new Vector2(1.0f / chunkSize, 1.0f / chunkSize);
// 计算每个 tile 在 UV 空间内的尺寸
Vector2 mainTileScale = new Vector2(1.0f / texResX, 1.0f / texResY);
matInstance.SetVector("_MainTileOffset", new Vector4(mainTileScale.x, mainTileScale.y, 0, 0));
// 设置 Terrain 尺寸参数Shader 内部可根据该参数计算全局 UV0X宽度Z高度
matInstance.SetVector("_TerrainSize", new Vector4((resX - 1), (resY - 1), 0, 0));
// 设置 Terrain 尺寸参数
matInstance.SetVector("_TerrainSize", new Vector4(resX, resY, 0, 0));
matInstance.SetFloat("_Repeat", resX);
// ---------------------- 贴图设置结束 -------------------------
matInstance.SetFloat("_Repeat", (resX - 1));
// ---------------------- 新增部分结束 -------------------------
// 生成 Terrain 游戏对象并设置位置Y 坐标偏移 globalMin
// 生成 Terrain 游戏对象并设置位置
GameObject terrainGO = Terrain.CreateTerrainGameObject(terrainData);
terrainGO.name = $"terrain_{i}_{j}";
terrainGO.transform.parent = terrainMap.transform;
terrainGO.transform.position = new Vector3(startX, globalMin, startY - chunkSize + 1);
Debug.Log($"terrainGO: {terrainGO.name} | pos: {terrainGO.transform.position}");
// 设置 Terrain 为自定义材质模式,并赋予生成的材质实例
// 修复:正确的位置计算,确保地形块能正确拼接
// 地图坐标系转换到Unity坐标系
// X轴保持不变Y轴是高度Z轴需要翻转
// 原地图的Y=0对应Unity的Z=map.HeightY=map.Height对应Unity的Z=0
float unityPosX = startX;
float unityPosY = globalMin;
float unityPosZ = map.Height - startY - resY;
Vector3 terrainPosition = new Vector3(unityPosX, unityPosY, unityPosZ);
terrainGO.transform.position = terrainPosition;
Debug.Log($"地形块 {terrainGO.name}: 位置={terrainPosition}, 尺寸={terrainData.size}, 高度图分辨率={newRes}");
// 设置 Terrain 组件
Terrain terrainComponent = terrainGO.GetComponent<Terrain>();
terrainComponent.materialType = Terrain.MaterialType.Custom;
terrainComponent.materialTemplate = matInstance;
TerrainCollider terrainCollider = terrainGO.GetComponent<TerrainCollider>();
// 设置地形渲染和性能参数
terrainComponent.heightmapPixelError = 5;
terrainComponent.basemapDistance = 1000;
terrainComponent.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On;
terrainComponent.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.BlendProbes;
// 确保TerrainCollider正确关联TerrainData
if (terrainCollider != null)
{
terrainCollider.terrainData = terrainData;
}
// 优化:为了更好的兼容性,同时支持自定义材质和内置材质系统
if (terrainMaterial != null)
{
terrainComponent.materialType = Terrain.MaterialType.Custom;
terrainComponent.materialTemplate = matInstance;
}
else
{
// 如果没有自定义材质,使用内置材质系统
terrainComponent.materialType = Terrain.MaterialType.BuiltInStandard;
}
// 设置地形的相邻关系,确保无缝拼接
SetupTerrainNeighbors(terrainMap.transform, i, j, yCnt, xCnt);
}
}
return textureList;
}
/// <summary>
/// 设置地形的相邻关系,确保地形块之间无缝拼接
/// </summary>
private static void SetupTerrainNeighbors(Transform terrainParent, int i, int j, int yCnt, int xCnt)
{
// 获取当前地形
Transform currentTerrain = terrainParent.Find($"terrain_{i}_{j}");
if (currentTerrain == null) return;
// 以下方法与原有代码一致
Terrain current = currentTerrain.GetComponent<Terrain>();
if (current == null) return;
// 设置相邻地形
Terrain left = GetTerrainAt(terrainParent, i, j - 1, yCnt, xCnt);
Terrain right = GetTerrainAt(terrainParent, i, j + 1, yCnt, xCnt);
Terrain top = GetTerrainAt(terrainParent, i + 1, j, yCnt, xCnt);
Terrain bottom = GetTerrainAt(terrainParent, i - 1, j, yCnt, xCnt);
current.SetNeighbors(left, top, right, bottom);
}
/// <summary>
/// 获取指定位置的地形组件
/// </summary>
private static Terrain GetTerrainAt(Transform terrainParent, int i, int j, int yCnt, int xCnt)
{
if (i < 0 || i >= yCnt || j < 0 || j >= xCnt) return null;
Transform terrainTransform = terrainParent.Find($"terrain_{i}_{j}");
return terrainTransform?.GetComponent<Terrain>();
}
private static float[,] GenerateHeightMap(MPMap map, int startX, int startY, int resX, int resY, int newRes,
float globalMin, float globalMax, bool isSmoothing)
{
// 采集原始高度数据,并归一化到 [0,1]
float[,] fullHeights = new float[resY, resX];
for (int z = 0; z < resY; z++)
// 注意:为了确保地形连续性,我们需要包含边界像素
int sampleResX = resX + 1;
int sampleResY = resY + 1;
float[,] fullHeights = new float[sampleResY, sampleResX];
for (int z = 0; z < sampleResY; z++)
{
for (int x = 0; x < resX; x++)
for (int x = 0; x < sampleResX; x++)
{
int worldX = startX + x;
int worldY = startY + z;
// 边界检查,确保不超出地图范围
worldX = Mathf.Clamp(worldX, 0, map.Width - 1);
worldY = Mathf.Clamp(worldY, 0, map.Height - 1);
MPTile tile = map.GetTile(worldX, worldY);
float norm = (globalMax - globalMin) > 0 ? (tile.Height - globalMin) / (globalMax - globalMin) : 0f;
fullHeights[resY - z - 1, x] = norm;
// 修复确保高度图方向与贴图采样一致Y轴需要翻转以匹配Unity的Terrain坐标系
fullHeights[sampleResY - z - 1, x] = norm;
}
}
// 更新采样参数
resX = sampleResX;
resY = sampleResY;
// 利用双三次插值生成新分辨率高度图
float[,] resampled = new float[newRes, newRes];
float scaleX = (float)(resX - 1) / (newRes - 1);