mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-25 08:30:13 +00:00
Add WMO portal culling infrastructure and fix single-player character flow
Portal-based visibility culling for WMO rendering (disabled by default, needs debugging for complex WMOs like Stormwind). Skip character creation screen when characters already exist in single-player mode.
This commit is contained in:
parent
ca84384402
commit
41ac8f646e
6 changed files with 262 additions and 3 deletions
|
|
@ -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<WMOPortal> portals;
|
||||
std::vector<WMOPortalPlane> portalPlanes;
|
||||
std::vector<glm::vec3> portalVertices;
|
||||
std::vector<WMOPortalRef> portalRefs; // MOPR chunk - portal-to-group links
|
||||
|
||||
// Lights
|
||||
std::vector<WMOLight> lights;
|
||||
|
|
|
|||
|
|
@ -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<uint16_t> 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<uint32_t> materialBlendModes;
|
||||
|
||||
// Portal visibility data
|
||||
std::vector<PortalData> portals;
|
||||
std::vector<glm::vec3> portalVertices;
|
||||
std::vector<PortalRef> portalRefs;
|
||||
// For each group: which portal refs belong to it (start index, count)
|
||||
std::vector<std::pair<uint16_t, uint16_t>> 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<uint32_t>& 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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -356,6 +356,21 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& 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<uint16_t>(wmoData, offset);
|
||||
ref.groupIndex = read<uint16_t>(wmoData, offset);
|
||||
ref.side = read<int16_t>(wmoData, offset);
|
||||
ref.padding = read<uint16_t>(wmoData, offset);
|
||||
model.portalRefs.push_back(ref);
|
||||
}
|
||||
core::Logger::getInstance().info("WMO portal refs: ", model.portalRefs.size());
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown chunk, skip it
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<uint32_t> 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<uint32_t>(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<float>::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<int>(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<float>(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<uint32_t>& 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<uint32_t>(gi));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// BFS through portals from camera's group
|
||||
std::vector<bool> visited(model.groups.size(), false);
|
||||
std::vector<uint32_t> queue;
|
||||
queue.push_back(static_cast<uint32_t>(cameraGroup));
|
||||
visited[cameraGroup] = true;
|
||||
outVisibleGroups.insert(static_cast<uint32_t>(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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue