using System; using System.Collections.Generic; using System.IO; using UnityEngine; using MindPowerSdk; public static class TerrainGenerator { /// /// 利用 MPMap 数据生成 Terrain,采用自定义材质实现贴图混合 /// /// 地图数据 /// 每块区域的瓦片数(例如 64) /// 基于自定义Shader(例如 Custom/TerrainUVBlendShader)的材质 /// 生成的 Terrain 父节点 /// /// public static List GenTerrain(MPMap map, int chunkSize, Material terrainMaterial, Transform parent, string entryMapName, bool isSmoothing = true) { List textureList = new List(); // 计算全局高度范围(归一化用) (float globalMin, float globalMax) = ComputeGlobalHeightRange(map); int xCnt = Mathf.CeilToInt((float)map.Width / chunkSize); 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; // 修复:正确计算边界,确保最后一块包含所有剩余像素 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}"); // 优化:使用合理的分辨率,避免过高的分辨率导致性能和兼容性问题 // 根据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; // 设置地形尺寸,确保与实际数据对应 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); // ---------------------- 贴图设置部分 ------------------------- // 生成贴图编号和遮罩贴图 // 注意:贴图采样使用原始的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); if (terrainMaterial== null) { Debug.LogError("地形材质未设置,请提供一个自定义材质或使用内置材质系统。"); return null; } // 创建材质实例,并设置贴图 Material matInstance = new Material(terrainMaterial); matInstance.SetTexture("_TexNo", texNo); matInstance.SetTexture("_MaskNo", maskNo); // 计算每个 tile 在 UV 空间内的尺寸 Vector2 mainTileScale = new Vector2(1.0f / texResX, 1.0f / texResY); matInstance.SetVector("_MainTileOffset", new Vector4(mainTileScale.x, mainTileScale.y, 0, 0)); // 设置 Terrain 尺寸参数 matInstance.SetVector("_TerrainSize", new Vector4(resX, resY, 0, 0)); matInstance.SetFloat("_Repeat", resX); // ---------------------- 贴图设置结束 ------------------------- // 生成 Terrain 游戏对象并设置位置 GameObject terrainGO = Terrain.CreateTerrainGameObject(terrainData); terrainGO.name = $"terrain_{i}_{j}"; terrainGO.transform.parent = terrainMap.transform; // 修复:正确的位置计算,确保地形块能正确拼接 // 地图坐标系转换到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(); 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] // 注意:为了确保地形连续性,我们需要包含边界像素 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 < 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; // 修复:确保高度图方向与贴图采样一致,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); float scaleY = (float)(resY - 1) / (newRes - 1); for (int z = 0; z < newRes; z++) { float origZ = z * scaleY; for (int x = 0; x < newRes; x++) { float origX = x * scaleX; resampled[z, x] = BicubicInterpolate(fullHeights, origX, origZ, resX, resY); } } if (isSmoothing) { // 后处理:高斯模糊平滑 float[,] smoothed = ApplyGaussianBlur(resampled, newRes, newRes); return smoothed; } else { return resampled; } } private static float BicubicInterpolate(float[,] data, float x, float z, int resX, int resY) { int xInt = Mathf.FloorToInt(x); int zInt = Mathf.FloorToInt(z); float s = x - xInt; float t = z - zInt; float[] arr = new float[4]; for (int m = -1; m <= 2; m++) { int sampleZ = Mathf.Clamp(zInt + m, 0, resY - 1); float p0 = data[sampleZ, Mathf.Clamp(xInt - 1, 0, resX - 1)]; float p1 = data[sampleZ, Mathf.Clamp(xInt, 0, resX - 1)]; float p2 = data[sampleZ, Mathf.Clamp(xInt + 1, 0, resX - 1)]; float p3 = data[sampleZ, Mathf.Clamp(xInt + 2, 0, resX - 1)]; arr[m + 1] = CubicInterpolate(p0, p1, p2, p3, s); } return CubicInterpolate(arr[0], arr[1], arr[2], arr[3], t); } private static float CubicInterpolate(float p0, float p1, float p2, float p3, float t) { float a0 = p3 - p2 - p0 + p1; float a1 = p0 - p1 - a0; float a2 = p2 - p0; float a3 = p1; return ((a0 * t + a1) * t + a2) * t + a3; } private static float[,] ApplyGaussianBlur(float[,] data, int width, int height) { float[,] result = new float[height, width]; int kernelSize = 3; int kernelRadius = kernelSize / 2; float[,] kernel = new float[,] { { 1f, 2f, 1f }, { 2f, 4f, 2f }, { 1f, 2f, 1f } }; float kernelSum = 16f; for (int z = 0; z < height; z++) { for (int x = 0; x < width; x++) { float sum = 0f; for (int kz = -kernelRadius; kz <= kernelRadius; kz++) { int sampleZ = Mathf.Clamp(z + kz, 0, height - 1); for (int kx = -kernelRadius; kx <= kernelRadius; kx++) { int sampleX = Mathf.Clamp(x + kx, 0, width - 1); float weight = kernel[kz + kernelRadius, kx + kernelRadius]; sum += data[sampleZ, sampleX] * weight; } } result[z, x] = sum / kernelSum; } } return result; } private static (float, float) ComputeGlobalHeightRange(MPMap map) { float min = float.MaxValue; float max = float.MinValue; for (int y = 0; y < map.Height; y++) { for (int x = 0; x < map.Width; x++) { float h = map.GetTile(x, y).Height; if (h < min) min = h; if (h > max) max = h; } } return (min, max); } }