2026-02-05 14:58:45 -08:00
|
|
|
#include "rendering/character_preview.hpp"
|
|
|
|
|
#include "rendering/character_renderer.hpp"
|
2026-02-22 05:58:45 -08:00
|
|
|
#include "rendering/vk_render_target.hpp"
|
2026-02-21 19:41:21 -08:00
|
|
|
#include "rendering/vk_texture.hpp"
|
|
|
|
|
#include "rendering/vk_context.hpp"
|
2026-02-22 05:58:45 -08:00
|
|
|
#include "rendering/vk_frame_data.hpp"
|
2026-02-05 14:58:45 -08:00
|
|
|
#include "rendering/camera.hpp"
|
2026-02-21 19:41:21 -08:00
|
|
|
#include "rendering/renderer.hpp"
|
2026-02-05 14:58:45 -08:00
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
#include "pipeline/m2_loader.hpp"
|
|
|
|
|
#include "pipeline/dbc_loader.hpp"
|
2026-02-12 22:56:36 -08:00
|
|
|
#include "pipeline/dbc_layout.hpp"
|
2026-02-05 14:58:45 -08:00
|
|
|
#include "core/logger.hpp"
|
2026-02-21 19:41:21 -08:00
|
|
|
#include "core/application.hpp"
|
2026-02-22 05:58:45 -08:00
|
|
|
#include <imgui.h>
|
|
|
|
|
#include <backends/imgui_impl_vulkan.h>
|
2026-02-05 14:58:45 -08:00
|
|
|
#include <glm/gtc/matrix_transform.hpp>
|
2026-02-20 21:50:32 -08:00
|
|
|
#include <algorithm>
|
2026-02-05 14:58:45 -08:00
|
|
|
#include <unordered_set>
|
2026-02-22 05:58:45 -08:00
|
|
|
#include <cstring>
|
2026-02-05 14:58:45 -08:00
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
namespace rendering {
|
|
|
|
|
|
|
|
|
|
CharacterPreview::CharacterPreview() = default;
|
|
|
|
|
|
|
|
|
|
CharacterPreview::~CharacterPreview() {
|
|
|
|
|
shutdown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CharacterPreview::initialize(pipeline::AssetManager* am) {
|
|
|
|
|
assetManager_ = am;
|
|
|
|
|
|
2026-02-22 05:58:45 -08:00
|
|
|
// If already initialized with valid resources, reuse them.
|
|
|
|
|
// This avoids destroying GPU resources that may still be referenced by
|
|
|
|
|
// an in-flight command buffer (compositePass recorded earlier this frame).
|
|
|
|
|
if (renderTarget_ && renderTarget_->isValid() && charRenderer_ && camera_) {
|
|
|
|
|
// Mark model as not loaded — loadCharacter() will handle instance cleanup
|
|
|
|
|
modelLoaded_ = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
auto* appRenderer = core::Application::getInstance().getRenderer();
|
2026-02-22 05:58:45 -08:00
|
|
|
vkCtx_ = appRenderer ? appRenderer->getVkContext() : nullptr;
|
2026-02-21 19:41:21 -08:00
|
|
|
VkDescriptorSetLayout perFrameLayout = appRenderer ? appRenderer->getPerFrameSetLayout() : VK_NULL_HANDLE;
|
2026-02-22 05:58:45 -08:00
|
|
|
|
|
|
|
|
if (!vkCtx_ || perFrameLayout == VK_NULL_HANDLE) {
|
|
|
|
|
LOG_ERROR("CharacterPreview: no VkContext or perFrameLayout available");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create off-screen render target first (need its render pass for pipeline creation)
|
|
|
|
|
createFBO();
|
|
|
|
|
if (!renderTarget_ || !renderTarget_->isValid()) {
|
|
|
|
|
LOG_ERROR("CharacterPreview: failed to create off-screen render target");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize CharacterRenderer with our off-screen render pass
|
|
|
|
|
charRenderer_ = std::make_unique<CharacterRenderer>();
|
2026-02-23 10:48:26 -08:00
|
|
|
if (!charRenderer_->initialize(vkCtx_, perFrameLayout, am, renderTarget_->getRenderPass(),
|
|
|
|
|
renderTarget_->getSampleCount())) {
|
2026-02-05 14:58:45 -08:00
|
|
|
LOG_ERROR("CharacterPreview: failed to initialize CharacterRenderer");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 12:21:07 -07:00
|
|
|
// Configure lighting for character preview
|
|
|
|
|
// Use distant fog to avoid clipping, enable shadows for visual depth
|
2026-02-05 14:58:45 -08:00
|
|
|
charRenderer_->setFog(glm::vec3(0.05f, 0.05f, 0.1f), 9999.0f, 10000.0f);
|
|
|
|
|
|
|
|
|
|
camera_ = std::make_unique<Camera>();
|
|
|
|
|
// Portrait-style camera: WoW Z-up coordinate system
|
|
|
|
|
// Model at origin, camera positioned along +Y looking toward -Y
|
|
|
|
|
camera_->setFov(30.0f);
|
|
|
|
|
camera_->setAspectRatio(static_cast<float>(fboWidth_) / static_cast<float>(fboHeight_));
|
|
|
|
|
// Pull camera back far enough to see full body + head with margin
|
|
|
|
|
camera_->setPosition(glm::vec3(0.0f, 4.5f, 0.9f));
|
|
|
|
|
camera_->setRotation(270.0f, 0.0f);
|
|
|
|
|
|
|
|
|
|
LOG_INFO("CharacterPreview initialized (", fboWidth_, "x", fboHeight_, ")");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CharacterPreview::shutdown() {
|
2026-02-22 05:58:45 -08:00
|
|
|
// Unregister from renderer before destroying resources
|
|
|
|
|
auto* appRenderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
if (appRenderer) appRenderer->unregisterPreview(this);
|
|
|
|
|
|
2026-02-05 14:58:45 -08:00
|
|
|
if (charRenderer_) {
|
|
|
|
|
charRenderer_->shutdown();
|
|
|
|
|
charRenderer_.reset();
|
|
|
|
|
}
|
|
|
|
|
camera_.reset();
|
2026-02-22 05:58:45 -08:00
|
|
|
destroyFBO();
|
2026-02-05 14:58:45 -08:00
|
|
|
modelLoaded_ = false;
|
2026-02-22 05:58:45 -08:00
|
|
|
compositeRendered_ = false;
|
2026-02-05 14:58:45 -08:00
|
|
|
instanceId_ = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CharacterPreview::createFBO() {
|
2026-02-22 05:58:45 -08:00
|
|
|
if (!vkCtx_) return;
|
|
|
|
|
VkDevice device = vkCtx_->getDevice();
|
|
|
|
|
VmaAllocator allocator = vkCtx_->getAllocator();
|
|
|
|
|
|
|
|
|
|
// 1. Create off-screen render target with depth
|
|
|
|
|
renderTarget_ = std::make_unique<VkRenderTarget>();
|
2026-02-23 10:48:26 -08:00
|
|
|
if (!renderTarget_->create(*vkCtx_, fboWidth_, fboHeight_, VK_FORMAT_R8G8B8A8_UNORM, true,
|
|
|
|
|
VK_SAMPLE_COUNT_4_BIT)) {
|
2026-02-22 05:58:45 -08:00
|
|
|
LOG_ERROR("CharacterPreview: failed to create render target");
|
|
|
|
|
renderTarget_.reset();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1b. Transition the color image from UNDEFINED to SHADER_READ_ONLY_OPTIMAL
|
|
|
|
|
// so that ImGui::Image doesn't sample an image in UNDEFINED layout before
|
|
|
|
|
// the first compositePass runs.
|
|
|
|
|
{
|
|
|
|
|
VkCommandBuffer cmd = vkCtx_->beginSingleTimeCommands();
|
|
|
|
|
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
|
|
|
|
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
|
|
|
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
|
|
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
|
|
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
|
|
|
barrier.image = renderTarget_->getColorImage();
|
|
|
|
|
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
|
|
|
barrier.subresourceRange.baseMipLevel = 0;
|
|
|
|
|
barrier.subresourceRange.levelCount = 1;
|
|
|
|
|
barrier.subresourceRange.baseArrayLayer = 0;
|
|
|
|
|
barrier.subresourceRange.layerCount = 1;
|
|
|
|
|
barrier.srcAccessMask = 0;
|
|
|
|
|
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
|
|
|
vkCmdPipelineBarrier(cmd,
|
|
|
|
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
|
|
|
|
0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
|
|
|
vkCtx_->endSingleTimeCommands(cmd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Create 1x1 dummy white texture (shadow map placeholder)
|
|
|
|
|
{
|
|
|
|
|
uint8_t white[] = {255, 255, 255, 255};
|
|
|
|
|
dummyWhiteTex_ = std::make_unique<VkTexture>();
|
|
|
|
|
dummyWhiteTex_->upload(*vkCtx_, white, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
|
|
|
|
|
dummyWhiteTex_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST,
|
|
|
|
|
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Create descriptor pool for per-frame sets (2 UBO + 2 sampler)
|
|
|
|
|
{
|
|
|
|
|
VkDescriptorPoolSize sizes[2]{};
|
|
|
|
|
sizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
|
|
|
sizes[0].descriptorCount = MAX_FRAMES;
|
|
|
|
|
sizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
sizes[1].descriptorCount = MAX_FRAMES;
|
|
|
|
|
|
|
|
|
|
VkDescriptorPoolCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
|
|
|
|
ci.maxSets = MAX_FRAMES;
|
|
|
|
|
ci.poolSizeCount = 2;
|
|
|
|
|
ci.pPoolSizes = sizes;
|
|
|
|
|
if (vkCreateDescriptorPool(device, &ci, nullptr, &previewDescPool_) != VK_SUCCESS) {
|
|
|
|
|
LOG_ERROR("CharacterPreview: failed to create descriptor pool");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Create per-frame UBOs and descriptor sets
|
|
|
|
|
auto* appRenderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
VkDescriptorSetLayout perFrameLayout = appRenderer->getPerFrameSetLayout();
|
|
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < MAX_FRAMES; i++) {
|
|
|
|
|
// Create mapped UBO
|
|
|
|
|
VkBufferCreateInfo bufInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
|
|
|
|
bufInfo.size = sizeof(GPUPerFrameData);
|
|
|
|
|
bufInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
|
|
|
|
|
|
|
|
|
|
VmaAllocationCreateInfo allocInfo{};
|
|
|
|
|
allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
|
|
|
|
|
allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT;
|
|
|
|
|
|
|
|
|
|
VmaAllocationInfo mapInfo{};
|
|
|
|
|
if (vmaCreateBuffer(allocator, &bufInfo, &allocInfo,
|
|
|
|
|
&previewUBO_[i], &previewUBOAlloc_[i], &mapInfo) != VK_SUCCESS) {
|
|
|
|
|
LOG_ERROR("CharacterPreview: failed to create UBO ", i);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
previewUBOMapped_[i] = mapInfo.pMappedData;
|
|
|
|
|
|
|
|
|
|
// Allocate descriptor set
|
|
|
|
|
VkDescriptorSetAllocateInfo setAlloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
|
|
|
|
setAlloc.descriptorPool = previewDescPool_;
|
|
|
|
|
setAlloc.descriptorSetCount = 1;
|
|
|
|
|
setAlloc.pSetLayouts = &perFrameLayout;
|
|
|
|
|
if (vkAllocateDescriptorSets(device, &setAlloc, &previewPerFrameSet_[i]) != VK_SUCCESS) {
|
|
|
|
|
LOG_ERROR("CharacterPreview: failed to allocate descriptor set ", i);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write UBO binding (0) and shadow sampler binding (1) using dummy white texture
|
|
|
|
|
VkDescriptorBufferInfo descBuf{};
|
|
|
|
|
descBuf.buffer = previewUBO_[i];
|
|
|
|
|
descBuf.offset = 0;
|
|
|
|
|
descBuf.range = sizeof(GPUPerFrameData);
|
|
|
|
|
|
|
|
|
|
VkDescriptorImageInfo shadowImg = dummyWhiteTex_->descriptorInfo();
|
|
|
|
|
|
|
|
|
|
VkWriteDescriptorSet writes[2]{};
|
|
|
|
|
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
writes[0].dstSet = previewPerFrameSet_[i];
|
|
|
|
|
writes[0].dstBinding = 0;
|
|
|
|
|
writes[0].descriptorCount = 1;
|
|
|
|
|
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
|
|
|
writes[0].pBufferInfo = &descBuf;
|
|
|
|
|
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
writes[1].dstSet = previewPerFrameSet_[i];
|
|
|
|
|
writes[1].dstBinding = 1;
|
|
|
|
|
writes[1].descriptorCount = 1;
|
|
|
|
|
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
writes[1].pImageInfo = &shadowImg;
|
|
|
|
|
|
|
|
|
|
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. Register the color attachment as an ImGui texture
|
|
|
|
|
imguiTextureId_ = ImGui_ImplVulkan_AddTexture(
|
|
|
|
|
renderTarget_->getSampler(),
|
|
|
|
|
renderTarget_->getColorImageView(),
|
|
|
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
|
|
|
|
|
|
|
|
|
LOG_INFO("CharacterPreview: off-screen FBO created (", fboWidth_, "x", fboHeight_, ")");
|
2026-02-05 14:58:45 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CharacterPreview::destroyFBO() {
|
2026-02-22 05:58:45 -08:00
|
|
|
if (!vkCtx_) return;
|
|
|
|
|
VkDevice device = vkCtx_->getDevice();
|
|
|
|
|
VmaAllocator allocator = vkCtx_->getAllocator();
|
|
|
|
|
|
|
|
|
|
if (imguiTextureId_) {
|
|
|
|
|
ImGui_ImplVulkan_RemoveTexture(imguiTextureId_);
|
|
|
|
|
imguiTextureId_ = VK_NULL_HANDLE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < MAX_FRAMES; i++) {
|
|
|
|
|
if (previewUBO_[i]) {
|
|
|
|
|
vmaDestroyBuffer(allocator, previewUBO_[i], previewUBOAlloc_[i]);
|
|
|
|
|
previewUBO_[i] = VK_NULL_HANDLE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (previewDescPool_) {
|
|
|
|
|
vkDestroyDescriptorPool(device, previewDescPool_, nullptr);
|
|
|
|
|
previewDescPool_ = VK_NULL_HANDLE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dummyWhiteTex_.reset();
|
|
|
|
|
|
|
|
|
|
if (renderTarget_) {
|
|
|
|
|
renderTarget_->destroy(device, allocator);
|
|
|
|
|
renderTarget_.reset();
|
|
|
|
|
}
|
2026-02-05 14:58:45 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|
|
|
|
uint8_t skin, uint8_t face,
|
|
|
|
|
uint8_t hairStyle, uint8_t hairColor,
|
2026-02-09 17:56:04 -08:00
|
|
|
uint8_t facialHair, bool useFemaleModel) {
|
2026-02-05 14:58:45 -08:00
|
|
|
if (!charRenderer_ || !assetManager_ || !assetManager_->isInitialized()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 05:58:45 -08:00
|
|
|
// Remove existing instance.
|
|
|
|
|
// Must wait for GPU to finish — compositePass() may have recorded draw commands
|
|
|
|
|
// referencing this instance's bone buffers earlier in the current frame.
|
2026-02-05 14:58:45 -08:00
|
|
|
if (instanceId_ > 0) {
|
2026-02-22 05:58:45 -08:00
|
|
|
if (vkCtx_) vkDeviceWaitIdle(vkCtx_->getDevice());
|
2026-02-05 14:58:45 -08:00
|
|
|
charRenderer_->removeInstance(instanceId_);
|
|
|
|
|
instanceId_ = 0;
|
|
|
|
|
modelLoaded_ = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 17:56:04 -08:00
|
|
|
std::string m2Path = game::getPlayerModelPath(race, gender, useFemaleModel);
|
2026-02-05 14:58:45 -08:00
|
|
|
std::string modelDir;
|
|
|
|
|
std::string baseName;
|
|
|
|
|
{
|
|
|
|
|
size_t slash = m2Path.rfind('\\');
|
|
|
|
|
if (slash != std::string::npos) {
|
|
|
|
|
modelDir = m2Path.substr(0, slash + 1);
|
|
|
|
|
baseName = m2Path.substr(slash + 1);
|
|
|
|
|
} else {
|
|
|
|
|
baseName = m2Path;
|
|
|
|
|
}
|
|
|
|
|
size_t dot = baseName.rfind('.');
|
|
|
|
|
if (dot != std::string::npos) {
|
|
|
|
|
baseName = baseName.substr(0, dot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto m2Data = assetManager_->readFile(m2Path);
|
|
|
|
|
if (m2Data.empty()) {
|
|
|
|
|
LOG_WARNING("CharacterPreview: failed to read M2: ", m2Path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto model = pipeline::M2Loader::load(m2Data);
|
|
|
|
|
|
2026-02-14 13:57:54 -08:00
|
|
|
// Load skin file (only for WotLK M2s - vanilla has embedded skin)
|
2026-02-05 14:58:45 -08:00
|
|
|
std::string skinPath = modelDir + baseName + "00.skin";
|
|
|
|
|
auto skinData = assetManager_->readFile(skinPath);
|
2026-02-14 13:57:54 -08:00
|
|
|
if (!skinData.empty() && model.version >= 264) {
|
2026-02-05 14:58:45 -08:00
|
|
|
pipeline::M2Loader::loadSkin(skinData, model);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!model.isValid()) {
|
|
|
|
|
LOG_WARNING("CharacterPreview: invalid model: ", m2Path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Look up CharSections.dbc for all appearance textures
|
|
|
|
|
uint32_t targetRaceId = static_cast<uint32_t>(race);
|
|
|
|
|
uint32_t targetSexId = (gender == game::Gender::FEMALE) ? 1u : 0u;
|
|
|
|
|
|
|
|
|
|
std::string faceLowerPath;
|
|
|
|
|
std::string faceUpperPath;
|
|
|
|
|
std::string hairScalpPath;
|
|
|
|
|
std::vector<std::string> underwearPaths;
|
2026-02-12 14:55:27 -08:00
|
|
|
bodySkinPath_.clear();
|
|
|
|
|
baseLayers_.clear();
|
2026-02-05 14:58:45 -08:00
|
|
|
|
|
|
|
|
auto charSectionsDbc = assetManager_->loadDBC("CharSections.dbc");
|
|
|
|
|
if (charSectionsDbc) {
|
|
|
|
|
bool foundSkin = false;
|
|
|
|
|
bool foundFace = false;
|
|
|
|
|
bool foundHair = false;
|
|
|
|
|
bool foundUnderwear = false;
|
|
|
|
|
|
2026-02-12 22:56:36 -08:00
|
|
|
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
|
|
|
|
|
2026-02-14 00:00:26 -08:00
|
|
|
uint32_t fRace = csL ? (*csL)["RaceID"] : 1;
|
|
|
|
|
uint32_t fSex = csL ? (*csL)["SexID"] : 2;
|
|
|
|
|
uint32_t fBase = csL ? (*csL)["BaseSection"] : 3;
|
2026-03-22 21:38:56 +03:00
|
|
|
uint32_t fVar = csL ? (*csL)["VariationIndex"] : 8;
|
|
|
|
|
uint32_t fColor = csL ? (*csL)["ColorIndex"] : 9;
|
2026-02-05 14:58:45 -08:00
|
|
|
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
2026-02-14 00:00:26 -08:00
|
|
|
uint32_t raceId = charSectionsDbc->getUInt32(r, fRace);
|
|
|
|
|
uint32_t sexId = charSectionsDbc->getUInt32(r, fSex);
|
|
|
|
|
uint32_t baseSection = charSectionsDbc->getUInt32(r, fBase);
|
|
|
|
|
uint32_t variationIndex = charSectionsDbc->getUInt32(r, fVar);
|
|
|
|
|
uint32_t colorIndex = charSectionsDbc->getUInt32(r, fColor);
|
2026-02-05 14:58:45 -08:00
|
|
|
|
|
|
|
|
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
|
|
|
|
|
2026-02-05 15:07:31 -08:00
|
|
|
// Section 0: Body skin (variation=0, colorIndex = skin color)
|
2026-02-05 14:58:45 -08:00
|
|
|
if (baseSection == 0 && !foundSkin &&
|
2026-02-05 15:07:31 -08:00
|
|
|
variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) {
|
2026-03-22 21:38:56 +03:00
|
|
|
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
|
2026-02-05 14:58:45 -08:00
|
|
|
if (!tex1.empty()) {
|
2026-02-12 14:55:27 -08:00
|
|
|
bodySkinPath_ = tex1;
|
2026-02-05 14:58:45 -08:00
|
|
|
foundSkin = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Section 1: Face (variation = face index, colorIndex = skin color)
|
|
|
|
|
else if (baseSection == 1 && !foundFace &&
|
|
|
|
|
variationIndex == static_cast<uint32_t>(face) &&
|
|
|
|
|
colorIndex == static_cast<uint32_t>(skin)) {
|
2026-03-22 21:38:56 +03:00
|
|
|
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
|
|
|
|
|
std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 5);
|
2026-02-05 14:58:45 -08:00
|
|
|
if (!tex1.empty()) faceLowerPath = tex1;
|
|
|
|
|
if (!tex2.empty()) faceUpperPath = tex2;
|
|
|
|
|
foundFace = true;
|
|
|
|
|
}
|
|
|
|
|
// Section 3: Hair (variation = hair style, colorIndex = hair color)
|
|
|
|
|
else if (baseSection == 3 && !foundHair &&
|
|
|
|
|
variationIndex == static_cast<uint32_t>(hairStyle) &&
|
|
|
|
|
colorIndex == static_cast<uint32_t>(hairColor)) {
|
2026-03-22 21:38:56 +03:00
|
|
|
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
|
2026-02-05 14:58:45 -08:00
|
|
|
if (!tex1.empty()) {
|
|
|
|
|
hairScalpPath = tex1;
|
|
|
|
|
foundHair = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-05 15:07:31 -08:00
|
|
|
// Section 4: Underwear (variation=0, colorIndex = skin color)
|
2026-02-05 14:58:45 -08:00
|
|
|
else if (baseSection == 4 && !foundUnderwear &&
|
2026-02-05 15:07:31 -08:00
|
|
|
variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) {
|
2026-03-22 21:38:56 +03:00
|
|
|
uint32_t texBase = csL ? (*csL)["Texture1"] : 4;
|
2026-02-12 22:56:36 -08:00
|
|
|
for (uint32_t f = texBase; f <= texBase + 2; f++) {
|
2026-02-05 14:58:45 -08:00
|
|
|
std::string tex = charSectionsDbc->getString(r, f);
|
|
|
|
|
if (!tex.empty()) {
|
|
|
|
|
underwearPaths.push_back(tex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
foundUnderwear = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 20:20:43 -08:00
|
|
|
|
|
|
|
|
LOG_INFO("CharSections lookup: skin=", foundSkin ? bodySkinPath_ : "(not found)",
|
|
|
|
|
" face=", foundFace ? (faceLowerPath.empty() ? "(empty)" : faceLowerPath) : "(not found)",
|
|
|
|
|
" hair=", foundHair ? (hairScalpPath.empty() ? "(empty)" : hairScalpPath) : "(not found)",
|
|
|
|
|
" underwear=", foundUnderwear, " (", underwearPaths.size(), " textures)");
|
|
|
|
|
} else {
|
|
|
|
|
LOG_WARNING("CharSections.dbc not loaded — no character textures");
|
2026-02-05 14:58:45 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assign texture filenames on model before GPU upload
|
2026-02-13 16:53:28 -08:00
|
|
|
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
|
|
|
|
auto& tex = model.textures[ti];
|
2026-02-14 20:20:43 -08:00
|
|
|
LOG_INFO(" Model texture[", ti, "]: type=", tex.type,
|
|
|
|
|
" filename='", tex.filename, "'");
|
2026-02-12 14:55:27 -08:00
|
|
|
if (tex.type == 1 && tex.filename.empty() && !bodySkinPath_.empty()) {
|
|
|
|
|
tex.filename = bodySkinPath_;
|
2026-02-05 14:58:45 -08:00
|
|
|
} else if (tex.type == 6 && tex.filename.empty() && !hairScalpPath.empty()) {
|
|
|
|
|
tex.filename = hairScalpPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load external .anim files
|
|
|
|
|
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
|
|
|
|
if (!(model.sequences[si].flags & 0x20)) {
|
|
|
|
|
char animFileName[256];
|
|
|
|
|
snprintf(animFileName, sizeof(animFileName),
|
|
|
|
|
"%s%s%04u-%02u.anim",
|
|
|
|
|
modelDir.c_str(),
|
|
|
|
|
baseName.c_str(),
|
|
|
|
|
model.sequences[si].id,
|
|
|
|
|
model.sequences[si].variationIndex);
|
2026-02-12 02:27:59 -08:00
|
|
|
auto animFileData = assetManager_->readFileOptional(animFileName);
|
2026-02-05 14:58:45 -08:00
|
|
|
if (!animFileData.empty()) {
|
|
|
|
|
pipeline::M2Loader::loadAnimFile(m2Data, animFileData, si, model);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 17:20:30 -08:00
|
|
|
if (!charRenderer_->loadModel(model, PREVIEW_MODEL_ID)) {
|
|
|
|
|
LOG_WARNING("CharacterPreview: failed to load model to GPU");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-05 14:58:45 -08:00
|
|
|
// Composite body skin + face + underwear overlays
|
2026-02-12 14:55:27 -08:00
|
|
|
if (!bodySkinPath_.empty()) {
|
2026-02-05 14:58:45 -08:00
|
|
|
std::vector<std::string> layers;
|
2026-02-12 14:55:27 -08:00
|
|
|
layers.push_back(bodySkinPath_);
|
2026-02-05 14:58:45 -08:00
|
|
|
// Face lower texture composited onto body at the face region
|
|
|
|
|
if (!faceLowerPath.empty()) {
|
|
|
|
|
layers.push_back(faceLowerPath);
|
|
|
|
|
}
|
|
|
|
|
if (!faceUpperPath.empty()) {
|
|
|
|
|
layers.push_back(faceUpperPath);
|
|
|
|
|
}
|
|
|
|
|
for (const auto& up : underwearPaths) {
|
|
|
|
|
layers.push_back(up);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 14:55:27 -08:00
|
|
|
// Cache for later equipment compositing.
|
|
|
|
|
// Keep baseLayers_ without the base skin (compositeWithRegions takes basePath separately).
|
|
|
|
|
if (!faceLowerPath.empty()) baseLayers_.push_back(faceLowerPath);
|
|
|
|
|
if (!faceUpperPath.empty()) baseLayers_.push_back(faceUpperPath);
|
|
|
|
|
for (const auto& up : underwearPaths) baseLayers_.push_back(up);
|
|
|
|
|
|
2026-02-05 14:58:45 -08:00
|
|
|
if (layers.size() > 1) {
|
2026-02-21 19:41:21 -08:00
|
|
|
VkTexture* compositeTex = charRenderer_->compositeTextures(layers);
|
|
|
|
|
if (compositeTex != nullptr) {
|
2026-02-05 14:58:45 -08:00
|
|
|
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
|
|
|
|
if (model.textures[ti].type == 1) {
|
|
|
|
|
charRenderer_->setModelTexture(PREVIEW_MODEL_ID, static_cast<uint32_t>(ti), compositeTex);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If hair scalp texture was found, ensure it's loaded for type-6 slot
|
|
|
|
|
if (!hairScalpPath.empty()) {
|
2026-02-21 19:41:21 -08:00
|
|
|
VkTexture* hairTex = charRenderer_->loadTexture(hairScalpPath);
|
|
|
|
|
if (hairTex != nullptr) {
|
2026-02-05 14:58:45 -08:00
|
|
|
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
|
|
|
|
if (model.textures[ti].type == 6) {
|
|
|
|
|
charRenderer_->setModelTexture(PREVIEW_MODEL_ID, static_cast<uint32_t>(ti), hairTex);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create instance at origin with current yaw
|
|
|
|
|
instanceId_ = charRenderer_->createInstance(PREVIEW_MODEL_ID,
|
|
|
|
|
glm::vec3(0.0f, 0.0f, 0.0f),
|
|
|
|
|
glm::vec3(0.0f, 0.0f, modelYaw_),
|
|
|
|
|
1.0f);
|
|
|
|
|
|
|
|
|
|
if (instanceId_ == 0) {
|
|
|
|
|
LOG_WARNING("CharacterPreview: failed to create instance");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set default geosets (naked character)
|
|
|
|
|
std::unordered_set<uint16_t> activeGeosets;
|
2026-02-13 16:53:28 -08:00
|
|
|
// Body parts (group 0: IDs 0-99, vanilla models use up to 27)
|
|
|
|
|
for (uint16_t i = 0; i <= 99; i++) {
|
2026-02-05 14:58:45 -08:00
|
|
|
activeGeosets.insert(i);
|
|
|
|
|
}
|
|
|
|
|
// Hair style geoset: group 1 = 100 + variation + 1
|
|
|
|
|
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyle + 1));
|
|
|
|
|
// Facial hair geoset: group 2 = 200 + variation + 1
|
|
|
|
|
activeGeosets.insert(static_cast<uint16_t>(200 + facialHair + 1));
|
2026-02-15 20:53:01 -08:00
|
|
|
activeGeosets.insert(401); // Bare forearms (no gloves) — group 4
|
2026-02-15 20:59:29 -08:00
|
|
|
activeGeosets.insert(502); // Bare shins (no boots) — group 5
|
Fix PopStyleVar mismatches and character geoset IDs
Fix 9 PopStyleVar(2) calls that should be PopStyleVar(1) across
player frame, target frame, cast bar, party frames, buff bar, escape
menu, death dialog, and resurrect dialog. Fix action bar from
PopStyleVar(2) to PopStyleVar(4) to match 4 pushes.
Fix character geoset defaults: 301→302 (bare hands), 701→702 (ears),
1501→1502 (back/cloak), add 802 (wristbands). No WoW character model
uses geoset 301/701/1501; all use 302/702/1502 as base. This fixes
missing hands/arms on undead and other races with separate hand meshes.
2026-02-15 06:09:38 -08:00
|
|
|
activeGeosets.insert(702); // Ears: default
|
2026-02-15 20:53:01 -08:00
|
|
|
activeGeosets.insert(801); // Bare wrists (no sleeves) — group 8
|
|
|
|
|
activeGeosets.insert(902); // Kneepads: default — group 9
|
|
|
|
|
activeGeosets.insert(1301); // Bare legs (no pants) — group 13
|
|
|
|
|
activeGeosets.insert(1502); // No cloak — group 15
|
|
|
|
|
activeGeosets.insert(2002); // Bare feet mesh — group 20
|
2026-02-05 14:58:45 -08:00
|
|
|
charRenderer_->setActiveGeosets(instanceId_, activeGeosets);
|
|
|
|
|
|
|
|
|
|
// Play idle animation (Stand = animation ID 0)
|
|
|
|
|
charRenderer_->playAnimation(instanceId_, 0, true);
|
|
|
|
|
|
2026-02-12 14:55:27 -08:00
|
|
|
// Cache core appearance for later equipment geosets.
|
|
|
|
|
race_ = race;
|
|
|
|
|
gender_ = gender;
|
|
|
|
|
useFemaleModel_ = useFemaleModel;
|
|
|
|
|
hairStyle_ = hairStyle;
|
|
|
|
|
facialHair_ = facialHair;
|
|
|
|
|
|
|
|
|
|
// Cache the type-1 texture slot index so applyEquipment can update it.
|
|
|
|
|
skinTextureSlotIndex_ = 0;
|
|
|
|
|
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
|
|
|
|
if (model.textures[ti].type == 1) {
|
|
|
|
|
skinTextureSlotIndex_ = static_cast<uint32_t>(ti);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 14:58:45 -08:00
|
|
|
modelLoaded_ = true;
|
|
|
|
|
LOG_INFO("CharacterPreview: loaded ", m2Path,
|
|
|
|
|
" skin=", (int)skin, " face=", (int)face,
|
|
|
|
|
" hair=", (int)hairStyle, " hairColor=", (int)hairColor,
|
|
|
|
|
" facial=", (int)facialHair);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 14:55:27 -08:00
|
|
|
bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& equipment) {
|
|
|
|
|
if (!modelLoaded_ || instanceId_ == 0 || !charRenderer_ || !assetManager_ || !assetManager_->isInitialized()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
|
|
|
|
|
if (!displayInfoDbc || !displayInfoDbc->isLoaded()) {
|
2026-03-10 02:23:54 -07:00
|
|
|
LOG_WARNING("applyEquipment: ItemDisplayInfo.dbc not loaded");
|
2026-02-12 14:55:27 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto hasInvType = [&](std::initializer_list<uint8_t> types) -> bool {
|
|
|
|
|
for (const auto& it : equipment) {
|
|
|
|
|
if (it.displayModel == 0) continue;
|
|
|
|
|
for (uint8_t t : types) {
|
|
|
|
|
if (it.inventoryType == t) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto findDisplayId = [&](std::initializer_list<uint8_t> types) -> uint32_t {
|
|
|
|
|
for (const auto& it : equipment) {
|
|
|
|
|
if (it.displayModel == 0) continue;
|
|
|
|
|
for (uint8_t t : types) {
|
2026-03-10 02:23:54 -07:00
|
|
|
if (it.inventoryType == t) return it.displayModel;
|
2026-02-12 14:55:27 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto getGeosetGroup = [&](uint32_t displayInfoId, int groupField) -> uint32_t {
|
|
|
|
|
if (displayInfoId == 0) return 0;
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
|
|
|
|
if (recIdx < 0) return 0;
|
2026-03-10 02:23:54 -07:00
|
|
|
uint32_t val = displayInfoDbc->getUInt32(static_cast<uint32_t>(recIdx), 7 + groupField);
|
|
|
|
|
return val;
|
2026-02-12 14:55:27 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- Geosets ---
|
|
|
|
|
std::unordered_set<uint16_t> geosets;
|
2026-02-13 16:53:28 -08:00
|
|
|
for (uint16_t i = 0; i <= 99; i++) geosets.insert(i);
|
2026-02-12 14:55:27 -08:00
|
|
|
geosets.insert(static_cast<uint16_t>(100 + hairStyle_ + 1)); // Hair style
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(200 + facialHair_ + 1)); // Facial hair
|
|
|
|
|
geosets.insert(701); // Ears
|
2026-02-15 20:53:01 -08:00
|
|
|
geosets.insert(902); // Kneepads: default (group 9)
|
|
|
|
|
geosets.insert(2002); // Bare feet mesh (group 20 = CG_FEET)
|
2026-02-12 14:55:27 -08:00
|
|
|
|
2026-02-15 20:53:01 -08:00
|
|
|
// CharGeosets: group 4=gloves(forearm), 5=boots(shin), 8=sleeves, 13=pants
|
|
|
|
|
uint16_t geosetGloves = 401; // Bare forearms (group 4)
|
2026-02-15 20:59:29 -08:00
|
|
|
uint16_t geosetBoots = 502; // Bare shins (group 5)
|
2026-02-15 20:53:01 -08:00
|
|
|
uint16_t geosetSleeves = 801; // Bare wrists (group 8)
|
|
|
|
|
uint16_t geosetPants = 1301; // Bare legs (group 13)
|
2026-02-12 14:55:27 -08:00
|
|
|
|
2026-02-15 20:53:01 -08:00
|
|
|
// Chest/Shirt/Robe → group 8 (sleeves)
|
2026-02-12 14:55:27 -08:00
|
|
|
{
|
|
|
|
|
uint32_t did = findDisplayId({4, 5, 20});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
2026-02-15 20:53:01 -08:00
|
|
|
if (gg > 0) geosetSleeves = static_cast<uint16_t>(801 + gg);
|
2026-02-12 14:55:27 -08:00
|
|
|
// Robe kilt legs
|
|
|
|
|
uint32_t gg3 = getGeosetGroup(did, 2);
|
|
|
|
|
if (gg3 > 0) geosetPants = static_cast<uint16_t>(1301 + gg3);
|
|
|
|
|
}
|
2026-02-15 20:53:01 -08:00
|
|
|
// Legs → group 13 (trousers)
|
2026-02-12 14:55:27 -08:00
|
|
|
{
|
|
|
|
|
uint32_t did = findDisplayId({7});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
|
|
|
|
if (gg > 0) geosetPants = static_cast<uint16_t>(1301 + gg);
|
|
|
|
|
}
|
2026-02-15 20:53:01 -08:00
|
|
|
// Boots → group 5 (shins)
|
2026-02-12 14:55:27 -08:00
|
|
|
{
|
|
|
|
|
uint32_t did = findDisplayId({8});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
2026-02-15 20:53:01 -08:00
|
|
|
if (gg > 0) geosetBoots = static_cast<uint16_t>(501 + gg);
|
2026-02-12 14:55:27 -08:00
|
|
|
}
|
2026-02-15 20:53:01 -08:00
|
|
|
// Gloves → group 4 (forearms)
|
2026-02-12 14:55:27 -08:00
|
|
|
{
|
|
|
|
|
uint32_t did = findDisplayId({10});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
2026-02-15 20:53:01 -08:00
|
|
|
if (gg > 0) geosetGloves = static_cast<uint16_t>(401 + gg);
|
2026-02-12 14:55:27 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
geosets.insert(geosetGloves);
|
|
|
|
|
geosets.insert(geosetBoots);
|
2026-02-15 20:53:01 -08:00
|
|
|
geosets.insert(geosetSleeves);
|
2026-02-12 14:55:27 -08:00
|
|
|
geosets.insert(geosetPants);
|
|
|
|
|
geosets.insert(hasInvType({16}) ? 1502 : 1501); // Cloak mesh toggle (visual may still be limited)
|
|
|
|
|
if (hasInvType({19})) geosets.insert(1201); // Tabard mesh toggle
|
|
|
|
|
|
|
|
|
|
// Hide hair under helmets (helmets are separate models; this still avoids hair clipping)
|
|
|
|
|
if (hasInvType({1})) {
|
|
|
|
|
geosets.erase(static_cast<uint16_t>(100 + hairStyle_ + 1));
|
|
|
|
|
geosets.insert(1); // Bald scalp cap
|
|
|
|
|
geosets.insert(101); // Default group-1 connector
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
charRenderer_->setActiveGeosets(instanceId_, geosets);
|
|
|
|
|
|
|
|
|
|
// --- Textures (equipment overlays onto body skin) ---
|
|
|
|
|
if (bodySkinPath_.empty()) return true; // geosets applied, but can't composite
|
|
|
|
|
|
|
|
|
|
static const char* componentDirs[] = {
|
|
|
|
|
"ArmUpperTexture", "ArmLowerTexture", "HandTexture",
|
|
|
|
|
"TorsoUpperTexture", "TorsoLowerTexture",
|
|
|
|
|
"LegUpperTexture", "LegLowerTexture", "FootTexture",
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-10 04:16:27 -07:00
|
|
|
// Texture component region fields — use DBC layout when available, fall back to binary offsets.
|
|
|
|
|
const auto* idiL = pipeline::getActiveDBCLayout()
|
|
|
|
|
? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
|
|
|
|
const uint32_t texRegionFields[8] = {
|
|
|
|
|
idiL ? (*idiL)["TextureArmUpper"] : 14u,
|
|
|
|
|
idiL ? (*idiL)["TextureArmLower"] : 15u,
|
|
|
|
|
idiL ? (*idiL)["TextureHand"] : 16u,
|
|
|
|
|
idiL ? (*idiL)["TextureTorsoUpper"] : 17u,
|
|
|
|
|
idiL ? (*idiL)["TextureTorsoLower"] : 18u,
|
|
|
|
|
idiL ? (*idiL)["TextureLegUpper"] : 19u,
|
|
|
|
|
idiL ? (*idiL)["TextureLegLower"] : 20u,
|
|
|
|
|
idiL ? (*idiL)["TextureFoot"] : 21u,
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-12 14:55:27 -08:00
|
|
|
std::vector<std::pair<int, std::string>> regionLayers;
|
|
|
|
|
regionLayers.reserve(32);
|
|
|
|
|
|
|
|
|
|
for (const auto& it : equipment) {
|
|
|
|
|
if (it.displayModel == 0) continue;
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(it.displayModel);
|
|
|
|
|
if (recIdx < 0) continue;
|
|
|
|
|
|
|
|
|
|
for (int region = 0; region < 8; region++) {
|
2026-03-10 04:16:27 -07:00
|
|
|
std::string texName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), texRegionFields[region]);
|
2026-02-12 14:55:27 -08:00
|
|
|
if (texName.empty()) continue;
|
|
|
|
|
|
|
|
|
|
std::string base = "Item\\TextureComponents\\" +
|
|
|
|
|
std::string(componentDirs[region]) + "\\" + texName;
|
|
|
|
|
|
|
|
|
|
std::string genderSuffix = (gender_ == game::Gender::FEMALE) ? "_F.blp" : "_M.blp";
|
|
|
|
|
std::string genderPath = base + genderSuffix;
|
|
|
|
|
std::string unisexPath = base + "_U.blp";
|
|
|
|
|
std::string fullPath;
|
2026-02-20 21:50:32 -08:00
|
|
|
std::string basePath = base + ".blp";
|
2026-02-12 14:55:27 -08:00
|
|
|
if (assetManager_->fileExists(genderPath)) {
|
|
|
|
|
fullPath = genderPath;
|
|
|
|
|
} else if (assetManager_->fileExists(unisexPath)) {
|
|
|
|
|
fullPath = unisexPath;
|
2026-02-20 21:50:32 -08:00
|
|
|
} else if (assetManager_->fileExists(basePath)) {
|
|
|
|
|
fullPath = basePath;
|
2026-02-12 14:55:27 -08:00
|
|
|
} else {
|
2026-02-20 21:50:32 -08:00
|
|
|
continue;
|
2026-02-12 14:55:27 -08:00
|
|
|
}
|
|
|
|
|
regionLayers.emplace_back(region, fullPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!regionLayers.empty()) {
|
2026-02-21 19:41:21 -08:00
|
|
|
VkTexture* newTex = charRenderer_->compositeWithRegions(bodySkinPath_, baseLayers_, regionLayers);
|
|
|
|
|
if (newTex != nullptr) {
|
2026-02-12 14:55:27 -08:00
|
|
|
charRenderer_->setModelTexture(PREVIEW_MODEL_ID, skinTextureSlotIndex_, newTex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 21:50:32 -08:00
|
|
|
// Cloak texture (group 15) is separate from body compositing.
|
|
|
|
|
if (hasInvType({16})) {
|
|
|
|
|
uint32_t capeDisplayId = findDisplayId({16});
|
|
|
|
|
if (capeDisplayId != 0) {
|
|
|
|
|
int32_t capeRecIdx = displayInfoDbc->findRecordById(capeDisplayId);
|
|
|
|
|
if (capeRecIdx >= 0) {
|
|
|
|
|
std::vector<std::string> capeNames;
|
|
|
|
|
auto addName = [&](const std::string& n) {
|
|
|
|
|
if (!n.empty() && std::find(capeNames.begin(), capeNames.end(), n) == capeNames.end()) {
|
|
|
|
|
capeNames.push_back(n);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
std::string leftName = displayInfoDbc->getString(static_cast<uint32_t>(capeRecIdx), 3);
|
|
|
|
|
std::string rightName = displayInfoDbc->getString(static_cast<uint32_t>(capeRecIdx), 4);
|
|
|
|
|
if (gender_ == game::Gender::FEMALE) {
|
|
|
|
|
addName(rightName);
|
|
|
|
|
addName(leftName);
|
|
|
|
|
} else {
|
|
|
|
|
addName(leftName);
|
|
|
|
|
addName(rightName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto hasBlpExt = [](const std::string& p) {
|
|
|
|
|
if (p.size() < 4) return false;
|
|
|
|
|
std::string ext = p.substr(p.size() - 4);
|
|
|
|
|
std::transform(ext.begin(), ext.end(), ext.begin(),
|
|
|
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
return ext == ".blp";
|
|
|
|
|
};
|
|
|
|
|
std::vector<std::string> candidates;
|
|
|
|
|
auto addCandidate = [&](const std::string& p) {
|
|
|
|
|
if (!p.empty() && std::find(candidates.begin(), candidates.end(), p) == candidates.end()) {
|
|
|
|
|
candidates.push_back(p);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
for (const auto& nameRaw : capeNames) {
|
|
|
|
|
std::string name = nameRaw;
|
|
|
|
|
std::replace(name.begin(), name.end(), '/', '\\');
|
|
|
|
|
bool hasDir = (name.find('\\') != std::string::npos);
|
|
|
|
|
bool hasExt = hasBlpExt(name);
|
|
|
|
|
if (hasDir) {
|
|
|
|
|
addCandidate(name);
|
|
|
|
|
if (!hasExt) addCandidate(name + ".blp");
|
|
|
|
|
} else {
|
|
|
|
|
std::string baseObj = "Item\\ObjectComponents\\Cape\\" + name;
|
|
|
|
|
std::string baseTex = "Item\\TextureComponents\\Cape\\" + name;
|
|
|
|
|
addCandidate(baseObj);
|
|
|
|
|
addCandidate(baseTex);
|
|
|
|
|
if (!hasExt) {
|
|
|
|
|
addCandidate(baseObj + ".blp");
|
|
|
|
|
addCandidate(baseTex + ".blp");
|
|
|
|
|
}
|
|
|
|
|
addCandidate(baseObj + (gender_ == game::Gender::FEMALE ? "_F.blp" : "_M.blp"));
|
|
|
|
|
addCandidate(baseObj + "_U.blp");
|
|
|
|
|
addCandidate(baseTex + (gender_ == game::Gender::FEMALE ? "_F.blp" : "_M.blp"));
|
|
|
|
|
addCandidate(baseTex + "_U.blp");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
VkTexture* whiteTex = charRenderer_->loadTexture("");
|
2026-02-20 21:50:32 -08:00
|
|
|
for (const auto& c : candidates) {
|
2026-02-21 19:41:21 -08:00
|
|
|
VkTexture* capeTex = charRenderer_->loadTexture(c);
|
|
|
|
|
if (capeTex != nullptr && capeTex != whiteTex) {
|
2026-02-20 21:50:32 -08:00
|
|
|
charRenderer_->setGroupTextureOverride(instanceId_, 15, capeTex);
|
|
|
|
|
if (const auto* md = charRenderer_->getModelData(PREVIEW_MODEL_ID)) {
|
|
|
|
|
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
|
|
|
|
if (md->textures[ti].type == 2) {
|
|
|
|
|
charRenderer_->setTextureSlotOverride(instanceId_, static_cast<uint16_t>(ti), capeTex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (const auto* md = charRenderer_->getModelData(PREVIEW_MODEL_ID)) {
|
|
|
|
|
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
|
|
|
|
if (md->textures[ti].type == 2) {
|
|
|
|
|
charRenderer_->clearTextureSlotOverride(instanceId_, static_cast<uint16_t>(ti));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 14:55:27 -08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 14:58:45 -08:00
|
|
|
void CharacterPreview::update(float deltaTime) {
|
|
|
|
|
if (charRenderer_ && modelLoaded_) {
|
|
|
|
|
charRenderer_->update(deltaTime);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CharacterPreview::render() {
|
2026-02-22 05:58:45 -08:00
|
|
|
// No-op — actual rendering happens in compositePass() called from Renderer::beginFrame()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CharacterPreview::compositePass(VkCommandBuffer cmd, uint32_t frameIndex) {
|
|
|
|
|
// Only composite when a UI screen actually requested it this frame
|
|
|
|
|
if (!compositeRequested_) return;
|
|
|
|
|
compositeRequested_ = false;
|
|
|
|
|
|
|
|
|
|
if (!charRenderer_ || !camera_ || !modelLoaded_ || !renderTarget_ || !renderTarget_->isValid()) {
|
2026-02-05 17:20:30 -08:00
|
|
|
return;
|
|
|
|
|
}
|
2026-02-05 14:58:45 -08:00
|
|
|
|
2026-02-22 05:58:45 -08:00
|
|
|
uint32_t fi = frameIndex % MAX_FRAMES;
|
|
|
|
|
|
|
|
|
|
// Update per-frame UBO with preview camera matrices and studio lighting
|
|
|
|
|
GPUPerFrameData ubo{};
|
|
|
|
|
ubo.view = camera_->getViewMatrix();
|
|
|
|
|
ubo.projection = camera_->getProjectionMatrix();
|
|
|
|
|
ubo.lightSpaceMatrix = glm::mat4(1.0f);
|
|
|
|
|
// Studio lighting: key light from upper-right-front
|
|
|
|
|
ubo.lightDir = glm::vec4(glm::normalize(glm::vec3(0.5f, -0.7f, 0.5f)), 0.0f);
|
|
|
|
|
ubo.lightColor = glm::vec4(1.0f, 0.95f, 0.9f, 0.0f);
|
|
|
|
|
ubo.ambientColor = glm::vec4(0.35f, 0.35f, 0.4f, 0.0f);
|
|
|
|
|
ubo.viewPos = glm::vec4(camera_->getPosition(), 0.0f);
|
|
|
|
|
// No fog in preview
|
|
|
|
|
ubo.fogColor = glm::vec4(0.05f, 0.05f, 0.1f, 0.0f);
|
|
|
|
|
ubo.fogParams = glm::vec4(9999.0f, 10000.0f, 0.0f, 0.0f);
|
2026-03-11 12:21:07 -07:00
|
|
|
// Enable shadows for visual depth in preview (strength=0.5 for subtle effect)
|
|
|
|
|
ubo.shadowParams = glm::vec4(1.0f, 0.5f, 0.0f, 0.0f);
|
2026-02-22 05:58:45 -08:00
|
|
|
|
|
|
|
|
std::memcpy(previewUBOMapped_[fi], &ubo, sizeof(GPUPerFrameData));
|
|
|
|
|
|
|
|
|
|
// Begin off-screen render pass
|
|
|
|
|
VkClearColorValue clearColor = {{0.05f, 0.05f, 0.1f, 1.0f}};
|
|
|
|
|
renderTarget_->beginPass(cmd, clearColor);
|
|
|
|
|
|
|
|
|
|
// Render the character model
|
|
|
|
|
charRenderer_->render(cmd, previewPerFrameSet_[fi], *camera_);
|
|
|
|
|
|
|
|
|
|
renderTarget_->endPass(cmd);
|
|
|
|
|
|
|
|
|
|
compositeRendered_ = true;
|
2026-02-05 14:58:45 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CharacterPreview::rotate(float yawDelta) {
|
|
|
|
|
modelYaw_ += yawDelta;
|
|
|
|
|
if (instanceId_ > 0 && charRenderer_) {
|
|
|
|
|
charRenderer_->setInstanceRotation(instanceId_, glm::vec3(0.0f, 0.0f, modelYaw_));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace rendering
|
|
|
|
|
} // namespace wowee
|