#pragma once #include #include #include #include #include #include #include #include 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 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; } 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; // 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 batches; // Collision geometry (positions only, for floor raycasting) std::vector collisionVertices; 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 */ struct ModelData { uint32_t id; std::vector groups; glm::vec3 boundingBoxMin; glm::vec3 boundingBoxMax; // Texture handles for this model (indexed by texture path order) std::vector textures; // Material texture indices (materialId -> texture index) std::vector materialTextureIndices; // 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) { 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> worldGroupBounds; void updateModelMatrix(); }; /** * Create GPU resources for a WMO group */ bool createGroupResources(const pipeline::WMOGroup& group, GroupResources& resources); /** * 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& 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()(c.x); size_t h2 = std::hash()(c.y); size_t h3 = std::hash()(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& outIndices) const; // Shader std::unique_ptr shader; // Asset manager for loading textures pipeline::AssetManager* assetManager = nullptr; // Texture cache (path -> texture ID) std::unordered_map textureCache; // Default white texture GLuint whiteTexture = 0; // Loaded models (modelId -> ModelData) std::unordered_map loadedModels; // Active instances std::vector 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 occlusionShader; // Query objects per (instance, group) - reused each frame // Key: (instanceId << 16) | groupIndex mutable std::unordered_map occlusionQueries; // Results from previous frame (1 frame latency to avoid GPU stalls) mutable std::unordered_map 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, GridCellHash> spatialGrid; std::unordered_map instanceIndexById; mutable std::vector candidateScratch; mutable std::unordered_set candidateIdScratch; // Collision query profiling (per frame). mutable double queryTimeMs = 0.0; mutable uint32_t queryCallCount = 0; }; } // namespace rendering } // namespace wowee