
1. 项目概述从噪声到山脉的旅程如果你在Unity里做过开放世界或者沙盒游戏肯定绕不开“地形生成”这个坎。手动在Unity Terrain上刷山造河对于一个小场景还行一旦地图尺寸上去了那工作量简直让人头皮发麻。更别提那些需要无限生成、每次开局都不同的Roguelike或生存建造游戏了。这时候程序化生成就成了救命稻草而Perlin噪声就是这根稻草里最坚韧的那几股纤维之一。我第一次接触程序化地形是为了做一个类似《我的世界》但风格更写实的探索demo。当时的需求很简单给我一块足够大、看起来自然、每次运行都不一样的地形。在网上搜了一圈Perlin噪声出现的频率最高。但真正动手时才发现从理解那一行行数学公式到在Unity里让一片平坦的Mesh隆起成连绵的山脉中间隔着无数个坑。比如为什么直接用Mathf.PerlinNoise生成的地形像是一坨坨规整的“馒头山”怎么控制山脉的走向和湖泊的分布如何让地形既有大尺度的山峦又有小尺度的碎石细节这篇文章就是我踩过这些坑之后整理出的一份从零到一的完全指南。我们不只讲Perlin噪声的原理更会一步步带你用C#在Unity里实现它并最终生成一个包含高度、纹理、甚至简单生态分布的完整3D自然景观。你会发现核心思路并不复杂关键在于对参数的理解和一系列“组合拳”式的后期处理。无论你是想为你的独立游戏快速搭建一个原型世界还是单纯对程序化生成技术感兴趣这篇指南都能给你一套可直接运行、随意魔改的代码和清晰的实现逻辑。2. 核心思路拆解为什么是Perlin噪声在动手写代码之前我们必须搞清楚面对“生成自然地形”这个问题我们手里有哪些牌以及为什么Perlin噪声是张好牌。2.1 自然地形的特征与噪声的选择自然地形比如山脉有几个关键视觉特征连续性山体是平滑过渡的不会突然出现悬崖峭壁式的直角、自相似性一座大山上有小山丘小山丘上又有更小的起伏、以及一定的随机性没有两座完全一样的山。传统的纯随机函数Random.Range生成的值彼此独立毫无关联得到的是一片尖锐、嘈杂的“白噪声”完全不符合要求。我们需要的是梯度噪声。这类噪声函数的关键在于给定一个坐标比如(x, y)它返回的噪声值不是完全随机的而是会考虑该点周围点的值通过平滑插值来计算最终结果。这使得噪声值在空间中连续变化形成了我们需要的平滑过渡。Perlin噪声正是梯度噪声的经典实现。它的聪明之处在于“晶格”和“梯度”的设计。想象在整个二维平面上打上均匀的网格晶格每个网格的顶点晶格点都随机分配一个梯度向量一个方向。对于平面上的任意一点P我们找到它所在的网格单元以及包围它的四个晶格点。计算从P点到这四个晶格点的向量并分别与各自晶格点的梯度向量做点积得到四个“影响力”值。最后根据P点在单元内的相对位置对这四个影响力值进行平滑插值得到P点最终的噪声值。这个过程保证了在晶格内部变化平滑在晶格边界上由于插值函数的设计通常使用fade函数也叫平滑函数也能保证一阶甚至二阶导数连续从而消除了接缝感。注意Unity内置的Mathf.PerlinNoise函数就是一个二维Perlin噪声的实现。对于绝大多数应用场景直接使用它完全足够性能也经过优化。我们自己实现一遍主要是为了深入理解原理方便后续的定制和优化比如实现三维或更高维度的噪声。2.2 从单一噪声到复杂地形分形与叠加单一的Perlin噪声生成的地形过于“光滑”和单调缺乏真实地形那种多尺度的细节。这就是分形布朗运动Fractal Brownian Motion FBM或者说分形噪声的概念登场的时候。其核心思想是“叠加”。我们用不同的“尺度”和“强度”去采样多次Perlin噪声然后把结果加起来。频率可以理解为“细节的缩放程度”。频率越高噪声变化越快表现出的细节尺度越小。在叠加时我们通常让后一层噪声的频率是前一层的倍数比如2倍这个倍数就是lacunarity间隙度。振幅可以理解为“这一层噪声对最终结果的影响强度”。在叠加时我们通常让后一层噪声的振幅是前一层的一个比例比如一半这个比例就是persistence持久度。倍频叠加的层数用octaves表示。octaves越大地形的细节越丰富但计算量也线性增加。通过调整octaves、lacunarity和persistence这三个参数我们可以模拟出从宏伟高原到细碎丘陵的各种地形特征。大振幅、低频率的噪声层塑造大陆板块和主要山脉走向小振幅、高频率的噪声层则添加岩石纹理和微小起伏。2.3 整体技术路线图我们的实现将遵循一个清晰的管道噪声生成层核心是Perlin噪声函数。我们将实现自己的2D Perlin噪声生成器并在此基础上构建支持多倍频FBM的噪声图生成函数。高度图处理层将生成的噪声图值域通常在[-1, 1]或[0, 1]映射为地形网格Mesh每个顶点的高度Y坐标。这里会引入AnimationCurve来控制高度分布曲线例如让低海拔区域更平坦高海拔区域更陡峭以及heightMultiplier来控制整体地形起伏的强度。网格构建层根据处理后的高度图数据动态生成一个平面网格。每个像素的噪声值对应一个顶点的高度。纹理着色层可选但推荐根据高度、坡度或其他衍生噪声图如湿度、温度为地形Mesh贴上相应的纹理实现雪线、草地、沙地等视觉效果。后期处理与优化进阶探讨如何通过侵蚀算法、域扭曲等技术让地形更自然以及如何优化大规模地形的性能如LOD、分块加载。3. 手把手实现从Perlin噪声函数到3D地形Mesh理论说再多不如一行代码。我们抛开Unity内置函数从头实现一个Perlin噪声生成器并完成整个地形生成流程。3.1 实现经典的2D Perlin噪声我们参考Ken Perlin在2002年SIGGRAPH论文《Improving Noise》中的改进算法。创建一个C#脚本PerlinNoiseGenerator.cs。using System.Collections.Generic; using UnityEngine; public static class PerlinNoiseGenerator { // 经典的256排列表用于哈希生成可重复的随机梯度 private static readonly int[] _permutation { 151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37, 240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177, 33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146, 158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25, 63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100, 109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206, 59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153, 101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218, 246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107, 49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205, 93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 }; private static readonly int[] _p; static PerlinNoiseGenerator() { // 将排列表复制一遍到512长度避免索引时取模运算 _p new int[512]; for (int i 0; i 512; i) { _p[i] _permutation[i 255]; } } // 平滑函数6t^5 - 15t^4 10t^3保证在t0和t1时一阶和二阶导数为0 private static float Fade(float t) { return t * t * t * (t * (t * 6 - 15) 10); } // 线性插值 private static float Lerp(float a, float b, float t) { return a (b - a) * t; } // 梯度函数根据哈希值返回点(dx, dy)与预设梯度向量的点积 // 这里预设梯度向量为(1,1), (1,-1), (-1,1), (-1,-1)简化计算 private static float Grad(int hash, float dx, float dy) { int h hash 7; // 取低3位得到0-7 float u h 4 ? dx : dy; // 根据hash决定用dx还是dy作为第一个分量 float v h 4 ? dy : dx; // 另一个作为第二个分量 return ((h 1) 0 ? u : -u) ((h 2) 0 ? v : -v); // 根据hash决定正负号 } // 核心的2D Perlin噪声函数输入坐标(x, y)返回范围大约在[-1, 1]的噪声值 public static float Noise(float x, float y) { // 1. 确定输入点所在的晶格单元 int xi (int)Mathf.Floor(x) 255; int yi (int)Mathf.Floor(y) 255; // 2. 计算点在单元内的相对位置 (范围[0,1)) float xf x - Mathf.Floor(x); float yf y - Mathf.Floor(y); // 3. 应用平滑函数 float u Fade(xf); float v Fade(yf); // 4. 哈希得到四个角点的梯度索引 int aa _p[_p[xi] yi]; int ab _p[_p[xi] yi 1]; int ba _p[_p[xi 1] yi]; int bb _p[_p[xi 1] yi 1]; // 5. 计算四个角点的梯度贡献值并进行双线性插值 float x1 Lerp(Grad(aa, xf, yf), Grad(ba, xf - 1, yf), u); float x2 Lerp(Grad(ab, xf, yf - 1), Grad(bb, xf - 1, yf - 1), u); return Lerp(x1, x2, v); } }关键点解析与避坑排列表_permutation数组是Perlin噪声的“随机种子”。它包含0-255数字的一个固定排列确保了对于相同的输入(x, y)输出总是相同的可哈希性。复制到512长度是为了在索引_p[_p[xi1] yi1]时不会越界这是一种用空间换时间的优化。梯度函数简化Grad函数看起来有点绕它实质上是将梯度向量限定在(±1, ±1)从而将点积运算简化为加减法极大提升了计算效率。这是经典Perlin噪声实现的一个技巧。输出范围这个实现输出的噪声值大致在[-1, 1]之间但并非严格。通常我们会将其归一化到[0, 1]以便使用。3.2 构建分形噪声FBM生成器单一噪声太光滑我们需要叠加。创建一个NoiseMapGenerator.cs。using UnityEngine; public static class NoiseMapGenerator { public static float[,] GenerateNoiseMap(int mapWidth, int mapHeight, int seed, float scale, int octaves, float persistence, float lacunarity, Vector2 offset) { float[,] noiseMap new float[mapWidth, mapHeight]; // 使用种子初始化随机偏移确保可重复性 System.Random prng new System.Random(seed); Vector2[] octaveOffsets new Vector2[octaves]; for (int i 0; i octaves; i) { float offsetX prng.Next(-100000, 100000) offset.x; float offsetY prng.Next(-100000, 100000) offset.y; octaveOffsets[i] new Vector2(offsetX, offsetY); } if (scale 0) scale 0.0001f; // 防止除零错误 float maxNoiseHeight float.MinValue; float minNoiseHeight float.MaxValue; // 为了缩放时从中心点开始而不是左上角 float halfWidth mapWidth / 2f; float halfHeight mapHeight / 2f; for (int y 0; y mapHeight; y) { for (int x 0; x mapWidth; x) { float amplitude 1; float frequency 1; float noiseHeight 0; // 叠加多个倍频的噪声 for (int i 0; i octaves; i) { // 计算当前倍频下的采样坐标 float sampleX (x - halfWidth) / scale * frequency octaveOffsets[i].x; float sampleY (y - halfHeight) / scale * frequency octaveOffsets[i].y; // 获取Perlin噪声值*2-1是为了将范围从[0,1]映射到[-1,1]使叠加有正负 float perlinValue PerlinNoiseGenerator.Noise(sampleX, sampleY) * 2 - 1; noiseHeight perlinValue * amplitude; amplitude * persistence; // 振幅递减 frequency * lacunarity; // 频率递增 } // 记录全局最大最小值用于后续归一化 if (noiseHeight maxNoiseHeight) maxNoiseHeight noiseHeight; if (noiseHeight minNoiseHeight) minNoiseHeight noiseHeight; noiseMap[x, y] noiseHeight; } } // 归一化将噪声图的所有值线性映射到[0, 1]区间 for (int y 0; y mapHeight; y) { for (int x 0; x mapWidth; x) { noiseMap[x, y] Mathf.InverseLerp(minNoiseHeight, maxNoiseHeight, noiseMap[x, y]); } } return noiseMap; } }参数详解与调参心得seed随机种子。相同的seed、scale、offset等参数下生成的噪声图完全一致。这是程序化生成“可控随机”的基石。scale这是最容易误解的参数。它不直接控制生成区域的大小而是控制采样频率。scale值越小意味着在同样的地图宽度上采样Perlin噪声的步长越小看到的噪声“细节”就越多感觉地形越“崎岖”。scale值越大采样步长越大看到的噪声“大局”部分越多地形越“平滑”。通常从10到100之间开始尝试。octaves倍频数。每增加一层就叠加一层更高频更细节、更低振幅影响更小的噪声。octaves越大地形细节越丰富但计算成本也越高。通常3到6层就能有很好的效果。persistence持久度决定每一层振幅衰减的程度。persistence越小后续高频层的影响越小地形整体越平滑。通常设置在0.3到0.7之间。persistence0.5意味着每一层的振幅是上一层的一半。lacunarity间隙度决定每一层频率增长的程度。lacunarity越大后续高频层的细节尺度越小。通常设置在1.5到3.0之间。lacunarity2是常用值意味着每一层的频率是前一层的两倍。offset整体偏移量。通过改变它可以在无限的噪声“画卷”上滑动窗口生成不同区域的地形。实操技巧调试时我习惯在OnDrawGizmos或OnGUI里用几个Slider实时调整scale、persistence、lacunarity并即时重新生成地形预览。这种即时反馈对理解每个参数的实际影响有巨大帮助。3.3 将噪声图转换为3D地形Mesh有了归一化到[0,1]的高度图噪声图下一步就是把它变成3D网格。我们创建一个TerrainGenerator.cs脚本挂载到空物体上运行。using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class TerrainGenerator : MonoBehaviour { [Header(Noise Map Settings)] public int mapWidth 256; public int mapHeight 256; public float noiseScale 50f; public int octaves 6; [Range(0,1)] public float persistence 0.5f; public float lacunarity 2f; public int seed; public Vector2 offset; [Header(Terrain Settings)] public float heightMultiplier 10f; public AnimationCurve heightCurve; // 用于控制高度分布可在Inspector中编辑曲线 public bool autoUpdate false; // 用于编辑器内实时预览 private Mesh _mesh; private MeshFilter _meshFilter; void Start() { GenerateTerrain(); } void OnValidate() { // 确保数值有效 if (mapWidth 1) mapWidth 1; if (mapHeight 1) mapHeight 1; if (lacunarity 1) lacunarity 1; if (octaves 0) octaves 0; if (autoUpdate Application.isPlaying) { GenerateTerrain(); } } public void GenerateTerrain() { // 1. 生成噪声图 float[,] noiseMap NoiseMapGenerator.GenerateNoiseMap(mapWidth, mapHeight, seed, noiseScale, octaves, persistence, lacunarity, offset); // 2. 生成Mesh _mesh CreateTerrainMesh(noiseMap); if (_meshFilter null) _meshFilter GetComponentMeshFilter(); _meshFilter.sharedMesh _mesh; // 3. 可选根据高度设置材质或纹理 UpdateTerrainMaterial(noiseMap); } Mesh CreateTerrainMesh(float[,] heightMap) { int width heightMap.GetLength(0); int height heightMap.GetLength(1); // 计算网格中心偏移让生成的地形以物体原点为中心 float topLeftX (width - 1) / -2f; float topLeftZ (height - 1) / 2f; Mesh mesh new Mesh(); Vector3[] vertices new Vector3[width * height]; Vector2[] uvs new Vector2[vertices.Length]; int[] triangles new int[(width - 1) * (height - 1) * 6]; int vertexIndex 0; int triangleIndex 0; for (int y 0; y height; y) { for (int x 0; x width; x) { // 根据高度图和曲线计算顶点Y坐标 float currentHeight heightMap[x, y]; float evaluatedHeight heightCurve.Evaluate(currentHeight) * heightMultiplier; vertices[vertexIndex] new Vector3(topLeftX x, evaluatedHeight, topLeftZ - y); // UV按平面坐标简单映射可用于贴图 uvs[vertexIndex] new Vector2(x / (float)width, y / (float)height); // 构建三角形跳过最右边和最下边的顶点 if (x width - 1 y height - 1) { // 每个格子由两个三角形组成 int bottomLeft vertexIndex; int bottomRight vertexIndex 1; int topLeft vertexIndex width; int topRight vertexIndex width 1; // 第一个三角形左下、右上、左上 triangles[triangleIndex] bottomLeft; triangles[triangleIndex 1] topRight; triangles[triangleIndex 2] topLeft; // 第二个三角形左下、右下、右上 triangles[triangleIndex 3] bottomLeft; triangles[triangleIndex 4] bottomRight; triangles[triangleIndex 5] topRight; triangleIndex 6; } vertexIndex; } } mesh.vertices vertices; mesh.uv uvs; mesh.triangles triangles; mesh.RecalculateNormals(); // 重要用于光照计算 mesh.RecalculateBounds(); return mesh; } void UpdateTerrainMaterial(float[,] heightMap) { // 这里可以扩展为根据高度、坡度等信息动态生成或混合纹理 // 例如低海拔为沙滩纹理中海拔为草地高海拔为岩石更高处为雪 MeshRenderer renderer GetComponentMeshRenderer(); // 简单示例创建一个从黑到白的渐变纹理来表示高度 Texture2D texture new Texture2D(mapWidth, mapHeight); for (int y 0; y mapHeight; y) { for (int x 0; x mapWidth; x) { float height heightMap[x, y]; Color color new Color(height, height, height); texture.SetPixel(x, y, color); } } texture.Apply(); renderer.sharedMaterial.mainTexture texture; } }高度曲线heightCurve的妙用这是控制地形地貌风格的关键。在Inspector中编辑这个曲线将曲线左端高度值0附近拉平可以让海平面或平原区域非常平坦。将曲线中段调陡可以让山体斜坡更明显。将曲线右端高度值1附近拉平甚至略微下拉可以制造出平顶山或高原的效果。通过巧妙地塑造这条曲线你可以轻松模拟出沙漠大部分平坦偶有沙丘、峡谷中段陡峭、火山口中间凹陷等多种地形而无需修改噪声生成算法。4. 进阶优化与自然化处理基础地形有了但它可能看起来还是有点“假”像松软的奶油蛋糕缺乏风蚀水刻的痕迹。下面介绍几种让地形更真实的进阶技巧。4.1 使用多张噪声图进行区域划分真实世界不是单纯按高度分层的。我们还需要考虑湿度、温度等因素。我们可以生成第二张、第三张Perlin噪声图作为“湿度图”和“温度图”。float[,] heightMap NoiseMapGenerator.GenerateNoiseMap(..., seed, ...); float[,] moistureMap NoiseMapGenerator.GenerateNoiseMap(..., seed 1, ...); // 使用不同种子偏移 float[,] heatMap NoiseMapGenerator.GenerateNoiseMap(..., seed 2, ...);然后在UpdateTerrainMaterial函数中不再仅仅根据高度而是结合高度、湿度、温度三个值通过一套规则例如高度低且湿度高是沼泽高度高且温度低是雪山来为每个顶点分配最终的颜色或纹理ID。这可以极大地增加地形的视觉多样性和真实感。4.2 热侵蚀算法的简单模拟热侵蚀Thermal Erosion是一种简化模型模拟物质在重力作用下从陡坡向缓坡滑落的过程。虽然不如水力侵蚀物理准确但计算量小效果直观。其核心思想是迭代处理每个顶点检查它与其邻居的高度差。如果邻居比它低很多就将自己的一部分“物质”转移给邻居。public static void ApplyThermalErosion(float[,] heightMap, int iterations, float talusAngle) { int width heightMap.GetLength(0); int height heightMap.GetLength(1); // talusAngle 是安息角的正切值超过这个坡度物质就会滑落 float talus Mathf.Tan(talusAngle * Mathf.Deg2Rad) * 1.0f; // 假设网格间距为1 for (int iter 0; iter iterations; iter) { float[,] newHeightMap (float[,])heightMap.Clone(); // 在新数组上操作 for (int y 1; y height - 1; y) { for (int x 1; x width - 1; x) { float currentHeight heightMap[x, y]; float totalDiff 0f; int transferCount 0; // 检查4邻域或8邻域 for (int ny -1; ny 1; ny) { for (int nx -1; nx 1; nx) { if (nx 0 ny 0) continue; // 跳过自身 int nxIdx x nx; int nyIdx y ny; float diff currentHeight - heightMap[nxIdx, nyIdx]; if (diff talus) { totalDiff diff; transferCount; } } } if (transferCount 0) { float avgTransfer totalDiff / transferCount * 0.5f; // 转移一半的差值 newHeightMap[x, y] - avgTransfer; // 将物质平均分配到符合条件的邻居这里简化处理实际应加权分配 for (int ny -1; ny 1; ny) { for (int nx -1; nx 1; nx) { if (nx 0 ny 0) continue; int nxIdx x nx; int nyIdx y ny; float diff currentHeight - heightMap[nxIdx, nyIdx]; if (diff talus) { newHeightMap[nxIdx, nyIdx] avgTransfer / transferCount; } } } } } } heightMap newHeightMap; // 更新高度图 } }注意侵蚀算法会破坏Perlin噪声的“可哈希性”。即相同的种子和参数在侵蚀前后会产生不同的高度图。因此通常将侵蚀作为后处理步骤并且如果需要确定性结果需要固定侵蚀算法的迭代次数和随机种子如果算法内有随机性。4.3 性能考量与LOD多层次细节当mapWidth和mapHeight很大时如1024x1024顶点数超过百万实时生成和渲染压力巨大。对于无限大地形必须分块Chunk加载。分块生成将大地图划分为多个小块例如64x64或128x128的Chunk。只生成和渲染玩家周围的Chunk。LOD根据Chunk距离相机的远近使用不同分辨率的高度图来生成Mesh。远处的Chunk可以用更低的mapWidth/Height如32x32来生成三角形数量大幅减少。异步生成Chunk的Mesh生成特别是包含侵蚀等复杂后处理时可能耗时。务必在后台线程如使用C#的Task或ThreadPool中完成噪声计算和Mesh数据构建然后在主线程将Mesh赋给MeshFilter。Unity的Job System和Burst Compiler是处理这类数据并行任务的绝佳选择能极大提升生成速度。5. 常见问题与实战排坑记录在实际项目中我遇到了不少问题这里总结几个最有代表性的。5.1 地形接缝与法线问题问题当使用分块系统时相邻Chunk的边界处可能出现高度不连续裂缝或光照不连续法线错误的问题。原因每个Chunk独立生成噪声图在边界处采样了不同的噪声值。即使使用相同的种子和参数由于浮点数精度或采样点不同也可能产生微小差异。解决方案共享采样点生成Chunk噪声图时使其宽度和高度各多出1个单元。相邻Chunk共享边界上的采样点。在构建Mesh时只使用内部单元确保边界顶点数据完全一致。法线平滑在RecalculateNormals之后手动处理边界顶点的法线。遍历所有Chunk边界找到相邻Chunk的对应顶点将它们的法线设置为两者或多者的平均值然后归一化。这能消除光照接缝。5.2 参数调节“玄学”与自动化问题scale、octaves、persistence、lacunarity这几个参数互相影响手动调出一个满意的地形非常耗时。经验scale是宏观尺度控制器。先把它调到能看出明显的大陆板块轮廓比如scale100。然后增加octaves如4-6来添加细节。用persistence控制细节的强度。想要尖锐的山峰就调高persistence0.6想要圆润的山丘就调低0.4。lacunarity控制细节的频率变化。2.0是经典值增加它会让高频细节更“密集”。自动化尝试可以写一个简单的脚本在编辑器模式下随机或按一定步进遍历多组参数并截图保存。然后人工从图库中挑选最符合预期的几组再在其基础上微调。5.3 内存与GC垃圾回收压力问题频繁生成大地形或动态修改地形时每帧创建新的Mesh、Vector3数组等会产生大量GC Alloc导致卡顿。优化策略对象池对于固定大小的Chunk预先创建好Mesh对象池。需要时从池中取用并更新其vertices和triangles而不是new Mesh()。复用数组在CreateTerrainMesh函数中vertices、uvs、triangles数组如果大小不变可以在类级别声明并复用避免每次生成都分配新数组。使用Mesh.SetVertices和Mesh.SetTriangles这些方法接受ListT参数并且有重载版本可以标记哪些数据发生了改变比直接赋值mesh.vertices更高效。拥抱ECS/Job System对于核心的高度图计算和顶点位置计算使用IJobParallelFor在多个CPU核心上并行处理并利用NativeArray来管理数据可以做到零GC分配性能提升一个数量级。5.4 从高度图到真实感纹理问题用灰度图或简单的高度分层着色看起来还是很游戏化。进阶方案三张纹理混合准备三张基础纹理草地、岩石、雪地。在Shader中根据顶点世界坐标的高度和法线坡度来计算混合权重。高度低处用草地高处用雪地。坡度法线与世界向上向量的点积小于某个阈值即坡度很陡的地方混合进岩石纹理。使用Splatmap这是一种更专业的方法。在生成地形时额外生成一张Texture2D作为“混合贴图”Splatmap它的RGBA四个通道分别存储了四种纹理在对应像素的混合权重。然后在Shader中采样这张Splatmap和四张纹理进行混合。这给了美术更大的控制权可以手绘或程序化生成更复杂的纹理分布。细节纹理在基础纹理之上再叠加一张高频率、高重复的细节纹理Detail Texture并采用视差映射Parallax Mapping或法线贴图Normal Mapping来增加表面细节避免近处观察时的模糊感。程序化地形生成是一个深不见底的领域从Perlin噪声出发你可以延伸到Simplex噪声、Worley噪声用于生成细胞状结构、Domain Warping域扭曲用于创造更有机的形态等等。但最重要的是先跑通一个基本流程看到你的代码生成第一座山丘。那份成就感会驱动你不断去优化它让它变得更复杂、更美丽、更高效。希望这份指南能成为你探索这个奇妙世界的坚实起点。