#include "editor_viewport.hpp" #include "rendering/vk_context.hpp" #include "rendering/vk_texture.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/m2_loader.hpp" #include "pipeline/wmo_loader.hpp" #include "pipeline/wowee_model.hpp" #include "core/logger.hpp" #include #include #include #include namespace wowee { namespace editor { EditorViewport::EditorViewport() = default; EditorViewport::~EditorViewport() { shutdown(); } bool EditorViewport::initialize(rendering::VkContext* ctx, pipeline::AssetManager* am, rendering::Camera* cam) { vkCtx_ = ctx; assetManager_ = am; camera_ = cam; if (!createPerFrameResources()) return false; terrainRenderer_ = std::make_unique(); if (!terrainRenderer_->initialize(ctx, perFrameSetLayout_, am)) { LOG_ERROR("Failed to initialize terrain renderer"); return false; } terrainRenderer_->setFogEnabled(false); m2Renderer_ = std::make_unique(); if (!m2Renderer_->initialize(ctx, perFrameSetLayout_, am)) { LOG_WARNING("M2 renderer init failed — object rendering disabled"); m2Renderer_.reset(); } else { m2Renderer_->setForceNoCull(true); } wmoRenderer_ = std::make_unique(); if (!wmoRenderer_->initialize(ctx, perFrameSetLayout_, am)) { LOG_WARNING("WMO renderer init failed — building rendering disabled"); wmoRenderer_.reset(); } waterRenderer_.initialize(ctx, ctx->getImGuiRenderPass(), perFrameSetLayout_); gizmo_.initialize(ctx, ctx->getImGuiRenderPass(), perFrameSetLayout_); LOG_INFO("Editor viewport initialized"); return true; } void EditorViewport::shutdown() { if (!vkCtx_) return; vkDeviceWaitIdle(vkCtx_->getDevice()); if (npcMarkerVB_) { vmaDestroyBuffer(vkCtx_->getAllocator(), npcMarkerVB_, npcMarkerVBAlloc_); npcMarkerVB_ = VK_NULL_HANDLE; } if (brushVB_) { vmaDestroyBuffer(vkCtx_->getAllocator(), brushVB_, brushVBAlloc_); brushVB_ = VK_NULL_HANDLE; } if (pathVB_) { vmaDestroyBuffer(vkCtx_->getAllocator(), pathVB_, pathVBAlloc_); pathVB_ = VK_NULL_HANDLE; } gizmo_.shutdown(); waterRenderer_.shutdown(); if (wmoRenderer_) { wmoRenderer_->shutdown(); wmoRenderer_.reset(); } if (m2Renderer_) { m2Renderer_->shutdown(); m2Renderer_.reset(); } if (terrainRenderer_) { terrainRenderer_->shutdown(); terrainRenderer_.reset(); } destroyPerFrameResources(); vkCtx_ = nullptr; } bool EditorViewport::loadTerrain(const pipeline::TerrainMesh& mesh, const std::vector& texturePaths, int tileX, int tileY) { return terrainRenderer_->loadTerrain(mesh, texturePaths, tileX, tileY); } void EditorViewport::clearTerrain() { if (terrainRenderer_) terrainRenderer_->clear(); } void EditorViewport::updateWater(const pipeline::ADTTerrain& terrain, int tileX, int tileY) { waterRenderer_.update(terrain, tileX, tileY); } void EditorViewport::updateMarkers(const std::vector& /*objects*/) { } void EditorViewport::placeM2(const std::string& path, const glm::vec3& pos, const glm::vec3& rot, float scale) { (void)path; (void)pos; (void)rot; (void)scale; } void EditorViewport::placeWMO(const std::string& path, const glm::vec3& pos, const glm::vec3& rot) { (void)path; (void)pos; (void)rot; } void EditorViewport::clearObjects() { // Clear ghost state since the M2 renderer is about to be wiped ghostActive_ = false; ghostInstanceId_ = 0; ghostModelId_ = 0; ghostModelPath_.clear(); if (m2Renderer_) { vkCtx_->waitAllUploads(); m2Renderer_->clear(); } if (wmoRenderer_) { wmoRenderer_->clearAll(); } } void EditorViewport::rebuildObjects(const std::vector& objects, const std::vector& npcs) { clearObjects(); if (objects.empty() && npcs.empty()) return; // Don't call beginUploadBatch here — loadModel starts its own batch uint32_t nextModelId = 1; std::unordered_map m2ModelIds, wmoModelIds; for (const auto& obj : objects) { if (obj.type == PlaceableType::M2 && m2Renderer_) { uint32_t modelId; auto it = m2ModelIds.find(obj.path); if (it != m2ModelIds.end()) { modelId = it->second; } else { pipeline::M2Model model; bool loaded = false; // Try WOM open format first { std::string womBase = obj.path; auto womDot = womBase.rfind('.'); if (womDot != std::string::npos) womBase = womBase.substr(0, womDot); std::replace(womBase.begin(), womBase.end(), '\\', '/'); for (const char* prefix : {"custom_zones/models/", "output/models/"}) { if (pipeline::WoweeModelLoader::exists(std::string(prefix) + womBase)) { auto wom = pipeline::WoweeModelLoader::load(std::string(prefix) + womBase); if (wom.isValid()) { model.name = wom.name; model.boundRadius = wom.boundRadius; for (const auto& v : wom.vertices) { pipeline::M2Vertex mv; mv.position = v.position; mv.normal = v.normal; mv.texCoords[0] = v.texCoord; std::memcpy(mv.boneWeights, v.boneWeights, 4); std::memcpy(mv.boneIndices, v.boneIndices, 4); model.vertices.push_back(mv); } for (uint32_t idx : wom.indices) model.indices.push_back(static_cast(idx)); for (const auto& tp : wom.texturePaths) { pipeline::M2Texture tex; tex.type = 0; tex.flags = 0; tex.filename = tp; model.textures.push_back(tex); } model.textureLookup = {0}; pipeline::M2Batch batch{}; batch.textureCount = std::min(1u, static_cast(wom.texturePaths.size())); batch.indexCount = static_cast(model.indices.size()); batch.vertexCount = static_cast(model.vertices.size()); model.batches.push_back(batch); pipeline::M2Material mat; mat.flags = 0; mat.blendMode = 0; model.materials.push_back(mat); loaded = true; break; } } } } // Fall back to M2 from game data if (!loaded) { auto data = assetManager_->readFile(obj.path); if (data.empty()) continue; model = pipeline::M2Loader::load(data); // Always load skin (WotLK M2s need it for geometry) { std::string skinPath = obj.path; auto dotPos = skinPath.rfind('.'); if (dotPos != std::string::npos) skinPath = skinPath.substr(0, dotPos) + "00.skin"; auto skinData = assetManager_->readFile(skinPath); if (!skinData.empty()) pipeline::M2Loader::loadSkin(skinData, model); } } if (!model.isValid()) continue; if (model.boundRadius < 1.0f) model.boundRadius = 50.0f; // Validate vertex data to prevent GPU crashes bool vertexOk = true; for (const auto& vert : model.vertices) { if (!std::isfinite(vert.position.x) || !std::isfinite(vert.position.y) || !std::isfinite(vert.position.z) || std::abs(vert.position.x) > 100000.0f) { vertexOk = false; break; } } if (!vertexOk) { LOG_WARNING("M2 has invalid vertex data, skipping: ", obj.path); continue; } modelId = nextModelId++; if (!m2Renderer_->loadModel(model, modelId)) { LOG_WARNING("M2 failed to upload to GPU: ", obj.path); continue; } LOG_INFO("M2 loaded: ", obj.path, " (modelId=", modelId, ", ", model.vertices.size(), " verts)"); m2ModelIds[obj.path] = modelId; } } else if (obj.type == PlaceableType::WMO && wmoRenderer_) { uint32_t modelId; auto it = wmoModelIds.find(obj.path); if (it != wmoModelIds.end()) { modelId = it->second; } else { auto data = assetManager_->readFile(obj.path); if (data.empty()) { LOG_WARNING("WMO file not found in manifest: ", obj.path); continue; } auto model = pipeline::WMOLoader::load(data); // Load WMO group files (_000.wmo, _001.wmo, etc.) std::string basePath = obj.path; auto dotPos = basePath.rfind('.'); if (dotPos != std::string::npos) basePath = basePath.substr(0, dotPos); for (uint32_t gi = 0; gi < model.nGroups; gi++) { char groupSuffix[16]; std::snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi); std::string groupPath = basePath + groupSuffix; auto groupData = assetManager_->readFile(groupPath); if (!groupData.empty()) { pipeline::WMOLoader::loadGroup(groupData, model, gi); } } if (!model.isValid()) { LOG_WARNING("WMO failed to parse (", data.size(), " bytes, ", model.nGroups, " groups expected): ", obj.path); continue; } modelId = nextModelId++; if (!wmoRenderer_->loadModel(model, modelId)) { LOG_WARNING("WMO failed to upload to GPU: ", obj.path); continue; } LOG_INFO("WMO loaded: ", obj.path, " (modelId=", modelId, ", ", model.groups.size(), " groups)"); wmoModelIds[obj.path] = modelId; } glm::vec3 wmoRotRad = glm::radians(obj.rotation); wmoRenderer_->createInstance(modelId, obj.position, wmoRotRad); } } // Render NPC creatures as M2 instances if (m2Renderer_ && !npcs.empty()) { LOG_WARNING("NPC rebuild: ", npcs.size(), " creatures to load"); for (const auto& npc : npcs) { if (npc.modelPath.empty()) { LOG_WARNING("NPC has empty modelPath: ", npc.name); continue; } uint32_t modelId; auto it = m2ModelIds.find(npc.modelPath); if (it != m2ModelIds.end()) { modelId = it->second; } else { // Try WOM open format first pipeline::M2Model model; bool loaded = false; { std::string womBase = npc.modelPath; auto womDot = womBase.rfind('.'); if (womDot != std::string::npos) womBase = womBase.substr(0, womDot); std::replace(womBase.begin(), womBase.end(), '\\', '/'); for (const char* prefix : {"custom_zones/models/", "output/models/"}) { if (pipeline::WoweeModelLoader::exists(std::string(prefix) + womBase)) { auto wom = pipeline::WoweeModelLoader::load(std::string(prefix) + womBase); if (wom.isValid()) { model.name = wom.name; model.boundRadius = wom.boundRadius; for (const auto& v : wom.vertices) { pipeline::M2Vertex mv; mv.position = v.position; mv.normal = v.normal; mv.texCoords[0] = v.texCoord; std::memcpy(mv.boneWeights, v.boneWeights, 4); std::memcpy(mv.boneIndices, v.boneIndices, 4); model.vertices.push_back(mv); } for (uint32_t idx : wom.indices) model.indices.push_back(static_cast(idx)); for (const auto& tp : wom.texturePaths) { pipeline::M2Texture tex; tex.type = 0; tex.flags = 0; tex.filename = tp; model.textures.push_back(tex); } model.textureLookup = {0}; pipeline::M2Batch batch{}; batch.textureCount = std::min(1u, static_cast(wom.texturePaths.size())); batch.indexCount = static_cast(model.indices.size()); batch.vertexCount = static_cast(model.vertices.size()); model.batches.push_back(batch); pipeline::M2Material mat; mat.flags = 0; mat.blendMode = 0; model.materials.push_back(mat); loaded = true; LOG_WARNING("NPC loaded from WOM: ", prefix, womBase); break; } } } } // Fall back to M2 from game data if (!loaded) { auto data = assetManager_->readFile(npc.modelPath); if (data.empty()) { LOG_WARNING("NPC model file not found: ", npc.modelPath); continue; } model = pipeline::M2Loader::load(data); { std::string skinPath = npc.modelPath; auto dotPos = skinPath.rfind('.'); if (dotPos != std::string::npos) skinPath = skinPath.substr(0, dotPos) + "00.skin"; auto skinData = assetManager_->readFile(skinPath); if (!skinData.empty()) pipeline::M2Loader::loadSkin(skinData, model); } } if (!model.isValid()) { LOG_WARNING("NPC model invalid: ", npc.modelPath, " (verts=", model.vertices.size(), " idx=", model.indices.size(), ")"); continue; } LOG_WARNING("NPC M2 OK: ", npc.modelPath, " (", model.vertices.size(), "v ", model.indices.size(), "i ", model.batches.size(), "b)"); if (model.boundRadius < 1.0f) model.boundRadius = 50.0f; // Validate vertex data bool ok = true; for (const auto& vert : model.vertices) { if (!std::isfinite(vert.position.x) || std::abs(vert.position.x) > 100000.0f) { ok = false; break; } } if (!ok) { LOG_WARNING("NPC M2 bad vertices: ", npc.modelPath); continue; } modelId = nextModelId++; if (!m2Renderer_->loadModel(model, modelId)) { LOG_WARNING("NPC M2 loadModel failed: ", npc.modelPath, " (", model.vertices.size(), "v ", model.indices.size(), "i ", model.batches.size(), "b)"); continue; } m2ModelIds[npc.modelPath] = modelId; } } } // Finalize all GPU uploads BEFORE creating instances // (vertex buffers must be valid for isValid() check in createInstance) vkCtx_->waitAllUploads(); vkCtx_->pollUploadBatches(); // Now create instances (vertex buffers are finalized) for (const auto& obj : objects) { if (obj.type == PlaceableType::M2) { auto it = m2ModelIds.find(obj.path); if (it == m2ModelIds.end()) continue; glm::vec3 rotRad = glm::radians(obj.rotation); m2Renderer_->createInstance(it->second, obj.position, rotRad, obj.scale); } } for (const auto& npc : npcs) { auto it = m2ModelIds.find(npc.modelPath); if (it == m2ModelIds.end()) { LOG_WARNING("NPC instance skip — no loaded model for: ", npc.modelPath); continue; } glm::vec3 rotRad = glm::radians(glm::vec3(0, 0, npc.orientation)); uint32_t instId = m2Renderer_->createInstance(it->second, npc.position, rotRad, npc.scale); LOG_WARNING("NPC instance created: id=", instId, " modelId=", it->second, " pos=(", npc.position.x, ",", npc.position.y, ",", npc.position.z, ")"); } // Update NPC markers via dedicated method updateNpcMarkers(npcs); } void EditorViewport::setBrushIndicator(const glm::vec3& center, float radius, bool active) { brushVisible_ = active; if (!active) return; // Rebuild circle vertex buffer if (brushVB_) { vmaDestroyBuffer(vkCtx_->getAllocator(), brushVB_, brushVBAlloc_); brushVB_ = VK_NULL_HANDLE; } constexpr int SEGMENTS = 48; struct BV { float pos[3]; float color[4]; }; std::vector verts; for (int i = 0; i < SEGMENTS; i++) { float a0 = static_cast(i) / SEGMENTS * 6.2831853f; float a1 = static_cast(i + 1) / SEGMENTS * 6.2831853f; float x0 = center.x + std::cos(a0) * radius; float y0 = center.y + std::sin(a0) * radius; float x1 = center.x + std::cos(a1) * radius; float y1 = center.y + std::sin(a1) * radius; float z = center.z + 1.0f; // slightly above terrain float w = 0.6f; // line width via thin quad float dx0 = std::cos(a0), dy0 = std::sin(a0); float dx1 = std::cos(a1), dy1 = std::sin(a1); BV v; v.color[0] = 1.0f; v.color[1] = 1.0f; v.color[2] = 0.3f; v.color[3] = 0.7f; // Thin quad for each segment v.pos[0] = x0 - dy0*w; v.pos[1] = y0 + dx0*w; v.pos[2] = z; verts.push_back(v); v.pos[0] = x0 + dy0*w; v.pos[1] = y0 - dx0*w; v.pos[2] = z; verts.push_back(v); v.pos[0] = x1 - dy1*w; v.pos[1] = y1 + dx1*w; v.pos[2] = z; verts.push_back(v); v.pos[0] = x1 - dy1*w; v.pos[1] = y1 + dx1*w; v.pos[2] = z; verts.push_back(v); v.pos[0] = x0 + dy0*w; v.pos[1] = y0 - dx0*w; v.pos[2] = z; verts.push_back(v); v.pos[0] = x1 + dy1*w; v.pos[1] = y1 - dx1*w; v.pos[2] = z; verts.push_back(v); } brushVertCount_ = static_cast(verts.size()); VkBufferCreateInfo bufInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; bufInfo.size = verts.size() * sizeof(BV); bufInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; VmaAllocationCreateInfo allocInfo{}; allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; VmaAllocationInfo mapInfo{}; if (vmaCreateBuffer(vkCtx_->getAllocator(), &bufInfo, &allocInfo, &brushVB_, &brushVBAlloc_, &mapInfo) == VK_SUCCESS) { std::memcpy(mapInfo.pMappedData, verts.data(), verts.size() * sizeof(BV)); } } void EditorViewport::setPathPreview(const glm::vec3& start, const glm::vec3& end, float width, bool visible) { pathVisible_ = visible; if (pathVB_) { vmaDestroyBuffer(vkCtx_->getAllocator(), pathVB_, pathVBAlloc_); pathVB_ = VK_NULL_HANDLE; pathVertCount_ = 0; } if (!visible) return; struct BV { float pos[3]; float color[4]; }; std::vector verts; glm::vec2 dir = glm::normalize(glm::vec2(end.x - start.x, end.y - start.y)); glm::vec2 perp(-dir.y, dir.x); float z0 = start.z + 2.0f; float z1 = end.z + 2.0f; float hw = width * 0.5f; // Path ribbon (semi-transparent) BV v; v.color[0] = 0.3f; v.color[1] = 0.6f; v.color[2] = 1.0f; v.color[3] = 0.35f; v.pos[0] = start.x - perp.x*hw; v.pos[1] = start.y - perp.y*hw; v.pos[2] = z0; verts.push_back(v); v.pos[0] = start.x + perp.x*hw; v.pos[1] = start.y + perp.y*hw; v.pos[2] = z0; verts.push_back(v); v.pos[0] = end.x - perp.x*hw; v.pos[1] = end.y - perp.y*hw; v.pos[2] = z1; verts.push_back(v); v.pos[0] = end.x - perp.x*hw; v.pos[1] = end.y - perp.y*hw; v.pos[2] = z1; verts.push_back(v); v.pos[0] = start.x + perp.x*hw; v.pos[1] = start.y + perp.y*hw; v.pos[2] = z0; verts.push_back(v); v.pos[0] = end.x + perp.x*hw; v.pos[1] = end.y + perp.y*hw; v.pos[2] = z1; verts.push_back(v); // Edge lines (brighter) float lw = 0.8f; v.color[0] = 0.4f; v.color[1] = 0.8f; v.color[2] = 1.0f; v.color[3] = 0.8f; for (int side = -1; side <= 1; side += 2) { float s = static_cast(side); glm::vec2 offset = perp * hw * s; glm::vec2 linePerp = perp * lw * s; v.pos[0] = start.x + offset.x - linePerp.x; v.pos[1] = start.y + offset.y - linePerp.y; v.pos[2] = z0; verts.push_back(v); v.pos[0] = start.x + offset.x + linePerp.x; v.pos[1] = start.y + offset.y + linePerp.y; v.pos[2] = z0; verts.push_back(v); v.pos[0] = end.x + offset.x - linePerp.x; v.pos[1] = end.y + offset.y - linePerp.y; v.pos[2] = z1; verts.push_back(v); v.pos[0] = end.x + offset.x - linePerp.x; v.pos[1] = end.y + offset.y - linePerp.y; v.pos[2] = z1; verts.push_back(v); v.pos[0] = start.x + offset.x + linePerp.x; v.pos[1] = start.y + offset.y + linePerp.y; v.pos[2] = z0; verts.push_back(v); v.pos[0] = end.x + offset.x + linePerp.x; v.pos[1] = end.y + offset.y + linePerp.y; v.pos[2] = z1; verts.push_back(v); } pathVertCount_ = static_cast(verts.size()); VkBufferCreateInfo bufInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; bufInfo.size = verts.size() * sizeof(BV); bufInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; VmaAllocationCreateInfo allocInfo{}; allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; VmaAllocationInfo mapInfo{}; if (vmaCreateBuffer(vkCtx_->getAllocator(), &bufInfo, &allocInfo, &pathVB_, &pathVBAlloc_, &mapInfo) == VK_SUCCESS) { std::memcpy(mapInfo.pMappedData, verts.data(), verts.size() * sizeof(BV)); } } void EditorViewport::updateNpcMarkers(const std::vector& npcs) { if (npcMarkerVB_) { vmaDestroyBuffer(vkCtx_->getAllocator(), npcMarkerVB_, npcMarkerVBAlloc_); npcMarkerVB_ = VK_NULL_HANDLE; npcMarkerVertCount_ = 0; } if (npcs.empty()) return; struct MV { float pos[3]; float color[4]; }; std::vector verts; for (const auto& npc : npcs) { float s = 1.5f; // base radius (was 5) float x = npc.position.x, y = npc.position.y, z = npc.position.z; float r = npc.hostile ? 1.0f : 0.1f; float g = npc.hostile ? 0.15f : 0.9f; float b = 0.1f, a = 0.7f; MV v; v.color[0]=r; v.color[1]=g; v.color[2]=b; v.color[3]=a; // Small octagonal base for (int seg = 0; seg < 8; seg++) { float a0 = seg * 0.7854f, a1 = (seg+1) * 0.7854f; v.pos[0]=x; v.pos[1]=y; v.pos[2]=z+0.2f; verts.push_back(v); v.pos[0]=x+std::cos(a0)*s; v.pos[1]=y+std::sin(a0)*s; v.pos[2]=z+0.2f; verts.push_back(v); v.pos[0]=x+std::cos(a1)*s; v.pos[1]=y+std::sin(a1)*s; v.pos[2]=z+0.2f; verts.push_back(v); } // Thin pole float pw = 0.3f, ph = 8.0f; // was 0.8 wide, 30 tall v.color[3] = 0.6f; v.pos[0]=x-pw; v.pos[1]=y; v.pos[2]=z; verts.push_back(v); v.pos[0]=x+pw; v.pos[1]=y; v.pos[2]=z; verts.push_back(v); v.pos[0]=x; v.pos[1]=y; v.pos[2]=z+ph; verts.push_back(v); v.pos[0]=x; v.pos[1]=y-pw; v.pos[2]=z; verts.push_back(v); v.pos[0]=x; v.pos[1]=y+pw; v.pos[2]=z; verts.push_back(v); v.pos[0]=x; v.pos[1]=y; v.pos[2]=z+ph; verts.push_back(v); // Small diamond top float ts = 1.0f, tz = z + ph; // was 3 v.color[0]=1; v.color[1]=1; v.color[2]=0.3f; v.color[3]=0.8f; v.pos[0]=x+ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); v.pos[0]=x; v.pos[1]=y+ts; v.pos[2]=tz; verts.push_back(v); v.pos[0]=x-ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); v.pos[0]=x+ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); v.pos[0]=x-ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); v.pos[0]=x; v.pos[1]=y-ts; v.pos[2]=tz; verts.push_back(v); } npcMarkerVertCount_ = static_cast(verts.size()); VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; bi.size = verts.size() * sizeof(MV); bi.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; VmaAllocationCreateInfo ai{}; ai.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; ai.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; VmaAllocationInfo mi{}; if (vmaCreateBuffer(vkCtx_->getAllocator(), &bi, &ai, &npcMarkerVB_, &npcMarkerVBAlloc_, &mi) == VK_SUCCESS) std::memcpy(mi.pMappedData, verts.data(), verts.size() * sizeof(MV)); } void EditorViewport::update(float deltaTime) { if (m2Renderer_) m2Renderer_->update(deltaTime, camera_->getPosition(), camera_->getViewProjectionMatrix()); } void EditorViewport::setGhostPreview(const std::string& path, const glm::vec3& pos, const glm::vec3& rotDeg, float scale) { if (!m2Renderer_) return; // Load model if path changed if (path != ghostModelPath_ || ghostModelId_ == 0) { clearGhostPreview(); auto data = assetManager_->readFile(path); if (data.empty()) { LOG_WARNING("Ghost: file not found: ", path); return; } auto model = pipeline::M2Loader::load(data); if (!model.isValid()) { std::string skinPath = path; auto dotPos = skinPath.rfind('.'); if (dotPos != std::string::npos) skinPath = skinPath.substr(0, dotPos) + "00.skin"; auto skinData = assetManager_->readFile(skinPath); if (!skinData.empty()) pipeline::M2Loader::loadSkin(skinData, model); } if (!model.isValid()) return; if (model.boundRadius < 1.0f) model.boundRadius = 50.0f; ghostModelId_ = 59999; // High ID to avoid collision with placed objects if (!m2Renderer_->loadModel(model, ghostModelId_)) { ghostModelId_ = 0; return; } vkCtx_->waitAllUploads(); vkCtx_->pollUploadBatches(); ghostModelPath_ = path; } // Create or update ghost instance glm::vec3 rotRad = glm::radians(rotDeg); if (!ghostActive_) { ghostInstanceId_ = m2Renderer_->createInstance(ghostModelId_, pos, rotRad, scale); ghostActive_ = (ghostInstanceId_ != 0); } else { m2Renderer_->setInstancePosition(ghostInstanceId_, pos); // Rebuild transform with new rotation/scale glm::mat4 mat = glm::mat4(1.0f); mat = glm::translate(mat, pos); mat = glm::rotate(mat, rotRad.x, glm::vec3(1, 0, 0)); mat = glm::rotate(mat, rotRad.y, glm::vec3(0, 1, 0)); mat = glm::rotate(mat, rotRad.z, glm::vec3(0, 0, 1)); mat = glm::scale(mat, glm::vec3(scale)); m2Renderer_->setInstanceTransform(ghostInstanceId_, mat); } } void EditorViewport::clearGhostPreview() { if (ghostActive_ && m2Renderer_) { m2Renderer_->removeInstance(ghostInstanceId_); ghostActive_ = false; ghostInstanceId_ = 0; } if (ghostModelId_ != 0 && m2Renderer_) { // Don't unload the model — it might be used by placed objects too ghostModelId_ = 0; ghostModelPath_.clear(); } } void EditorViewport::render(VkCommandBuffer cmd) { updatePerFrameUBO(); uint32_t frame = vkCtx_->getCurrentFrame(); VkDescriptorSet perFrameSet = perFrameDescSets_[frame]; terrainRenderer_->render(cmd, perFrameSet, *camera_); if (m2Renderer_) { static int diagCounter = 0; if (m2Renderer_->getInstanceCount() > 0 && (diagCounter++ % 300) == 0) { LOG_WARNING("M2 render: ", m2Renderer_->getModelCount(), " models, ", m2Renderer_->getInstanceCount(), " instances, ", m2Renderer_->getDrawCallCount(), " draws"); } m2Renderer_->render(cmd, perFrameSet, *camera_); } if (wmoRenderer_) wmoRenderer_->render(cmd, perFrameSet, *camera_); waterRenderer_.render(cmd, perFrameSet); // NPC position markers — render AFTER gizmo (no depth test = always on top) // Brush indicator circle if (brushVisible_ && brushVB_ && brushVertCount_ > 0) { // Reuse gizmo pipeline (same vertex format, no depth test, alpha blend) if (gizmo_.getMode() == TransformMode::None && !gizmo_.isActive()) { // Use water pipeline for brush (it has alpha blend + depth test) // Actually just render through the water pipeline } // Render brush circle using the water renderer's pipeline setup // (same pos+color vertex format) auto* waterPipeline = waterRenderer_.getPipeline(); auto* waterLayout = waterRenderer_.getPipelineLayout(); if (waterPipeline && waterLayout) { vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterPipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterLayout, 0, 1, &perFrameSet, 0, nullptr); VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &brushVB_, &off); vkCmdDraw(cmd, brushVertCount_, 1, 0, 0); } } // Path preview line (river/road tool) if (pathVisible_ && pathVB_ && pathVertCount_ > 0) { auto* waterPipeline = waterRenderer_.getPipeline(); auto* waterLayout = waterRenderer_.getPipelineLayout(); if (waterPipeline && waterLayout) { vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterPipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterLayout, 0, 1, &perFrameSet, 0, nullptr); VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &pathVB_, &off); vkCmdDraw(cmd, pathVertCount_, 1, 0, 0); } } gizmo_.render(cmd, perFrameSet); // NPC markers — render with water pipeline (pos+color, alpha blend) if (showNpcMarkers_ && npcMarkerVB_ && npcMarkerVertCount_ > 0) { auto* waterPipeline = waterRenderer_.getPipeline(); auto* waterLayout = waterRenderer_.getPipelineLayout(); if (waterPipeline && waterLayout) { vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterPipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, waterLayout, 0, 1, &perFrameSet, 0, nullptr); VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &npcMarkerVB_, &off); vkCmdDraw(cmd, npcMarkerVertCount_, 1, 0, 0); } } } void EditorViewport::setWireframe(bool enabled) { wireframe_ = enabled; if (terrainRenderer_) terrainRenderer_->setWireframe(enabled); } bool EditorViewport::createPerFrameResources() { VkDevice device = vkCtx_->getDevice(); VkDescriptorSetLayoutBinding bindings[2]{}; bindings[0].binding = 0; bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; bindings[0].descriptorCount = 1; bindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; bindings[1].binding = 1; bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; bindings[1].descriptorCount = 1; bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; VkDescriptorSetLayoutCreateInfo layoutInfo{}; layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; layoutInfo.bindingCount = 2; layoutInfo.pBindings = bindings; if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &perFrameSetLayout_) != VK_SUCCESS) return false; VkDescriptorPoolSize poolSizes[2]{}; poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; poolSizes[0].descriptorCount = MAX_FRAMES; poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; poolSizes[1].descriptorCount = MAX_FRAMES; VkDescriptorPoolCreateInfo poolInfo{}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; poolInfo.maxSets = MAX_FRAMES; poolInfo.poolSizeCount = 2; poolInfo.pPoolSizes = poolSizes; if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &sceneDescPool_) != VK_SUCCESS) return false; dummyShadowTexture_ = std::make_unique(); if (!dummyShadowTexture_->createDepth(*vkCtx_, 1, 1)) return false; VkSamplerCreateInfo sampCI{}; sampCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; sampCI.magFilter = VK_FILTER_LINEAR; sampCI.minFilter = VK_FILTER_LINEAR; sampCI.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; sampCI.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; sampCI.compareEnable = VK_TRUE; sampCI.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL; shadowSampler_ = vkCtx_->getOrCreateSampler(sampCI); vkCtx_->immediateSubmit([this](VkCommandBuffer cmd) { VkImageMemoryBarrier barrier{}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = dummyShadowTexture_->getImage(); barrier.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1}; barrier.srcAccessMask = 0; barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); }); for (uint32_t i = 0; i < MAX_FRAMES; i++) { VkBufferCreateInfo bufInfo{}; bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufInfo.size = sizeof(rendering::GPUPerFrameData); bufInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; VmaAllocationCreateInfo allocInfo{}; allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; VmaAllocationInfo mapInfo{}; if (vmaCreateBuffer(vkCtx_->getAllocator(), &bufInfo, &allocInfo, &perFrameUBOs_[i], &perFrameUBOAllocs_[i], &mapInfo) != VK_SUCCESS) return false; perFrameUBOMapped_[i] = mapInfo.pMappedData; VkDescriptorSetAllocateInfo setAlloc{}; setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; setAlloc.descriptorPool = sceneDescPool_; setAlloc.descriptorSetCount = 1; setAlloc.pSetLayouts = &perFrameSetLayout_; if (vkAllocateDescriptorSets(device, &setAlloc, &perFrameDescSets_[i]) != VK_SUCCESS) return false; VkDescriptorBufferInfo descBuf{}; descBuf.buffer = perFrameUBOs_[i]; descBuf.offset = 0; descBuf.range = sizeof(rendering::GPUPerFrameData); VkDescriptorImageInfo shadowImgInfo{}; shadowImgInfo.sampler = shadowSampler_; shadowImgInfo.imageView = dummyShadowTexture_->getImageView(); shadowImgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkWriteDescriptorSet writes[2]{}; writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[0].dstSet = perFrameDescSets_[i]; writes[0].dstBinding = 0; writes[0].descriptorCount = 1; writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[0].pBufferInfo = &descBuf; writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[1].dstSet = perFrameDescSets_[i]; writes[1].dstBinding = 1; writes[1].descriptorCount = 1; writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[1].pImageInfo = &shadowImgInfo; vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); } return true; } void EditorViewport::destroyPerFrameResources() { if (!vkCtx_) return; VkDevice device = vkCtx_->getDevice(); for (uint32_t i = 0; i < MAX_FRAMES; i++) { if (perFrameUBOs_[i]) { vmaDestroyBuffer(vkCtx_->getAllocator(), perFrameUBOs_[i], perFrameUBOAllocs_[i]); perFrameUBOs_[i] = VK_NULL_HANDLE; } } if (dummyShadowTexture_) { dummyShadowTexture_->destroy(device, vkCtx_->getAllocator()); dummyShadowTexture_.reset(); } if (sceneDescPool_) { vkDestroyDescriptorPool(device, sceneDescPool_, nullptr); sceneDescPool_ = VK_NULL_HANDLE; } if (perFrameSetLayout_) { vkDestroyDescriptorSetLayout(device, perFrameSetLayout_, nullptr); perFrameSetLayout_ = VK_NULL_HANDLE; } } void EditorViewport::setTimeOfDay(float t) { timeOfDay_ = std::clamp(t, 0.0f, 24.0f); float hour = timeOfDay_; // Sun angle: noon=overhead, 6am/6pm=horizon, night=below float sunAngle = (hour - 6.0f) / 12.0f * 3.14159f; lightDir_ = glm::normalize(glm::vec3(std::cos(sunAngle) * 0.5f, -1.0f, std::sin(sunAngle))); // Dawn/dusk warm tones, noon white, night blue if (hour >= 6.0f && hour <= 8.0f) { float t2 = (hour - 6.0f) / 2.0f; lightColor_ = glm::mix(glm::vec3(1.0f, 0.5f, 0.2f), glm::vec3(1.0f, 0.95f, 0.85f), t2); ambientColor_ = glm::mix(glm::vec3(0.15f, 0.1f, 0.2f), glm::vec3(0.3f, 0.3f, 0.35f), t2); fogColor_ = glm::mix(glm::vec3(0.5f, 0.3f, 0.3f), glm::vec3(0.6f, 0.7f, 0.8f), t2); } else if (hour >= 17.0f && hour <= 19.0f) { float t2 = (hour - 17.0f) / 2.0f; lightColor_ = glm::mix(glm::vec3(1.0f, 0.95f, 0.85f), glm::vec3(1.0f, 0.4f, 0.15f), t2); ambientColor_ = glm::mix(glm::vec3(0.3f, 0.3f, 0.35f), glm::vec3(0.1f, 0.08f, 0.15f), t2); fogColor_ = glm::mix(glm::vec3(0.6f, 0.7f, 0.8f), glm::vec3(0.4f, 0.25f, 0.3f), t2); } else if (hour < 6.0f || hour > 19.0f) { lightColor_ = glm::vec3(0.15f, 0.15f, 0.25f); ambientColor_ = glm::vec3(0.05f, 0.05f, 0.1f); fogColor_ = glm::vec3(0.1f, 0.1f, 0.15f); } else { lightColor_ = glm::vec3(1.0f, 0.95f, 0.85f); ambientColor_ = glm::vec3(0.3f, 0.3f, 0.35f); fogColor_ = glm::vec3(0.6f, 0.7f, 0.8f); } // Sky/clear color follows fog clearR_ = fogColor_.x * 0.7f; clearG_ = fogColor_.y * 0.7f; clearB_ = fogColor_.z * 0.7f; } void EditorViewport::updatePerFrameUBO() { uint32_t frame = vkCtx_->getCurrentFrame(); rendering::GPUPerFrameData data{}; data.view = camera_->getViewMatrix(); data.projection = camera_->getProjectionMatrix(); data.lightSpaceMatrix = glm::mat4(1.0f); data.lightDir = glm::vec4(lightDir_, 0.0f); data.lightColor = glm::vec4(lightColor_, 0.0f); data.ambientColor = glm::vec4(ambientColor_, 0.0f); data.viewPos = glm::vec4(camera_->getPosition(), 0.0f); data.fogColor = glm::vec4(fogColor_, 0.0f); data.fogParams = glm::vec4(fogNear_, fogFar_, 0.0f, 0.0f); data.shadowParams = glm::vec4(0.0f, 0.0f, 0.0f, 0.0f); std::memcpy(perFrameUBOMapped_[frame], &data, sizeof(data)); } } // namespace editor } // namespace wowee