You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何计算 icosphere 上各六边形中心位置?求对应实现函数

基于Icosphere的六边形星球地图实现方案

1. 核心函数:生成六边形中心球坐标

要实现动态细分等级的需求,核心是先生成对应等级的icosphere,再过滤掉原始12个顶点附近的五边形区域(隐藏在海洋),提取剩余区域的面中心并转换为球坐标。以下是游戏开发中常用的C#示例实现:

// 球坐标结构体,便于后续缩放与旋转
public struct SphericalCoord {
    public float Theta; // 方位角(0-2π),绕Y轴旋转角度
    public float Phi;   // 极角(0-π),与Y轴的夹角
    public float Radius;// 球半径,默认1(后续可按星球尺寸缩放)

    public SphericalCoord(float theta, float phi, float radius = 1f) {
        Theta = theta;
        Phi = phi;
        Radius = radius;
    }
}

// 返回指定细分等级下所有六边形面的球坐标
public List<SphericalCoord> GetHexagonCenters(int subdivisionLevel) {
    List<SphericalCoord> hexCenters = new List<SphericalCoord>();
    IcosphereData icoData = GenerateIcosphere(subdivisionLevel);

    // 标记原始icosphere的12个顶点(这些顶点周围对应五边形区域)
    HashSet<int> pentagonVertexIndices = new HashSet<int>(icoData.OriginalVertexIndices);

    foreach (var face in icoData.Faces) {
        // 跳过属于五边形区域的面
        bool isPentagonRegion = false;
        foreach (int vertexIdx in face.VertexIndices) {
            if (pentagonVertexIndices.Contains(vertexIdx)) {
                isPentagonRegion = true;
                break;
            }
        }
        if (isPentagonRegion) continue;

        // 计算三角形面的中心,转换为单位球坐标
        Vector3 faceCenter = Vector3.zero;
        foreach (int vertexIdx in face.VertexIndices) {
            faceCenter += icoData.Vertices[vertexIdx];
        }
        faceCenter /= 3f;
        faceCenter = faceCenter.normalized; // 归一化到单位球

        hexCenters.Add(CartesianToSpherical(faceCenter));
    }

    return hexCenters;
}

// 笛卡尔坐标转球坐标
private SphericalCoord CartesianToSpherical(Vector3 cartesian) {
    float radius = cartesian.magnitude;
    float phi = (float)Math.Acos(Math.Clamp(cartesian.y / radius, -1f, 1f));
    float theta = (float)Math.Atan2(cartesian.z, cartesian.x);
    return new SphericalCoord(theta, phi, radius);
}

