Unity里也能玩Townscaper?手把手教你用C#复现那个超治愈的网格生成算法
·
在Unity中实现Townscaper风格的有机网格生成
第一次玩Townscaper时,我就被它那种看似随意却又和谐统一的建筑布局深深吸引。作为一个Unity开发者,我一直在思考如何在自己的项目中实现类似的有机网格生成效果。经过几周的探索和实验,我终于找到了一套可行的方案,现在就来分享这个将复杂算法转化为Unity实用工具的全过程。
1. 理解Townscaper网格的核心原理
Townscaper的魅力在于它用看似简单的四边形网格构建出丰富多变的城镇景观。要实现这种效果,我们需要理解几个关键算法:
- Delaunay三角剖分 :这是生成基础网格的数学方法,能确保生成的三角形尽可能接近等边三角形
- 边剔除与四边形形成 :通过随机剔除三角形边来形成不规则四边形
- 网格细分 :将剩余的三角形和四边形进一步细分,形成更密集的网格
- 松弛优化 :通过迭代调整顶点位置,使网格看起来更自然美观
在Unity中实现这些算法时,我们需要特别注意性能优化和实时预览功能。下面这段代码展示了基础数据结构的设计:
[ExecuteInEditMode]
public class OrganicGrid : MonoBehaviour {
[Range(2, 12)] public int gridSize = 6;
[Range(0, 10000)] public int randomSeed = 4242;
private List<Vector2> points;
private List<Triangle> triangles;
private List<Quad> quads;
void OnValidate() {
GenerateGrid();
}
void GenerateGrid() {
// 初始化数据结构
points = new List<Vector2>();
triangles = new List<Triangle>();
quads = new List<Quad>();
// 生成基础网格
CreateBaseTriangulation();
// 后续处理步骤
RemoveEdgesToFormQuads();
SubdivideRemainingFaces();
RelaxVertices();
}
}
2. 构建基础三角网格
第一步是创建一个均匀分布的点集,然后进行Delaunay三角剖分。在Unity中,我们可以采用简化的方法:
- 在六边形区域内生成随机点
- 计算这些点的Delaunay三角剖分
- 将结果存储在易于操作的网格结构中
关键参数说明 :
| 参数名 | 作用 | 推荐值 |
|---|---|---|
| gridSize | 控制网格密度 | 4-8 |
| randomSeed | 确保可重复的随机结果 | 任意整数 |
以下是生成基础网格的核心代码:
void CreateBaseTriangulation() {
// 生成六边形边界点
float radius = gridSize * 0.5f;
for (int i = 0; i < 6; i++) {
float angle = i * Mathf.PI / 3f;
points.Add(new Vector2(
Mathf.Cos(angle) * radius,
Mathf.Sin(angle) * radius
));
}
// 内部随机点生成
System.Random rand = new System.Random(randomSeed);
for (int i = 0; i < gridSize * 10; i++) {
Vector2 point = RandomPointInHexagon(rand, radius);
points.Add(point);
}
// 执行Delaunay三角剖分
triangles = DelaunayTriangulation(points);
}
3. 从三角网格到四边形网格
Townscaper的独特之处在于它主要使用四边形而非三角形。我们可以通过以下步骤实现转换:
- 随机选择三角形边进行剔除
- 将相邻三角形合并为四边形
- 处理剩余的独立三角形
这个过程中的随机性控制非常重要:
- 边剔除概率 :影响最终四边形的规整程度
- 随机种子 :确保结果可重复
- 迭代次数 :决定网格的复杂程度
void RemoveEdgesToFormQuads() {
System.Random rand = new System.Random(randomSeed);
List<Triangle> remainingTris = new List<Triangle>(triangles);
while (remainingTris.Count > 0) {
int index = rand.Next(remainingTris.Count);
Triangle tri = remainingTris[index];
// 查找共享边的相邻三角形
Triangle neighbor = FindAdjacentTriangle(tri);
if (neighbor != null) {
// 合并为四边形
Quad quad = MergeToQuad(tri, neighbor);
quads.Add(quad);
// 从剩余列表中移除
remainingTris.Remove(tri);
remainingTris.Remove(neighbor);
} else {
// 无法合并的三角形保留到最后处理
remainingTris.RemoveAt(index);
}
}
// 处理剩余的独立三角形
foreach (Triangle tri in remainingTris) {
quads.AddRange(SubdivideTriangle(tri));
}
}
4. 网格细分与优化
为了获得更丰富的细节,我们需要对现有网格进行细分:
- 将每个四边形细分为四个较小的四边形
- 将剩余的三角形细分为三个四边形
- 通过松弛算法优化顶点位置
细分策略对比 :
| 方法 | 优点 | 缺点 |
|---|---|---|
| 均匀细分 | 实现简单 | 缺乏变化 |
| 随机细分 | 更有机 | 可能产生畸形面片 |
| 基于曲率细分 | 保留特征 | 计算复杂 |
以下是细分和松弛的实现:
void SubdivideRemainingFaces() {
List<Quad> newQuads = new List<Quad>();
foreach (Quad quad in quads) {
// 计算中点
Vector2 center = (points[quad.a] + points[quad.b]
+ points[quad.c] + points[quad.d]) / 4f;
int centerIndex = points.Count;
points.Add(center);
// 创建四个子四边形
newQuads.Add(new Quad(quad.a, quad.b, centerIndex, centerIndex));
newQuads.Add(new Quad(quad.b, quad.c, centerIndex, centerIndex));
newQuads.Add(new Quad(quad.c, quad.d, centerIndex, centerIndex));
newQuads.Add(new Quad(quad.d, quad.a, centerIndex, centerIndex));
}
quads = newQuads;
}
void RelaxVertices() {
// 构建邻接关系
Dictionary<int, List<int>> neighbors = new Dictionary<int, List<int>>();
foreach (Quad quad in quads) {
AddNeighbor(neighbors, quad.a, quad.b);
AddNeighbor(neighbors, quad.a, quad.d);
// 添加其他边的邻接关系...
}
// 迭代松弛
for (int iter = 0; iter < 5; iter++) {
for (int i = 0; i < points.Count; i++) {
if (!neighbors.ContainsKey(i)) continue;
Vector2 avg = Vector2.zero;
foreach (int j in neighbors[i]) {
avg += points[j];
}
avg /= neighbors[i].Count;
// 渐进式移动
points[i] = Vector2.Lerp(points[i], avg, 0.3f);
}
}
}
5. 在Unity中可视化网格
为了让这个算法真正可用,我们需要在Unity编辑器中实现实时预览:
- 使用Gizmos绘制网格线框
- 添加参数控制面板
- 支持网格导出为Mesh资源
void OnDrawGizmos() {
if (points == null || quads == null) return;
// 绘制顶点
Gizmos.color = Color.white;
foreach (Vector2 point in points) {
Gizmos.DrawSphere(point, 0.05f);
}
// 绘制四边形边
Gizmos.color = Color.green;
foreach (Quad quad in quads) {
DrawQuadGizmo(quad);
}
}
void DrawQuadGizmo(Quad quad) {
Vector3 a = points[quad.a];
Vector3 b = points[quad.b];
Vector3 c = points[quad.c];
Vector3 d = points[quad.d];
Gizmos.DrawLine(a, b);
Gizmos.DrawLine(b, c);
Gizmos.DrawLine(c, d);
Gizmos.DrawLine(d, a);
}
6. 实际应用与扩展
有了基础网格生成器后,我们可以进一步扩展功能:
- 3D化 :将2D网格赋予高度,创建丘陵地形
- 建筑生成 :在每个四边形区域生成不同风格的建筑
- 动态编辑 :允许玩家在运行时修改网格
性能优化技巧 :
- 使用Job System进行并行计算
- 实现增量式网格更新
- 对静态区域进行烘焙
// 示例:简单的3D化处理
Mesh Generate3DMesh() {
Mesh mesh = new Mesh();
// 将2D点转换为3D顶点
Vector3[] vertices = new Vector3[points.Count];
for (int i = 0; i < points.Count; i++) {
float height = Mathf.PerlinNoise(
points[i].x * 0.1f,
points[i].y * 0.1f
) * 2f;
vertices[i] = new Vector3(points[i].x, height, points[i].y);
}
// 构建三角形面片
List<int> triangles = new List<int>();
foreach (Quad quad in quads) {
triangles.Add(quad.a);
triangles.Add(quad.b);
triangles.Add(quad.c);
triangles.Add(quad.a);
triangles.Add(quad.c);
triangles.Add(quad.d);
}
mesh.vertices = vertices;
mesh.triangles = triangles.ToArray();
mesh.RecalculateNormals();
return mesh;
}
在实际项目中,我发现最有价值的不是完美复现Townscaper的效果,而是理解其背后的设计哲学——在规则与随机之间找到平衡点。通过调整各种参数,你可以创造出从规整城市到杂乱村庄的各种风格。
更多推荐
所有评论(0)