fix: align shared terrain tile edges

Derive terrain vertex XY from one tile-relative grid expression instead of independently subtracting chunk and vertex offsets. Adjacent tiles now calculate their common edge with identical float values, preventing hairline T-junction seams far from the map origin.
This commit is contained in:
Kelsi
2026-07-23 20:17:19 -07:00
parent a8d7582f8a
commit 94ee231b12
2 changed files with 21 additions and 12 deletions

View File

@@ -3,6 +3,7 @@
## [v2.0.29-preview] — 2026-07-23
### World
- **Hairline seams between terrain tiles are gone.** Each tile's edge vertices were built by subtracting the chunk and per-vertex steps from the tile corner, so a tile's far edge (`…×TILE TILE`) and its neighbour's near edge (`(…1)×TILE`) — mathematically the same point — rounded to slightly different float32 values. The sub-yard gap opened T-junction cracks that showed as thin lines, worst far from the map origin (across Kalimdor). Vertex XY is now a single multiply from the tile index, `TILE_SIZE × (32 tile step/128)`, so the shared edge is bit-identical on both sides and the tiles meet exactly — no mesh-overlap or scale hacks
- **Game objects went missing after leaving an area and coming back, for the rest of the session.** Walking away dropped every instance of a mailbox or chest model, and the 60-second unused-model reaper then evicted the model itself; the reload on return did not reliably produce a drawn object. Game object models are now pinned in the renderer, so the reap/reload cycle never happens for them. They are a small bounded set — one per display ID actually encountered — and ambient doodads are still reaped normally
- Game objects are exempt from the adaptive doodad render distance. That distance collapses to its densest-scene value in any populated area, which in a city means roughly 200 units, so mailboxes and chests vanished well inside the range the server still considered them visible. They now hold a 600-unit floor; frustum and occlusion culling are unaffected
- GPU cull results are matched back to instances by ID rather than array index. The visibility buffer is read a full frame-slot cycle after it is written, and instances are appended and swap-removed in between, so a respawned object landing at the volatile tail of the array could inherit the verdict of whatever transient object held that slot two frames earlier

View File

@@ -189,15 +189,20 @@ std::vector<TerrainVertex> TerrainMeshGenerator::generateVertices(const MapChunk
const HeightMap& heightMap = chunk.heightMap;
// WoW terrain uses 145 heights stored in a 9x17 row-major grid layout
const float unitSize = CHUNK_SIZE / 8.0f; // 33.333/8 units per vertex step
// Compute render-space base from tile/chunk indices (same formula as generateChunkMesh).
const float tileNW_renderX = (32.0f - static_cast<float>(tileY)) * core::coords::TILE_SIZE;
const float tileNW_renderY = (32.0f - static_cast<float>(tileX)) * core::coords::TILE_SIZE;
float chunkBaseX = tileNW_renderX - static_cast<float>(chunkY) * CHUNK_SIZE; // iy controls renderX (east-west)
float chunkBaseY = tileNW_renderY - static_cast<float>(chunkX) * CHUNK_SIZE; // ix controls renderY (north-south)
float chunkBaseZ = chunk.position[2]; // height base (wowZ) from MCNK offset 112
// WoW terrain uses 145 heights stored in a 9x17 row-major grid layout.
//
// Vertex XY is derived as a SINGLE multiply from the tile index:
// pos = TILE_SIZE * (32 - tile - gridStep/128)
// where gridStep = chunk*8 + vertexOffset runs 0..128 across the 16 chunks of a
// tile. The previous form subtracted the chunk base and the per-vertex step
// separately, so a tile's far edge (…*C - C) and the neighbouring tile's near edge
// ((…-1)*C) rounded to slightly different float32 values — a sub-yard gap that
// opened hairline "blue" T-junction cracks between tiles, worst far from the map
// origin (across Kalimdor). Collapsing both to one multiply makes the shared edge
// bit-identical on either side, closing the seam without the overlap hacks.
const float TS = core::coords::TILE_SIZE;
constexpr float kStepsPerTile = 128.0f; // 16 chunks * 8 vertex steps
const float chunkBaseZ = chunk.position[2]; // height base (wowZ) from MCNK offset 112
for (int index = 0; index < 145; index++) {
int y = index / 17; // Row (0-8)
@@ -222,9 +227,12 @@ std::vector<TerrainVertex> TerrainMeshGenerator::generateVertices(const MapChunk
// vertex would propagate into normal computations and crash culling.
float h = heightMap.heights[index];
if (!std::isfinite(h)) h = 0.0f;
vertex.position[0] = chunkBaseX - (offsetY * unitSize); // renderX (row = west→east)
vertex.position[1] = chunkBaseY - (offsetX * unitSize); // renderY (col = north→south)
vertex.position[2] = chunkBaseZ + h; // renderZ
// Fractional grid position within the tile along each render axis (0..128).
const float gridX = static_cast<float>(chunkY) * 8.0f + offsetY; // row → renderX (west→east)
const float gridY = static_cast<float>(chunkX) * 8.0f + offsetX; // col → renderY (north→south)
vertex.position[0] = (32.0f - static_cast<float>(tileY) - gridX / kStepsPerTile) * TS; // renderX
vertex.position[1] = (32.0f - static_cast<float>(tileX) - gridY / kStepsPerTile) * TS; // renderY
vertex.position[2] = chunkBaseZ + h; // renderZ
// Normal
if (index * 3 + 2 < static_cast<int>(chunk.normals.size())) {