KopMap/Assets/MindPowerSdk/EditorWindow/TerrainGenerator.cs

330 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
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>
/// <param name="isSmoothing"></param>
public static List<Texture> GenTerrain(MPMap map, int chunkSize, Material terrainMaterial, Transform parent,
string entryMapName, bool isSmoothing = true)
{
List<Texture> textureList = new List<Texture>();
// 计算全局高度范围(归一化用)
(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.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>();
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]
// 注意:为了确保地形连续性,我们需要包含边界像素
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);
}
}