#include "rendering/world_map.hpp" #include "rendering/vk_context.hpp" #include "rendering/vk_texture.hpp" #include "rendering/vk_render_target.hpp" #include "rendering/vk_pipeline.hpp" #include "rendering/vk_shader.hpp" #include "rendering/vk_utils.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_layout.hpp" #include "core/coordinates.hpp" #include "core/input.hpp" #include "core/logger.hpp" #include #include #include #include namespace wowee { namespace rendering { namespace { bool isRootContinent(const std::vector& zones, int idx) { if (idx < 0 || idx >= static_cast(zones.size())) return false; const auto& c = zones[idx]; if (c.areaID != 0 || c.wmaID == 0) return false; for (const auto& z : zones) { if (z.areaID == 0 && z.parentWorldMapID == c.wmaID) { return true; } } return false; } bool isLeafContinent(const std::vector& zones, int idx) { if (idx < 0 || idx >= static_cast(zones.size())) return false; const auto& c = zones[idx]; if (c.areaID != 0) return false; return c.parentWorldMapID != 0; } } // namespace // Push constant for world map tile composite vertex shader struct WorldMapTilePush { glm::vec2 gridOffset; // 8 bytes float gridCols; // 4 bytes float gridRows; // 4 bytes }; // 16 bytes WorldMap::WorldMap() = default; WorldMap::~WorldMap() { shutdown(); } bool WorldMap::initialize(VkContext* ctx, pipeline::AssetManager* am) { if (initialized) return true; vkCtx = ctx; assetManager = am; VkDevice device = vkCtx->getDevice(); // --- Composite render target (1024x768) --- compositeTarget = std::make_unique(); if (!compositeTarget->create(*vkCtx, FBO_W, FBO_H)) { LOG_ERROR("WorldMap: failed to create composite render target"); return false; } // --- Quad vertex buffer (unit quad: pos2 + uv2) --- float quadVerts[] = { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, }; auto quadBuf = uploadBuffer(*vkCtx, quadVerts, sizeof(quadVerts), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); quadVB = quadBuf.buffer; quadVBAlloc = quadBuf.allocation; // --- Descriptor set layout: 1 combined image sampler at binding 0 --- VkDescriptorSetLayoutBinding samplerBinding{}; samplerBinding.binding = 0; samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; samplerBinding.descriptorCount = 1; samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; samplerSetLayout = createDescriptorSetLayout(device, { samplerBinding }); // --- Descriptor pool (24 tile + 1 display = 25) --- VkDescriptorPoolSize poolSize{}; poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; poolSize.descriptorCount = MAX_DESC_SETS; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; poolInfo.maxSets = MAX_DESC_SETS; poolInfo.poolSizeCount = 1; poolInfo.pPoolSizes = &poolSize; vkCreateDescriptorPool(device, &poolInfo, nullptr, &descPool); // --- Allocate descriptor sets: 12*2 tile + 1 display = 25 --- constexpr uint32_t totalSets = 25; std::vector layouts(totalSets, samplerSetLayout); VkDescriptorSetAllocateInfo allocInfo{}; allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; allocInfo.descriptorPool = descPool; allocInfo.descriptorSetCount = totalSets; allocInfo.pSetLayouts = layouts.data(); VkDescriptorSet allSets[25]; vkAllocateDescriptorSets(device, &allocInfo, allSets); for (int f = 0; f < 2; f++) for (int t = 0; t < 12; t++) tileDescSets[f][t] = allSets[f * 12 + t]; imguiDisplaySet = allSets[24]; // --- Write display descriptor set → composite render target --- VkDescriptorImageInfo compositeImgInfo = compositeTarget->descriptorInfo(); VkWriteDescriptorSet displayWrite{}; displayWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; displayWrite.dstSet = imguiDisplaySet; displayWrite.dstBinding = 0; displayWrite.descriptorCount = 1; displayWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; displayWrite.pImageInfo = &compositeImgInfo; vkUpdateDescriptorSets(device, 1, &displayWrite, 0, nullptr); // --- Pipeline layout: samplerSetLayout + push constant (16 bytes, vertex) --- VkPushConstantRange tilePush{}; tilePush.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; tilePush.offset = 0; tilePush.size = sizeof(WorldMapTilePush); tilePipelineLayout = createPipelineLayout(device, { samplerSetLayout }, { tilePush }); // --- Vertex input: pos2 (loc 0) + uv2 (loc 1), stride 16 --- VkVertexInputBindingDescription binding{}; binding.binding = 0; binding.stride = 4 * sizeof(float); binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; std::vector attrs(2); attrs[0] = { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }; attrs[1] = { 1, 0, VK_FORMAT_R32G32_SFLOAT, 2 * sizeof(float) }; // --- Load tile shaders and build pipeline --- { VkShaderModule vs, fs; if (!vs.loadFromFile(device, "assets/shaders/world_map.vert.spv") || !fs.loadFromFile(device, "assets/shaders/world_map.frag.spv")) { LOG_ERROR("WorldMap: failed to load tile shaders"); return false; } tilePipeline = PipelineBuilder() .setShaders(vs.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), fs.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) .setVertexInput({ binding }, attrs) .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setNoDepthTest() .setColorBlendAttachment(PipelineBuilder::blendDisabled()) .setLayout(tilePipelineLayout) .setRenderPass(compositeTarget->getRenderPass()) .setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }) .build(device); vs.destroy(); fs.destroy(); } if (!tilePipeline) { LOG_ERROR("WorldMap: failed to create tile pipeline"); return false; } initialized = true; LOG_INFO("WorldMap initialized (", FBO_W, "x", FBO_H, " composite)"); return true; } void WorldMap::shutdown() { if (!vkCtx) return; VkDevice device = vkCtx->getDevice(); VmaAllocator alloc = vkCtx->getAllocator(); vkDeviceWaitIdle(device); if (tilePipeline) { vkDestroyPipeline(device, tilePipeline, nullptr); tilePipeline = VK_NULL_HANDLE; } if (tilePipelineLayout) { vkDestroyPipelineLayout(device, tilePipelineLayout, nullptr); tilePipelineLayout = VK_NULL_HANDLE; } if (descPool) { vkDestroyDescriptorPool(device, descPool, nullptr); descPool = VK_NULL_HANDLE; } if (samplerSetLayout) { vkDestroyDescriptorSetLayout(device, samplerSetLayout, nullptr); samplerSetLayout = VK_NULL_HANDLE; } if (quadVB) { vmaDestroyBuffer(alloc, quadVB, quadVBAlloc); quadVB = VK_NULL_HANDLE; } destroyZoneTextures(); if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); } zones.clear(); initialized = false; vkCtx = nullptr; } void WorldMap::destroyZoneTextures() { if (!vkCtx) return; VkDevice device = vkCtx->getDevice(); VmaAllocator alloc = vkCtx->getAllocator(); for (auto& tex : zoneTextures) { if (tex) tex->destroy(device, alloc); } zoneTextures.clear(); for (auto& zone : zones) { for (auto& tex : zone.tileTextures) tex = nullptr; zone.tilesLoaded = false; } } void WorldMap::setMapName(const std::string& name) { if (mapName == name && !zones.empty()) return; mapName = name; destroyZoneTextures(); zones.clear(); continentIdx = -1; currentIdx = -1; compositedIdx = -1; pendingCompositeIdx = -1; viewLevel = ViewLevel::WORLD; } void WorldMap::setServerExplorationMask(const std::vector& masks, bool hasData) { if (!hasData || masks.empty()) { hasServerExplorationMask = false; serverExplorationMask.clear(); return; } hasServerExplorationMask = true; serverExplorationMask = masks; } // -------------------------------------------------------- // DBC zone loading (identical to GL version) // -------------------------------------------------------- void WorldMap::loadZonesFromDBC() { if (!zones.empty() || !assetManager) return; const auto* activeLayout = pipeline::getActiveDBCLayout(); const auto* mapL = activeLayout ? activeLayout->getLayout("Map") : nullptr; int mapID = -1; auto mapDbc = assetManager->loadDBC("Map.dbc"); if (mapDbc && mapDbc->isLoaded()) { for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) { std::string dir = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1); if (dir == mapName) { mapID = static_cast(mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0)); LOG_INFO("WorldMap: Map.dbc '", mapName, "' -> mapID=", mapID); break; } } } if (mapID < 0) { if (mapName == "Azeroth") mapID = 0; else if (mapName == "Kalimdor") mapID = 1; else if (mapName == "Expansion01") mapID = 530; else if (mapName == "Northrend") mapID = 571; else { LOG_WARNING("WorldMap: unknown map '", mapName, "'"); return; } } const auto* atL = activeLayout ? activeLayout->getLayout("AreaTable") : nullptr; std::unordered_map exploreFlagByAreaId; auto areaDbc = assetManager->loadDBC("AreaTable.dbc"); if (areaDbc && areaDbc->isLoaded() && areaDbc->getFieldCount() > 3) { for (uint32_t i = 0; i < areaDbc->getRecordCount(); i++) { const uint32_t areaId = areaDbc->getUInt32(i, atL ? (*atL)["ID"] : 0); const uint32_t exploreFlag = areaDbc->getUInt32(i, atL ? (*atL)["ExploreFlag"] : 3); if (areaId != 0) exploreFlagByAreaId[areaId] = exploreFlag; } } auto wmaDbc = assetManager->loadDBC("WorldMapArea.dbc"); if (!wmaDbc || !wmaDbc->isLoaded()) { LOG_WARNING("WorldMap: WorldMapArea.dbc not found"); return; } const auto* wmaL = activeLayout ? activeLayout->getLayout("WorldMapArea") : nullptr; for (uint32_t i = 0; i < wmaDbc->getRecordCount(); i++) { uint32_t recMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["MapID"] : 1); if (static_cast(recMapID) != mapID) continue; WorldMapZone zone; zone.wmaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ID"] : 0); zone.areaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["AreaID"] : 2); zone.areaName = wmaDbc->getString(i, wmaL ? (*wmaL)["AreaName"] : 3); zone.locLeft = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocLeft"] : 4); zone.locRight = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocRight"] : 5); zone.locTop = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocTop"] : 6); zone.locBottom = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocBottom"] : 7); zone.displayMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["DisplayMapID"] : 8); zone.parentWorldMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ParentWorldMapID"] : 10); auto exploreIt = exploreFlagByAreaId.find(zone.areaID); if (exploreIt != exploreFlagByAreaId.end()) zone.exploreFlag = exploreIt->second; int idx = static_cast(zones.size()); LOG_INFO("WorldMap: zone[", idx, "] areaID=", zone.areaID, " '", zone.areaName, "' L=", zone.locLeft, " R=", zone.locRight, " T=", zone.locTop, " B=", zone.locBottom); if (zone.areaID == 0 && continentIdx < 0) continentIdx = idx; zones.push_back(std::move(zone)); } // Derive continent bounds from child zones if missing for (int ci = 0; ci < static_cast(zones.size()); ci++) { auto& cont = zones[ci]; if (cont.areaID != 0) continue; if (std::abs(cont.locLeft) > 0.001f || std::abs(cont.locRight) > 0.001f || std::abs(cont.locTop) > 0.001f || std::abs(cont.locBottom) > 0.001f) continue; bool first = true; for (const auto& z : zones) { if (z.areaID == 0) continue; if (std::abs(z.locLeft - z.locRight) < 0.001f || std::abs(z.locTop - z.locBottom) < 0.001f) continue; if (z.parentWorldMapID != 0 && cont.wmaID != 0 && z.parentWorldMapID != cont.wmaID) continue; if (first) { cont.locLeft = z.locLeft; cont.locRight = z.locRight; cont.locTop = z.locTop; cont.locBottom = z.locBottom; first = false; } else { cont.locLeft = std::max(cont.locLeft, z.locLeft); cont.locRight = std::min(cont.locRight, z.locRight); cont.locTop = std::max(cont.locTop, z.locTop); cont.locBottom = std::min(cont.locBottom, z.locBottom); } } } LOG_INFO("WorldMap: loaded ", zones.size(), " zones for mapID=", mapID, ", continentIdx=", continentIdx); } int WorldMap::findBestContinentForPlayer(const glm::vec3& playerRenderPos) const { float wowX = playerRenderPos.y; float wowY = playerRenderPos.x; int bestIdx = -1; float bestArea = std::numeric_limits::max(); float bestCenterDist2 = std::numeric_limits::max(); bool hasLeafContinent = false; for (int i = 0; i < static_cast(zones.size()); i++) { if (zones[i].areaID == 0 && !isRootContinent(zones, i)) { hasLeafContinent = true; break; } } for (int i = 0; i < static_cast(zones.size()); i++) { const auto& z = zones[i]; if (z.areaID != 0) continue; if (hasLeafContinent && isRootContinent(zones, i)) continue; float minX = std::min(z.locLeft, z.locRight); float maxX = std::max(z.locLeft, z.locRight); float minY = std::min(z.locTop, z.locBottom); float maxY = std::max(z.locTop, z.locBottom); float spanX = maxX - minX; float spanY = maxY - minY; if (spanX < 0.001f || spanY < 0.001f) continue; bool contains = (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY); float area = spanX * spanY; if (contains) { if (area < bestArea) { bestArea = area; bestIdx = i; } } else if (bestIdx < 0) { float cx = (minX + maxX) * 0.5f, cy = (minY + maxY) * 0.5f; float dist2 = (wowX - cx) * (wowX - cx) + (wowY - cy) * (wowY - cy); if (dist2 < bestCenterDist2) { bestCenterDist2 = dist2; bestIdx = i; } } } return bestIdx; } int WorldMap::findZoneForPlayer(const glm::vec3& playerRenderPos) const { float wowX = playerRenderPos.y; float wowY = playerRenderPos.x; int bestIdx = -1; float bestArea = std::numeric_limits::max(); for (int i = 0; i < static_cast(zones.size()); i++) { const auto& z = zones[i]; if (z.areaID == 0) continue; float minX = std::min(z.locLeft, z.locRight); float maxX = std::max(z.locLeft, z.locRight); float minY = std::min(z.locTop, z.locBottom); float maxY = std::max(z.locTop, z.locBottom); float spanX = maxX - minX, spanY = maxY - minY; if (spanX < 0.001f || spanY < 0.001f) continue; if (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY) { float area = spanX * spanY; if (area < bestArea) { bestArea = area; bestIdx = i; } } } return bestIdx; } bool WorldMap::zoneBelongsToContinent(int zoneIdx, int contIdx) const { if (zoneIdx < 0 || zoneIdx >= static_cast(zones.size())) return false; if (contIdx < 0 || contIdx >= static_cast(zones.size())) return false; const auto& z = zones[zoneIdx]; const auto& cont = zones[contIdx]; if (z.areaID == 0) return false; if (z.parentWorldMapID != 0 && cont.wmaID != 0) return z.parentWorldMapID == cont.wmaID; auto rectMinX = [](const WorldMapZone& a) { return std::min(a.locLeft, a.locRight); }; auto rectMaxX = [](const WorldMapZone& a) { return std::max(a.locLeft, a.locRight); }; auto rectMinY = [](const WorldMapZone& a) { return std::min(a.locTop, a.locBottom); }; auto rectMaxY = [](const WorldMapZone& a) { return std::max(a.locTop, a.locBottom); }; float zMinX = rectMinX(z), zMaxX = rectMaxX(z); float zMinY = rectMinY(z), zMaxY = rectMaxY(z); if ((zMaxX - zMinX) < 0.001f || (zMaxY - zMinY) < 0.001f) return false; int bestContIdx = -1; float bestOverlap = 0.0f; for (int i = 0; i < static_cast(zones.size()); i++) { const auto& c = zones[i]; if (c.areaID != 0) continue; float cMinX = rectMinX(c), cMaxX = rectMaxX(c); float cMinY = rectMinY(c), cMaxY = rectMaxY(c); if ((cMaxX - cMinX) < 0.001f || (cMaxY - cMinY) < 0.001f) continue; float ox = std::max(0.0f, std::min(zMaxX, cMaxX) - std::max(zMinX, cMinX)); float oy = std::max(0.0f, std::min(zMaxY, cMaxY) - std::max(zMinY, cMinY)); float overlap = ox * oy; if (overlap > bestOverlap) { bestOverlap = overlap; bestContIdx = i; } } if (bestContIdx >= 0) return bestContIdx == contIdx; float centerX = (z.locLeft + z.locRight) * 0.5f; float centerY = (z.locTop + z.locBottom) * 0.5f; return centerX >= rectMinX(cont) && centerX <= rectMaxX(cont) && centerY >= rectMinY(cont) && centerY <= rectMaxY(cont); } bool WorldMap::getContinentProjectionBounds(int contIdx, float& left, float& right, float& top, float& bottom) const { if (contIdx < 0 || contIdx >= static_cast(zones.size())) return false; const auto& cont = zones[contIdx]; if (cont.areaID != 0) return false; if (std::abs(cont.locLeft - cont.locRight) > 0.001f && std::abs(cont.locTop - cont.locBottom) > 0.001f) { left = cont.locLeft; right = cont.locRight; top = cont.locTop; bottom = cont.locBottom; return true; } std::vector northEdges, southEdges, westEdges, eastEdges; for (int zi = 0; zi < static_cast(zones.size()); zi++) { if (!zoneBelongsToContinent(zi, contIdx)) continue; const auto& z = zones[zi]; if (std::abs(z.locLeft - z.locRight) < 0.001f || std::abs(z.locTop - z.locBottom) < 0.001f) continue; northEdges.push_back(std::max(z.locLeft, z.locRight)); southEdges.push_back(std::min(z.locLeft, z.locRight)); westEdges.push_back(std::max(z.locTop, z.locBottom)); eastEdges.push_back(std::min(z.locTop, z.locBottom)); } if (northEdges.size() < 3) { left = cont.locLeft; right = cont.locRight; top = cont.locTop; bottom = cont.locBottom; return std::abs(left - right) > 0.001f && std::abs(top - bottom) > 0.001f; } left = *std::max_element(northEdges.begin(), northEdges.end()); right = *std::min_element(southEdges.begin(), southEdges.end()); top = *std::max_element(westEdges.begin(), westEdges.end()); bottom = *std::min_element(eastEdges.begin(), eastEdges.end()); if (left <= right || top <= bottom) { left = cont.locLeft; right = cont.locRight; top = cont.locTop; bottom = cont.locBottom; } return std::abs(left - right) > 0.001f && std::abs(top - bottom) > 0.001f; } // -------------------------------------------------------- // Per-zone texture loading (Vulkan) // -------------------------------------------------------- void WorldMap::loadZoneTextures(int zoneIdx) { if (zoneIdx < 0 || zoneIdx >= static_cast(zones.size())) return; auto& zone = zones[zoneIdx]; if (zone.tilesLoaded) return; zone.tilesLoaded = true; const std::string& folder = zone.areaName; if (folder.empty()) return; std::vector candidateFolders; candidateFolders.push_back(folder); if (zone.areaID == 0 && mapName == "Azeroth") { if (folder != "Azeroth") candidateFolders.push_back("Azeroth"); if (folder != "EasternKingdoms") candidateFolders.push_back("EasternKingdoms"); } VkDevice device = vkCtx->getDevice(); int loaded = 0; for (int i = 0; i < 12; i++) { pipeline::BLPImage blpImage; bool found = false; for (const auto& testFolder : candidateFolders) { std::string path = "Interface\\WorldMap\\" + testFolder + "\\" + testFolder + std::to_string(i + 1) + ".blp"; blpImage = assetManager->loadTexture(path); if (blpImage.isValid()) { found = true; break; } } if (!found) { zone.tileTextures[i] = nullptr; continue; } auto tex = std::make_unique(); tex->upload(*vkCtx, blpImage.data.data(), blpImage.width, blpImage.height, VK_FORMAT_R8G8B8A8_UNORM, false); tex->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f); zone.tileTextures[i] = tex.get(); zoneTextures.push_back(std::move(tex)); loaded++; } LOG_INFO("WorldMap: loaded ", loaded, "/12 tiles for '", folder, "'"); } // -------------------------------------------------------- // Request composite (deferred to compositePass) // -------------------------------------------------------- void WorldMap::requestComposite(int zoneIdx) { if (zoneIdx < 0 || zoneIdx >= static_cast(zones.size())) return; pendingCompositeIdx = zoneIdx; } // -------------------------------------------------------- // Off-screen composite pass (call BEFORE main render pass) // -------------------------------------------------------- void WorldMap::compositePass(VkCommandBuffer cmd) { if (!initialized || pendingCompositeIdx < 0 || !compositeTarget) return; if (pendingCompositeIdx >= static_cast(zones.size())) { pendingCompositeIdx = -1; return; } int zoneIdx = pendingCompositeIdx; pendingCompositeIdx = -1; if (compositedIdx == zoneIdx) return; const auto& zone = zones[zoneIdx]; uint32_t frameIdx = vkCtx->getCurrentFrame(); VkDevice device = vkCtx->getDevice(); // Update tile descriptor sets for this frame for (int i = 0; i < 12; i++) { VkTexture* tileTex = zone.tileTextures[i]; if (!tileTex || !tileTex->isValid()) continue; VkDescriptorImageInfo imgInfo = tileTex->descriptorInfo(); VkWriteDescriptorSet write{}; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; write.dstSet = tileDescSets[frameIdx][i]; write.dstBinding = 0; write.descriptorCount = 1; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &imgInfo; vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); } // Begin off-screen render pass VkClearColorValue clearColor = {{ 0.05f, 0.08f, 0.12f, 1.0f }}; compositeTarget->beginPass(cmd, clearColor); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, tilePipeline); VkDeviceSize offset = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &quadVB, &offset); // Draw 4x3 tile grid for (int i = 0; i < 12; i++) { if (!zone.tileTextures[i] || !zone.tileTextures[i]->isValid()) continue; int col = i % GRID_COLS; int row = i / GRID_COLS; vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, tilePipelineLayout, 0, 1, &tileDescSets[frameIdx][i], 0, nullptr); WorldMapTilePush push{}; push.gridOffset = glm::vec2(static_cast(col), static_cast(row)); push.gridCols = static_cast(GRID_COLS); push.gridRows = static_cast(GRID_ROWS); vkCmdPushConstants(cmd, tilePipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(push), &push); vkCmdDraw(cmd, 6, 1, 0, 0); } compositeTarget->endPass(cmd); compositedIdx = zoneIdx; } void WorldMap::enterWorldView() { viewLevel = ViewLevel::WORLD; int rootIdx = -1; for (int i = 0; i < static_cast(zones.size()); i++) { if (isRootContinent(zones, i)) { rootIdx = i; break; } } if (rootIdx >= 0) { loadZoneTextures(rootIdx); bool hasAnyTile = false; for (VkTexture* tex : zones[rootIdx].tileTextures) { if (tex != nullptr) { hasAnyTile = true; break; } } if (hasAnyTile) { requestComposite(rootIdx); currentIdx = rootIdx; return; } } int fallbackContinent = -1; for (int i = 0; i < static_cast(zones.size()); i++) { if (isLeafContinent(zones, i)) { fallbackContinent = i; break; } } if (fallbackContinent < 0) { for (int i = 0; i < static_cast(zones.size()); i++) { if (zones[i].areaID == 0 && !isRootContinent(zones, i)) { fallbackContinent = i; break; } } } if (fallbackContinent >= 0) { loadZoneTextures(fallbackContinent); requestComposite(fallbackContinent); currentIdx = fallbackContinent; return; } currentIdx = -1; compositedIdx = -1; // Render target will be cleared by next compositePass pendingCompositeIdx = -2; // Signal "clear only" } // -------------------------------------------------------- // Coordinate projection // -------------------------------------------------------- glm::vec2 WorldMap::renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) const { if (zoneIdx < 0 || zoneIdx >= static_cast(zones.size())) return glm::vec2(0.5f, 0.5f); const auto& zone = zones[zoneIdx]; float wowX = renderPos.y; float wowY = renderPos.x; float left = zone.locLeft, right = zone.locRight; float top = zone.locTop, bottom = zone.locBottom; if (zone.areaID == 0) { float l, r, t, b; if (getContinentProjectionBounds(zoneIdx, l, r, t, b)) { left = l; right = r; top = t; bottom = b; } } float denom_h = left - right; float denom_v = top - bottom; if (std::abs(denom_h) < 0.001f || std::abs(denom_v) < 0.001f) return glm::vec2(0.5f, 0.5f); float u = (left - wowX) / denom_h; float v = (top - wowY) / denom_v; if (zone.areaID == 0) { constexpr float kVScale = 1.0f; constexpr float kVOffset = -0.15f; v = (v - 0.5f) * kVScale + 0.5f + kVOffset; } return glm::vec2(u, v); } // -------------------------------------------------------- // Exploration tracking (identical to GL version) // -------------------------------------------------------- void WorldMap::updateExploration(const glm::vec3& playerRenderPos) { auto isExploreFlagSet = [this](uint32_t flag) -> bool { if (!hasServerExplorationMask || serverExplorationMask.empty() || flag == 0) return false; const auto isSet = [this](uint32_t bitIndex) -> bool { const size_t word = bitIndex / 32; if (word >= serverExplorationMask.size()) return false; return (serverExplorationMask[word] & (1u << (bitIndex % 32))) != 0; }; if (isSet(flag)) return true; if (flag > 0 && isSet(flag - 1)) return true; return false; }; bool markedAny = false; if (hasServerExplorationMask) { exploredZones.clear(); for (int i = 0; i < static_cast(zones.size()); i++) { const auto& z = zones[i]; if (z.areaID == 0 || z.exploreFlag == 0) continue; if (isExploreFlagSet(z.exploreFlag)) { exploredZones.insert(i); markedAny = true; } } } if (markedAny) return; float wowX = playerRenderPos.y; float wowY = playerRenderPos.x; for (int i = 0; i < static_cast(zones.size()); i++) { const auto& z = zones[i]; if (z.areaID == 0) continue; float minX = std::min(z.locLeft, z.locRight), maxX = std::max(z.locLeft, z.locRight); float minY = std::min(z.locTop, z.locBottom), maxY = std::max(z.locTop, z.locBottom); if (maxX - minX < 0.001f || maxY - minY < 0.001f) continue; if (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY) { exploredZones.insert(i); markedAny = true; } } if (!markedAny) { int zoneIdx = findZoneForPlayer(playerRenderPos); if (zoneIdx >= 0) exploredZones.insert(zoneIdx); } } void WorldMap::zoomIn(const glm::vec3& playerRenderPos) { if (viewLevel == ViewLevel::WORLD) { if (continentIdx >= 0) { loadZoneTextures(continentIdx); requestComposite(continentIdx); currentIdx = continentIdx; viewLevel = ViewLevel::CONTINENT; } } else if (viewLevel == ViewLevel::CONTINENT) { int zoneIdx = findZoneForPlayer(playerRenderPos); if (zoneIdx >= 0 && zoneBelongsToContinent(zoneIdx, continentIdx)) { loadZoneTextures(zoneIdx); requestComposite(zoneIdx); currentIdx = zoneIdx; viewLevel = ViewLevel::ZONE; } } } void WorldMap::zoomOut() { if (viewLevel == ViewLevel::ZONE) { if (continentIdx >= 0) { requestComposite(continentIdx); currentIdx = continentIdx; viewLevel = ViewLevel::CONTINENT; } } else if (viewLevel == ViewLevel::CONTINENT) { enterWorldView(); } } // -------------------------------------------------------- // Main render (input + ImGui overlay) // -------------------------------------------------------- void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight) { if (!initialized || !assetManager) return; auto& input = core::Input::getInstance(); if (!zones.empty()) updateExploration(playerRenderPos); if (open) { if (input.isKeyJustPressed(SDL_SCANCODE_M) || input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) { open = false; return; } auto& io = ImGui::GetIO(); float wheelDelta = io.MouseWheel; if (std::abs(wheelDelta) < 0.001f) wheelDelta = input.getMouseWheelDelta(); if (wheelDelta > 0.0f) zoomIn(playerRenderPos); else if (wheelDelta < 0.0f) zoomOut(); } else { auto& io = ImGui::GetIO(); if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_M)) { open = true; if (zones.empty()) loadZonesFromDBC(); int bestContinent = findBestContinentForPlayer(playerRenderPos); if (bestContinent >= 0 && bestContinent != continentIdx) { continentIdx = bestContinent; compositedIdx = -1; } int playerZone = findZoneForPlayer(playerRenderPos); if (playerZone >= 0 && continentIdx >= 0 && zoneBelongsToContinent(playerZone, continentIdx)) { loadZoneTextures(playerZone); requestComposite(playerZone); currentIdx = playerZone; viewLevel = ViewLevel::ZONE; } else if (continentIdx >= 0) { loadZoneTextures(continentIdx); requestComposite(continentIdx); currentIdx = continentIdx; viewLevel = ViewLevel::CONTINENT; } } } if (!open) return; renderImGuiOverlay(playerRenderPos, screenWidth, screenHeight); } // -------------------------------------------------------- // ImGui overlay // -------------------------------------------------------- void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight) { float sw = static_cast(screenWidth); float sh = static_cast(screenHeight); ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGui::SetNextWindowSize(ImVec2(sw, sh)); ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoFocusOnAppearing; ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.75f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); if (ImGui::Begin("##WorldMap", nullptr, flags)) { float mapAspect = static_cast(FBO_W) / static_cast(FBO_H); float availW = sw * 0.85f; float availH = sh * 0.85f; float displayW, displayH; if (availW / availH > mapAspect) { displayH = availH; displayW = availH * mapAspect; } else { displayW = availW; displayH = availW / mapAspect; } float mapX = (sw - displayW) / 2.0f; float mapY = (sh - displayH) / 2.0f; ImGui::SetCursorPos(ImVec2(mapX, mapY)); // Display composite render target via ImGui (VkDescriptorSet as ImTextureID) ImGui::Image(reinterpret_cast(imguiDisplaySet), ImVec2(displayW, displayH), ImVec2(0, 0), ImVec2(1, 1)); ImVec2 imgMin = ImGui::GetItemRectMin(); ImDrawList* drawList = ImGui::GetWindowDrawList(); std::vector continentIndices; bool hasLeafContinents = false; for (int i = 0; i < static_cast(zones.size()); i++) { if (isLeafContinent(zones, i)) { hasLeafContinents = true; break; } } for (int i = 0; i < static_cast(zones.size()); i++) { if (zones[i].areaID != 0) continue; if (hasLeafContinents) { if (isLeafContinent(zones, i)) continentIndices.push_back(i); } else if (!isRootContinent(zones, i)) { continentIndices.push_back(i); } } if (continentIndices.size() > 1) { std::vector filtered; filtered.reserve(continentIndices.size()); for (int idx : continentIndices) { if (zones[idx].areaName == mapName) continue; filtered.push_back(idx); } if (!filtered.empty()) continentIndices = std::move(filtered); } if (continentIndices.empty()) { for (int i = 0; i < static_cast(zones.size()); i++) { if (zones[i].areaID == 0) continentIndices.push_back(i); } } // World-level continent selection UI if (viewLevel == ViewLevel::WORLD && !continentIndices.empty()) { ImVec2 titleSz = ImGui::CalcTextSize("World"); ImGui::SetCursorPos(ImVec2((sw - titleSz.x) * 0.5f, mapY + 8.0f)); ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 0.95f), "World"); ImGui::SetCursorPos(ImVec2(mapX + 8.0f, mapY + 32.0f)); for (size_t i = 0; i < continentIndices.size(); i++) { int ci = continentIndices[i]; if (i > 0) ImGui::SameLine(); const bool selected = (ci == continentIdx); if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.25f, 0.05f, 0.9f)); std::string rawName = zones[ci].areaName.empty() ? "Continent" : zones[ci].areaName; if (rawName == "Azeroth") rawName = "Eastern Kingdoms"; std::string label = rawName + "##" + std::to_string(ci); if (ImGui::Button(label.c_str())) { continentIdx = ci; loadZoneTextures(continentIdx); requestComposite(continentIdx); currentIdx = continentIdx; viewLevel = ViewLevel::CONTINENT; } if (selected) ImGui::PopStyleColor(); } } else if (viewLevel == ViewLevel::CONTINENT && continentIndices.size() > 1) { ImGui::SetCursorPos(ImVec2(mapX + 8.0f, mapY + 8.0f)); for (size_t i = 0; i < continentIndices.size(); i++) { int ci = continentIndices[i]; if (i > 0) ImGui::SameLine(); const bool selected = (ci == continentIdx); if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.35f, 0.25f, 0.05f, 0.9f)); std::string rawName = zones[ci].areaName.empty() ? "Continent" : zones[ci].areaName; if (rawName == "Azeroth") rawName = "Eastern Kingdoms"; std::string label = rawName + "##" + std::to_string(ci); if (ImGui::Button(label.c_str())) { continentIdx = ci; loadZoneTextures(continentIdx); requestComposite(continentIdx); currentIdx = continentIdx; } if (selected) ImGui::PopStyleColor(); } } // Player marker if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) { glm::vec2 playerUV = renderPosToMapUV(playerRenderPos, currentIdx); if (playerUV.x >= 0.0f && playerUV.x <= 1.0f && playerUV.y >= 0.0f && playerUV.y <= 1.0f) { float px = imgMin.x + playerUV.x * displayW; float py = imgMin.y + playerUV.y * displayH; drawList->AddCircleFilled(ImVec2(px, py), 6.0f, IM_COL32(255, 40, 40, 255)); drawList->AddCircle(ImVec2(px, py), 6.0f, IM_COL32(0, 0, 0, 200), 0, 2.0f); } } // Continent view: clickable zone overlays if (viewLevel == ViewLevel::CONTINENT && continentIdx >= 0) { const auto& cont = zones[continentIdx]; float cLeft = cont.locLeft, cRight = cont.locRight; float cTop = cont.locTop, cBottom = cont.locBottom; getContinentProjectionBounds(continentIdx, cLeft, cRight, cTop, cBottom); float cDenomU = cLeft - cRight; float cDenomV = cTop - cBottom; ImVec2 mousePos = ImGui::GetMousePos(); int hoveredZone = -1; if (std::abs(cDenomU) > 0.001f && std::abs(cDenomV) > 0.001f) { for (int zi = 0; zi < static_cast(zones.size()); zi++) { if (!zoneBelongsToContinent(zi, continentIdx)) continue; const auto& z = zones[zi]; if (std::abs(z.locLeft - z.locRight) < 0.001f || std::abs(z.locTop - z.locBottom) < 0.001f) continue; float zuMin = (cLeft - z.locLeft) / cDenomU; float zuMax = (cLeft - z.locRight) / cDenomU; float zvMin = (cTop - z.locTop) / cDenomV; float zvMax = (cTop - z.locBottom) / cDenomV; constexpr float kOverlayShrink = 0.92f; float cu = (zuMin + zuMax) * 0.5f, cv = (zvMin + zvMax) * 0.5f; float hu = (zuMax - zuMin) * 0.5f * kOverlayShrink; float hv = (zvMax - zvMin) * 0.5f * kOverlayShrink; zuMin = cu - hu; zuMax = cu + hu; zvMin = cv - hv; zvMax = cv + hv; constexpr float kVOffset = -0.15f; zvMin = (zvMin - 0.5f) + 0.5f + kVOffset; zvMax = (zvMax - 0.5f) + 0.5f + kVOffset; zuMin = std::clamp(zuMin, 0.0f, 1.0f); zuMax = std::clamp(zuMax, 0.0f, 1.0f); zvMin = std::clamp(zvMin, 0.0f, 1.0f); zvMax = std::clamp(zvMax, 0.0f, 1.0f); if (zuMax - zuMin < 0.001f || zvMax - zvMin < 0.001f) continue; float sx0 = imgMin.x + zuMin * displayW; float sy0 = imgMin.y + zvMin * displayH; float sx1 = imgMin.x + zuMax * displayW; float sy1 = imgMin.y + zvMax * displayH; bool explored = exploredZones.count(zi) > 0; bool hovered = (mousePos.x >= sx0 && mousePos.x <= sx1 && mousePos.y >= sy0 && mousePos.y <= sy1); if (!explored) { drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1), IM_COL32(0, 0, 0, 160)); } if (hovered) { hoveredZone = zi; drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1), IM_COL32(255, 255, 200, 40)); drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1), IM_COL32(255, 215, 0, 180), 0.0f, 0, 2.0f); } else if (explored) { drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1), IM_COL32(255, 255, 255, 30), 0.0f, 0, 1.0f); } } } if (hoveredZone >= 0) { ImGui::SetTooltip("%s", zones[hoveredZone].areaName.c_str()); if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { loadZoneTextures(hoveredZone); requestComposite(hoveredZone); currentIdx = hoveredZone; viewLevel = ViewLevel::ZONE; } } } // Zone view: back to continent if (viewLevel == ViewLevel::ZONE && continentIdx >= 0) { auto& io = ImGui::GetIO(); bool goBack = io.MouseClicked[1]; ImGui::SetCursorPos(ImVec2(mapX + 8.0f, mapY + 8.0f)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.1f, 0.9f)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f)); if (ImGui::Button("< Back")) goBack = true; ImGui::PopStyleColor(3); if (goBack) { requestComposite(continentIdx); currentIdx = continentIdx; viewLevel = ViewLevel::CONTINENT; } const char* zoneName = zones[currentIdx].areaName.c_str(); ImVec2 nameSize = ImGui::CalcTextSize(zoneName); float nameY = mapY - nameSize.y - 8.0f; if (nameY > 0.0f) { ImGui::SetCursorPos(ImVec2((sw - nameSize.x) / 2.0f, nameY)); ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 0.9f), "%s", zoneName); } } // Continent view: back to world if (viewLevel == ViewLevel::CONTINENT) { auto& io = ImGui::GetIO(); bool goWorld = io.MouseClicked[1]; float worldBtnY = mapY + (continentIndices.size() > 1 ? 40.0f : 8.0f); ImGui::SetCursorPos(ImVec2(mapX + 8.0f, worldBtnY)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.1f, 0.9f)); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f)); if (ImGui::Button("< World")) goWorld = true; ImGui::PopStyleColor(3); if (goWorld) enterWorldView(); } // Help text const char* helpText; if (viewLevel == ViewLevel::ZONE) helpText = "Scroll out or right-click to zoom out | M or Escape to close"; else if (viewLevel == ViewLevel::WORLD) helpText = "Select a continent | Scroll in to zoom | M or Escape to close"; else helpText = "Click zone or scroll in to zoom | Scroll out / right-click for World | M or Escape to close"; ImVec2 textSize = ImGui::CalcTextSize(helpText); float textY = mapY + displayH + 8.0f; if (textY + textSize.y < sh) { ImGui::SetCursorPos(ImVec2((sw - textSize.x) / 2.0f, textY)); ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.8f), "%s", helpText); } } ImGui::End(); ImGui::PopStyleVar(); ImGui::PopStyleColor(); } } // namespace rendering } // namespace wowee