#pragma once #include "pipeline/m2_loader.hpp" #include #include #include #include #include #include #include #include #include #include namespace wowee { namespace pipeline { class AssetManager; } namespace rendering { // Forward declarations class Camera; class VkContext; class VkTexture; // 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 via bone SSBO) * - Texture loading from BLP via AssetManager */ class CharacterRenderer { public: CharacterRenderer(); ~CharacterRenderer(); bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* am, VkRenderPass renderPassOverride = VK_NULL_HANDLE, VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT); 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(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera); void recreatePipelines(); bool initializeShadow(VkRenderPass shadowRenderPass); void renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix, const glm::vec3& shadowCenter = glm::vec3(0), float shadowRadius = 1e9f); 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, VkTexture* texture); void setTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot, VkTexture* texture); 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; bool getInstanceFootZ(uint32_t instanceId, float& outFootZ) 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. */ bool getAttachmentTransform(uint32_t instanceId, uint32_t attachmentId, glm::mat4& outTransform); size_t getInstanceCount() const { return instances.size(); } // Normal mapping / POM settings void setNormalMappingEnabled(bool enabled) { normalMappingEnabled_ = enabled; } void setNormalMapStrength(float strength) { normalMapStrength_ = strength; } void setPOMEnabled(bool enabled) { pomEnabled_ = enabled; } void setPOMQuality(int quality) { pomQuality_ = quality; } // Fog/lighting/shadow are now in per-frame UBO — keep stubs for callers that haven't been updated void setFog(const glm::vec3&, float, float) {} void setLighting(const float[3], const float[3], const float[3]) {} void setShadowMap(VkTexture*, const glm::mat4&) {} void clearShadowMap() {} private: // GPU representation of M2 model struct M2ModelGPU { VkBuffer vertexBuffer = VK_NULL_HANDLE; VmaAllocation vertexAlloc = VK_NULL_HANDLE; VkBuffer indexBuffer = VK_NULL_HANDLE; VmaAllocation indexAlloc = VK_NULL_HANDLE; uint32_t indexCount = 0; uint32_t vertexCount = 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 → VkTexture*) std::unordered_map groupTextureOverrides; // Per-texture-slot overrides (slot → VkTexture*) 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}; // Per-instance bone SSBO (double-buffered per frame) VkBuffer boneBuffer[2] = {}; VmaAllocation boneAlloc[2] = {}; void* boneMapped[2] = {}; VkDescriptorSet boneSet[2] = {}; }; 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; void destroyModelGPU(M2ModelGPU& gpuModel); void destroyInstanceBones(CharacterInstance& inst); // 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 onto a base skin BLP. Returns the resulting VkTexture*. */ VkTexture* compositeTextures(const std::vector& layerPaths); /** * Build a composited character skin with explicit region-based equipment overlays. */ VkTexture* 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 VkTexture* (cached). */ VkTexture* loadTexture(const std::string& path); VkTexture* getTransparentTexture() const { return transparentTexture_.get(); } /** Replace a loaded model's texture at the given slot. */ void setModelTexture(uint32_t modelId, uint32_t textureSlot, VkTexture* texture); /** Reset a model's texture slot back to white fallback. */ void resetModelTexture(uint32_t modelId, uint32_t textureSlot); private: VkContext* vkCtx_ = nullptr; VkRenderPass renderPassOverride_ = VK_NULL_HANDLE; VkSampleCountFlagBits msaaSamplesOverride_ = VK_SAMPLE_COUNT_1_BIT; pipeline::AssetManager* assetManager = nullptr; // Vulkan pipelines (one per blend mode) VkPipeline opaquePipeline_ = VK_NULL_HANDLE; VkPipeline alphaTestPipeline_ = VK_NULL_HANDLE; VkPipeline alphaPipeline_ = VK_NULL_HANDLE; VkPipeline additivePipeline_ = VK_NULL_HANDLE; VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE; // Descriptor set layouts VkDescriptorSetLayout perFrameLayout_ = VK_NULL_HANDLE; // set 0 (owned by Renderer) VkDescriptorSetLayout materialSetLayout_ = VK_NULL_HANDLE; // set 1 VkDescriptorSetLayout boneSetLayout_ = VK_NULL_HANDLE; // set 2 // Descriptor pool VkDescriptorPool materialDescPools_[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; VkDescriptorPool boneDescPool_ = VK_NULL_HANDLE; uint32_t lastMaterialPoolResetFrame_ = 0xFFFFFFFFu; std::vector> transientMaterialUbos_[2]; // Texture cache struct TextureCacheEntry { std::unique_ptr texture; std::unique_ptr normalHeightMap; float heightMapVariance = 0.0f; size_t approxBytes = 0; uint64_t lastUse = 0; bool hasAlpha = false; bool colorKeyBlack = false; }; std::unordered_map textureCache; std::unordered_map textureHasAlphaByPtr_; std::unordered_map textureColorKeyBlackByPtr_; std::unordered_map compositeCache_; // key → texture for reuse std::unordered_set failedTextureCache_; // negative cache for budget exhaustion std::unordered_set loggedTextureLoadFails_; // dedup warning logs size_t textureCacheBytes_ = 0; uint64_t textureCacheCounter_ = 0; size_t textureCacheBudgetBytes_ = 1024ull * 1024 * 1024; uint32_t textureBudgetRejectWarnings_ = 0; std::unique_ptr whiteTexture_; std::unique_ptr transparentTexture_; std::unique_ptr flatNormalTexture_; std::unordered_map models; std::unordered_map instances; uint32_t nextInstanceId = 1; // Normal map generation (same algorithm as WMO renderer) std::unique_ptr generateNormalHeightMap( const uint8_t* pixels, uint32_t width, uint32_t height, float& outVariance); // Normal mapping / POM settings bool normalMappingEnabled_ = true; float normalMapStrength_ = 0.8f; bool pomEnabled_ = true; int pomQuality_ = 1; // 0=Low(16), 1=Medium(32), 2=High(64) // Maximum bones supported static constexpr int MAX_BONES = 240; uint32_t numAnimThreads_ = 1; std::vector> animFutures_; // Shadow pipeline resources VkPipeline shadowPipeline_ = VK_NULL_HANDLE; VkPipelineLayout shadowPipelineLayout_ = VK_NULL_HANDLE; VkDescriptorSetLayout shadowParamsLayout_ = VK_NULL_HANDLE; VkDescriptorPool shadowParamsPool_ = VK_NULL_HANDLE; VkDescriptorSet shadowParamsSet_ = VK_NULL_HANDLE; VkBuffer shadowParamsUBO_ = VK_NULL_HANDLE; VmaAllocation shadowParamsAlloc_ = VK_NULL_HANDLE; }; } // namespace rendering } // namespace wowee