2025-04-03 02:30:16 +08:00
|
|
|
|
using System;
|
2025-05-09 17:39:24 +08:00
|
|
|
|
using System.Collections.Generic;
|
2025-04-03 02:30:16 +08:00
|
|
|
|
using System.IO;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using MindPowerSdk;
|
|
|
|
|
|
|
|
|
|
public static class TerrainGenerator
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 利用 MPMap 数据生成 Terrain,采用自定义材质实现贴图混合
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="map">地图数据</param>
|
|
|
|
|
/// <param name="chunkSize">每块区域的瓦片数(例如 64)</param>
|
|
|
|
|
/// <param name="terrainMaterial">基于自定义Shader(例如 Custom/TerrainUVBlendShader)的材质</param>
|
|
|
|
|
/// <param name="parent">生成的 Terrain 父节点</param>
|
|
|
|
|
/// <param name="entryMapName"></param>
|
2025-06-16 17:35:44 +08:00
|
|
|
|
/// <param name="isSmoothing"></param>
|
2025-05-09 17:39:24 +08:00
|
|
|
|
public static List<Texture> GenTerrain(MPMap map, int chunkSize, Material terrainMaterial, Transform parent,
|
|
|
|
|
string entryMapName, bool isSmoothing = true)
|
2025-04-03 02:30:16 +08:00
|
|
|
|
{
|
2025-05-09 17:39:24 +08:00
|
|
|
|
List<Texture> textureList = new List<Texture>();
|
2025-04-03 02:30:16 +08:00
|
|
|
|
// 计算全局高度范围(归一化用)
|
|
|
|
|
(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;
|
2025-06-16 17:35:44 +08:00
|
|
|
|
|
|
|
|
|
Debug.Log($"开始生成地形: 地图尺寸({map.Width}, {map.Height}), 块大小={chunkSize}, 将生成{xCnt}x{yCnt}={xCnt*yCnt}个地形块");
|
|
|
|
|
|
2025-04-03 02:30:16 +08:00
|
|
|
|
for (int i = 0; i < yCnt; i++)
|
|
|
|
|
{
|
|
|
|
|
for (int j = 0; j < xCnt; j++)
|
|
|
|
|
{
|
|
|
|
|
int startX = j * chunkSize;
|
|
|
|
|
int startY = i * chunkSize;
|
2025-06-16 17:35:44 +08:00
|
|
|
|
// 修复:正确计算边界,确保最后一块包含所有剩余像素
|
|
|
|
|
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); // 限制在合理范围内
|
2025-04-03 02:30:16 +08:00
|
|
|
|
|
|
|
|
|
// 创建 TerrainData,设置高度图和混合图分辨率以及尺寸
|
|
|
|
|
TerrainData terrainData = new TerrainData();
|
|
|
|
|
terrainData.heightmapResolution = newRes;
|
|
|
|
|
terrainData.alphamapResolution = newRes;
|
2025-06-16 17:35:44 +08:00
|
|
|
|
|
|
|
|
|
// 设置地形尺寸,确保与实际数据对应
|
|
|
|
|
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
|
2025-04-03 02:30:16 +08:00
|
|
|
|
|
|
|
|
|
// 生成高度图(归一化到 [0,1])
|
|
|
|
|
float[,] heights = GenerateHeightMap(map, startX, startY, resX, resY, newRes, globalMin, globalMax,
|
|
|
|
|
isSmoothing);
|
|
|
|
|
terrainData.SetHeights(0, 0, heights);
|
|
|
|
|
|
2025-06-16 17:35:44 +08:00
|
|
|
|
// ---------------------- 贴图设置部分 -------------------------
|
|
|
|
|
// 生成贴图编号和遮罩贴图
|
|
|
|
|
// 注意:贴图采样使用原始的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);
|
2025-04-03 02:30:16 +08:00
|
|
|
|
|
2025-05-09 17:39:24 +08:00
|
|
|
|
textureList.Add(texNo);
|
|
|
|
|
textureList.Add(maskNo);
|
2025-06-16 17:35:44 +08:00
|
|
|
|
if (terrainMaterial== null)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError("地形材质未设置,请提供一个自定义材质或使用内置材质系统。");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-04-03 02:30:16 +08:00
|
|
|
|
// 创建材质实例,并设置贴图
|
|
|
|
|
Material matInstance = new Material(terrainMaterial);
|
|
|
|
|
matInstance.SetTexture("_TexNo", texNo);
|
|
|
|
|
matInstance.SetTexture("_MaskNo", maskNo);
|
|
|
|
|
|
2025-06-16 17:35:44 +08:00
|
|
|
|
// 计算每个 tile 在 UV 空间内的尺寸
|
|
|
|
|
Vector2 mainTileScale = new Vector2(1.0f / texResX, 1.0f / texResY);
|
2025-04-03 02:30:16 +08:00
|
|
|
|
matInstance.SetVector("_MainTileOffset", new Vector4(mainTileScale.x, mainTileScale.y, 0, 0));
|
|
|
|
|
|
2025-06-16 17:35:44 +08:00
|
|
|
|
// 设置 Terrain 尺寸参数
|
|
|
|
|
matInstance.SetVector("_TerrainSize", new Vector4(resX, resY, 0, 0));
|
|
|
|
|
matInstance.SetFloat("_Repeat", resX);
|
|
|
|
|
// ---------------------- 贴图设置结束 -------------------------
|
2025-04-03 02:30:16 +08:00
|
|
|
|
|
2025-06-16 17:35:44 +08:00
|
|
|
|
// 生成 Terrain 游戏对象并设置位置
|
2025-04-03 02:30:16 +08:00
|
|
|
|
GameObject terrainGO = Terrain.CreateTerrainGameObject(terrainData);
|
|
|
|
|
terrainGO.name = $"terrain_{i}_{j}";
|
|
|
|
|
terrainGO.transform.parent = terrainMap.transform;
|
2025-06-16 17:35:44 +08:00
|
|
|
|
|
|
|
|
|
// 修复:正确的位置计算,确保地形块能正确拼接
|
|
|
|
|
// 地图坐标系转换到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 组件
|
2025-04-03 02:30:16 +08:00
|
|
|
|
Terrain terrainComponent = terrainGO.GetComponent<Terrain>();
|
2025-06-16 17:35:44 +08:00
|
|
|
|
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);
|
2025-04-03 02:30:16 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-09 17:39:24 +08:00
|
|
|
|
|
|
|
|
|
return textureList;
|
2025-04-03 02:30:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-16 17:35:44 +08:00
|
|
|
|
/// <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);
|
|
|
|
|
}
|
2025-04-03 02:30:16 +08:00
|
|
|
|
|
2025-06-16 17:35:44 +08:00
|
|
|
|
/// <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>();
|
|
|
|
|
}
|
2025-04-03 02:30:16 +08:00
|
|
|
|
|
|
|
|
|
private static float[,] GenerateHeightMap(MPMap map, int startX, int startY, int resX, int resY, int newRes,
|
|
|
|
|
float globalMin, float globalMax, bool isSmoothing)
|
|
|
|
|
{
|
|
|
|
|
// 采集原始高度数据,并归一化到 [0,1]
|
2025-06-16 17:35:44 +08:00
|
|
|
|
// 注意:为了确保地形连续性,我们需要包含边界像素
|
|
|
|
|
int sampleResX = resX + 1;
|
|
|
|
|
int sampleResY = resY + 1;
|
|
|
|
|
float[,] fullHeights = new float[sampleResY, sampleResX];
|
|
|
|
|
|
|
|
|
|
for (int z = 0; z < sampleResY; z++)
|
2025-04-03 02:30:16 +08:00
|
|
|
|
{
|
2025-06-16 17:35:44 +08:00
|
|
|
|
for (int x = 0; x < sampleResX; x++)
|
2025-04-03 02:30:16 +08:00
|
|
|
|
{
|
|
|
|
|
int worldX = startX + x;
|
|
|
|
|
int worldY = startY + z;
|
2025-06-16 17:35:44 +08:00
|
|
|
|
|
|
|
|
|
// 边界检查,确保不超出地图范围
|
|
|
|
|
worldX = Mathf.Clamp(worldX, 0, map.Width - 1);
|
|
|
|
|
worldY = Mathf.Clamp(worldY, 0, map.Height - 1);
|
|
|
|
|
|
2025-04-03 02:30:16 +08:00
|
|
|
|
MPTile tile = map.GetTile(worldX, worldY);
|
|
|
|
|
float norm = (globalMax - globalMin) > 0 ? (tile.Height - globalMin) / (globalMax - globalMin) : 0f;
|
2025-06-16 17:35:44 +08:00
|
|
|
|
// 修复:确保高度图方向与贴图采样一致,Y轴需要翻转以匹配Unity的Terrain坐标系
|
|
|
|
|
fullHeights[sampleResY - z - 1, x] = norm;
|
2025-04-03 02:30:16 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-16 17:35:44 +08:00
|
|
|
|
|
|
|
|
|
// 更新采样参数
|
|
|
|
|
resX = sampleResX;
|
|
|
|
|
resY = sampleResY;
|
2025-04-03 02:30:16 +08:00
|
|
|
|
|
|
|
|
|
// 利用双三次插值生成新分辨率高度图
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|