From 1180f0227cf887f9e3d6ebd47ceb36fe5a8f8ba2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 11:51:43 -0700 Subject: [PATCH] rendering: fix WMO portal AABB transform for rotated WMOs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isPortalVisible() was computing the world-space AABB by directly transforming pMin/pMax with the model matrix. This is incorrect for rotated WMOs — when the model matrix includes rotations, components can be swapped or negated, yielding an inverted AABB (worldMin.x > worldMax.x) that causes frustum.intersectsAABB() to fail. Fix: transform all 8 corners of the portal bounding box and take the component-wise min/max, which gives the correct world-space AABB for any rotation/scale. This was the root cause of portals being incorrectly culled in rotated WMO instances (e.g. many dungeon and city WMOs). Also squash the earlier spline-speed no-op fix (parse guid + float instead of consuming the full packet for SMSG_SPLINE_SET_FLIGHT_SPEED and friends) into this commit. --- src/rendering/wmo_renderer.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 4d52fd76..3df2b3fd 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -2049,12 +2049,25 @@ bool WMORenderer::isPortalVisible(const ModelData& model, uint16_t portalIndex, } center /= static_cast(portal.vertexCount); - // Transform bounds to world space for frustum test - glm::vec4 worldMin = modelMatrix * glm::vec4(pMin, 1.0f); - glm::vec4 worldMax = modelMatrix * glm::vec4(pMax, 1.0f); + // Transform all 8 corners to world space to build the correct world AABB. + // Direct transform of pMin/pMax is wrong for rotated WMOs — the matrix can + // swap or negate components, inverting min/max and causing frustum test failures. + const glm::vec3 corners[8] = { + {pMin.x, pMin.y, pMin.z}, {pMax.x, pMin.y, pMin.z}, + {pMin.x, pMax.y, pMin.z}, {pMax.x, pMax.y, pMin.z}, + {pMin.x, pMin.y, pMax.z}, {pMax.x, pMin.y, pMax.z}, + {pMin.x, pMax.y, pMax.z}, {pMax.x, pMax.y, pMax.z}, + }; + glm::vec3 worldMin( std::numeric_limits::max()); + glm::vec3 worldMax(-std::numeric_limits::max()); + for (const auto& c : corners) { + glm::vec3 wc = glm::vec3(modelMatrix * glm::vec4(c, 1.0f)); + worldMin = glm::min(worldMin, wc); + worldMax = glm::max(worldMax, wc); + } // Check if portal AABB intersects frustum (more robust than point test) - return frustum.intersectsAABB(glm::vec3(worldMin), glm::vec3(worldMax)); + return frustum.intersectsAABB(worldMin, worldMax); } void WMORenderer::getVisibleGroupsViaPortals(const ModelData& model,