diff --git a/include/pipeline/wmo_loader.hpp b/include/pipeline/wmo_loader.hpp index f3c76cd3..718042c8 100644 --- a/include/pipeline/wmo_loader.hpp +++ b/include/pipeline/wmo_loader.hpp @@ -100,6 +100,14 @@ struct WMOPortalPlane { float distance; }; +// WMO Portal Reference (MOPR chunk) - links portals to groups +struct WMOPortalRef { + uint16_t portalIndex; // Index into portals array + uint16_t groupIndex; // Group on other side of portal + int16_t side; // Which side of portal plane (-1 or 1) + uint16_t padding; +}; + // WMO Liquid (MLIQ chunk data) struct WMOLiquid { uint32_t xVerts = 0; // Vertices in X direction @@ -192,6 +200,7 @@ struct WMOModel { std::vector portals; std::vector portalPlanes; std::vector portalVertices; + std::vector portalRefs; // MOPR chunk - portal-to-group links // Lights std::vector lights; diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 7e2fe66d..75a190e1 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -20,6 +20,7 @@ namespace rendering { class Camera; class Shader; +class Frustum; /** * WMO (World Model Object) Renderer @@ -129,6 +130,17 @@ public: */ void setFrustumCulling(bool enabled) { frustumCulling = enabled; } + /** + * Enable/disable portal-based visibility culling + */ + void setPortalCulling(bool enabled) { portalCulling = enabled; } + bool isPortalCullingEnabled() const { return portalCulling; } + + /** + * Get number of groups culled by portals last frame + */ + uint32_t getPortalCulledGroups() const { return lastPortalCulledGroups; } + void setFog(const glm::vec3& color, float start, float end) { fogColor = color; fogStart = start; fogEnd = end; } @@ -209,6 +221,22 @@ private: std::vector collisionIndices; }; + /** + * Portal data for visibility culling + */ + struct PortalData { + uint16_t startVertex; + uint16_t vertexCount; + glm::vec3 normal; + float distance; + }; + + struct PortalRef { + uint16_t portalIndex; + uint16_t groupIndex; + int16_t side; + }; + /** * Loaded WMO model data */ @@ -227,6 +255,13 @@ private: // Material blend modes (materialId -> blendMode; 1 = alpha-test cutout) std::vector materialBlendModes; + // Portal visibility data + std::vector portals; + std::vector portalVertices; + std::vector portalRefs; + // For each group: which portal refs belong to it (start index, count) + std::vector> groupPortalRefs; + uint32_t getTotalTriangles() const { uint32_t total = 0; for (const auto& group : groups) { @@ -272,6 +307,34 @@ private: bool isGroupVisible(const GroupResources& group, const glm::mat4& modelMatrix, const Camera& camera) const; + /** + * Find which group index contains a position (model space) + * @return Group index or -1 if outside all groups + */ + int findContainingGroup(const ModelData& model, const glm::vec3& localPos) const; + + /** + * Get visible groups via portal traversal + * @param model The WMO model data + * @param cameraLocalPos Camera position in model space + * @param frustum Frustum for portal visibility testing + * @param modelMatrix Transform for world-space frustum test + * @param outVisibleGroups Output set of visible group indices + */ + void getVisibleGroupsViaPortals(const ModelData& model, + const glm::vec3& cameraLocalPos, + const Frustum& frustum, + const glm::mat4& modelMatrix, + std::unordered_set& outVisibleGroups) const; + + /** + * Test if a portal polygon is visible from a position through a frustum + */ + bool isPortalVisible(const ModelData& model, uint16_t portalIndex, + const glm::vec3& cameraLocalPos, + const Frustum& frustum, + const glm::mat4& modelMatrix) const; + /** * Load a texture from path */ @@ -320,7 +383,9 @@ private: // Rendering state bool wireframeMode = false; bool frustumCulling = true; + bool portalCulling = false; // Disabled by default - needs debugging uint32_t lastDrawCalls = 0; + mutable uint32_t lastPortalCulledGroups = 0; // Fog parameters glm::vec3 fogColor = glm::vec3(0.5f, 0.6f, 0.7f); diff --git a/src/core/application.cpp b/src/core/application.cpp index ed19803f..28c9bce9 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -477,9 +477,14 @@ void Application::setupUICallbacks() { gameHandler->setSinglePlayerMode(true); gameHandler->setSinglePlayerCharListReady(); } - uiManager->getCharacterCreateScreen().reset(); - uiManager->getCharacterCreateScreen().initializePreview(assetManager.get()); - setState(AppState::CHARACTER_CREATION); + // If characters exist, go to selection; otherwise go to creation + if (gameHandler && !gameHandler->getCharacters().empty()) { + setState(AppState::CHARACTER_SELECTION); + } else { + uiManager->getCharacterCreateScreen().reset(); + uiManager->getCharacterCreateScreen().initializePreview(assetManager.get()); + setState(AppState::CHARACTER_CREATION); + } }); // Realm selection callback diff --git a/src/pipeline/wmo_loader.cpp b/src/pipeline/wmo_loader.cpp index 8d330d86..99f631fe 100644 --- a/src/pipeline/wmo_loader.cpp +++ b/src/pipeline/wmo_loader.cpp @@ -356,6 +356,21 @@ WMOModel WMOLoader::load(const std::vector& wmoData) { break; } + case MOPR: { + // Portal references - links groups via portals + uint32_t nRefs = chunkSize / 8; // Each reference is 8 bytes + for (uint32_t i = 0; i < nRefs; i++) { + WMOPortalRef ref; + ref.portalIndex = read(wmoData, offset); + ref.groupIndex = read(wmoData, offset); + ref.side = read(wmoData, offset); + ref.padding = read(wmoData, offset); + model.portalRefs.push_back(ref); + } + core::Logger::getInstance().info("WMO portal refs: ", model.portalRefs.size()); + break; + } + default: // Unknown chunk, skip it break; diff --git a/src/rendering/performance_hud.cpp b/src/rendering/performance_hud.cpp index e887294e..3124ec1b 100644 --- a/src/rendering/performance_hud.cpp +++ b/src/rendering/performance_hud.cpp @@ -370,6 +370,9 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) { ImGui::Text("Instances: %u", wmoRenderer->getInstanceCount()); ImGui::Text("Triangles: %u", wmoRenderer->getTotalTriangleCount()); ImGui::Text("Draw Calls: %u", wmoRenderer->getDrawCallCount()); + if (wmoRenderer->isPortalCullingEnabled()) { + ImGui::Text("Portal Culled: %u groups", wmoRenderer->getPortalCulledGroups()); + } ImGui::Spacing(); } diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 68dc7fb1..1a4f8947 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -296,6 +296,43 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { return false; } + // Copy portal data for visibility culling + modelData.portalVertices = model.portalVertices; + for (const auto& portal : model.portals) { + PortalData pd; + pd.startVertex = portal.startVertex; + pd.vertexCount = portal.vertexCount; + // Compute portal plane from vertices if we have them + if (portal.vertexCount >= 3 && portal.startVertex + portal.vertexCount <= model.portalVertices.size()) { + glm::vec3 v0 = model.portalVertices[portal.startVertex]; + glm::vec3 v1 = model.portalVertices[portal.startVertex + 1]; + glm::vec3 v2 = model.portalVertices[portal.startVertex + 2]; + pd.normal = glm::normalize(glm::cross(v1 - v0, v2 - v0)); + pd.distance = glm::dot(pd.normal, v0); + } else { + pd.normal = glm::vec3(0.0f, 0.0f, 1.0f); + pd.distance = 0.0f; + } + modelData.portals.push_back(pd); + } + for (const auto& ref : model.portalRefs) { + PortalRef pr; + pr.portalIndex = ref.portalIndex; + pr.groupIndex = ref.groupIndex; + pr.side = ref.side; + modelData.portalRefs.push_back(pr); + } + // Build per-group portal ref ranges from WMOGroup data + modelData.groupPortalRefs.resize(model.groups.size(), {0, 0}); + for (size_t gi = 0; gi < model.groups.size(); gi++) { + modelData.groupPortalRefs[gi] = {model.groups[gi].portalStart, model.groups[gi].portalCount}; + } + + if (!modelData.portals.empty()) { + core::Logger::getInstance().info("WMO portals: ", modelData.portals.size(), + " refs: ", modelData.portalRefs.size()); + } + loadedModels[id] = std::move(modelData); core::Logger::getInstance().info("WMO model ", id, " loaded successfully (", loadedGroups, " groups)"); return true; @@ -527,6 +564,8 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: Frustum frustum; frustum.extractFromMatrix(projection * view); + lastPortalCulledGroups = 0; + // Render all instances with instance-level culling for (const auto& instance : instances) { // NOTE: Disabled hard instance-distance culling for WMOs. @@ -549,8 +588,26 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: const ModelData& model = modelIt->second; shader->setUniform("uModel", instance.modelMatrix); + // Portal-based visibility culling + std::unordered_set portalVisibleGroups; + bool usePortalCulling = portalCulling && !model.portals.empty() && !model.portalRefs.empty(); + + if (usePortalCulling) { + // Transform camera position to model's local space + glm::vec4 localCamPos = instance.invModelMatrix * glm::vec4(camera.getPosition(), 1.0f); + glm::vec3 cameraLocalPos(localCamPos); + + getVisibleGroupsViaPortals(model, cameraLocalPos, frustum, instance.modelMatrix, portalVisibleGroups); + } + // Render all groups using cached world-space bounds for (size_t gi = 0; gi < model.groups.size(); ++gi) { + // Portal culling check + if (usePortalCulling && portalVisibleGroups.find(static_cast(gi)) == portalVisibleGroups.end()) { + lastPortalCulledGroups++; + continue; + } + if (frustumCulling && gi < instance.worldGroupBounds.size()) { const auto& [gMin, gMax] = instance.worldGroupBounds[gi]; if (!frustum.intersectsAABB(gMin, gMax)) { @@ -799,6 +856,111 @@ bool WMORenderer::isGroupVisible(const GroupResources& group, const glm::mat4& m return behindCount < 8; } +int WMORenderer::findContainingGroup(const ModelData& model, const glm::vec3& localPos) const { + // Find which group's bounding box contains the position + // Prefer interior groups (smaller volume) when multiple match + int bestGroup = -1; + float bestVolume = std::numeric_limits::max(); + + for (size_t gi = 0; gi < model.groups.size(); gi++) { + const auto& group = model.groups[gi]; + if (localPos.x >= group.boundingBoxMin.x && localPos.x <= group.boundingBoxMax.x && + localPos.y >= group.boundingBoxMin.y && localPos.y <= group.boundingBoxMax.y && + localPos.z >= group.boundingBoxMin.z && localPos.z <= group.boundingBoxMax.z) { + glm::vec3 size = group.boundingBoxMax - group.boundingBoxMin; + float volume = size.x * size.y * size.z; + if (volume < bestVolume) { + bestVolume = volume; + bestGroup = static_cast(gi); + } + } + } + return bestGroup; +} + +bool WMORenderer::isPortalVisible(const ModelData& model, uint16_t portalIndex, + [[maybe_unused]] const glm::vec3& cameraLocalPos, + const Frustum& frustum, + const glm::mat4& modelMatrix) const { + if (portalIndex >= model.portals.size()) return false; + + const auto& portal = model.portals[portalIndex]; + if (portal.vertexCount < 3) return false; + if (portal.startVertex + portal.vertexCount > model.portalVertices.size()) return false; + + // Get portal polygon center and bounds for frustum test + glm::vec3 center(0.0f); + glm::vec3 pMin = model.portalVertices[portal.startVertex]; + glm::vec3 pMax = pMin; + for (uint16_t i = 0; i < portal.vertexCount; i++) { + const auto& v = model.portalVertices[portal.startVertex + i]; + center += v; + pMin = glm::min(pMin, v); + pMax = glm::max(pMax, v); + } + 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); + + // Check if portal AABB intersects frustum (more robust than point test) + return frustum.intersectsAABB(glm::vec3(worldMin), glm::vec3(worldMax)); +} + +void WMORenderer::getVisibleGroupsViaPortals(const ModelData& model, + const glm::vec3& cameraLocalPos, + const Frustum& frustum, + const glm::mat4& modelMatrix, + std::unordered_set& outVisibleGroups) const { + // Find camera's containing group + int cameraGroup = findContainingGroup(model, cameraLocalPos); + + // If camera is outside all groups, fall back to frustum culling only + if (cameraGroup < 0) { + // Camera outside WMO - mark all groups as potentially visible + // (will still be frustum culled in render) + for (size_t gi = 0; gi < model.groups.size(); gi++) { + outVisibleGroups.insert(static_cast(gi)); + } + return; + } + + // BFS through portals from camera's group + std::vector visited(model.groups.size(), false); + std::vector queue; + queue.push_back(static_cast(cameraGroup)); + visited[cameraGroup] = true; + outVisibleGroups.insert(static_cast(cameraGroup)); + + size_t queueIdx = 0; + while (queueIdx < queue.size()) { + uint32_t currentGroup = queue[queueIdx++]; + + // Get portal refs for this group + if (currentGroup >= model.groupPortalRefs.size()) continue; + auto [portalStart, portalCount] = model.groupPortalRefs[currentGroup]; + + for (uint16_t pi = 0; pi < portalCount; pi++) { + uint16_t refIdx = portalStart + pi; + if (refIdx >= model.portalRefs.size()) continue; + + const auto& ref = model.portalRefs[refIdx]; + uint32_t targetGroup = ref.groupIndex; + + if (targetGroup >= model.groups.size()) continue; + if (visited[targetGroup]) continue; + + // Check if portal is visible from camera + if (isPortalVisible(model, ref.portalIndex, cameraLocalPos, frustum, modelMatrix)) { + visited[targetGroup] = true; + outVisibleGroups.insert(targetGroup); + queue.push_back(targetGroup); + } + } + } +} + void WMORenderer::WMOInstance::updateModelMatrix() { modelMatrix = glm::mat4(1.0f); modelMatrix = glm::translate(modelMatrix, position);