diff --git a/include/pipeline/wmo_loader.hpp b/include/pipeline/wmo_loader.hpp index 718042c8..fecb287d 100644 --- a/include/pipeline/wmo_loader.hpp +++ b/include/pipeline/wmo_loader.hpp @@ -216,6 +216,7 @@ struct WMOModel { // Group names std::vector groupNames; + std::vector groupNameRaw; // Raw MOGN chunk for offset-based name lookup bool isValid() const { return nGroups > 0 && !groups.empty(); diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index ebe6269c..a2a2d435 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -346,6 +346,7 @@ private: uint32_t groupFlags = 0; bool allUntextured = false; // True if ALL batches use fallback white texture (collision/placeholder group) + bool isLOD = false; // Distance-only group (skip when camera is close) // Material batches (start index, count, material ID) struct Batch { diff --git a/src/pipeline/wmo_loader.cpp b/src/pipeline/wmo_loader.cpp index b947004e..b2d50ea0 100644 --- a/src/pipeline/wmo_loader.cpp +++ b/src/pipeline/wmo_loader.cpp @@ -192,7 +192,10 @@ WMOModel WMOLoader::load(const std::vector& wmoData) { } case MOGN: { - // Group names + // Group names — store raw chunk for offset-based lookup (MOGI nameOffset) + if (chunkSize > 0 && chunkEnd <= wmoData.size()) { + model.groupNameRaw.assign(wmoData.begin() + chunkStart, wmoData.begin() + chunkEnd); + } uint32_t nameOffset = chunkStart; while (nameOffset < chunkEnd) { std::string name = readString(wmoData, nameOffset); diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 6add507e..024e7be7 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -483,9 +483,23 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { modelData.materialFlags.push_back(mat.flags); } + // Helper: look up group name from MOGN raw data via MOGI nameOffset + auto getGroupName = [&](uint32_t groupIdx) -> std::string { + if (groupIdx < model.groupInfo.size()) { + int32_t nameOff = model.groupInfo[groupIdx].nameOffset; + if (nameOff >= 0 && static_cast(nameOff) < model.groupNameRaw.size()) { + const char* str = reinterpret_cast(model.groupNameRaw.data() + nameOff); + size_t maxLen = model.groupNameRaw.size() - nameOff; + return std::string(str, strnlen(str, maxLen)); + } + } + return {}; + }; + // Create GPU resources for each group uint32_t loadedGroups = 0; - for (const auto& wmoGroup : model.groups) { + for (size_t gi = 0; gi < model.groups.size(); gi++) { + const auto& wmoGroup = model.groups[gi]; // Skip empty groups if (wmoGroup.vertices.empty() || wmoGroup.indices.empty()) { continue; @@ -493,6 +507,17 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { GroupResources resources; if (createGroupResources(wmoGroup, resources, wmoGroup.flags)) { + // Detect distance-only LOD groups by name pattern + std::string gname = getGroupName(static_cast(gi)); + if (!gname.empty()) { + std::string lower = gname; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (lower.find("lod") != std::string::npos) { + resources.isLOD = true; + LOG_INFO("WMO group ", gi, " '", gname, "' marked as LOD (distance-only)"); + } + } modelData.groups.push_back(resources); loadedGroups++; } @@ -510,11 +535,12 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { uintptr_t texPtr; bool alphaTest; bool unlit; - bool operator==(const BatchKey& o) const { return texPtr == o.texPtr && alphaTest == o.alphaTest && unlit == o.unlit; } + bool isWindow; + bool operator==(const BatchKey& o) const { return texPtr == o.texPtr && alphaTest == o.alphaTest && unlit == o.unlit && isWindow == o.isWindow; } }; struct BatchKeyHash { size_t operator()(const BatchKey& k) const { - return std::hash()(k.texPtr) ^ (std::hash()(k.alphaTest) << 1) ^ (std::hash()(k.unlit) << 2); + return std::hash()(k.texPtr) ^ (std::hash()(k.alphaTest) << 1) ^ (std::hash()(k.unlit) << 2) ^ (std::hash()(k.isWindow) << 3); } }; std::unordered_map batchMap; @@ -551,7 +577,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { // lamp posts, lanterns, etc. — those should NOT be glass. bool isWindow = (matFlags & 0x40) != 0; - BatchKey key{ reinterpret_cast(tex), alphaTest, unlit }; + BatchKey key{ reinterpret_cast(tex), alphaTest, unlit, isWindow }; auto& mb = batchMap[key]; if (mb.draws.empty()) { mb.texture = tex; @@ -1385,6 +1411,13 @@ void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const vkCmdPushConstants(cmd, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUPushConstants), &push); + // Compute camera distance to WMO bounding box center for LOD decisions + glm::vec3 wmoCenter = instance.modelMatrix * glm::vec4( + (model.boundingBoxMin + model.boundingBoxMax) * 0.5f, 1.0f); + float camDistSq = glm::dot(camPos - wmoCenter, camPos - wmoCenter); + // LOD groups render only beyond this distance squared (200 units) + static constexpr float LOD_SHOW_DIST_SQ = 200.0f * 200.0f; + // Render visible groups for (uint32_t gi : dl.visibleGroups) { const auto& group = model.groups[gi]; @@ -1392,6 +1425,9 @@ void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const // Only skip antiportal geometry if (group.groupFlags & 0x4000000) continue; + // Skip distance-only LOD groups when camera is close + if (group.isLOD && camDistSq < LOD_SHOW_DIST_SQ) continue; + // Bind vertex + index buffers VkDeviceSize offset = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &group.vertexBuffer, &offset); @@ -1630,6 +1666,9 @@ void WMORenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceM // Skip antiportal geometry if (group.groupFlags & 0x4000000) continue; + // Skip LOD groups in shadow pass (they overlap real geometry) + if (group.isLOD) continue; + VkDeviceSize offset = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &group.vertexBuffer, &offset); vkCmdBindIndexBuffer(cmd, group.indexBuffer, 0, VK_INDEX_TYPE_UINT16);