在已合入的平面 hexmap 之上增加球面世界,复用其单元记录(HexCellData / HexValues / HexFlags)、几何常量(HexMetrics)、网格容器(HexMeshData)与地形顶点编码 (HexTerrainVertexCode): - HexSphereTopology:二十面体(Goldberg)拓扑。单元是细分后二十面体的顶点, cellCount = 10f^2 + 2,12 个五边形恒为 [0,12)。方向不是算术:五边形只有 5 条边, 跨边查询一律走 directionOf(cell, other),因为不存在处处成立的 opposite。 - HexSphereMap:球面对应物。逐格脏标记(球面无分块网格),高程为径向, 含拾取、距离与笔刷。 - HexSphereMesh:整球地形与海洋网格。平面版用弦 v1->v5 跨越梯田带,这在平面上安全 只因其 5 个采样点共线;球面采样点在弧上,故按扇形同样拆成 4 段以免 T 型接缝。 - HexSphereGenerator:三维值噪声 fBm、按噪声自身分位数定海平面、生物群系。 - HexMapModule:28 个 hexmap.sphere* 脚本绑定。 - examples/hex-planet:轨道相机 + 两个片元着色器。平面着色器把 vWorldPos.y 当海拔、 N.y 当“上”、细节噪声投影到 XZ、远处加距离雾,这四点在球面上均不成立。 - test/hexmap_sphere.cpp:31 个用例;docs/usr/modules/hexmap.md 补充球面章节。 验证:hexmap 套件 66/66 通过(含 31 个球面用例);示例实机跑通并自捕获 hex-planet.png。网格闭合性由焊接后的欧拉特征 V - E + F = 2 断言,朝向由 “无内向三角形”断言。
348 lines
24 KiB
Markdown
348 lines
24 KiB
Markdown
# 六边形立体地图(Hex Map)
|
||
|
||
**脚本入口:** `hexmap`(`eve.HexMap`,模块槽位 `hexmap`)
|
||
|
||
`hexmap` 是引擎的**可编辑 3D 六边形地图**层:持有尖顶(pointy-top)六边形格网、
|
||
按 5×5 分块、按面(terrain / water / river / road)生成 CPU 网格并上传 GPU,同时
|
||
提供世界坐标拾取、邻居查询与笔刷编辑。参考实现是 Catlike Coding 的
|
||
[Hex Map](https://catlikecoding.com/unity/hex-map/) 工程;坐标、高程台地、悬崖、
|
||
水面与河道几何均与之一致。
|
||
|
||
## 布局与常量
|
||
|
||
| 项 | 值 | 说明 |
|
||
|----|----|------|
|
||
| 朝向 | 尖顶 | XZ 平面,+X 向东,+Z 向北,Y 为上 |
|
||
| `outerRadius` | 10 | 六边形外接圆半径 |
|
||
| `innerRadius` | 8.66025404 | `outerRadius * √3/2` |
|
||
| `elevationStep` | 3 | 每级高程的世界高度 |
|
||
| `solidFactor` / `waterFactor` | 0.8 / 0.6 | 实心区与水面内缩因子 |
|
||
| `terracesPerSlope` | 2(`terraceSteps` = 5) | 斜坡台地级数 |
|
||
| `kChunkSizeX/Z` | 5 / 5 | 分块尺寸;地图尺寸必须是它的整数倍 |
|
||
| 地面扰动 | XZ ±4,Y ±1.5 | `HexNoise`(种子决定,无需贴图资源) |
|
||
|
||
偏移坐标到轴坐标:`axial = offset - (z/2, 0)`(`z/2` 向下取整)。世界坐标:
|
||
`x = (axialX + 0.5·axialZ) · innerDiameter`,`z = axialZ · outerRadius · 1.5`。
|
||
`HexCoordinates::fromWorldPosition` 是其精确逆变换(含立方体取整修正)。
|
||
|
||
## C++ API 快查
|
||
|
||
### `HexMetrics`([HexMetrics.h](../../../src/modules/hexmap/HexMetrics.h))
|
||
|
||
常量与角点/边采样助手:`firstCorner` / `firstSolidCorner` / `firstWaterCorner` /
|
||
`bridge` / `waterBridge` / `solidEdgeMiddle` / `terraceLerp` / `wallLerp`,
|
||
以及 `elevationY` / `streamBedY` / `waterSurfaceY`。`EdgeVertices` 是边上 5 个
|
||
采样点(v1…v5)。
|
||
|
||
### `HexCoordinates`([HexCoordinates.h](../../../src/modules/hexmap/HexCoordinates.h))
|
||
|
||
轴坐标 `(x, z)`,`y = -x - z`。`step(direction, distance)`、`distanceTo`、
|
||
`fromOffset` / `offsetX` / `offsetZ`、`chunkColumn` / `chunkRow`、
|
||
`toWorldPosition` / `fromWorldPosition`。
|
||
|
||
### `HexMap`([HexMap.h](../../../src/modules/hexmap/HexMap.h))
|
||
|
||
值类型格网,`reset(cellCountX, cellCountZ, seed)` 建立地图(尺寸必须是 5 的倍数)。
|
||
|
||
- 拓扑:`contains` / `indexOf` / `coordinatesAt` / `tryGetNeighbor` / `edgeTypeTo`
|
||
- 分块:`chunkIndexOf` / `chunkColumnOf` / `chunkRowOf` / `chunkCenter` /
|
||
`takeDirtyChunk` / `markAllChunksDirty` / `dirtyChunkCount`
|
||
- 拾取:`pickCell(rayOrigin, rayDirection)` —— 无碰撞体,改为在候选格高程平面上
|
||
迭代求交并二分收敛
|
||
- 查询:`elevation` / `waterLevel` / `terrainType` / `urbanLevel` / `farmLevel` /
|
||
`plantLevel` / `specialIndex` / `isUnderwater` / `hasRiver` / `hasRoad` / `isWalled`
|
||
- 编辑:`setElevation` / `setWaterLevel` / `setTerrainType` / `setUrbanLevel` /
|
||
`setFarmLevel` / `setPlantLevel` / `setSpecialIndex` / `setWalled` /
|
||
`setOutgoingRiver` / `removeRiver` / `addRoad` / `removeRoads`
|
||
- 笔刷:`editElevation` / `editWaterLevel` / `editTerrainType` / `editFeatureLevel`
|
||
(六边形圆盘,`collectBrush` 可单独枚举)
|
||
|
||
跨格约束与参考工程一致:河流只能顺坡或从湖面流出;道路不能跨两级以上高程、
|
||
不能穿过河流中段、城市/特殊地物会移除道路;特殊地物在有河时被忽略。
|
||
|
||
### 网格生成([HexMapMesh.h](../../../src/modules/hexmap/HexMapMesh.h))
|
||
|
||
`buildChunkSurfaceMesh(map, chunkIndex, surface, out)`,`surface` 取
|
||
`HexSurface::{Terrain, Water, River, Road}`。生成物是 `HexMeshData`
|
||
(position / normal / uv / index,三角形不共享顶点,`finalize()` 写平面法线)。
|
||
|
||
分块所有权与参考工程一致:地面扇形按本块 25 格生成,**只有 NE / E / SE**
|
||
方向的混合带、台地与悬崖归属本块,其余由邻块生成,因此块与块之间不会重复或漏缝。
|
||
|
||
顶点只有两个纹理坐标,地形三层信息按
|
||
`u = layerA + layerB·8 + layerC·64`、`v = weightB + weightC·16` 打包;这是当前
|
||
顶点布局的**兼容桥**,不是地形数据格式。
|
||
|
||
`HexSurface::Fog`(值 4)是唯一不能由 `buildChunkSurfaceMesh` 生成的流,因为它
|
||
除了格子还需要可见性计数器,由 `buildFogMesh(map, visibility, chunkIndex, out)`
|
||
生成:每个"当前不可见"的格子输出一根六棱柱(顶面 + 侧裙),裙脚垂到最低可编辑
|
||
高程之下,因此迷雾边界上的悬崖立面也会被覆盖;第一纹理坐标 `u` 承载明暗
|
||
(`0` = 曾经探索过但此刻无人看见,`1` = 从未探索)。迷雾流在可见性跨越 0/1 时
|
||
自动把所在块标脏。
|
||
|
||
另外两条流由 [HexFeatures.h](../../../src/modules/hexmap/HexFeatures.h) 生成,也不走
|
||
`buildChunkSurfaceMesh`:`HexSurface::Wall`(值 5,`buildWallMesh`)输出城墙、墙塔
|
||
与桥梁,`HexSurface::Feature`(值 6,`buildFeatureMesh`)输出 urban / farm / plant /
|
||
special 地物。两者共用一条片元着色器,因为第一纹理坐标已经标明了部件种类:
|
||
|
||
| `u` | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|
||
|---|---|---|---|---|---|---|---|
|
||
| 部件 | 城墙 | 墙塔 | 桥梁 | urban | farm | plant | special |
|
||
|
||
城墙的归属规则与地形一致:只有当 `d ∈ {NE, E, SE}` 或该方向已经在网格之外时,
|
||
才由本块生成该边的城墙——这样每条内部边恰好被一侧生成,而地图边界仍然闭合。
|
||
道路横穿城墙边界时会开出城门(只生成两侧外段 + 两个端帽);高程差产生楔形挡墙。
|
||
|
||
### 地物与城墙([HexFeatures.h](../../../src/modules/hexmap/HexFeatures.h))
|
||
|
||
参考工程实例化 prefab 模型;本引擎不携带资源,因此 `HexFeatures.cpp` 用程序化几何
|
||
替代(墙=两侧面 + 顶盖,塔/桥/建筑=盒体,plant=压扁八面体,special=锥形柱)。
|
||
`HexHashGrid` 是参考工程 `hashGrid` 表的确定性替代:由 `(gridX, gridZ, seed,
|
||
channel)` 的整数哈希直接导出五个分量,因此不需要分配表、也没有全局状态,同一地图
|
||
种子永远得到同一套地物布局。`featureThreshold(level, index)` 返回参考工程的
|
||
三级阈值表 `{0,0,0.4} / {0,0.4,0.6} / {0.4,0.6,0.8}`,越界参数被钳制。
|
||
|
||
拾取规则与参考工程一致:urban / farm / plant 三个集合各自用格子哈希判定是否
|
||
命中,命中的里面哈希最小者胜出;`specialIndex` 非零时该格不生成普通地物。
|
||
|
||
### 地图生成([HexMapGenerator.h](../../../src/modules/hexmap/HexMapGenerator.h))
|
||
|
||
`generateHexMap(map, settings)` 把参考工程 `MapGenerator` 的流水线移植为纯 CPU 步骤:
|
||
|
||
1. 全部格子初始化到 `elevationMinimum`;
|
||
2. `regionCount` 个陆地区域从种子格生长(抖动、随机抬升/下沉,遵守地图与区域边界);
|
||
3. 海岸线侵蚀直至陆地占比接近 `landPercentage`;
|
||
4. 顺坡开凿河流(含湖泊概率与已有的汇流判定);
|
||
5. 40 轮下风向水汽传输(蒸发、降水、坡面径流、下渗),得到湿度与温度;
|
||
6. 按参考工程的生物群系表写地形类型与植被等级。
|
||
|
||
可复现性只依赖 `settings.seed` 与网格本身:不使用时间、`random_device` 或可变静态
|
||
状态。`elevationMaximum` 会被钳制到 `HexMetrics::kMaxElevation`;不合法或放不下的
|
||
区域参数在改动地图之前就以 `InvalidArgument` 拒绝。生成后的地图**全部格子未探索**,
|
||
因此调用方必须重新放置单位并重建全部块。
|
||
|
||
### 搜索与寻路([HexSearch.h](../../../src/modules/hexmap/HexSearch.h))
|
||
|
||
- `HexSearchContext`:可复用的搜索草稿(每格一条 `HexSearchData` + 按优先级分桶
|
||
的前沿队列)。用**相位**(`beginPhase()`,每次 +2)代替"清空 visited",因此一次
|
||
搜索不付全图重置的代价。
|
||
- `isValidDestination(map, c, occupied)`:`explored && explorable && !underwater &&
|
||
!occupied`;地形部分由 `HexCell.h` 的 `canHoldUnit()` 单点定义。
|
||
- `moveCost(map, from, to, direction, occupied)`:悬崖与墙界不可通行;起点有路 = 1,
|
||
否则平地 5 / 斜坡 10,再加目标格的 urban+farm+plant 等级。返回**负数**表示阻断。
|
||
- `findPath(map, scratch, from, to, rules, occupied) -> Result<HexPath>`:A\*(启发式
|
||
为六边形距离),代价按 `speed` 折算成"回合",`HexPath::turns` 给出每个格子到达
|
||
时的回合数。目标不可进入时返回 `NotFound`,端点越界或 scratch 尺寸不匹配返回
|
||
`InvalidArgument`。
|
||
- `collectVisibleCells(map, scratch, from, range, out)`:视线可达格(距离 + 目标格
|
||
`viewElevation` 与 `range + 起点 viewElevation` 比较,且不超过直接六边形距离)。
|
||
|
||
### 可见性与迷雾([HexVisibility.h](../../../src/modules/hexmap/HexVisibility.h))
|
||
|
||
`HexVisibility` 是**引用计数**而非布尔:多个观察者重叠时,只有最后一个离开才让格子
|
||
转回不可见;某格第一次被看见时同时把地图上的 `explored` **单向闩锁**置位,此后不再
|
||
回落。`increase` / `decrease` / `clear` / `resetVisibility`(按当前单位重算全图)。
|
||
|
||
### 单位([HexUnits.h](../../../src/modules/hexmap/HexUnits.h))
|
||
|
||
`HexUnitRegistry` 持有单位、寻路占用与视野贡献:
|
||
|
||
- `addUnit` 立即授予视野;`unitIdAt` / `isOccupied` 用于寻路占用谓词
|
||
(`occupancyQuery()` 返回借用 `*this` 的可调用对象)。
|
||
- 单位 id 就是注册表下标,删除会让后面的 id 前移;调用方不要跨删除缓存 id。
|
||
- 移动沿二次贝塞尔曲线(Unity 参考工程的转角做法),`advance(map, visibility,
|
||
scratch, id, dt)` 既推进动画又返回**当前位姿样本**(世界坐标已含高程与扰动),
|
||
`dt <= 0` 表示只读不推进。**目的地自 `beginTravel` 起即被占用**,视野则跟着
|
||
实际走过的格子移动(`visionIndex`)。
|
||
- `restore` 先清空再校验,被拒绝的载荷不会留下"半恢复"的注册表。
|
||
|
||
### 存档([HexSerializer.h](../../../src/modules/hexmap/HexSerializer.h))
|
||
|
||
`saveHexMap(map, units, out)` / `loadHexMap(bytes, map, units)`:小端、显式逐字节
|
||
写出的自描述载荷(magic `EVEHEX\0\0` + 版本 + 尺寸 + 种子 + 每格 `HexValues` /
|
||
`HexFlags` + 单位表)。版本不匹配会**拒绝**而不是猜测;载荷在触碰输出前整体校验
|
||
(长度、magic、版本、尺寸上限 512 且为 5 的倍数、单位格在界内、单位格可站立、
|
||
单位不重复),因此被拒绝的载荷不会留下部分写入的地图。修订号字段只作诊断。
|
||
|
||
### 球面后端([HexSphereTopology.h](../../../src/modules/hexmap/HexSphereTopology.h)、[HexSphereMap.h](../../../src/modules/hexmap/HexSphereMap.h)、[HexSphereMesh.h](../../../src/modules/hexmap/HexSphereMesh.h)、[HexSphereGenerator.h](../../../src/modules/hexmap/HexSphereGenerator.h))
|
||
|
||
前面所有小节都是**平面**地图:单元身份是 `HexCoordinates`,邻居是固定的整数方向偏移。
|
||
球面后端复用同一套单元记录(`HexCellData` / `HexValues` / `HexFlags`)、同一套几何常量
|
||
(`HexMetrics`)、同一个 `HexMeshData` 与同一份顶点编码,只替换这两件事——因为在闭合
|
||
球面上它们才真正不成立:
|
||
|
||
- **单元身份**是稠密的 `HexSphereCell`(`int32`),来自**二十面体(Goldberg)拓扑**:
|
||
二十面体按 `f = 2^subdivision` 细分后的**顶点**就是单元,于是
|
||
`cellCount = 10f² + 2`,其中恰好 12 个是五边形(恒为 `[0, 12)`,因为中点细分从不
|
||
重编号原始顶点);`edgeCount = 30f²`,`cornerCount = 20f²`,每个角点恰好被 3 格共享。
|
||
- **邻居关系**来自半边表。**方向不是算术**:六条边的方位逐格变化,而五边形只有五条边,
|
||
因此不存在处处成立的 `opposite(direction)`——所有跨边查询走
|
||
`directionOf(cell, other)`。方向 `d` 的边位于 `corner(d)` 与 `corner(d+1)` 之间,
|
||
这两个角点由该边两侧的单元**以相反顺序**共享。
|
||
|
||
`HexSphereMap` 是 `HexMap` 的球面对应物:持有单元存储、`HexNoise`、**逐格**脏标记
|
||
(球面没有分块网格,拓扑已经给每格一个全局稳定 id)、拾取、距离、刷子与全套编辑接口。
|
||
高程是**径向**的:`surfaceRadius = sphereRadius + elevation · elevationStep`。
|
||
`elevationStep` 默认取半径的 `0.006`,而不是平面的 `HexMetrics::kElevationStep`
|
||
(3 世界单位)——后者在半径 100 的星球上会让 13 级高程跨越 39% 半径。`HexFlags` 的
|
||
道路/河流 6 个 bit 槽直接由方向索引寻址,五边形上槽 5 永不置位。
|
||
|
||
`buildSphereTerrainMesh` / `buildSphereWaterMesh` 沿用平面构建器的角点矩阵方案,顶点
|
||
编码(`HexTerrainVertexCode`)完全一致,因此地形着色器的读法不变。三处平面假设被替换:
|
||
|
||
1. **边数**不是 6(五边形是 5 条),没有任何循环可以写死 `kHexDirectionCount`;
|
||
2. **边归属**:平面版靠分块网格(`direction <= SE`)保证每条共享边只发一次,球面改用
|
||
**较小的单元 id** 拥有该边——同一组边上的全序,同样保证恰好一次;
|
||
3. **"垂直"是径向**:坡度台地沿**弧**走水平步、沿**半径**走垂直步。
|
||
|
||
平面版的梯田带用弦 `v1 → v5` 跨越整条边,这在平面上安全**只因为**它的 5 个采样点共线;
|
||
球面上采样点在弧上,弦会在每条带坡度或崖壁的边留下 T 型接缝,因此球面版按扇形同样的
|
||
4 段拆分。地形顶点扰动也改为**切向**并把结果投影回原半径:切向偏移会以 `O(d²/r)` 改变
|
||
半径,而这里扰动量是单元尺寸的可观比例。
|
||
|
||
`generateSphereMap` 采样**三维**值噪声:`HexNoise` 是 XZ 平面场,用单元方向去采样会在
|
||
两极塌缩(方向的 XZ 投影趋零)、在经度回绕处接缝。海平面由噪声自身的分位数确定,因此
|
||
`landPercentage` 真正决定陆地占比,而不是听任噪声分布。输出是
|
||
`(seed, 拓扑, 半径)` 的纯函数。
|
||
|
||
## 脚本 API(`hexmap`)
|
||
|
||
```squirrel
|
||
hexmap.newGrid(gfx, cellCountX, cellCountZ, seed) // -> Result
|
||
hexmap.hasGrid() / cellCountX() / cellCountZ() / chunkCountX() / chunkCountZ() / chunkCount() / seed()
|
||
hexmap.surfaceCount() // -> 7,见下面的 surface 编号表
|
||
|
||
hexmap.takeDirtyChunk() // -> 脏块索引,-1 表示没有
|
||
hexmap.rebuildChunk(gfx, chunkIndex) // -> 生成出几何的面数
|
||
hexmap.rebuildDirtyChunks(gfx) // -> 重建全部脏块,返回面数合计
|
||
hexmap.chunkMeshAt(chunkIndex, surface) // -> Mesh(借用),无几何时为 null
|
||
hexmap.releaseMeshes(gfx) // 释放本模块创建的全部网格
|
||
|
||
hexmap.elevation(x, z) / waterLevel(x, z) / terrainType(x, z) / urbanLevel(x, z) / ...
|
||
hexmap.isUnderwater(x, z) / hasRiver(x, z) / hasRoad(x, z) / isWalled(x, z)
|
||
hexmap.specialIndex(x, z) / farmLevel(x, z) / plantLevel(x, z)
|
||
hexmap.setElevation(x, z, v) / setWaterLevel(x, z, v) / setTerrainType(x, z, v) / ...
|
||
hexmap.setUrbanLevel(x, z, v) / setFarmLevel(x, z, v) / setPlantLevel(x, z, v) / setSpecialIndex(x, z, v)
|
||
hexmap.setWalled(x, z, v) / setOutgoingRiver(x, z, direction) / removeRiver(x, z)
|
||
hexmap.addRoad(x, z, direction) / removeRoads(x, z)
|
||
hexmap.editElevation(x, z, radius, delta) / editWaterLevel(x, z, radius, delta)
|
||
hexmap.editTerrainType(x, z, radius, terrainType) / editFeatureLevel(x, z, radius, feature, delta)
|
||
|
||
// 世界坐标(含高程与扰动);同一坐标系的逐分量版本,供脚本直接喂给 renderable
|
||
hexmap.cellPositionX(x, z) / cellPositionY(x, z) / cellPositionZ(x, z)
|
||
hexmap.chunkCenterX(chunkIndex) / chunkCenterY(chunkIndex) / chunkCenterZ(chunkIndex)
|
||
hexmap.pickCell(ox, oy, oz, dx, dy, dz) // -> {ok, value=[x, z]}
|
||
|
||
// 迷雾
|
||
hexmap.isExplored(x, z) / isExplorable(x, z) / setExplored(x, z, v) / setExplorable(x, z, v)
|
||
hexmap.isCellVisible(x, z) / visibleCellCount() / resetVisibility()
|
||
|
||
// 单位
|
||
hexmap.unitCount() / addUnit(x, z, orientation) / removeUnit(id) / removeAllUnits()
|
||
hexmap.unitIdAt(x, z)
|
||
hexmap.unitSample(id) // -> {ok, value=[cellX, cellZ, worldX, worldY, worldZ, yaw, traveling]}
|
||
hexmap.travelUnit(id, path) / advanceUnits(dt)
|
||
|
||
// 寻路 / 存档 / 生成
|
||
hexmap.findPath(fx, fz, tx, tz) // -> {ok, value=[[x, z, turn], ...]}
|
||
hexmap.saveMap() // -> {ok, value=<二进制安全字符串>}
|
||
hexmap.loadMap(gfx, blob) // -> Result
|
||
hexmap.generateMap(gfx, seed, landPercentage, waterLevel, riverPercentage) // -> Result
|
||
|
||
// 球面(与平面地图相互独立,可同时存在)
|
||
hexmap.newSphere(gfx, subdivision, radius, seed) // -> Result;重建拓扑并释放上一颗星球的网格
|
||
hexmap.sphereReady() / sphereCellCount() / spherePentagonCount() / sphereSubdivision()
|
||
hexmap.sphereRadius() / sphereElevationStep() / sphereCellSpacing()
|
||
hexmap.sphereIsPentagon(cell) / sphereNeighborCount(cell) / sphereNeighbor(cell, direction)
|
||
hexmap.sphereDirection(cell) / sphereCornerDirection(cell, corner) / sphereCornerCount(cell)
|
||
hexmap.sphereElevation(cell) / sphereWaterLevel(cell) / sphereTerrainType(cell) / sphereIsUnderwater(cell)
|
||
hexmap.sphereCellAt(x, y, z) / sphereDistance(a, b)
|
||
hexmap.spherePickCell(ox, oy, oz, dx, dy, dz) // -> {ok, value=<cell>}
|
||
hexmap.sphereSetElevation(cell, v) / sphereSetTerrainType(cell, v)
|
||
hexmap.sphereEditElevation(cell, radius, delta) / sphereEditTerrainType(cell, radius, terrainType)
|
||
hexmap.generateSphere(gfx, seed, landPercentage, waterLevel) // -> Result;生成并重建两个网格
|
||
hexmap.rebuildSphere(gfx) // -> Result;批量编辑后重建
|
||
hexmap.sphereTerrainMesh() / sphereWaterMesh() // -> Mesh(借用),无几何时为 null
|
||
hexmap.sphereReleaseMeshes(gfx)
|
||
```
|
||
|
||
`surface` 取 `0=Terrain, 1=Water, 2=River, 3=Road, 4=Fog, 5=Wall, 6=Feature`。
|
||
`x, z` 是偏移坐标(列、行)——`pickCell` 的返回值同样是偏移坐标,与 `elevation` /
|
||
`editElevation` / `cellPositionX` 等所有格子接口同一坐标系,因此拾取结果可以直接回喂
|
||
给它们(`findPath` 与 `unitSample` 返回的格子也已经是偏移坐标)。`unitSample` 复用
|
||
`advance(dt=0)`,因此返回的是含高程与扰动的插值位姿,不是地格中心。`generateMap` 按
|
||
**当前网格尺寸**重新生成,会丢弃全部单位与迷雾状态,调用方必须在成功后重新放置单位
|
||
并重建全部块。
|
||
|
||
球面接口用 `cell`(稠密 id,`0 .. sphereCellCount()-1`),与平面接口的偏移坐标 `(x, z)`
|
||
是**两套互不相通的地址空间**:不要拿平面坐标去喂球面接口,反之亦然。`sphereDirection`
|
||
与 `sphereCornerDirection` 返回 `[x, y, z]` 单位向量。`newSphere` 只重建拓扑,
|
||
`generateSphere` 才写入单元并重建网格;两者都会释放上一颗星球的网格,因此调用方必须在
|
||
成功后重新绑定 renderable。`rebuildSphere` 是批量编辑后的重建入口——球面是**整张一个
|
||
网格**(没有分块),每次重建都是全量,应当攒够一批编辑再调一次,而不是每改一格就调。
|
||
|
||
## 生命周期与所有权
|
||
|
||
- 模块**只持有一张活动地图**(参考工程的编辑器同样只编辑一张格子);`newGrid`
|
||
会释放上一张地图生成的全部 GPU 网格。
|
||
- 模块拥有 `HexMap` 与它创建的每个 `graphics::Mesh`;`chunkMeshAt` 返回**借用**
|
||
指针,`Renderable3D` 只能在模块存活且未换图期间引用它。
|
||
- 某面在一张地图内可能从"有几何"变为"无几何"(例如最后一条道路被擦掉):此时
|
||
该面的 GPU 网格**延迟释放**(保留到下次 `newGrid`),`chunkMeshAt` 返回
|
||
`null`,调用方据此隐藏 renderable,不会出现悬垂指针。
|
||
- `loadMap` 走 `adoptGrid`:先释放旧网格、接管已解码的地图、重建搜索草稿与迷雾
|
||
计数器,再恢复单位。**调用方必须在成功后重建全部块并重新绑定 renderable**,
|
||
因为所有块都是脏的且没有任何 renderable 指向新网格。
|
||
- 主/渲染线程亲和;不调用未知回调,不跨外部调用持锁。
|
||
|
||
## 示例
|
||
|
||
`examples/hex-terrain-3d` 是本模块的完整示例(与之无关的 `examples/hex-terrain` 是
|
||
`mesh.hexterrain` 配方,保持原样):`WASD` 平移、`Q/E` 旋转、滚轮缩放,
|
||
`1`–`5` 切换高程/水面/地形/河流/道路模式,`[`/`]` 调整笔刷半径,`,`/`.` 切换地形
|
||
调色板,左右键升降或拖拽绘制河流与道路,`R` 换种子、`F` 换地图尺寸、`N` 重建。
|
||
第二轮补上战争迷雾、单位与寻路、存档:`tab` 切换选中单位,`g` 规划到光标格的路径,
|
||
`space` 出发,`escape` 取消预览,`t`/`y` 增删单位,`F5`/`F9` 存档与读档。第三轮补上
|
||
城墙/桥梁/地物与地图生成器:`m` 按当前尺寸生成一张程序化地图(重新随机种子)。
|
||
|
||
存档载荷经 `eve.Filesystem().writeText()` 落到保存目录,读回时先整体校验再接管
|
||
网格,因此损坏的文件不会破坏当前地图。
|
||
|
||
`examples/hex-planet` 是**球面后端**的示例:轨道相机绕星球旋转,`LMB` 拖拽改变
|
||
方位角/仰角,滚轮缩放,`space` 自动旋转,`R` 换种子,`[`/`]` 切换细分级别,`-`/`=`
|
||
调整陆地占比,`F5` 抓帧。默认姿态是静止的,所以两次运行抓到的画面几乎逐像素
|
||
一致(仅 ImGui 覆盖层有像素级差异)。它自带两个片元着色器:平面地形着色器把
|
||
`vWorldPos.y` 当海拔、`N.y` 当"上"、细节噪声投影到 XZ、远处用距离雾——这四件事在球面上
|
||
都不成立,因此球面版保留调色板与 BRDF、只替换这四处。另外宿主会在片元输出之后再套一层
|
||
Reinhard 风格的 tonemap(调试着色器输出常量 `1.0` 到达帧缓冲是 `224/255`,即
|
||
`y = x/(x+0.139)`),两个球面着色器都用 `hostTonemapInverse` 反解这条曲线,否则颜色会被
|
||
压缩两次且无法调参。
|
||
|
||
相机方面示例做了一个必要的修正:`eve.Camera3D()` 的默认裁剪范围是 `0.1..100`,
|
||
比一张地图的跨度还小,超过 100 世界单位的地形会被静默裁掉,远端的缩放档位因此只
|
||
能看到空屏。示例现在每帧按地图跨度推导远裁剪面,并让最远缩放距离随之放大
|
||
(`ZOOM_FAR_MARGIN`),所以任何地图尺寸都能完整入画。
|
||
|
||
## 已知边界(尚未实现)
|
||
|
||
瀑布与河口(estuary)几何尚未移植;河道是等宽中心线带,未做 7 分支交汇解算;迷雾按
|
||
"已探索 / 当前可见"两态渲染(不区分"当前可见"之外的中间态透明度),且迷雾柱顶取
|
||
自身与邻居的最高面,因此在高差极大的边界上会比参考工程略"鼓";地物是程序化几何
|
||
而非参考工程的 prefab 比例。
|
||
|
||
水面与参考工程逐方向对齐:邻居是水就发一个中心三角形 + 桥接四边形(桥接只由
|
||
NE/E/SE 侧发,避免水/水边界重复),邻居是陆地就发四个细分楔形三角形 + 四段岸带;
|
||
岸带**平铺在水面高度**,只借用邻格的实体角点平面位置,不借用它的高程——倾斜到岸顶的
|
||
写法会在每条六边形边上留下一条亮带。`HexSurface::Water` 因此完全水平,测试
|
||
`hexmap.mesh.waterSurfaceIsFlatAtAShore` 会断言所有水面顶点同高、所有法线朝 +Y。
|
||
|
||
地图生成器的两处刻意偏离参考工程(均已在源码中注释):侵蚀取共享搜索前沿中最低的
|
||
格子,而参考工程是均匀随机挑选可侵蚀格;温度抖动使用私有的值噪声,而参考工程采样
|
||
噪声贴图。生成器的 `HemisphereMode` 只实现参考工程的 `North` 映射。
|
||
|
||
球面后端的已知边界:球面**没有**河流、道路、城墙、地物、迷雾与单位的网格与查询接口
|
||
(`HexFlags` 的位可以设置,但没有对应的球面几何与寻路);海洋是每格一个六边形盖面,
|
||
没有浅滩、折射与岸线浪花,且按模块自身的规则 `waterLevel == elevation` 不算淹没,
|
||
所以海平面上的格子会露出;崖壁是径向直墙,没有悬垂或侵蚀细节;球面生成器没有河流、
|
||
侵蚀与板块构造——平面的 `HexMapGenerator` 没有移植过来。球面地图也**没有存档格式**。
|