#pragma once #include "pipeline/m2_loader.hpp" #include #include #include #include #include #include #include namespace wowee { namespace pipeline { class AssetManager; } namespace rendering { // Forward declarations class Shader; class Texture; class Camera; // Weapon attached to a character instance at a bone attachment point struct WeaponAttachment { uint32_t weaponModelId; uint32_t weaponInstanceId; uint32_t attachmentId; // 1=RightHand, 2=LeftHand uint16_t boneIndex; glm::vec3 offset; }; /** * Character renderer for M2 models with skeletal animation * * Features: * - Skeletal animation with bone transformations * - Keyframe interpolation (linear position/scale, slerp rotation) * - Vertex skinning (GPU-accelerated) * - Texture loading from BLP via AssetManager */ class CharacterRenderer { public: CharacterRenderer(); ~CharacterRenderer(); bool initialize(); void shutdown(); void setAssetManager(pipeline::AssetManager* am) { assetManager = am; } bool loadModel(const pipeline::M2Model& model, uint32_t id); uint32_t createInstance(uint32_t modelId, const glm::vec3& position, const glm::vec3& rotation = glm::vec3(0.0f), float scale = 1.0f); void playAnimation(uint32_t instanceId, uint32_t animationId, bool loop = true); void update(float deltaTime, const glm::vec3& cameraPos = glm::vec3(0.0f)); void render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection); void renderShadow(const glm::mat4& lightSpaceMatrix); void setInstancePosition(uint32_t instanceId, const glm::vec3& position); void setInstanceRotation(uint32_t instanceId, const glm::vec3& rotation); void moveInstanceTo(uint32_t instanceId, const glm::vec3& destination, float durationSeconds); void startFadeIn(uint32_t instanceId, float durationSeconds); const pipeline::M2Model* getModelData(uint32_t modelId) const; void setActiveGeosets(uint32_t instanceId, const std::unordered_set& geosets); void setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, GLuint textureId); void setTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot, GLuint textureId); void clearTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot); void setInstanceVisible(uint32_t instanceId, bool visible); void removeInstance(uint32_t instanceId); bool getAnimationState(uint32_t instanceId, uint32_t& animationId, float& animationTimeMs, float& animationDurationMs) const; bool hasAnimation(uint32_t instanceId, uint32_t animationId) const; bool getAnimationSequences(uint32_t instanceId, std::vector& out) const; bool getInstanceModelName(uint32_t instanceId, std::string& modelName) const; bool getInstanceBounds(uint32_t instanceId, glm::vec3& outCenter, float& outRadius) const; /** Debug: Log all available animations for an instance */ void dumpAnimations(uint32_t instanceId) const; /** Attach a weapon model to a character instance at the given attachment point. */ bool attachWeapon(uint32_t charInstanceId, uint32_t attachmentId, const pipeline::M2Model& weaponModel, uint32_t weaponModelId, const std::string& texturePath); /** Detach a weapon from the given attachment point. */ void detachWeapon(uint32_t charInstanceId, uint32_t attachmentId); /** Get the world-space transform of an attachment point on an instance. * Used for mount seats, weapon positions, etc. * @param instanceId The character/mount instance * @param attachmentId The attachment point ID (0=Mount, 1=RightHand, 2=LeftHand, etc.) * @param outTransform The resulting world-space transform matrix * @return true if attachment found and matrix computed */ bool getAttachmentTransform(uint32_t instanceId, uint32_t attachmentId, glm::mat4& outTransform); size_t getInstanceCount() const { return instances.size(); } void setFog(const glm::vec3& color, float start, float end) { fogColor = color; fogStart = start; fogEnd = end; } void setLighting(const float lightDirIn[3], const float lightColorIn[3], const float ambientColorIn[3]) { lightDir = glm::vec3(lightDirIn[0], lightDirIn[1], lightDirIn[2]); lightColor = glm::vec3(lightColorIn[0], lightColorIn[1], lightColorIn[2]); ambientColor = glm::vec3(ambientColorIn[0], ambientColorIn[1], ambientColorIn[2]); } void setShadowMap(GLuint depthTex, const glm::mat4& lightSpace) { shadowDepthTex = depthTex; lightSpaceMatrix = lightSpace; shadowEnabled = true; } void clearShadowMap() { shadowEnabled = false; } private: // GPU representation of M2 model struct M2ModelGPU { uint32_t vao = 0; uint32_t vbo = 0; uint32_t ebo = 0; pipeline::M2Model data; // Original model data std::vector bindPose; // Inverse bind pose matrices // Textures loaded from BLP (indexed by texture array position) std::vector textureIds; }; // Character instance struct CharacterInstance { uint32_t id; uint32_t modelId; glm::vec3 position; glm::vec3 rotation; float scale; bool visible = true; // For first-person camera hiding // Animation state uint32_t currentAnimationId = 0; int currentSequenceIndex = -1; // Index into M2Model::sequences float animationTime = 0.0f; bool animationLoop = true; bool isDead = false; // Prevents movement while in death state std::vector boneMatrices; // Current bone transforms // Geoset visibility — which submesh IDs to render // Empty = render all (for non-character models) std::unordered_set activeGeosets; // Per-geoset-group texture overrides (group → GL texture ID) std::unordered_map groupTextureOverrides; // Per-texture-slot overrides (slot → GL texture ID) std::unordered_map textureSlotOverrides; // Weapon attachments (weapons parented to this instance's bones) std::vector weaponAttachments; // Opacity (for fade-in) float opacity = 1.0f; float fadeInTime = 0.0f; // elapsed fade time (seconds) float fadeInDuration = 0.0f; // total fade duration (0 = no fade) // Movement interpolation bool isMoving = false; glm::vec3 moveStart{0.0f}; glm::vec3 moveEnd{0.0f}; float moveDuration = 0.0f; // seconds float moveElapsed = 0.0f; // Override model matrix (used for weapon instances positioned by parent bone) bool hasOverrideModelMatrix = false; glm::mat4 overrideModelMatrix{1.0f}; }; void setupModelBuffers(M2ModelGPU& gpuModel); void calculateBindPose(M2ModelGPU& gpuModel); void updateAnimation(CharacterInstance& instance, float deltaTime); void calculateBoneMatrices(CharacterInstance& instance); glm::mat4 getBoneTransform(const pipeline::M2Bone& bone, float time, int sequenceIndex); glm::mat4 getModelMatrix(const CharacterInstance& instance) const; // Keyframe interpolation helpers static int findKeyframeIndex(const std::vector& timestamps, float time); static glm::vec3 interpolateVec3(const pipeline::M2AnimationTrack& track, int seqIdx, float time, const glm::vec3& defaultVal); static glm::quat interpolateQuat(const pipeline::M2AnimationTrack& track, int seqIdx, float time); public: /** * Build a composited character skin texture by alpha-blending overlay * layers (e.g. underwear) onto a base skin BLP. Each overlay is placed * at the correct CharComponentTextureSections region based on its * filename (pelvis, torso, etc.). Returns the resulting GL texture ID. */ GLuint compositeTextures(const std::vector& layerPaths); /** * Build a composited character skin with explicit region-based equipment overlays. * @param basePath Body skin texture path * @param baseLayers Underwear overlay paths (placed by filename keyword) * @param regionLayers Pairs of (region_index, blp_path) for equipment textures * @return GL texture ID of the composited result */ GLuint compositeWithRegions(const std::string& basePath, const std::vector& baseLayers, const std::vector>& regionLayers); /** Clear the composite texture cache (forces re-compositing on next call). */ void clearCompositeCache(); /** Load a BLP texture from MPQ and return the GL texture ID (cached). */ GLuint loadTexture(const std::string& path); /** Replace a loaded model's texture at the given slot with a new GL texture. */ void setModelTexture(uint32_t modelId, uint32_t textureSlot, GLuint textureId); /** Reset a model's texture slot back to white fallback. */ void resetModelTexture(uint32_t modelId, uint32_t textureSlot); private: std::unique_ptr characterShader; GLuint shadowCasterProgram = 0; pipeline::AssetManager* assetManager = nullptr; // Fog parameters glm::vec3 fogColor = glm::vec3(0.5f, 0.6f, 0.7f); float fogStart = 400.0f; float fogEnd = 1200.0f; // Lighting parameters glm::vec3 lightDir = glm::vec3(0.0f, -1.0f, 0.3f); glm::vec3 lightColor = glm::vec3(1.5f, 1.4f, 1.3f); glm::vec3 ambientColor = glm::vec3(0.4f, 0.4f, 0.45f); // Shadow mapping GLuint shadowDepthTex = 0; glm::mat4 lightSpaceMatrix = glm::mat4(1.0f); bool shadowEnabled = false; // Texture cache struct TextureCacheEntry { GLuint id = 0; size_t approxBytes = 0; uint64_t lastUse = 0; bool hasAlpha = false; bool colorKeyBlack = false; }; std::unordered_map textureCache; std::unordered_map textureHasAlphaById_; std::unordered_map textureColorKeyBlackById_; std::unordered_map compositeCache_; // key → GPU texture for reuse std::unordered_set failedTextureCache_; // negative cache for missing textures size_t textureCacheBytes_ = 0; uint64_t textureCacheCounter_ = 0; size_t textureCacheBudgetBytes_ = 1024ull * 1024 * 1024; // Default, overridden at init GLuint whiteTexture = 0; std::unordered_map models; std::unordered_map instances; uint32_t nextInstanceId = 1; // Maximum bones supported (GPU uniform limit) // WoW character models can have 210+ bones; GPU reports 4096 components (~256 mat4) static constexpr int MAX_BONES = 240; }; } // namespace rendering } // namespace wowee