Files
WoWee/include/rendering/wmo_renderer.hpp
Kelsi ab527716e5 Add per-group spatial grid for WMO collision and reduce collision call frequency
Build a 2D triangle grid per WMO group at load time so getFloorHeight and
checkWallCollision only test triangles in nearby cells instead of brute-forcing
all triangles. Also reduce sweep steps (12→4), ground probes (3→1), camera
floor probes (5→2), throttle isInsideWMO to every 10 frames, and early-out
wall collision on first hit.
2026-02-07 15:47:43 -08:00

556 lines
18 KiB
C++

#pragma once
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <string>
#include <optional>
namespace wowee {
namespace pipeline {
struct WMOModel;
struct WMOGroup;
class AssetManager;
}
namespace rendering {
class Camera;
class Shader;
class Frustum;
/**
* WMO (World Model Object) Renderer
*
* Renders buildings, dungeons, and large structures from WMO files.
* Features:
* - Multi-material rendering
* - Batched rendering per group
* - Frustum culling
* - Portal visibility (future)
* - Dynamic lighting support (future)
*/
class WMORenderer {
public:
WMORenderer();
~WMORenderer();
/**
* Initialize renderer and create shaders
* @param assetManager Asset manager for loading textures (optional)
*/
bool initialize(pipeline::AssetManager* assetManager = nullptr);
/**
* Cleanup GPU resources
*/
void shutdown();
/**
* Load WMO model and create GPU resources
* @param model WMO model with geometry data
* @param id Unique identifier for this WMO instance
* @return True if successful
*/
bool loadModel(const pipeline::WMOModel& model, uint32_t id);
/**
* Unload WMO model and free GPU resources
* @param id WMO model identifier
*/
void unloadModel(uint32_t id);
/**
* Create a WMO instance in the world
* @param modelId WMO model to instantiate
* @param position World position
* @param rotation Rotation (euler angles in radians)
* @param scale Uniform scale
* @return Instance ID
*/
uint32_t createInstance(uint32_t modelId, const glm::vec3& position,
const glm::vec3& rotation = glm::vec3(0.0f),
float scale = 1.0f);
/**
* Remove WMO instance
* @param instanceId Instance to remove
*/
void removeInstance(uint32_t instanceId);
/**
* Remove all instances
*/
void clearInstances();
/**
* Render all WMO instances
* @param camera Camera for view/projection matrices
* @param view View matrix
* @param projection Projection matrix
*/
void render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection);
/**
* Get number of loaded models
*/
uint32_t getModelCount() const { return loadedModels.size(); }
/**
* Get number of active instances
*/
uint32_t getInstanceCount() const { return instances.size(); }
/**
* Remove models that have no instances referencing them
* Call periodically to free GPU memory
*/
void cleanupUnusedModels();
/**
* Get total triangle count (all instances)
*/
uint32_t getTotalTriangleCount() const;
/**
* Get total draw call count (last frame)
*/
uint32_t getDrawCallCount() const { return lastDrawCalls; }
/**
* Enable/disable wireframe rendering
*/
void setWireframeMode(bool enabled) { wireframeMode = enabled; }
/**
* Enable/disable frustum culling
*/
void setFrustumCulling(bool enabled) { frustumCulling = enabled; }
/**
* Enable/disable portal-based visibility culling
*/
void setPortalCulling(bool enabled) { portalCulling = enabled; }
bool isPortalCullingEnabled() const { return portalCulling; }
/**
* Enable/disable distance-based group culling
*/
void setDistanceCulling(bool enabled, float maxDistance = 500.0f) {
distanceCulling = enabled;
maxGroupDistance = maxDistance;
maxGroupDistanceSq = maxDistance * maxDistance;
}
bool isDistanceCullingEnabled() const { return distanceCulling; }
float getMaxGroupDistance() const { return maxGroupDistance; }
/**
* Get number of groups culled by portals last frame
*/
uint32_t getPortalCulledGroups() const { return lastPortalCulledGroups; }
/**
* Get number of groups culled by distance last frame
*/
uint32_t getDistanceCulledGroups() const { return lastDistanceCulledGroups; }
/**
* Enable/disable GPU occlusion query culling
*/
void setOcclusionCulling(bool enabled) { occlusionCulling = enabled; }
bool isOcclusionCullingEnabled() const { return occlusionCulling; }
/**
* Get number of groups culled by occlusion queries last frame
*/
uint32_t getOcclusionCulledGroups() const { return lastOcclusionCulledGroups; }
void setFog(const glm::vec3& color, float start, float end) {
fogColor = color; fogStart = start; fogEnd = end;
}
void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) {
shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true;
}
void clearShadowMap() { shadowEnabled = false; }
/**
* Render depth-only for shadow casting (reuses VAOs)
*/
void renderShadow(const glm::mat4& lightView, const glm::mat4& lightProj, Shader& shadowShader);
/**
* Get floor height at a GL position via ray-triangle intersection
*/
std::optional<float> getFloorHeight(float glX, float glY, float glZ) const;
/**
* Check wall collision and adjust position
* @param from Starting position
* @param to Desired position
* @param adjustedPos Output adjusted position (pushed away from walls)
* @return true if collision occurred
*/
bool checkWallCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3& adjustedPos) const;
/**
* Check if a position is inside any WMO
* @param outModelId If not null, receives the model ID of the WMO
* @return true if inside a WMO
*/
bool isInsideWMO(float glX, float glY, float glZ, uint32_t* outModelId = nullptr) const;
/**
* Raycast against WMO bounding boxes for camera collision
* @param origin Ray origin (e.g., character head position)
* @param direction Ray direction (normalized)
* @param maxDistance Maximum ray distance to check
* @return Distance to first intersection, or maxDistance if no hit
*/
float raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3& direction, float maxDistance) const;
/**
* Limit expensive collision/raycast queries to objects near a focus point.
*/
void setCollisionFocus(const glm::vec3& worldPos, float radius);
void clearCollisionFocus();
void resetQueryStats();
double getQueryTimeMs() const { return queryTimeMs; }
uint32_t getQueryCallCount() const { return queryCallCount; }
// Floor cache persistence (zone-specific files)
void setMapName(const std::string& name) { mapName_ = name; }
const std::string& getMapName() const { return mapName_; }
bool saveFloorCache() const; // Saves to cache/wmo_floor_<mapName>.bin
bool loadFloorCache(); // Loads from cache/wmo_floor_<mapName>.bin
size_t getFloorCacheSize() const { return precomputedFloorGrid.size(); }
// Pre-compute floor cache for all loaded WMO instances
void precomputeFloorCache();
private:
/**
* WMO group GPU resources
*/
struct GroupResources {
GLuint vao = 0;
GLuint vbo = 0;
GLuint ebo = 0;
uint32_t indexCount = 0;
uint32_t vertexCount = 0;
glm::vec3 boundingBoxMin;
glm::vec3 boundingBoxMax;
uint32_t groupFlags = 0;
// Material batches (start index, count, material ID)
struct Batch {
uint32_t startIndex; // First index in EBO
uint32_t indexCount; // Number of indices to draw
uint8_t materialId; // Material/texture reference
};
std::vector<Batch> batches;
// Pre-merged batches for efficient rendering (computed at load time)
struct MergedBatch {
GLuint texId;
bool hasTexture;
bool alphaTest;
bool unlit = false;
uint32_t blendMode = 0;
std::vector<GLsizei> counts;
std::vector<const void*> offsets;
};
std::vector<MergedBatch> mergedBatches;
// Collision geometry (positions only, for floor raycasting)
std::vector<glm::vec3> collisionVertices;
std::vector<uint16_t> collisionIndices;
// 2D spatial grid for fast triangle lookup (built at load time).
// Bins triangles by their XY bounding box into grid cells.
static constexpr float COLLISION_CELL_SIZE = 4.0f;
int gridCellsX = 0;
int gridCellsY = 0;
glm::vec2 gridOrigin; // XY of bounding box min
// cellTriangles[cellY * gridCellsX + cellX] = list of triangle start indices
std::vector<std::vector<uint32_t>> cellTriangles;
// Build the spatial grid from collision geometry
void buildCollisionGrid();
// Get triangle indices for a local-space XY point
const std::vector<uint32_t>* getTrianglesAtLocal(float localX, float localY) const;
// Get triangle indices for a local-space XY range (for wall collision)
void getTrianglesInRange(float minX, float minY, float maxX, float maxY,
std::vector<uint32_t>& out) const;
};
/**
* 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
*/
struct ModelData {
uint32_t id;
std::vector<GroupResources> groups;
glm::vec3 boundingBoxMin;
glm::vec3 boundingBoxMax;
// Texture handles for this model (indexed by texture path order)
std::vector<GLuint> textures;
// Material texture indices (materialId -> texture index)
std::vector<uint32_t> materialTextureIndices;
// Material blend modes (materialId -> blendMode; 1 = alpha-test cutout)
std::vector<uint32_t> materialBlendModes;
// Material flags (materialId -> flags; 0x01 = unlit)
std::vector<uint32_t> materialFlags;
// 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) {
total += group.indexCount / 3;
}
return total;
}
};
/**
* WMO instance in the world
*/
struct WMOInstance {
uint32_t id;
uint32_t modelId;
glm::vec3 position;
glm::vec3 rotation; // Euler angles (radians)
float scale;
glm::mat4 modelMatrix;
glm::mat4 invModelMatrix; // Cached inverse for collision
glm::vec3 worldBoundsMin;
glm::vec3 worldBoundsMax;
std::vector<std::pair<glm::vec3, glm::vec3>> worldGroupBounds;
void updateModelMatrix();
};
/**
* Create GPU resources for a WMO group
*/
bool createGroupResources(const pipeline::WMOGroup& group, GroupResources& resources, uint32_t groupFlags = 0);
/**
* Render a single group
*/
void renderGroup(const GroupResources& group, const ModelData& model,
const glm::mat4& modelMatrix,
const glm::mat4& view, const glm::mat4& projection);
/**
* Check if group is visible in frustum
*/
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
*/
GLuint loadTexture(const std::string& path);
/**
* Initialize occlusion query resources (bbox VAO, shader)
*/
void initOcclusionResources();
/**
* Run occlusion query pre-pass for an instance
*/
void runOcclusionQueries(const WMOInstance& instance, const ModelData& model,
const glm::mat4& view, const glm::mat4& projection);
/**
* Check if a group passed occlusion test (uses previous frame results)
*/
bool isGroupOccluded(uint32_t instanceId, uint32_t groupIndex) const;
struct GridCell {
int x;
int y;
int z;
bool operator==(const GridCell& other) const {
return x == other.x && y == other.y && z == other.z;
}
};
struct GridCellHash {
size_t operator()(const GridCell& c) const {
size_t h1 = std::hash<int>()(c.x);
size_t h2 = std::hash<int>()(c.y);
size_t h3 = std::hash<int>()(c.z);
return h1 ^ (h2 * 0x9e3779b9u) ^ (h3 * 0x85ebca6bu);
}
};
GridCell toCell(const glm::vec3& p) const;
void rebuildSpatialIndex();
void gatherCandidates(const glm::vec3& queryMin, const glm::vec3& queryMax, std::vector<size_t>& outIndices) const;
// Shader
std::unique_ptr<Shader> shader;
// Asset manager for loading textures
pipeline::AssetManager* assetManager = nullptr;
// Current map name for zone-specific floor cache
std::string mapName_;
// Texture cache (path -> texture ID)
std::unordered_map<std::string, GLuint> textureCache;
// Default white texture
GLuint whiteTexture = 0;
// Loaded models (modelId -> ModelData)
std::unordered_map<uint32_t, ModelData> loadedModels;
// Active instances
std::vector<WMOInstance> instances;
uint32_t nextInstanceId = 1;
// Rendering state
bool wireframeMode = false;
bool frustumCulling = true;
bool portalCulling = false; // Disabled by default - needs debugging
bool distanceCulling = false; // Disabled - causes ground to disappear
bool occlusionCulling = false; // GPU occlusion queries - disabled, adds overhead
float maxGroupDistance = 500.0f;
float maxGroupDistanceSq = 250000.0f; // maxGroupDistance^2
uint32_t lastDrawCalls = 0;
mutable uint32_t lastPortalCulledGroups = 0;
mutable uint32_t lastDistanceCulledGroups = 0;
mutable uint32_t lastOcclusionCulledGroups = 0;
// Occlusion query resources
GLuint bboxVao = 0;
GLuint bboxVbo = 0;
std::unique_ptr<Shader> occlusionShader;
// Query objects per (instance, group) - reused each frame
// Key: (instanceId << 16) | groupIndex
mutable std::unordered_map<uint32_t, GLuint> occlusionQueries;
// Results from previous frame (1 frame latency to avoid GPU stalls)
mutable std::unordered_map<uint32_t, bool> occlusionResults;
// Fog parameters
glm::vec3 fogColor = glm::vec3(0.5f, 0.6f, 0.7f);
float fogStart = 400.0f;
float fogEnd = 1200.0f;
// Shadow mapping
GLuint shadowDepthTex = 0;
glm::mat4 lightSpaceMatrix = glm::mat4(1.0f);
bool shadowEnabled = false;
// Optional query-space culling for collision/raycast hot paths.
bool collisionFocusEnabled = false;
glm::vec3 collisionFocusPos = glm::vec3(0.0f);
float collisionFocusRadius = 0.0f;
float collisionFocusRadiusSq = 0.0f;
// Uniform grid for fast local collision queries.
static constexpr float SPATIAL_CELL_SIZE = 64.0f;
std::unordered_map<GridCell, std::vector<uint32_t>, GridCellHash> spatialGrid;
std::unordered_map<uint32_t, size_t> instanceIndexById;
mutable std::vector<size_t> candidateScratch;
mutable std::vector<uint32_t> wallTriScratch; // Scratch for wall collision grid queries
mutable std::unordered_set<uint32_t> candidateIdScratch;
// Parallel visibility culling
uint32_t numCullThreads_ = 1;
struct InstanceDrawList {
size_t instanceIndex;
std::vector<uint32_t> visibleGroups; // group indices that passed culling
uint32_t portalCulled = 0;
uint32_t distanceCulled = 0;
uint32_t occlusionCulled = 0;
};
// Collision query profiling (per frame).
mutable double queryTimeMs = 0.0;
mutable uint32_t queryCallCount = 0;
// Floor height cache - persistent precomputed grid
static constexpr float FLOOR_GRID_CELL_SIZE = 2.0f; // 2 unit grid cells
mutable std::unordered_map<uint64_t, float> precomputedFloorGrid; // key -> floor height
mutable bool floorGridDirty = true; // Rebuild when instances change
mutable uint32_t currentFrameId = 0;
uint64_t floorGridKey(float x, float y) const {
int32_t ix = static_cast<int32_t>(std::floor(x / FLOOR_GRID_CELL_SIZE));
int32_t iy = static_cast<int32_t>(std::floor(y / FLOOR_GRID_CELL_SIZE));
return (static_cast<uint64_t>(static_cast<uint32_t>(ix)) << 32) |
static_cast<uint64_t>(static_cast<uint32_t>(iy));
}
// Compute floor height for a single cell (expensive, done at load time)
std::optional<float> computeFloorHeightSlow(float x, float y, float refZ) const;
};
} // namespace rendering
} // namespace wowee