做太空题材或策略游戏时,程序化生成行星几乎是必答题。这篇文章要拆解的,是基于 Unity 和 Blender 构建一套六边形行星生成器引擎的完整技术路线:用 C# 在 Unity 里生成六边形球面网格,用噪声算法做出山脉、海洋和生态分布,再用 Blender 配合制作面板资产、烘焙贴图和材质预览。整个过程不仅覆盖网格生成,还会落到关卡设计:六边形网格上的寻路、地块类型、批量生成多个行星,以及运行时性能怎么观察、怎么优化。
文章会先讲清楚“为什么是六边形”,再给六边形球面网格的算法基础,然后分别拆解 Unity 侧脚本与 Blender 侧资产流程。后面补上噪声地形、关卡设计落地的具体写法、性能观察和排查清单。适合正在做星球探索、太空策略、RTS 或回合制品类,又不想靠手工摆地形,想用程序化管线批量产出星球场景的开发者。即使你只是对六边形网格算法感兴趣,这篇也值得收藏后慢慢看。
项目本身不是某个仓库名,而是一套完整的程序化内容生成(PCG)技术方案。你不需要第三方收费插件,用 Unity 的 Mesh API、C# 基础和 Blender 的几何节点就能把主体跑起来。这也就意味着,自己动手改造的余量非常大,从地形风格到玩法规则全部可控。
1. 核心概念与技术选型
1.1 什么是六边形行星生成器
六边形行星生成器,顾名思义,是把一颗星球从“球体几何”到“地形高度”再到“地块属性”都交给程序生成的引擎工具。它不是简单地在球上贴一张高度图,而是先把球面拆成紧凑的六边形网格,然后对每个六边形地块做高度偏移、生态类型、资源分布和颜色控制。最终得到的不是一张贴图,而是一套可以被游戏逻辑查询和修改的数据结构。
这和传统的 Quad Sphere 方案有本质区别。四边形球面方案在极点区域会有严重的 UV 拉伸,而且网格拓扑结构不够均匀;六边形网格有更好的拓扑表示,地块之间共享边而不是共享角,非常适合策略游戏里的区域操作、移动消耗计算和势力划分。
1.2 Unity 与 Blender 的分工
这套架构里,Unity 和 Blender 不是二选一的关系,而是各管一段:
| 阶段 | 工具 | 职责 |
|---|---|---|
| 运行时网格生成 | Unity | 生成二十面体、细分、转对偶网格、采样噪声、输出 Mesh |
| 地形与生态逻辑 | Unity | 根据噪声和温度/湿度信息设置地块类型、资源权重 |
| 面板资产制作 | Blender | 制作六边形面板、岩壁、建筑底座的低模资产,预览配色 |
| 贴图与光照烘焙 | Blender | 烘焙 AO、法线贴图,导出 FBX/Prefab 到 Unity |
| 批量生成与配置 | Unity | 使用 ScriptableObject 管理参数,批量生成多个行星 |
简单理解:Blender 负责“把单块六边形地板做得好看”,Unity 负责“把几千上万个六边形地板拼成一颗星球,并且能响应游戏玩法”。
1.3 为什么是六边形网格而不是四边形网格
六边形网格在关卡设计里的优势非常明显:
- 每个格子有 6 个邻居,移动路径比四边形更自然,转弯角度更平滑。
- 相邻地块共享整条边,适合做边界判定、区域接壤、战争迷雾扩散。
- 六边形中心到每个邻居中心的距离一致,不会出现四边形里“斜角距离更远”的问题。
- 在球面上,六边形网格比矩形网格更接近等距分布,极点畸变更小。
代价也很直接:六边形网格的顶点和三角形关系更复杂,UV 展开困难,而且球面上无法做到全部是六边形,一定会混入五边形。这一点后面会详细讲。
2. 六边形球面网格的算法基础
2.1 从二十面体开始:为什么用二十面体
要在球面上生成尽量均匀的六边形网格,最常见的做法是先建一个二十面体(Icosphere),然后不断细分,最后做对偶网格转换。
二十面体有 20 个三角形面、30 条边、12 个顶点。它的顶点均匀分布在球面上,是所有多面体里最适合做球形细分的基础形状。
初始顶点可以用黄金比例计算:
float phi = (1f + Mathf.Sqrt(5f)) * 0.5f; Vector3[] vertices = new Vector3[12] { new Vector3(-1, phi, 0), new Vector3( 1, phi, 0), new Vector3(-1, -phi, 0), new Vector3( 1, -phi, 0), new Vector3( 0, -1, phi), new Vector3( 0, 1, phi), new Vector3( 0, -1, -phi), new Vector3( 0, 1, -phi), new Vector3( phi, 0, -1), new Vector3( phi, 0, 1), new Vector3(-phi, 0, -1), new Vector3(-phi, 0, 1) };这些顶点需要归一化到单位球面上,然后按二十面体的固定索引表组成 20 个三角形。这里的索引表是一组公开的标准几何数据,在 Unity 里可以直接存入int[] triangles。
2.2 细分:把三角形变成更细的网格
细分的核心逻辑是:对每一条边插入中点,然后把一个三角形拆成四个小三角形。关键点是边的中点不能重复创建,因为相邻三角形共享同一条边。
Dictionary<long, int> midpointCache = new Dictionary<long, int>(); int GetMidpointIndex(int a, int b) { long key = a < b ? (a << 16) + b : (b << 16) + a; if (midpointCache.TryGetValue(key, out int index)) return index; Vector3 mid = (vertices[a] + vertices[b]) * 0.5f; mid.Normalize(); index = vertices.Count; vertices.Add(mid); midpointCache[key] = index; return index; }一次细分后,三角形数量从 20 变为 80。做 3 次细分,三角形数量约为 1280,六边形地块数量在几百到一千左右,已经可以看清星球轮廓;做 5 次细分,三角形数量约 20480,地块数接近一万一,适合正式关卡。细分次数每增加一次,三角形数翻四倍,所以性能瓶颈会非常快地出现。
2.3 对偶网格:三角形转六边形
三角形网格本身不是六边形。要把三角形网格变成六边形网格,需要做“对偶”操作:对原始网格中的每一个顶点,收集它周围的所有三角形,把这些三角形的质心按顺序连接成一个多边形。
这个操作在任意球面上都会产生两种面:
- 原始顶点连接 6 个三角形时,得到六边形。
- 原始顶点连接 5 个三角形时,得到五边形。
这里必须强调一个数学事实:一个封闭的球面网格,不可能全部由六边形构成。根据欧拉公式,球面上至少要有 12 个五边形顶点。这 12 个五边形在二十面体的 12 个原始顶点位置附近。它们不是 bug,而是球面几何的必然结果。处理得好,它们可以变成地表建筑的锚点,或者资源富集区,让关卡更有记忆点。
对偶转换的示意代码:
Dictionary<int, List<int>> vertexTriangles = new Dictionary<int, List<int>>(); for (int i = 0; i < triangles.Count; i += 3) { int a = triangles[i]; int b = triangles[i + 1]; int c = triangles[i + 2]; AddToDict(vertexTriangles, a, i / 3); AddToDict(vertexTriangles, b, i / 3); AddToDict(vertexTriangles, c, i / 3); } List<Vector3> dualVertices = new List<Vector3>(); List<int> dualTriangles = new List<int>(); foreach (KeyValuePair<int, List<int>> pair in vertexTriangles) { List<int> incident = pair.Value; Vector3 center = Vector3.zero; foreach (int triIndex in incident) { Vector3 v0 = vertices[triangles[triIndex * 3]]; Vector3 v1 = vertices[triangles[triIndex * 3 + 1]]; Vector3 v2 = vertices[triangles[triIndex * 3 + 2]]; center += (v0 + v1 + v2) / 3f; } center /= incident.Count; center.Normalize(); dualVertices.Add(center); }这里最需要小心的坑是:入射三角形的质心顺序不保证是绕顶点一圈排列的,直接连多边形会出现面片交叉。正确做法是先按质心相对顶点的方位角排序,再生成三角形扇。这一步处理不好,生成的行星表面会产生大量扭曲三角形,肉眼看起来就像噪声糊在球面上。
2.4 噪声采样的坐标系选择
六边形球面网格生成后,要给每个地块计算海拔。常见的错误是在纬度/经度坐标系里采样二维噪声,再映射到球面,这样极区会有严重的拉伸和重复。正确做法是直接用三维噪声采样:把六边形顶点的归一化方向向量当作采样坐标。
三维 Perlin/Simplex 噪声天然没有接缝,采样点在球面均匀分布时,生成的噪声场连续且方向无关。如果需要山脊效果,可以用abs()扭曲噪声值;需要海洋和大陆,可以用频率更低的多频叠加。
3. 环境准备与前置条件
一套完整的开发环境大致如下,具体版本建议以你本机的 Unity 和 Blender 版本为准,不强制踩新版本:
| 工具 | 用途 | 建议版本 |
|---|---|---|
| Unity | 运行时行星生成、C# 逻辑 | 2021 LTS 或更高版本 |
| Blender | 六边形资产建模、烘焙贴图 | 3.6 LTS 或更高版本 |
| C# IDE | 脚本编写与调试 | Visual Studio / Rider |
| 版本控制 | 管理代码和场景文件 | Git + Git LFS |
配置页面建议开启内置渲染管线的 URP,方便后处理效果;如果只做纯数据生成,不依赖渲染效果,内置渲染管线也完全够用。
启动工程前检查以下几点:
- Unity 已安装对应模块,能创建 3D 项目。
- Blender 能打开几何节点工作区。
- 磁盘预留足够空间,Unity 烘焙光照贴图和 Blender 导出纹理都需要临时空间。
- Git LFS 提前配置,避免 FBX 和纹理文件损坏。
4. 在 Unity 中实现六边形行星生成器核心
4.1 项目结构与脚本规划
建议把生成器代码按职责拆成四个脚本:
| 脚本 | 职责 |
|---|---|
| IcosahedronBuilder.cs | 生成二十面体顶点和三角形 |
| SphereSubdivider.cs | 细分三角形,维护边中点缓存 |
| DualMeshConverter.cs | 对偶网格转换,输出多边形面 |
| PlanetMeshGenerator.cs | 高层入口,串联全部流程,赋值 MeshFilter/MeshCollider |
再加一个数据配置类PlanetConfig,用 ScriptableObject 保存细分次数、噪声参数、颜色梯度、高度缩放。这样后续生成多个行星只需要创建多个配置文件,不需要改代码。
4.2 构建二十面体与细分管线
在PlanetMeshGenerator的入口处,把整条流程按顺序调用:
public Mesh BuildPlanetMesh(PlanetConfig config) { IcosahedronBuilder builder = new IcosahedronBuilder(); builder.Build(); SphereSubdivider subdivider = new SphereSubdivider(builder.Vertices, builder.Triangles); for (int i = 0; i < config.SubdivisionLevels; i++) { subdivider.SubdivideOnce(); } DualMeshConverter dual = new DualMeshConverter( subdivider.Vertices, subdivider.Triangles ); dual.Convert(); ApplyNoiseElevation(dual.Vertices, config); Mesh mesh = BuildMeshFrom(dual); return mesh; }命名和接口可以按你的工程风格调整,但这个流程本身是稳定的:二十面体 → 细分 → 对偶 → 噪声位移 → 生成 Mesh。
4.3 地形高度与多频噪声
对每个对偶顶点,需要把它的方向向量作为三轴噪声采样坐标。多频叠加的做法如下:
float SampleElevation(Vector3 point, PlanetConfig config) { float value = 0f; float amplitude = 1f; float frequency = 1f; for (int octave = 0; octave < config.Octaves; octave++) { float noise = Mathf.PerlinNoise( point.x * frequency + config.SeedOffset, point.y * frequency + config.SeedOffset ); // 这里是 2D 噪声简化示例,生产环境建议用 3D Simplex value += noise * amplitude; amplitude *= config.Persistence; frequency *= config.Lacunarity; } return value; }注意:上面示例用 2D Perlin 只是为了展示叠加思路。真正的球面采样强烈建议用 3D Simplex 或 OpenSimplex,避免极点接缝。Unity 内置的Mathf.PerlinNoise是 2D API,直接用它做球面地形会有明显缺陷。更好的选择是引入 LibNoise.Unity 或自己写一个 3D Simplex 实现。
采样后,把高度作用到顶点位置:
Vector3 dir = dualVertices[i].normalized; dualVertices[i] = dir * (baseRadius + elevation * heightScale);生成 Mesh 时,法线可以直接使用dir方向,因为高度只改变半径,不改变方位。
4.4 顶点色与地块材质
六边形行星不能只有高度。按高度和噪声分配顶点色,可以让海洋、沙滩、平原、山脉、雪峰一目了然。
Color EvaluateColor(float elevation, float moisture) { if (elevation < config.SeaLevel) return config.WaterGradient.Evaluate(elevation / config.SeaLevel); if (elevation < config.BeachLevel) return Color.Lerp(config.SandColor, config.GrassColor, (elevation - config.SeaLevel) / (config.BeachLevel - config.SeaLevel)); return config.MountainGradient.Evaluate((elevation - config.BeachLevel) / (config.MaxElevation - config.BeachLevel)); }这只是一个示例规则。你可以在 Actual 开发里加入温度带、湿度噪声、轴倾斜导致的气候带,把行星生成器从“地形工具”升级为“生态工具”。
5. Blender 侧资产准备与流程配合
5.1 使用几何节点生成六边形面板
Blender 的几何节点非常适合快速制作六边形面板的基础资产。思路是新建一个Grid节点,再通过Dual Mesh节点得到六边形排列,然后挤出厚度;或者直接使用Cylinder节点并设置顶点数为 6,得到单个六边形柱子底座。
如果你想做更复杂的星球表面建筑基地,可以用几何节点构建一个六边形瓦片的集合:面板中心、边缘凸起、四角铆钉。这样每次修改都不会破坏基础拓扑。
5.2 导出 FBX 和贴图的注意点
- 导出前把 Blender 单位设置成米,和 Unity 的默认单位一致。
- 贴图命名不能用中文,避免 Unity 导入时出现路径告警。
- 如果面板资产带倒角,烘焙 AO 后在 Unity 里看起来体积感会强很多。
- 法线贴图需要选择“Tangent Space”导出,直接导出 Object Space 法线会在 Unity 里出现方向错乱。
- 在 Blender 里先清掉未使用的材质球,再导出 FBX,可以减小工程体积。
5.3 与 Unity 脚本配合的资产管线
Blender 做好的单个六边形面板导入 Unity 后,不要直接把它当成地面格子逐块摆。行星运行时生成的网格数量非常大,用 Prefab 逐块实例化会带来巨大的 Draw Call 压力。
正确做法是:把 Blender 面板作为“视觉母版”,在 Unity 里用 Mesh 合并或 GPU Instancing 把相同的面板按地块位置批量实例化;或者只把 Blender 面板的高模和贴图作为烘焙参考,运行时使用 Unity 生成的低模六边形网格,配合顶点色和 Triplanar 贴图采样。
简单说:Blender 负责“长什么样”,Unity 负责“怎么快速铺满星球”。
6. 关卡设计:六边形行星上的玩法落地
6.1 六边形网格坐标模型
在地块逻辑层面,不要直接操作 Mesh 顶点。更好的做法是先建立一块数据网格,把每个地块的坐标、邻居、高度、生态类型、归属势力放在一个类里。比如使用 Cube Coordinates 或 Offset Coordinates,六边形寻路会非常顺手。
public struct HexCoord { public int X; public int Y; public int Z; public static List<HexCoord> Neighbors(HexCoord h) { return new List<HexCoord>() { new HexCoord { X = h.X + 1, Y = h.Y - 1, Z = h.Z }, new HexCoord { X = h.X + 1, Y = h.Y, Z = h.Z - 1 }, new HexCoord { X = h.X, Y = h.Y + 1, Z = h.Z - 1 }, new HexCoord { X = h.X - 1, Y = h.Y + 1, Z = h.Z }, new HexCoord { X = h.X - 1, Y = h.Y, Z = h.Z + 1 }, new HexCoord { X = h.X, Y = h.Y - 1, Z = h.Z + 1 } }; } }这只是一个平面六边形的邻居模型。球形六边形网格的邻居关系不需要重新发明,把三角形网格对偶后得到的多边形面的“共享边关系”导出成一张邻接表即可。
6.2 地块类型与规则生成
地块类型可以按高度和湿度进行规则划分:
| 高度区间 | 湿度条件 | 地块类型 |
|---|---|---|
| 低于海平面 | 任意 | 海洋 |
| 海平面到沙滩线 | 任意 | 沙滩 |
| 沙滩线到中海拔 | 高湿度 | 森林 |
| 沙滩线到中海拔 | 低湿度 | 草原 |
| 高海拔 | 任意 | 山地 |
| 极高海拔 | 任意 | 雪峰 |
这张规则表可以写到PlanetConfig里,让策划在不改代码的前提下调权重。行星生成器从“纯技术工具”变成“可配置的关卡生产管线”,关键就在于把规则暴露到配置层。
6.3 批量生成多个行星与参数配置
用 ScriptableObject 管理配置后,批量生成多颗行星只需要遍历配置列表:
public List<PlanetData> GeneratePlanets(List<PlanetConfig> configs) { List<PlanetData> results = new List<PlanetData>(); foreach (PlanetConfig config in configs) { PlanetData data = new PlanetData(); data.Mesh = BuildPlanetMesh(config); data.BiomeMap = GenerateBiomeMap(data.Vertices, config); data.HexNodes = BuildHexNodesFromMesh(data.Mesh, config); results.Add(data); } return results; }这里的关键点是:Mesh、BiomeMap、HexNodes 必须解耦。Mesh 只是渲染数据,BiomeMap 决定地块属性,HexNodes 才是玩法逻辑直接查询的对象。如果你让玩法逻辑直接依赖 Mesh 三角形索引,后面改分辨率或换网格算法时会非常痛苦。
6.4 五边形地块的特殊处理
前面提到球面必然有 12 个五边形。这些五边形的邻居只有 5 个,而不是 6 个。寻路时如果直接按六边形规则遍历邻居,五边形地块的其中一个访问方向会越界或缺失。
实际关卡设计里,可以把五边形做成“特殊节点”:
- 资源高产区。
- 副本入口。
- 大型建筑的地基。
- 行星传送门。
这样不仅规避了寻路问题,还能让玩家在星球上找到结构性地标。这是六边形行星生成器相较于四边形球的一个隐藏优点:天然拥有视觉奇观点。
7. 性能优化与资源占用观察
7.1 观察性能的几个维度
运行六边形行星生成器时,需要重点观察以下数据:
| 维度 | 观察方式 | 关注点 |
|---|---|---|
| 三角形数量 | Unity Profiler / Mesh 信息面板 | 生成时间与渲染压力 |
| 内存占用 | Profiler Memory Module | 顶点数组、索引数组、邻接表占用 |
| 主线程耗时 | CPU Usage | 集中式循环是否卡顿 |
| Draw Call | Frame Debugger | 实例化是否合并 |
| Mesh Collider 重建 | Profiler | 物理碰撞计算成本 |
注意:没有固定的“显存占用”数值可以套用,因为行星网格的渲染开销取决于三角形数量、材质复杂度和后处理特效。在低端设备上,建议控制细分次数,先用 3 次细分跑通逻辑,再逐步提高。
7.2 降低顶点数量的手段
- 细分次数不要无脑调高。5 次细分已经是很多策略游戏的上限。
- 对远离玩家的半球做简化,或者使用基于与相机距离的 LOD 切换。
- 把相邻的同生态地块合并成一个 Mesh 子网格,减少渲染批次。
- 六边形地板不需要每块都带大量顶点,平地区域可以展平,减少高度采样精度。
7.3 减少主线程压力
生成大量网格时,CPU 会停顿。减少卡顿的手段,按推荐顺序排列:
- 把生成流程放到协程里,分帧执行。
- 使用 Unity Job System,把噪声采样和顶点计算放到工作线程。
- 用 Burst Compiler 编译高频循环。
- 对 Mesh 的
MarkDynamic()做必要设置,避免不必要的 CPU 与 GPU 间数据拷贝。
7.4 内存与资源释放
每次重新生成行星时,必须手动释放旧 Mesh 和旧邻接表:
if (existingMesh != null) { Destroy(existingMesh); existingMesh = null; }这句话写不好,反复生成多颗行星后在 Profiler 里会看到网格内存居高不下。旧 Mesh 不会因为MeshFilter.mesh被替换就自动释放,需要手动销毁。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 行星表面出现大量扭曲三角形 | 对偶网格顶点未按环绕排序 | 查看对偶网格的面索引顺序 | 按质心方位角排序后生成三角扇 |
| 部分地块看起来是三角形而不是六边形 | 细分次数不足,五边形占比过高 | 统计地块边数分布 | 增加细分次数,但保留对 12 个五边形的特殊处理 |
| 地形在极点区域出现拉伸或接缝 | 使用了 2D 噪声采样球面坐标 | 检查噪声采样输入向量 | 改用 3D Simplex/OpenSimplex 噪声 |
| 生成后的网格背面朝外 | 三角形环绕序反向 | 在 Scene 视图检查法线方向 | 反转三角形索引顺序或重新计算法线 |
| 重复生成多个行星后内存暴涨 | 旧 Mesh 未销毁 | Profiler Memory 查看 Mesh 数量 | 生成前销毁旧 Mesh 和邻接表 |
| Mesh Collider 碰撞极不准确 | Collider 直接使用高密度 Mesh | Physics Debug 视图 | 为碰撞单独生成低密度 Mesh |
| Blender 导出的六边形资产在 Unity 中颜色发黑 | 法线贴图空间不一致 | 检查导入设置 | 导出 Tangent Space 法线,并确认 Unity 导入参数 |
| 整体生成时间过长 | 细分次数过高或主线程集中计算 | Profiler CPU Usage | 调低细分,改用 Job System |
| 游戏逻辑找不到某个六边形地块的邻居 | 邻接表未同步对偶网格拓扑 | 检查五边形地块的邻居数量 | 手动补邻居或把五边形设为特殊地块 |
9. 最佳实践与合规建议
9.1 工程组织建议
- 把噪声采样、网格生成、地块逻辑分成三个独立程序集,方便单测。
- 用 ScriptableObject 存放所有生成参数,禁止在代码里写死星球半径、噪声频率、颜色梯度。
- 保持一套最小可运行配置,用于每次改动后回归测试。
- 把输出行星的数据序列化为 JSON,便于调试和版本对比。
{ "planetName": "TestPlanet", "subdivisionLevel": 3, "baseRadius": 100, "seaLevel": 0.35, "octaves": 5, "seedOffset": 12345 }这个 JSON 可以导出为配置文件,也可以用同一结构在 Unity 里生成ScriptableObject。策划和程序只要约定好字段名,就可以无痛协作。
9.2 测试与回归
第一次跑通后,先把细分次数设为 1、关闭噪声,确认球体成型;再把噪声打开,观察地形是否连续;最后加入地块类型和寻路,验证玩法数据。这个顺序能快速定位问题出在哪一层。
9.3 版权与合规提醒
- 用 Blender 制作六边形资产时,如果参考了外部模型或纹理,确认授权范围能否用于商业项目。
- AI 辅助生成地形或贴图素材时,检查生成模型的使用条款,避免商用版权纠纷。
- 如果项目涉及真实星球数据或公开地图数据,确认数据来源的许可协议。
- 游戏上线前,对资源、文本、音效做完整合规核查,程序化生成工具也要纳入版本归档。
10. 总结与后续扩展
这套六边形行星生成器引擎最值得尝试的点,是它把程序化几何、噪声地形、关卡设计和美术资产管线完整串联起来。你最先应该验证的,不是画面好看,而是“细分之后对偶网格有没有正确变成六边形”,以及“12 个五边形是否被妥善处理”。最容易踩坑的也恰好是这两个点:对偶网格的顶点排序,和五边形地块的寻路边界。
后续可以继续扩展的方向非常多:加上 Lod 调度,让行星可以从太空无缝切入地表;引入资源分布噪声,让采矿玩法有据可依;加上轴倾斜和气候带,把地形生成升级成生态生成。如果再把 Job System 和 Burst 编译器接进去,完全可以在中等配置的笔记本上批量生成几十颗可探索星球。
方案不复杂,先跑通一个小型六边形球,再做噪声、再做地块逻辑。建议收藏备用,等你把第一个会转的六边形星球跑出来,后面的扩张空间会非常开阔。