#include "rendering/terrain_manager.hpp" #include "rendering/terrain_renderer.hpp" #include "rendering/water_renderer.hpp" #include "rendering/m2_renderer.hpp" #include "rendering/wmo_renderer.hpp" #include "rendering/camera.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/adt_loader.hpp" #include "pipeline/m2_loader.hpp" #include "pipeline/wmo_loader.hpp" #include "pipeline/terrain_mesh.hpp" #include "core/logger.hpp" #include #include #include #include #include #include #include namespace wowee { namespace rendering { namespace { bool decodeLayerAlpha(const pipeline::MapChunk& chunk, size_t layerIdx, std::vector& outAlpha) { if (layerIdx >= chunk.layers.size()) return false; const auto& layer = chunk.layers[layerIdx]; if (!layer.useAlpha() || layer.offsetMCAL >= chunk.alphaMap.size()) return false; size_t offset = layer.offsetMCAL; size_t layerSize = chunk.alphaMap.size() - offset; for (size_t j = layerIdx + 1; j < chunk.layers.size(); j++) { if (chunk.layers[j].useAlpha()) { layerSize = chunk.layers[j].offsetMCAL - offset; break; } } outAlpha.assign(4096, 255); if (layer.compressedAlpha()) { size_t readPos = offset; size_t writePos = 0; while (writePos < 4096 && readPos < chunk.alphaMap.size()) { uint8_t cmd = chunk.alphaMap[readPos++]; bool fill = (cmd & 0x80) != 0; int count = (cmd & 0x7F) + 1; if (fill) { if (readPos >= chunk.alphaMap.size()) break; uint8_t val = chunk.alphaMap[readPos++]; for (int i = 0; i < count && writePos < 4096; i++) { outAlpha[writePos++] = val; } } else { for (int i = 0; i < count && writePos < 4096 && readPos < chunk.alphaMap.size(); i++) { outAlpha[writePos++] = chunk.alphaMap[readPos++]; } } } return true; } if (layerSize >= 4096) { std::copy(chunk.alphaMap.begin() + offset, chunk.alphaMap.begin() + offset + 4096, outAlpha.begin()); return true; } if (layerSize >= 2048) { for (size_t i = 0; i < 2048; i++) { uint8_t v = chunk.alphaMap[offset + i]; outAlpha[i * 2] = (v & 0x0F) * 17; outAlpha[i * 2 + 1] = (v >> 4) * 17; } return true; } return false; } } // namespace TerrainManager::TerrainManager() { } TerrainManager::~TerrainManager() { // Stop worker thread before cleanup (containers clean up via destructors) if (workerRunning.load()) { workerRunning.store(false); queueCV.notify_all(); if (workerThread.joinable()) { workerThread.join(); } } } bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* renderer) { assetManager = assets; terrainRenderer = renderer; if (!assetManager) { LOG_ERROR("Asset manager is null"); return false; } if (!terrainRenderer) { LOG_ERROR("Terrain renderer is null"); return false; } // Start background worker thread workerRunning.store(true); workerThread = std::thread(&TerrainManager::workerLoop, this); LOG_INFO("Terrain manager initialized (async loading enabled)"); LOG_INFO(" Map: ", mapName); LOG_INFO(" Load radius: ", loadRadius, " tiles"); LOG_INFO(" Unload radius: ", unloadRadius, " tiles"); return true; } void TerrainManager::update(const Camera& camera, float deltaTime) { if (!streamingEnabled || !assetManager || !terrainRenderer) { return; } // Always process ready tiles each frame (GPU uploads from background thread) processReadyTiles(); timeSinceLastUpdate += deltaTime; // Only update streaming periodically (not every frame) if (timeSinceLastUpdate < updateInterval) { return; } timeSinceLastUpdate = 0.0f; // Get current tile from camera position // GL coordinate mapping: GL Y = -(wowX - ZEROPOINT), GL X = -(wowZ - ZEROPOINT), GL Z = height // worldToTile expects: worldX = -glY (maps to tileX), worldY = glX (maps to tileY) glm::vec3 camPos = camera.getPosition(); TileCoord newTile = worldToTile(-camPos.y, camPos.x); // Check if we've moved to a different tile if (newTile.x != currentTile.x || newTile.y != currentTile.y) { LOG_DEBUG("Camera moved to tile [", newTile.x, ",", newTile.y, "]"); currentTile = newTile; } // Stream tiles if we've moved significantly or initial load if (newTile.x != lastStreamTile.x || newTile.y != lastStreamTile.y) { LOG_INFO("Streaming: cam=(", camPos.x, ",", camPos.y, ",", camPos.z, ") tile=[", newTile.x, ",", newTile.y, "] loaded=", loadedTiles.size()); streamTiles(); lastStreamTile = newTile; } } // Synchronous fallback for initial tile loading (before worker thread is useful) bool TerrainManager::loadTile(int x, int y) { TileCoord coord = {x, y}; // Check if already loaded if (loadedTiles.find(coord) != loadedTiles.end()) { return true; } // Don't retry tiles that already failed if (failedTiles.find(coord) != failedTiles.end()) { return false; } LOG_INFO("Loading terrain tile [", x, ",", y, "] (synchronous)"); auto pending = prepareTile(x, y); if (!pending) { failedTiles[coord] = true; return false; } finalizeTile(std::move(pending)); return true; } std::unique_ptr TerrainManager::prepareTile(int x, int y) { TileCoord coord = {x, y}; LOG_INFO("Preparing tile [", x, ",", y, "] (CPU work)"); // Load ADT file std::string adtPath = getADTPath(coord); auto adtData = assetManager->readFile(adtPath); if (adtData.empty()) { LOG_WARNING("Failed to load ADT file: ", adtPath); return nullptr; } // Parse ADT pipeline::ADTTerrain terrain = pipeline::ADTLoader::load(adtData); if (!terrain.isLoaded()) { LOG_ERROR("Failed to parse ADT terrain: ", adtPath); return nullptr; } // Set tile coordinates so mesh knows where to position this tile in world terrain.coord.x = x; terrain.coord.y = y; // Generate mesh pipeline::TerrainMesh mesh = pipeline::TerrainMeshGenerator::generate(terrain); if (mesh.validChunkCount == 0) { LOG_ERROR("Failed to generate terrain mesh: ", adtPath); return nullptr; } auto pending = std::make_unique(); pending->coord = coord; pending->terrain = std::move(terrain); pending->mesh = std::move(mesh); // Pre-load M2 doodads (CPU: read files, parse models) if (!pending->terrain.doodadPlacements.empty()) { std::unordered_set preparedModelIds; int skippedNameId = 0, skippedFileNotFound = 0, skippedInvalid = 0, skippedSkinNotFound = 0; for (const auto& placement : pending->terrain.doodadPlacements) { if (placement.nameId >= pending->terrain.doodadNames.size()) { skippedNameId++; continue; } std::string m2Path = pending->terrain.doodadNames[placement.nameId]; // Convert .mdx to .m2 if needed if (m2Path.size() > 4) { std::string ext = m2Path.substr(m2Path.size() - 4); for (char& c : ext) c = std::tolower(c); if (ext == ".mdx") { m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2"; } } // Use path hash as globally unique model ID (nameId is per-tile local) uint32_t modelId = static_cast(std::hash{}(m2Path)); // Parse model if not already done for this tile if (preparedModelIds.find(modelId) == preparedModelIds.end()) { std::vector m2Data = assetManager->readFile(m2Path); if (!m2Data.empty()) { pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data); // Try to load skin file std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin"; std::vector skinData = assetManager->readFile(skinPath); if (!skinData.empty()) { pipeline::M2Loader::loadSkin(skinData, m2Model); } else { skippedSkinNotFound++; LOG_WARNING("M2 skin not found: ", skinPath); } if (m2Model.isValid()) { PendingTile::M2Ready ready; ready.modelId = modelId; ready.model = std::move(m2Model); ready.path = m2Path; pending->m2Models.push_back(std::move(ready)); preparedModelIds.insert(modelId); } else { skippedInvalid++; LOG_WARNING("M2 model invalid (no verts/indices): ", m2Path); } } else { skippedFileNotFound++; LOG_WARNING("M2 file not found: ", m2Path); } } // Store placement data for instance creation on main thread if (preparedModelIds.count(modelId)) { const float ZEROPOINT = 32.0f * 533.33333f; float wowX = placement.position[0]; float wowY = placement.position[1]; float wowZ = placement.position[2]; PendingTile::M2Placement p; p.modelId = modelId; p.uniqueId = placement.uniqueId; p.position = glm::vec3( -(wowZ - ZEROPOINT), -(wowX - ZEROPOINT), wowY ); p.rotation = glm::vec3( -placement.rotation[2] * 3.14159f / 180.0f, -placement.rotation[0] * 3.14159f / 180.0f, placement.rotation[1] * 3.14159f / 180.0f ); p.scale = placement.scale / 1024.0f; pending->m2Placements.push_back(p); } } if (skippedNameId > 0 || skippedFileNotFound > 0 || skippedInvalid > 0) { LOG_WARNING("Tile [", x, ",", y, "] doodad issues: ", skippedNameId, " bad nameId, ", skippedFileNotFound, " file not found, ", skippedInvalid, " invalid model, ", skippedSkinNotFound, " skin not found"); } } // Pre-load WMOs (CPU: read files, parse models and groups) if (!pending->terrain.wmoPlacements.empty()) { for (const auto& placement : pending->terrain.wmoPlacements) { if (placement.nameId >= pending->terrain.wmoNames.size()) continue; const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId]; std::vector wmoData = assetManager->readFile(wmoPath); if (wmoData.empty()) continue; pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData); if (wmoModel.nGroups > 0) { std::string basePath = wmoPath; std::string extension; if (basePath.size() > 4) { extension = basePath.substr(basePath.size() - 4); std::string extLower = extension; for (char& c : extLower) c = std::tolower(c); if (extLower == ".wmo") { basePath = basePath.substr(0, basePath.size() - 4); } } for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) { char groupSuffix[16]; snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str()); std::string groupPath = basePath + groupSuffix; std::vector groupData = assetManager->readFile(groupPath); if (groupData.empty()) { snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi); groupData = assetManager->readFile(basePath + groupSuffix); } if (groupData.empty()) { snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi); groupData = assetManager->readFile(basePath + groupSuffix); } if (!groupData.empty()) { pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi); } } } if (!wmoModel.groups.empty()) { const float ZEROPOINT = 32.0f * 533.33333f; glm::vec3 pos( -(placement.position[2] - ZEROPOINT), -(placement.position[0] - ZEROPOINT), placement.position[1] ); glm::vec3 rot( -placement.rotation[2] * 3.14159f / 180.0f, -placement.rotation[0] * 3.14159f / 180.0f, (placement.rotation[1] + 180.0f) * 3.14159f / 180.0f ); // Pre-load WMO doodads (M2 models inside WMO) if (!wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) { glm::mat4 wmoMatrix(1.0f); wmoMatrix = glm::translate(wmoMatrix, pos); wmoMatrix = glm::rotate(wmoMatrix, rot.z, glm::vec3(0, 0, 1)); wmoMatrix = glm::rotate(wmoMatrix, rot.y, glm::vec3(0, 1, 0)); wmoMatrix = glm::rotate(wmoMatrix, rot.x, glm::vec3(1, 0, 0)); const auto& doodadSet = wmoModel.doodadSets[0]; for (uint32_t di = 0; di < doodadSet.count; di++) { uint32_t doodadIdx = doodadSet.startIndex + di; if (doodadIdx >= wmoModel.doodads.size()) break; const auto& doodad = wmoModel.doodads[doodadIdx]; auto nameIt = wmoModel.doodadNames.find(doodad.nameIndex); if (nameIt == wmoModel.doodadNames.end()) continue; std::string m2Path = nameIt->second; if (m2Path.empty()) continue; if (m2Path.size() > 4) { std::string ext = m2Path.substr(m2Path.size() - 4); for (char& c : ext) c = std::tolower(c); if (ext == ".mdx" || ext == ".mdl") { m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2"; } } uint32_t doodadModelId = static_cast(std::hash{}(m2Path)); std::vector m2Data = assetManager->readFile(m2Path); if (m2Data.empty()) continue; pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data); std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin"; std::vector skinData = assetManager->readFile(skinPath); if (!skinData.empty()) { pipeline::M2Loader::loadSkin(skinData, m2Model); } if (!m2Model.isValid()) continue; // Build doodad's local transform (WoW coordinates) // WMO doodads use quaternion rotation glm::mat4 doodadLocal(1.0f); doodadLocal = glm::translate(doodadLocal, doodad.position); doodadLocal *= glm::mat4_cast(doodad.rotation); doodadLocal = glm::scale(doodadLocal, glm::vec3(doodad.scale)); // Full world transform = WMO world transform * doodad local transform glm::mat4 worldMatrix = wmoMatrix * doodadLocal; // Extract world position for frustum culling glm::vec3 worldPos = glm::vec3(worldMatrix[3]); PendingTile::WMODoodadReady doodadReady; doodadReady.modelId = doodadModelId; doodadReady.model = std::move(m2Model); doodadReady.worldPosition = worldPos; doodadReady.modelMatrix = worldMatrix; pending->wmoDoodads.push_back(std::move(doodadReady)); } } PendingTile::WMOReady ready; ready.modelId = placement.uniqueId; ready.model = std::move(wmoModel); ready.position = pos; ready.rotation = rot; pending->wmoModels.push_back(std::move(ready)); } } } LOG_INFO("Prepared tile [", x, ",", y, "]: ", pending->m2Models.size(), " M2 models, ", pending->m2Placements.size(), " M2 placements, ", pending->wmoModels.size(), " WMOs, ", pending->wmoDoodads.size(), " WMO doodads"); return pending; } void TerrainManager::finalizeTile(std::unique_ptr pending) { int x = pending->coord.x; int y = pending->coord.y; TileCoord coord = pending->coord; LOG_INFO("Finalizing tile [", x, ",", y, "] (GPU upload)"); // Check if tile was already loaded (race condition guard) or failed if (loadedTiles.find(coord) != loadedTiles.end()) { return; } if (failedTiles.find(coord) != failedTiles.end()) { return; } // Upload terrain to GPU if (!terrainRenderer->loadTerrain(pending->mesh, pending->terrain.textures, x, y)) { LOG_ERROR("Failed to upload terrain to GPU for tile [", x, ",", y, "]"); failedTiles[coord] = true; return; } // Load water if (waterRenderer) { waterRenderer->loadFromTerrain(pending->terrain, true, x, y); } std::vector m2InstanceIds; std::vector wmoInstanceIds; std::vector tileUniqueIds; // Upload M2 models to GPU and create instances if (m2Renderer && assetManager) { if (!m2Renderer->getModelCount()) { m2Renderer->initialize(assetManager); } // Upload unique models std::unordered_set uploadedModelIds; for (auto& m2Ready : pending->m2Models) { if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) { uploadedModelIds.insert(m2Ready.modelId); } } // Create instances (deduplicate by uniqueId across tile boundaries) int loadedDoodads = 0; int skippedDedup = 0; for (const auto& p : pending->m2Placements) { // Skip if this doodad was already placed by a neighboring tile if (placedDoodadIds.count(p.uniqueId)) { skippedDedup++; continue; } uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale); if (instId) { m2InstanceIds.push_back(instId); placedDoodadIds.insert(p.uniqueId); tileUniqueIds.push_back(p.uniqueId); loadedDoodads++; } } LOG_INFO(" Loaded doodads for tile [", x, ",", y, "]: ", loadedDoodads, " instances (", uploadedModelIds.size(), " new models, ", skippedDedup, " dedup skipped)"); } // Upload WMO models to GPU and create instances if (wmoRenderer && assetManager) { if (!wmoRenderer->getModelCount()) { wmoRenderer->initialize(assetManager); } int loadedWMOs = 0; int loadedLiquids = 0; for (auto& wmoReady : pending->wmoModels) { if (wmoRenderer->loadModel(wmoReady.model, wmoReady.modelId)) { uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation); if (wmoInstId) { wmoInstanceIds.push_back(wmoInstId); loadedWMOs++; // Load WMO liquids (canals, pools, etc.) if (waterRenderer) { // Compute the same model matrix as WMORenderer uses glm::mat4 modelMatrix = glm::mat4(1.0f); modelMatrix = glm::translate(modelMatrix, wmoReady.position); modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)); modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f)); // Load liquids from each WMO group for (const auto& group : wmoReady.model.groups) { if (group.liquid.hasLiquid()) { waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId); loadedLiquids++; } } } } } } if (loadedLiquids > 0) { LOG_INFO(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids); } // Upload WMO doodad M2 models if (m2Renderer) { for (auto& doodad : pending->wmoDoodads) { m2Renderer->loadModel(doodad.model, doodad.modelId); uint32_t wmoDoodadInstId = m2Renderer->createInstanceWithMatrix( doodad.modelId, doodad.modelMatrix, doodad.worldPosition); if (wmoDoodadInstId) m2InstanceIds.push_back(wmoDoodadInstId); } } if (loadedWMOs > 0) { LOG_INFO(" Loaded WMOs for tile [", x, ",", y, "]: ", loadedWMOs); } } // Create tile entry auto tile = std::make_unique(); tile->coord = coord; tile->terrain = std::move(pending->terrain); tile->mesh = std::move(pending->mesh); tile->loaded = true; tile->m2InstanceIds = std::move(m2InstanceIds); tile->wmoInstanceIds = std::move(wmoInstanceIds); tile->doodadUniqueIds = std::move(tileUniqueIds); // Calculate world bounds getTileBounds(coord, tile->minX, tile->minY, tile->maxX, tile->maxY); loadedTiles[coord] = std::move(tile); LOG_INFO(" Finalized tile [", x, ",", y, "]"); } void TerrainManager::workerLoop() { LOG_INFO("Terrain worker thread started"); while (workerRunning.load()) { TileCoord coord; bool hasWork = false; { std::unique_lock lock(queueMutex); queueCV.wait(lock, [this]() { return !loadQueue.empty() || !workerRunning.load(); }); if (!workerRunning.load()) { break; } if (!loadQueue.empty()) { coord = loadQueue.front(); loadQueue.pop(); hasWork = true; } } if (hasWork) { auto pending = prepareTile(coord.x, coord.y); std::lock_guard lock(queueMutex); if (pending) { readyQueue.push(std::move(pending)); } else { // Mark as failed so we don't re-enqueue // We'll set failedTiles on the main thread in processReadyTiles // For now, just remove from pending tracking pendingTiles.erase(coord); } } } LOG_INFO("Terrain worker thread stopped"); } void TerrainManager::processReadyTiles() { // Process up to 2 ready tiles per frame to spread GPU work int processed = 0; const int maxPerFrame = 2; while (processed < maxPerFrame) { std::unique_ptr pending; { std::lock_guard lock(queueMutex); if (readyQueue.empty()) { break; } pending = std::move(readyQueue.front()); readyQueue.pop(); } if (pending) { TileCoord coord = pending->coord; finalizeTile(std::move(pending)); pendingTiles.erase(coord); processed++; } } } void TerrainManager::unloadTile(int x, int y) { TileCoord coord = {x, y}; // Also remove from pending if it was queued but not yet loaded pendingTiles.erase(coord); auto it = loadedTiles.find(coord); if (it == loadedTiles.end()) { return; } LOG_INFO("Unloading terrain tile [", x, ",", y, "]"); const auto& tile = it->second; // Remove doodad unique IDs from dedup set for (uint32_t uid : tile->doodadUniqueIds) { placedDoodadIds.erase(uid); } // Remove M2 doodad instances if (m2Renderer) { for (uint32_t id : tile->m2InstanceIds) { m2Renderer->removeInstance(id); } LOG_DEBUG(" Removed ", tile->m2InstanceIds.size(), " M2 instances"); } // Remove WMO instances and their liquids if (wmoRenderer) { for (uint32_t id : tile->wmoInstanceIds) { // Remove WMO liquids associated with this instance if (waterRenderer) { waterRenderer->removeWMO(id); } wmoRenderer->removeInstance(id); } LOG_DEBUG(" Removed ", tile->wmoInstanceIds.size(), " WMO instances"); } // Remove terrain chunks for this tile if (terrainRenderer) { terrainRenderer->removeTile(x, y); } // Remove water surfaces for this tile if (waterRenderer) { waterRenderer->removeTile(x, y); } loadedTiles.erase(it); // Clean up any models that are no longer referenced if (m2Renderer) { m2Renderer->cleanupUnusedModels(); } if (wmoRenderer) { wmoRenderer->cleanupUnusedModels(); } } void TerrainManager::unloadAll() { // Stop worker thread if (workerRunning.load()) { workerRunning.store(false); queueCV.notify_all(); if (workerThread.joinable()) { workerThread.join(); } } // Clear queues { std::lock_guard lock(queueMutex); while (!loadQueue.empty()) loadQueue.pop(); while (!readyQueue.empty()) readyQueue.pop(); } pendingTiles.clear(); placedDoodadIds.clear(); LOG_INFO("Unloading all terrain tiles"); loadedTiles.clear(); failedTiles.clear(); // Clear terrain renderer if (terrainRenderer) { terrainRenderer->clear(); } // Clear water if (waterRenderer) { waterRenderer->clear(); } } TileCoord TerrainManager::worldToTile(float worldX, float worldY) const { // WoW world coordinate system: // - Tiles are 8533.33 units wide (TILE_SIZE) // - Tile (32, 32) is roughly at world origin for continents // - Coordinates increase going east (X) and south (Y) int tileX = 32 + static_cast(std::floor(worldX / TILE_SIZE)); int tileY = 32 - static_cast(std::floor(worldY / TILE_SIZE)); // Clamp to valid range (0-63) tileX = std::max(0, std::min(63, tileX)); tileY = std::max(0, std::min(63, tileY)); return {tileX, tileY}; } void TerrainManager::getTileBounds(const TileCoord& coord, float& minX, float& minY, float& maxX, float& maxY) const { // Calculate world bounds for this tile // Tile (32, 32) is at origin float offsetX = (32 - coord.x) * TILE_SIZE; float offsetY = (32 - coord.y) * TILE_SIZE; minX = offsetX - TILE_SIZE; minY = offsetY - TILE_SIZE; maxX = offsetX; maxY = offsetY; } std::string TerrainManager::getADTPath(const TileCoord& coord) const { // Format: World\Maps\{MapName}\{MapName}_{X}_{Y}.adt // Example: Azeroth_32_49.adt for tile at coord.x=32, coord.y=49 return "World\\Maps\\" + mapName + "\\" + mapName + "_" + std::to_string(coord.x) + "_" + std::to_string(coord.y) + ".adt"; } std::optional TerrainManager::getHeightAt(float glX, float glY) const { // Terrain mesh vertices use chunk.position directly (WoW coordinates) // But camera is in GL coordinates. We query using the mesh coordinates directly // since terrain is rendered without model transformation. // // The terrain mesh generation puts vertices at: // vertex.position[0] = chunk.position[0] - (offsetY * unitSize) // vertex.position[1] = chunk.position[1] - (offsetX * unitSize) // vertex.position[2] = chunk.position[2] + height // // So chunk spans: // X: [chunk.position[0] - 8*unitSize, chunk.position[0]] // Y: [chunk.position[1] - 8*unitSize, chunk.position[1]] const float unitSize = CHUNK_SIZE / 8.0f; // Query coordinates are the same as what we pass (terrain uses identity model matrix) float queryX = glX; float queryY = glY; for (const auto& [coord, tile] : loadedTiles) { if (!tile || !tile->loaded) continue; for (int cy = 0; cy < 16; cy++) { for (int cx = 0; cx < 16; cx++) { const auto& chunk = tile->terrain.getChunk(cx, cy); if (!chunk.hasHeightMap()) continue; float chunkMaxX = chunk.position[0]; float chunkMinX = chunk.position[0] - 8.0f * unitSize; float chunkMaxY = chunk.position[1]; float chunkMinY = chunk.position[1] - 8.0f * unitSize; if (queryX < chunkMinX || queryX > chunkMaxX || queryY < chunkMinY || queryY > chunkMaxY) { continue; } // Fractional position within chunk (0-8 range) float fracY = (chunk.position[0] - glX) / unitSize; // maps to offsetY float fracX = (chunk.position[1] - glY) / unitSize; // maps to offsetX fracX = glm::clamp(fracX, 0.0f, 8.0f); fracY = glm::clamp(fracY, 0.0f, 8.0f); // Bilinear interpolation on 9x9 outer grid int gx0 = static_cast(std::floor(fracX)); int gy0 = static_cast(std::floor(fracY)); int gx1 = std::min(gx0 + 1, 8); int gy1 = std::min(gy0 + 1, 8); float tx = fracX - gx0; float ty = fracY - gy0; // Outer vertex heights from the 9x17 layout // Outer vertex (gx, gy) is at index: gy * 17 + gx float h00 = chunk.heightMap.heights[gy0 * 17 + gx0]; float h10 = chunk.heightMap.heights[gy0 * 17 + gx1]; float h01 = chunk.heightMap.heights[gy1 * 17 + gx0]; float h11 = chunk.heightMap.heights[gy1 * 17 + gx1]; float h = h00 * (1 - tx) * (1 - ty) + h10 * tx * (1 - ty) + h01 * (1 - tx) * ty + h11 * tx * ty; return chunk.position[2] + h; } } } return std::nullopt; } std::optional TerrainManager::getDominantTextureAt(float glX, float glY) const { const float unitSize = CHUNK_SIZE / 8.0f; std::vector alphaScratch; for (const auto& [coord, tile] : loadedTiles) { (void)coord; if (!tile || !tile->loaded) continue; for (int cy = 0; cy < 16; cy++) { for (int cx = 0; cx < 16; cx++) { const auto& chunk = tile->terrain.getChunk(cx, cy); if (!chunk.hasHeightMap() || chunk.layers.empty()) continue; float chunkMaxX = chunk.position[0]; float chunkMinX = chunk.position[0] - 8.0f * unitSize; float chunkMaxY = chunk.position[1]; float chunkMinY = chunk.position[1] - 8.0f * unitSize; if (glX < chunkMinX || glX > chunkMaxX || glY < chunkMinY || glY > chunkMaxY) { continue; } float fracY = (chunk.position[0] - glX) / unitSize; float fracX = (chunk.position[1] - glY) / unitSize; fracX = glm::clamp(fracX, 0.0f, 8.0f); fracY = glm::clamp(fracY, 0.0f, 8.0f); int alphaX = glm::clamp(static_cast((fracX / 8.0f) * 63.0f), 0, 63); int alphaY = glm::clamp(static_cast((fracY / 8.0f) * 63.0f), 0, 63); int alphaIndex = alphaY * 64 + alphaX; std::vector weights(chunk.layers.size(), 0); int accum = 0; for (size_t layerIdx = 1; layerIdx < chunk.layers.size(); layerIdx++) { int alpha = 0; if (decodeLayerAlpha(chunk, layerIdx, alphaScratch) && alphaIndex < static_cast(alphaScratch.size())) { alpha = alphaScratch[alphaIndex]; } weights[layerIdx] = alpha; accum += alpha; } weights[0] = glm::clamp(255 - accum, 0, 255); size_t bestLayer = 0; int bestWeight = weights[0]; for (size_t i = 1; i < weights.size(); i++) { if (weights[i] > bestWeight) { bestWeight = weights[i]; bestLayer = i; } } uint32_t texId = chunk.layers[bestLayer].textureId; if (texId < tile->terrain.textures.size()) { return tile->terrain.textures[texId]; } return std::nullopt; } } } return std::nullopt; } void TerrainManager::streamTiles() { // Enqueue tiles in radius around current tile for async loading { std::lock_guard lock(queueMutex); for (int dy = -loadRadius; dy <= loadRadius; dy++) { for (int dx = -loadRadius; dx <= loadRadius; dx++) { int tileX = currentTile.x + dx; int tileY = currentTile.y + dy; // Check valid range if (tileX < 0 || tileX > 63 || tileY < 0 || tileY > 63) { continue; } TileCoord coord = {tileX, tileY}; // Skip if already loaded, pending, or failed if (loadedTiles.find(coord) != loadedTiles.end()) continue; if (pendingTiles.find(coord) != pendingTiles.end()) continue; if (failedTiles.find(coord) != failedTiles.end()) continue; loadQueue.push(coord); pendingTiles[coord] = true; } } } // Notify worker thread that there's work queueCV.notify_one(); // Unload tiles beyond unload radius (well past the camera far clip) std::vector tilesToUnload; for (const auto& pair : loadedTiles) { const TileCoord& coord = pair.first; int dx = std::abs(coord.x - currentTile.x); int dy = std::abs(coord.y - currentTile.y); // Chebyshev distance if (dx > unloadRadius || dy > unloadRadius) { tilesToUnload.push_back(coord); } } for (const auto& coord : tilesToUnload) { unloadTile(coord.x, coord.y); } if (!tilesToUnload.empty()) { LOG_INFO("Unloaded ", tilesToUnload.size(), " distant tiles, ", loadedTiles.size(), " remain"); } } } // namespace rendering } // namespace wowee