Fix WMO LOD shell culling and MOGP header parsing

- Fix MOGP header: skip 8-byte groupName/descriptiveName prefix before flags
- Fix fogIndices: read as 4×uint8 (4 bytes) instead of 4×uint32 (16 bytes)
- Detect LOD shell groups: city shells, facades, flag 0x80 indoor, low-vert
- Per-group distance culling at 196 units instead of whole-WMO distance
This commit is contained in:
Kelsi 2026-02-23 03:23:18 -08:00
parent 3ffb7ccc50
commit a7cf0d0c4e
2 changed files with 41 additions and 23 deletions

View file

@ -429,9 +429,11 @@ bool WMOLoader::loadGroup(const std::vector<uint8_t>& groupData,
}
// Read MOGP header
// NOTE: In WMO group files, the MOGP data starts directly at flags
// (groupName/descriptiveGroupName are handled by the root WMO's MOGI chunk).
// MOGP starts with groupName(4) + descriptiveName(4) offsets into MOGN,
// followed by flags at offset +8.
uint32_t mogpOffset = offset;
mogpOffset += 4; // skip groupName offset
mogpOffset += 4; // skip descriptiveGroupName offset
group.flags = read<uint32_t>(groupData, mogpOffset);
bool isInterior = (group.flags & 0x2000) != 0;
core::Logger::getInstance().debug(" Group flags: 0x", std::hex, group.flags, std::dec,
@ -442,14 +444,14 @@ bool WMOLoader::loadGroup(const std::vector<uint8_t>& groupData,
group.boundingBoxMax.x = read<float>(groupData, mogpOffset);
group.boundingBoxMax.y = read<float>(groupData, mogpOffset);
group.boundingBoxMax.z = read<float>(groupData, mogpOffset);
mogpOffset += 4; // nameOffset
group.portalStart = read<uint16_t>(groupData, mogpOffset);
group.portalCount = read<uint16_t>(groupData, mogpOffset);
mogpOffset += 8; // transBatchCount, intBatchCount, extBatchCount, padding
group.fogIndices[0] = read<uint32_t>(groupData, mogpOffset);
group.fogIndices[1] = read<uint32_t>(groupData, mogpOffset);
group.fogIndices[2] = read<uint32_t>(groupData, mogpOffset);
group.fogIndices[3] = read<uint32_t>(groupData, mogpOffset);
// fogIndices: 4 × uint8 (4 bytes total, NOT 4 × uint32)
group.fogIndices[0] = read<uint8_t>(groupData, mogpOffset);
group.fogIndices[1] = read<uint8_t>(groupData, mogpOffset);
group.fogIndices[2] = read<uint8_t>(groupData, mogpOffset);
group.fogIndices[3] = read<uint8_t>(groupData, mogpOffset);
group.liquidType = read<uint32_t>(groupData, mogpOffset);
// Skip to end of 68-byte header
mogpOffset = offset + 68;

View file

@ -481,6 +481,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
modelData.materialTextureIndices.push_back(texIndex);
modelData.materialBlendModes.push_back(mat.blendMode);
modelData.materialFlags.push_back(mat.flags);
}
// Helper: look up group name from MOGN raw data via MOGI nameOffset
@ -507,16 +508,31 @@ 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
// Detect distance-only LOD/exterior shell groups:
// 1. Very low vertex count (<100) — portal connectors, tiny shells
// 2. ALWAYS_DRAW (0x10000) with low verts — distant LOD stand-ins
// 3. Pure OUTDOOR groups (0x8 set, 0x2000 not set) in large WMOs —
// exterior cityscape shells (e.g. "city01" in Stormwind)
bool alwaysDraw = (wmoGroup.flags & 0x10000) != 0;
size_t nVerts = wmoGroup.vertices.size();
bool isLargeWmo = model.nGroups > 50;
// Detect facade groups by name (exterior face of buildings)
std::string gname = getGroupName(static_cast<uint32_t>(gi));
bool isFacade = false;
bool isCityShell = false;
if (!gname.empty()) {
std::string lower = gname;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lower.find("lod") != std::string::npos) {
resources.isLOD = true;
LOG_INFO("WMO group ", gi, " '", gname, "' marked as LOD (distance-only)");
}
isFacade = lower.find("facade") != std::string::npos;
// "city01" etc are exterior cityscape shells in large WMOs
isCityShell = (lower.find("city") == 0 && lower.size() <= 8);
}
// Flag 0x80 on INDOOR groups in large WMOs = interior cathedral shell
bool hasFlag80 = (wmoGroup.flags & 0x80) != 0;
bool isIndoor = (wmoGroup.flags & 0x2000) != 0;
if (nVerts < 100 || (alwaysDraw && nVerts < 5000) || (isFacade && isLargeWmo) || (isCityShell && isLargeWmo) || (hasFlag80 && isIndoor && isLargeWmo)) {
resources.isLOD = true;
}
modelData.groups.push_back(resources);
loadedGroups++;
@ -572,9 +588,7 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
unlit = (matFlags & 0x01) != 0;
}
// Only F_WINDOW (0x40) materials render as transparent glass.
// F_SIDN (0x20) is the night-glow/self-illuminated flag used on
// lamp posts, lanterns, etc. — those should NOT be glass.
// F_WINDOW = 0x40, renders as transparent glass.
bool isWindow = (matFlags & 0x40) != 0;
BatchKey key{ reinterpret_cast<uintptr_t>(tex), alphaTest, unlit, isWindow };
@ -1405,18 +1419,15 @@ void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
if (modelIt == loadedModels.end()) continue;
const ModelData& model = modelIt->second;
// Push model matrix
GPUPushConstants push{};
push.model = instance.modelMatrix;
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;
// LOD shell groups render only beyond this distance squared (190 units)
static constexpr float LOD_SHELL_DIST_SQ = 196.0f * 196.0f;
// Render visible groups
for (uint32_t gi : dl.visibleGroups) {
@ -1425,8 +1436,13 @@ 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;
// Skip distance-only LOD shell groups when camera is close to the group
if (group.isLOD) {
glm::vec3 groupCenter = instance.modelMatrix * glm::vec4(
(group.boundingBoxMin + group.boundingBoxMax) * 0.5f, 1.0f);
float groupDistSq = glm::dot(camPos - groupCenter, camPos - groupCenter);
if (groupDistSq < LOD_SHELL_DIST_SQ) continue;
}
// Bind vertex + index buffers
VkDeviceSize offset = 0;