// 生成指定细分等级的icosphere数据
private IcosphereData GenerateIcosphere(int subdivisionLevel) {
    IcosphereData data = new IcosphereData();
    data.Vertices = new List<Vector3>();
    data.Faces = new List<TriangleFace>();
    data.OriginalVertexIndices = new List<int>();

    // 1. 初始化原始icosphere的12个顶点
    float t = (1f + (float)Math.Sqrt(5f)) / 2f;
    Vector3[] baseVertices = new Vector3[] {
        new Vector3(-1f, t, 0f), new Vector3(1f, t, 0f), new Vector3(-1f, -t, 0f), new Vector3(1f, -t, 0f),
        new Vector3(0f, -1f, t), new Vector3(0f, 1f, t), new Vector3(0f, -1f, -t), new Vector3(0f, 1f, -t),
        new Vector3(t, 0f, -1f), new Vector3(t, 0f, 1f), new Vector3(-t, 0f, -1f), new Vector3(-t, 0f, 1f)
    };
    foreach (var v in baseVertices) {
        data.Vertices.Add(v.normalized);
        data.OriginalVertexIndices.Add(data.Vertices.Count - 1);
    }

    // 2. 初始化原始20个三角形面
    int[][] baseFaces = new int[][] {
        new int[]{0,11,5}, new int[]{0,5,1}, new int[]{0,1,7}, new int[]{0,7,10}, new int[]{0,10,11},
        new int[]{1,5,9}, new int[]{5,11,4}, new int[]{11,10,2}, new int[]{10,7,6}, new int[]{7,1,8},
        new int[]{3,9,4}, new int[]{3,4,2}, new int[]{3,2,6}, new int[]{3,6,8}, new int[]{3,8,9},
        new int[]{4,9,5}, new int[]{2,4,11}, new int[]{6,2,10}, new int[]{8,6,7}, new int[]{9,8,1}
    };
    foreach (var face in baseFaces) {
        data.Faces.Add(new TriangleFace { VertexIndices = face });
    }

    // 3. 执行细分逻辑
    for (int i = 0; i < subdivisionLevel; i++) {
        List<TriangleFace> newFaces = new List<TriangleFace>();
        Dictionary<long, int> midPointCache = new Dictionary<long, int>();

        foreach (var face in data.Faces) {
            int a = face.VertexIndices[0];
            int b = face.VertexIndices[1];
            int c = face.VertexIndices[2];

            // 获取或生成边的中点
            int ab = GetMidPointIndex(data.Vertices, midPointCache, a, b);
            int bc = GetMidPointIndex(data.Vertices, midPointCache, b, c);
            int ca = GetMidPointIndex(data.Vertices, midPointCache, c, a);

            // 拆分原三角形为4个新三角形
            newFaces.Add(new TriangleFace { VertexIndices = new int[]{a, ab, ca} });
            newFaces.Add(new TriangleFace { VertexIndices = new int[]{b, bc, ab} });
            newFaces.Add(new TriangleFace { VertexIndices = new int[]{c, ca, bc} });
            newFaces.Add(new TriangleFace { VertexIndices = new int[]{ab, bc, ca} });
        }

        data.Faces = newFaces;
    }

    return data;
}

// 获取边的中点索引(缓存避免重复生成)
private int GetMidPointIndex(List<Vector3> vertices, Dictionary<long, int> cache, int a, int b) {
    long key = ((long)Math.Min(a, b) << 32) | Math.Max(a, b);
    if (cache.ContainsKey(key)) return cache[key];

    Vector3 midPoint = ((vertices[a] + vertices[b]) / 2f).normalized;
    vertices.Add(midPoint);
    int index = vertices.Count - 1;
    cache.Add(key, index);
    return index;
}

// 辅助结构体:存储icosphere数据
public struct IcosphereData {
    public List<Vector3> Vertices;
    public List<TriangleFace> Faces;
    public List<int> OriginalVertexIndices;
}

public struct TriangleFace {
    public int[] VertexIndices;
}

2. 动态调整细分等级

函数本身支持传入任意细分等级,可根据星球尺寸动态选择:

// 根据星球半径选择合适的细分等级(示例逻辑,可按需调整)
int GetSubdivisionLevel(float planetRadius) {
    if (planetRadius < 40f) return 1;
    if (planetRadius < 80f) return 2;
    if (planetRadius < 160f) return 3;
    return 4;
}

3. 可选:最优细分等级计算(最小瓦片拉伸)

基于2米的瓦片宽度,通过匹配单位球上的弧长来计算最优等级:

int CalculateOptimalSubdivisionLevel(float planetRadius, float tileWidth = 2f) {
    float targetUnitArc = tileWidth / planetRadius; // 单位球上的目标弧长(弧度)
    int level = 0;

    while (level <= 5) { // 限制最大细分等级避免性能问题
        int faceCount = 20 * (int)Math.Pow(4, level);
        float triangleArea = (4 * Math.PI) / faceCount;
        // 近似正三角形的边长(单位球弧长)
        float edgeLength = (float)Math.Sqrt(2 * triangleArea / Math.Sqrt(3));

        // 匹配目标弧长的±10%范围
        if (edgeLength >= targetUnitArc * 0.9f && edgeLength <= targetUnitArc * 1.1f) {
            break;
        }
        level++;
    }

    return level;
}

关键说明

  • 原始icosphere的12个顶点周围会形成五边形区域,代码中通过标记原始顶点索引过滤这些区域,确保玩家看不到五边形瓦片。
  • 生成的球坐标默认是单位球,后续只需将父物体缩放至星球半径,再根据Theta和Phi旋转瓦片即可完成布局。

内容的提问来源于stack exchange,提问作者Louis VERAN

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 10:55:59