如何计算 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
相关产品推荐
相关产品推荐

