From 3ffb7ccc50d89a1a23e499dc045015972c42dde2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 23 Feb 2026 01:54:05 -0800 Subject: [PATCH] Fix lamp posts as glass and hide distance-only LOD groups when close Two WMO rendering fixes: 1. Glass batch merging: BatchKey didn't include isWindow, so window and non-window batches sharing the same texture got merged together. If the window batch was processed first, the entire merged batch (lamp base, iron frame, etc.) rendered as transparent glass. Add isWindow to the batch key so glass/non-glass batches stay separate. 2. LOD group culling: WMO groups named with "LOD" are distance-only impostor geometry (e.g. cathedral tower extension, hill tower). They should only render beyond 200 units. Store raw MOGN chunk for offset-based name lookup, detect "lod" in group names during load, and skip LOD groups in both main and shadow passes when camera is within range. --- include/pipeline/wmo_loader.hpp | 1 + include/rendering/wmo_renderer.hpp | 1 + src/pipeline/wmo_loader.cpp | 5 +++- src/rendering/wmo_renderer.cpp | 47 +++++++++++++++++++++++++++--- 4 files changed, 49 insertions(+), 5 deletions(-) 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);