From a826fdaafda0c7d40ec1925220e77244806bd565 Mon Sep 17 00:00:00 2001 From: ZombieKitty Date: Mon, 16 Jun 2025 17:35:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E9=A2=84=E5=88=B6=E4=BD=93=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditorWindow/MapDataEditorWindow.cs | 171 ++++++++++++----- .../EditorWindow/TerrainGenerator.cs | 176 ++++++++++++++---- 2 files changed, 266 insertions(+), 81 deletions(-) diff --git a/Assets/MindPowerSdk/EditorWindow/MapDataEditorWindow.cs b/Assets/MindPowerSdk/EditorWindow/MapDataEditorWindow.cs index 5fc0f3a..f156546 100644 --- a/Assets/MindPowerSdk/EditorWindow/MapDataEditorWindow.cs +++ b/Assets/MindPowerSdk/EditorWindow/MapDataEditorWindow.cs @@ -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) + /// + /// 创建场景对象并智能地关联到地形系统 + /// + /// 场景对象文件路径 + /// 地图数据 + /// 父物体 + /// 地图名称 + /// 区块大小 + 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 terrainMap = new Dictionary(); + 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(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(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}"); + } + + + /// + /// 收集所有地形组件 + /// + void CollectTerrainComponents(Transform terrainRoot, Dictionary terrainMap, int chunkSize) + { + foreach (Transform child in terrainRoot) + { + Terrain terrain = child.GetComponent(); + 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(); var terrainCollider = child.GetComponent(); - + 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,9 +435,9 @@ 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"; - + // 检查纹理是否已经是项目中的资产 string originalPath = AssetDatabase.GetAssetPath(texture); if (!string.IsNullOrEmpty(originalPath)) @@ -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 } /// - /// 根据传入的类型名称(如“草”或“树”)进行模糊匹配, + /// 根据传入的类型名称(如"草"或"树")进行模糊匹配, /// 遍历材质映射配置,返回第一个匹配的目标材质,未匹配返回 null。 /// public Material GetMappedMaterial(string typeName) diff --git a/Assets/MindPowerSdk/EditorWindow/TerrainGenerator.cs b/Assets/MindPowerSdk/EditorWindow/TerrainGenerator.cs index 34e48eb..a5c5866 100644 --- a/Assets/MindPowerSdk/EditorWindow/TerrainGenerator.cs +++ b/Assets/MindPowerSdk/EditorWindow/TerrainGenerator.cs @@ -13,8 +13,8 @@ public static class TerrainGenerator /// 每块区域的瓦片数(例如 64) /// 基于自定义Shader(例如 Custom/TerrainUVBlendShader)的材质 /// 生成的 Terrain 父节点 - /// /// + /// public static List GenTerrain(MPMap map, int chunkSize, Material terrainMaterial, Transform parent, string entryMapName, bool isSmoothing = true) { @@ -26,94 +26,198 @@ 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; // 纵向瓦片数 + + Debug.Log($"地形块[{i},{j}]: startX={startX}, startY={startY}, endX={endX}, endY={endY}, resX={resX}, resY={resY}"); - // 为了保证生成的 Terrain 与原始地图高度一致,并且位置正确,需要使用固定分辨率 - int newRes = 4097; + // 优化:使用合理的分辨率,避免过高的分辨率导致性能和兼容性问题 + // 根据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 内部可根据该参数计算全局 UV0(X:宽度,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.Height,Y=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(); - terrainComponent.materialType = Terrain.MaterialType.Custom; - terrainComponent.materialTemplate = matInstance; + TerrainCollider terrainCollider = terrainGO.GetComponent(); + + // 设置地形渲染和性能参数 + 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; } + /// + /// 设置地形的相邻关系,确保地形块之间无缝拼接 + /// + 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(); + 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); + } + + /// + /// 获取指定位置的地形组件 + /// + 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(); + } 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];