在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中,我们可以采用简化的方法:

  1. 在六边形区域内生成随机点
  2. 计算这些点的Delaunay三角剖分
  3. 将结果存储在易于操作的网格结构中

关键参数说明

参数名 作用 推荐值
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的独特之处在于它主要使用四边形而非三角形。我们可以通过以下步骤实现转换:

  1. 随机选择三角形边进行剔除
  2. 将相邻三角形合并为四边形
  3. 处理剩余的独立三角形

这个过程中的随机性控制非常重要:

  • 边剔除概率 :影响最终四边形的规整程度
  • 随机种子 :确保结果可重复
  • 迭代次数 :决定网格的复杂程度
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. 网格细分与优化

为了获得更丰富的细节,我们需要对现有网格进行细分:

  1. 将每个四边形细分为四个较小的四边形
  2. 将剩余的三角形细分为三个四边形
  3. 通过松弛算法优化顶点位置

细分策略对比

方法 优点 缺点
均匀细分 实现简单 缺乏变化
随机细分 更有机 可能产生畸形面片
基于曲率细分 保留特征 计算复杂

以下是细分和松弛的实现:

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编辑器中实现实时预览:

  1. 使用Gizmos绘制网格线框
  2. 添加参数控制面板
  3. 支持网格导出为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. 实际应用与扩展

有了基础网格生成器后,我们可以进一步扩展功能:

  1. 3D化 :将2D网格赋予高度,创建丘陵地形
  2. 建筑生成 :在每个四边形区域生成不同风格的建筑
  3. 动态编辑 :允许玩家在运行时修改网格

性能优化技巧

  • 使用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的效果,而是理解其背后的设计哲学——在规则与随机之间找到平衡点。通过调整各种参数,你可以创造出从规整城市到杂乱村庄的各种风格。

更多推荐