mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 15:50:20 +00:00
360 lines
15 KiB
C++
360 lines
15 KiB
C++
#pragma once
|
|
|
|
#include "pipeline/m2_loader.hpp"
|
|
#include "pipeline/blp_loader.hpp"
|
|
#include <vulkan/vulkan.h>
|
|
#include <vk_mem_alloc.h>
|
|
#include <glm/glm.hpp>
|
|
#include <memory>
|
|
#include <vector>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <future>
|
|
#include <deque>
|
|
#include <mutex>
|
|
#include <atomic>
|
|
|
|
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 clear(); // Remove all models/instances/textures but keep pipelines/pools
|
|
|
|
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));
|
|
|
|
/** Pre-allocate GPU resources (bone SSBOs, descriptors) on main thread before parallel render. */
|
|
void prepareRender(uint32_t frameIndex);
|
|
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);
|
|
void setInstanceOpacity(uint32_t instanceId, float opacity);
|
|
const pipeline::M2Model* getModelData(uint32_t modelId) const;
|
|
void setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& 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<pipeline::M2Sequence>& 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;
|
|
bool getInstancePosition(uint32_t instanceId, glm::vec3& outPos) 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() {}
|
|
|
|
// Pre-decoded BLP cache: set before calling loadModel() to skip main-thread BLP decode
|
|
void setPredecodedBLPCache(std::unordered_map<std::string, pipeline::BLPImage>* cache) { predecodedBLPCache_ = cache; }
|
|
|
|
private:
|
|
std::unordered_map<std::string, pipeline::BLPImage>* predecodedBLPCache_ = nullptr;
|
|
// 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<glm::mat4> bindPose; // Inverse bind pose matrices
|
|
|
|
// Textures loaded from BLP (indexed by texture array position)
|
|
std::vector<VkTexture*> 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<glm::mat4> boneMatrices; // Current bone transforms
|
|
|
|
// Geoset visibility — which submesh IDs to render
|
|
// Empty = render all (for non-character models)
|
|
std::unordered_set<uint16_t> activeGeosets;
|
|
|
|
// Per-geoset-group texture overrides (group → VkTexture*)
|
|
std::unordered_map<uint16_t, VkTexture*> groupTextureOverrides;
|
|
|
|
// Per-texture-slot overrides (slot → VkTexture*)
|
|
std::unordered_map<uint16_t, VkTexture*> textureSlotOverrides;
|
|
|
|
// Weapon attachments (weapons parented to this instance's bones)
|
|
std::vector<WeaponAttachment> 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};
|
|
|
|
// Bone update throttling (skip frames for distant characters)
|
|
uint32_t boneUpdateCounter = 0;
|
|
const M2ModelGPU* cachedModel = nullptr; // Avoid per-frame hash lookups
|
|
|
|
// 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<uint32_t>& 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);
|
|
|
|
// Attachment point lookup helper — shared by attachWeapon() and getAttachmentTransform()
|
|
bool findAttachmentBone(uint32_t modelId, uint32_t attachmentId,
|
|
uint16_t& outBoneIndex, glm::vec3& outOffset) const;
|
|
|
|
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<std::string>& layerPaths);
|
|
|
|
/**
|
|
* Build a composited character skin with explicit region-based equipment overlays.
|
|
*/
|
|
VkTexture* compositeWithRegions(const std::string& basePath,
|
|
const std::vector<std::string>& baseLayers,
|
|
const std::vector<std::pair<int, std::string>>& 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;
|
|
|
|
// Material UBO ring buffer — pre-allocated per frame slot, sub-allocated each draw
|
|
VkBuffer materialRingBuffer_[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
|
|
VmaAllocation materialRingAlloc_[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
|
|
void* materialRingMapped_[2] = {nullptr, nullptr};
|
|
uint32_t materialRingOffset_[2] = {0, 0};
|
|
uint32_t materialUboAlignment_ = 256; // minUniformBufferOffsetAlignment
|
|
static constexpr uint32_t MATERIAL_RING_CAPACITY = 4096;
|
|
|
|
// Texture cache
|
|
struct TextureCacheEntry {
|
|
std::unique_ptr<VkTexture> texture;
|
|
std::unique_ptr<VkTexture> normalHeightMap;
|
|
float heightMapVariance = 0.0f;
|
|
size_t approxBytes = 0;
|
|
uint64_t lastUse = 0;
|
|
bool hasAlpha = false;
|
|
bool colorKeyBlack = false;
|
|
bool normalMapPending = false; // deferred normal map generation
|
|
};
|
|
std::unordered_map<std::string, TextureCacheEntry> textureCache;
|
|
std::unordered_map<VkTexture*, bool> textureHasAlphaByPtr_;
|
|
std::unordered_map<VkTexture*, bool> textureColorKeyBlackByPtr_;
|
|
std::unordered_map<std::string, VkTexture*> compositeCache_; // key → texture for reuse
|
|
std::unordered_set<std::string> failedTextureCache_; // negative cache for budget exhaustion
|
|
std::unordered_map<std::string, uint64_t> failedTextureRetryAt_;
|
|
std::unordered_set<std::string> loggedTextureLoadFails_; // dedup warning logs
|
|
uint64_t textureLookupSerial_ = 0;
|
|
size_t textureCacheBytes_ = 0;
|
|
uint64_t textureCacheCounter_ = 0;
|
|
size_t textureCacheBudgetBytes_ = 1024ull * 1024 * 1024;
|
|
uint32_t textureBudgetRejectWarnings_ = 0;
|
|
std::unique_ptr<VkTexture> whiteTexture_;
|
|
std::unique_ptr<VkTexture> transparentTexture_;
|
|
std::unique_ptr<VkTexture> flatNormalTexture_;
|
|
|
|
std::unordered_map<uint32_t, M2ModelGPU> models;
|
|
std::unordered_map<uint32_t, CharacterInstance> instances;
|
|
|
|
uint32_t nextInstanceId = 1;
|
|
|
|
// Normal map generation (same algorithm as WMO renderer)
|
|
std::unique_ptr<VkTexture> generateNormalHeightMap(
|
|
const uint8_t* pixels, uint32_t width, uint32_t height, float& outVariance);
|
|
|
|
// Background normal map generation — CPU work on thread pool, GPU upload on main thread
|
|
struct NormalMapResult {
|
|
std::string cacheKey;
|
|
std::vector<uint8_t> pixels; // RGBA normal map output
|
|
uint32_t width, height;
|
|
float variance;
|
|
};
|
|
// Completed results ready for GPU upload (populated by background threads)
|
|
std::mutex normalMapResultsMutex_;
|
|
std::deque<NormalMapResult> completedNormalMaps_;
|
|
std::atomic<int> pendingNormalMapCount_{0}; // in-flight background tasks
|
|
|
|
// Pure CPU normal map generation (thread-safe, no GPU access)
|
|
static NormalMapResult generateNormalHeightMapCPU(
|
|
std::string cacheKey, std::vector<uint8_t> pixels, uint32_t width, uint32_t height);
|
|
public:
|
|
void processPendingNormalMaps(int budget = 4);
|
|
private:
|
|
|
|
// 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<std::future<void>> 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
|