mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 07:40:14 +00:00
Work on character rendering and frustrum culling etc
This commit is contained in:
parent
fc5294eb0f
commit
7dd1dada5f
16 changed files with 559 additions and 138 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "game/character.hpp"
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
|
@ -15,6 +16,7 @@ class CharacterRenderer;
|
|||
class Camera;
|
||||
class VkContext;
|
||||
class VkTexture;
|
||||
class VkRenderTarget;
|
||||
|
||||
class CharacterPreview {
|
||||
public:
|
||||
|
|
@ -36,8 +38,15 @@ public:
|
|||
void render();
|
||||
void rotate(float yawDelta);
|
||||
|
||||
// TODO: Vulkan offscreen render target for preview
|
||||
VkTexture* getTextureId() const { return nullptr; }
|
||||
// Off-screen composite pass — call from Renderer::beginFrame() before main render pass
|
||||
void compositePass(VkCommandBuffer cmd, uint32_t frameIndex);
|
||||
|
||||
// Mark that the preview needs compositing this frame (call from UI each frame)
|
||||
void requestComposite() { compositeRequested_ = true; }
|
||||
|
||||
// Returns the ImGui texture handle. Returns VK_NULL_HANDLE until the first
|
||||
// compositePass has run (image is in UNDEFINED layout before that).
|
||||
VkDescriptorSet getTextureId() const { return compositeRendered_ ? imguiTextureId_ : VK_NULL_HANDLE; }
|
||||
int getWidth() const { return fboWidth_; }
|
||||
int getHeight() const { return fboHeight_; }
|
||||
|
||||
|
|
@ -51,17 +60,35 @@ private:
|
|||
void destroyFBO();
|
||||
|
||||
pipeline::AssetManager* assetManager_ = nullptr;
|
||||
VkContext* vkCtx_ = nullptr;
|
||||
std::unique_ptr<CharacterRenderer> charRenderer_;
|
||||
std::unique_ptr<Camera> camera_;
|
||||
|
||||
// TODO: Vulkan offscreen render target
|
||||
// VkRenderTarget* renderTarget_ = nullptr;
|
||||
// Off-screen render target (color + depth)
|
||||
std::unique_ptr<VkRenderTarget> renderTarget_;
|
||||
|
||||
// Per-frame UBO for preview camera/lighting (double-buffered)
|
||||
static constexpr uint32_t MAX_FRAMES = 2;
|
||||
VkDescriptorPool previewDescPool_ = VK_NULL_HANDLE;
|
||||
VkBuffer previewUBO_[MAX_FRAMES] = {};
|
||||
VmaAllocation previewUBOAlloc_[MAX_FRAMES] = {};
|
||||
void* previewUBOMapped_[MAX_FRAMES] = {};
|
||||
VkDescriptorSet previewPerFrameSet_[MAX_FRAMES] = {};
|
||||
|
||||
// Dummy 1x1 white texture for shadow map placeholder
|
||||
std::unique_ptr<VkTexture> dummyWhiteTex_;
|
||||
|
||||
// ImGui texture handle for displaying the preview (VkDescriptorSet in Vulkan backend)
|
||||
VkDescriptorSet imguiTextureId_ = VK_NULL_HANDLE;
|
||||
|
||||
static constexpr int fboWidth_ = 400;
|
||||
static constexpr int fboHeight_ = 500;
|
||||
|
||||
static constexpr uint32_t PREVIEW_MODEL_ID = 9999;
|
||||
uint32_t instanceId_ = 0;
|
||||
bool modelLoaded_ = false;
|
||||
bool compositeRequested_ = false;
|
||||
bool compositeRendered_ = false; // True after first successful compositePass
|
||||
float modelYaw_ = 180.0f;
|
||||
|
||||
// Cached info from loadCharacter() for later recompositing.
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ public:
|
|||
CharacterRenderer();
|
||||
~CharacterRenderer();
|
||||
|
||||
bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* am);
|
||||
bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* am,
|
||||
VkRenderPass renderPassOverride = VK_NULL_HANDLE);
|
||||
void shutdown();
|
||||
|
||||
void setAssetManager(pipeline::AssetManager* am) { assetManager = am; }
|
||||
|
|
@ -219,7 +220,9 @@ public:
|
|||
|
||||
private:
|
||||
VkContext* vkCtx_ = nullptr;
|
||||
VkRenderPass renderPassOverride_ = VK_NULL_HANDLE;
|
||||
pipeline::AssetManager* assetManager = nullptr;
|
||||
int renderLogCounter_ = 0; // per-instance debug counter
|
||||
|
||||
// Vulkan pipelines (one per blend mode)
|
||||
VkPipeline opaquePipeline_ = VK_NULL_HANDLE;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class M2Renderer;
|
|||
class Minimap;
|
||||
class WorldMap;
|
||||
class QuestMarkerRenderer;
|
||||
class CharacterPreview;
|
||||
class Shader;
|
||||
|
||||
class Renderer {
|
||||
|
|
@ -239,6 +240,10 @@ private:
|
|||
int shadowPostMoveFrames_ = 0; // transition marker for movement->idle shadow recenter
|
||||
|
||||
public:
|
||||
// Character preview registration (for off-screen composite pass)
|
||||
void registerPreview(CharacterPreview* preview);
|
||||
void unregisterPreview(CharacterPreview* preview);
|
||||
|
||||
void setShadowsEnabled(bool enabled) { shadowsEnabled = enabled; }
|
||||
bool areShadowsEnabled() const { return shadowsEnabled; }
|
||||
void setMsaaSamples(VkSampleCountFlagBits samples);
|
||||
|
|
@ -384,6 +389,9 @@ private:
|
|||
void destroyPerFrameResources();
|
||||
void updatePerFrameUBO();
|
||||
|
||||
// Active character previews for off-screen rendering
|
||||
std::vector<CharacterPreview*> activePreviews_;
|
||||
|
||||
bool terrainEnabled = true;
|
||||
bool terrainLoaded = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,10 @@ public:
|
|||
/**
|
||||
* Create the render target with given dimensions and format.
|
||||
* Creates: color image, image view, sampler, render pass, framebuffer.
|
||||
* When withDepth is true, also creates a D32_SFLOAT depth attachment.
|
||||
*/
|
||||
bool create(VkContext& ctx, uint32_t width, uint32_t height,
|
||||
VkFormat format = VK_FORMAT_R8G8B8A8_UNORM);
|
||||
VkFormat format = VK_FORMAT_R8G8B8A8_UNORM, bool withDepth = false);
|
||||
|
||||
/**
|
||||
* Destroy all Vulkan resources.
|
||||
|
|
@ -48,6 +49,7 @@ public:
|
|||
void endPass(VkCommandBuffer cmd);
|
||||
|
||||
// Accessors
|
||||
VkImage getColorImage() const { return colorImage_.image; }
|
||||
VkImageView getColorImageView() const { return colorImage_.imageView; }
|
||||
VkSampler getSampler() const { return sampler_; }
|
||||
VkRenderPass getRenderPass() const { return renderPass_; }
|
||||
|
|
@ -62,6 +64,8 @@ public:
|
|||
|
||||
private:
|
||||
AllocatedImage colorImage_{};
|
||||
AllocatedImage depthImage_{};
|
||||
bool hasDepth_ = false;
|
||||
VkSampler sampler_ = VK_NULL_HANDLE;
|
||||
VkRenderPass renderPass_ = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer_ = VK_NULL_HANDLE;
|
||||
|
|
|
|||
|
|
@ -336,8 +336,9 @@ void Application::run() {
|
|||
totalSwapMs += std::chrono::duration<double, std::milli>(t4 - t3).count();
|
||||
|
||||
if (++frameCount >= 60) {
|
||||
printf("[Frame] Update: %.1f ms, Render: %.1f ms, Swap: %.1f ms\n",
|
||||
totalUpdateMs / 60.0, totalRenderMs / 60.0, totalSwapMs / 60.0);
|
||||
LOG_INFO("[Frame] Update: ", totalUpdateMs / 60.0,
|
||||
"ms Render: ", totalRenderMs / 60.0,
|
||||
"ms Swap: ", totalSwapMs / 60.0, "ms");
|
||||
frameCount = 0;
|
||||
totalUpdateMs = totalRenderMs = totalSwapMs = 0;
|
||||
}
|
||||
|
|
@ -358,9 +359,12 @@ void Application::shutdown() {
|
|||
}
|
||||
}
|
||||
|
||||
// Stop renderer first: terrain streaming workers may still be reading via
|
||||
// AssetManager during shutdown, so renderer/terrain teardown must complete
|
||||
// before AssetManager is destroyed.
|
||||
// Explicitly shut down the renderer before destroying it — this ensures
|
||||
// all sub-renderers free their VMA allocations in the correct order,
|
||||
// before VkContext::shutdown() calls vmaDestroyAllocator().
|
||||
if (renderer) {
|
||||
renderer->shutdown();
|
||||
}
|
||||
renderer.reset();
|
||||
|
||||
world.reset();
|
||||
|
|
|
|||
|
|
@ -42,12 +42,15 @@ glm::vec3 Camera::getUp() const {
|
|||
|
||||
Ray Camera::screenToWorldRay(float screenX, float screenY, float screenW, float screenH) const {
|
||||
float ndcX = (2.0f * screenX / screenW) - 1.0f;
|
||||
float ndcY = 1.0f - (2.0f * screenY / screenH);
|
||||
// Vulkan Y-flip is baked into projectionMatrix, so NDC Y maps directly:
|
||||
// screen top (y=0) → NDC -1, screen bottom (y=H) → NDC +1
|
||||
float ndcY = (2.0f * screenY / screenH) - 1.0f;
|
||||
|
||||
glm::mat4 invVP = glm::inverse(projectionMatrix * viewMatrix);
|
||||
|
||||
glm::vec4 nearPt = invVP * glm::vec4(ndcX, ndcY, -1.0f, 1.0f);
|
||||
glm::vec4 farPt = invVP * glm::vec4(ndcX, ndcY, 1.0f, 1.0f);
|
||||
// Vulkan / GLM_FORCE_DEPTH_ZERO_TO_ONE: NDC z ∈ [0, 1]
|
||||
glm::vec4 nearPt = invVP * glm::vec4(ndcX, ndcY, 0.0f, 1.0f);
|
||||
glm::vec4 farPt = invVP * glm::vec4(ndcX, ndcY, 1.0f, 1.0f);
|
||||
nearPt /= nearPt.w;
|
||||
farPt /= farPt.w;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
#include "rendering/character_preview.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/vk_render_target.hpp"
|
||||
#include "rendering/vk_texture.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "rendering/vk_frame_data.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
|
|
@ -10,9 +12,12 @@
|
|||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include <imgui.h>
|
||||
#include <backends/imgui_impl_vulkan.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <algorithm>
|
||||
#include <unordered_set>
|
||||
#include <cstring>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
|
@ -26,11 +31,34 @@ CharacterPreview::~CharacterPreview() {
|
|||
bool CharacterPreview::initialize(pipeline::AssetManager* am) {
|
||||
assetManager_ = am;
|
||||
|
||||
charRenderer_ = std::make_unique<CharacterRenderer>();
|
||||
// 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;
|
||||
}
|
||||
|
||||
auto* appRenderer = core::Application::getInstance().getRenderer();
|
||||
VkContext* vkCtx = appRenderer ? appRenderer->getVkContext() : nullptr;
|
||||
vkCtx_ = appRenderer ? appRenderer->getVkContext() : nullptr;
|
||||
VkDescriptorSetLayout perFrameLayout = appRenderer ? appRenderer->getPerFrameSetLayout() : VK_NULL_HANDLE;
|
||||
if (!charRenderer_->initialize(vkCtx, perFrameLayout, am)) {
|
||||
|
||||
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>();
|
||||
if (!charRenderer_->initialize(vkCtx_, perFrameLayout, am, renderTarget_->getRenderPass())) {
|
||||
LOG_ERROR("CharacterPreview: failed to initialize CharacterRenderer");
|
||||
return false;
|
||||
}
|
||||
|
|
@ -45,35 +73,187 @@ bool CharacterPreview::initialize(pipeline::AssetManager* am) {
|
|||
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
|
||||
// Human ~2 units tall, Tauren ~2.5. At distance 4.5 with FOV 30:
|
||||
// vertical visible = 2 * 4.5 * tan(15°) ≈ 2.41 units
|
||||
camera_->setPosition(glm::vec3(0.0f, 4.5f, 0.9f));
|
||||
camera_->setRotation(270.0f, 0.0f);
|
||||
|
||||
// TODO: create Vulkan offscreen render target
|
||||
// createFBO();
|
||||
|
||||
LOG_INFO("CharacterPreview initialized (", fboWidth_, "x", fboHeight_, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
void CharacterPreview::shutdown() {
|
||||
// destroyFBO(); // TODO: Vulkan offscreen cleanup
|
||||
// Unregister from renderer before destroying resources
|
||||
auto* appRenderer = core::Application::getInstance().getRenderer();
|
||||
if (appRenderer) appRenderer->unregisterPreview(this);
|
||||
|
||||
if (charRenderer_) {
|
||||
charRenderer_->shutdown();
|
||||
charRenderer_.reset();
|
||||
}
|
||||
camera_.reset();
|
||||
destroyFBO();
|
||||
modelLoaded_ = false;
|
||||
compositeRendered_ = false;
|
||||
instanceId_ = 0;
|
||||
}
|
||||
|
||||
void CharacterPreview::createFBO() {
|
||||
// TODO: Create Vulkan offscreen render target for character preview
|
||||
if (!vkCtx_) return;
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
VmaAllocator allocator = vkCtx_->getAllocator();
|
||||
|
||||
// 1. Create off-screen render target with depth
|
||||
renderTarget_ = std::make_unique<VkRenderTarget>();
|
||||
if (!renderTarget_->create(*vkCtx_, fboWidth_, fboHeight_, VK_FORMAT_R8G8B8A8_UNORM, true)) {
|
||||
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_, ")");
|
||||
}
|
||||
|
||||
void CharacterPreview::destroyFBO() {
|
||||
// TODO: Destroy Vulkan offscreen render target
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
||||
|
|
@ -84,8 +264,11 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
|
|||
return false;
|
||||
}
|
||||
|
||||
// Remove existing instance
|
||||
// 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.
|
||||
if (instanceId_ > 0) {
|
||||
if (vkCtx_) vkDeviceWaitIdle(vkCtx_->getDevice());
|
||||
charRenderer_->removeInstance(instanceId_);
|
||||
instanceId_ = 0;
|
||||
modelLoaded_ = false;
|
||||
|
|
@ -592,14 +775,48 @@ void CharacterPreview::update(float deltaTime) {
|
|||
}
|
||||
|
||||
void CharacterPreview::render() {
|
||||
if (!charRenderer_ || !camera_ || !modelLoaded_) {
|
||||
// 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()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Vulkan offscreen rendering for character preview
|
||||
// Need a VkRenderTarget, begin a render pass into it, then:
|
||||
// charRenderer_->render(cmd, perFrameSet, *camera_);
|
||||
// For now, the preview is non-functional until Vulkan offscreen is wired up.
|
||||
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);
|
||||
// Shadows disabled
|
||||
ubo.shadowParams = glm::vec4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void CharacterPreview::rotate(float yawDelta) {
|
||||
|
|
|
|||
|
|
@ -86,12 +86,14 @@ CharacterRenderer::~CharacterRenderer() {
|
|||
}
|
||||
|
||||
bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout,
|
||||
pipeline::AssetManager* am) {
|
||||
pipeline::AssetManager* am,
|
||||
VkRenderPass renderPassOverride) {
|
||||
core::Logger::getInstance().info("Initializing character renderer (Vulkan)...");
|
||||
|
||||
vkCtx_ = ctx;
|
||||
assetManager = am;
|
||||
perFrameLayout_ = perFrameLayout;
|
||||
renderPassOverride_ = renderPassOverride;
|
||||
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
|
||||
|
|
@ -182,7 +184,8 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
|
|||
return false;
|
||||
}
|
||||
|
||||
VkRenderPass mainPass = vkCtx_->getImGuiRenderPass();
|
||||
VkRenderPass mainPass = renderPassOverride_ ? renderPassOverride_ : vkCtx_->getImGuiRenderPass();
|
||||
VkSampleCountFlagBits samples = renderPassOverride_ ? VK_SAMPLE_COUNT_1_BIT : vkCtx_->getMsaaSamples();
|
||||
|
||||
// --- Vertex input ---
|
||||
// M2Vertex: vec3 pos(12) + uint8[4] boneWeights(4) + uint8[4] boneIndices(4) +
|
||||
|
|
@ -210,7 +213,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
|
|||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL)
|
||||
.setColorBlendAttachment(blendState)
|
||||
.setMultisample(vkCtx_->getMsaaSamples())
|
||||
.setMultisample(samples)
|
||||
.setLayout(pipelineLayout_)
|
||||
.setRenderPass(mainPass)
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||
|
|
@ -252,6 +255,9 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
|
|||
void CharacterRenderer::shutdown() {
|
||||
if (!vkCtx_) return;
|
||||
|
||||
LOG_INFO("CharacterRenderer::shutdown instances=", instances.size(),
|
||||
" models=", models.size(), " override=", (void*)renderPassOverride_);
|
||||
|
||||
vkDeviceWaitIdle(vkCtx_->getDevice());
|
||||
VkDevice device = vkCtx_->getDevice();
|
||||
VmaAllocator alloc = vkCtx_->getAllocator();
|
||||
|
|
@ -1321,6 +1327,13 @@ glm::mat4 CharacterRenderer::getBoneTransform(const pipeline::M2Bone& bone, floa
|
|||
// --- Rendering ---
|
||||
|
||||
void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) {
|
||||
// Periodic instance count log (every ~10s at 30fps)
|
||||
if (!renderPassOverride_) {
|
||||
renderLogCounter_++;
|
||||
if (renderLogCounter_ % 300 == 1) {
|
||||
LOG_INFO("CharRenderer[WORLD]::render instances=", instances.size());
|
||||
}
|
||||
}
|
||||
if (instances.empty() || !opaquePipeline_) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -2196,6 +2209,9 @@ void CharacterRenderer::clearTextureSlotOverride(uint32_t instanceId, uint16_t t
|
|||
void CharacterRenderer::setInstanceVisible(uint32_t instanceId, bool visible) {
|
||||
auto it = instances.find(instanceId);
|
||||
if (it != instances.end()) {
|
||||
if (it->second.visible != visible) {
|
||||
LOG_INFO("CharacterRenderer::setInstanceVisible id=", instanceId, " visible=", visible);
|
||||
}
|
||||
it->second.visible = visible;
|
||||
|
||||
// Also hide/show attached weapons (for first-person mode)
|
||||
|
|
@ -2212,6 +2228,11 @@ void CharacterRenderer::removeInstance(uint32_t instanceId) {
|
|||
auto it = instances.find(instanceId);
|
||||
if (it == instances.end()) return;
|
||||
|
||||
LOG_INFO("CharacterRenderer::removeInstance id=", instanceId,
|
||||
" pos=(", it->second.position.x, ",", it->second.position.y, ",", it->second.position.z, ")",
|
||||
" remaining=", instances.size() - 1,
|
||||
" override=", (void*)renderPassOverride_);
|
||||
|
||||
// Remove child attachments first (helmets/weapons), otherwise they leak as
|
||||
// orphan render instances when the parent creature despawns.
|
||||
auto attachments = it->second.weaponAttachments;
|
||||
|
|
@ -2585,7 +2606,8 @@ void CharacterRenderer::recreatePipelines() {
|
|||
return;
|
||||
}
|
||||
|
||||
VkRenderPass mainPass = vkCtx_->getImGuiRenderPass();
|
||||
VkRenderPass mainPass = renderPassOverride_ ? renderPassOverride_ : vkCtx_->getImGuiRenderPass();
|
||||
VkSampleCountFlagBits samples = renderPassOverride_ ? VK_SAMPLE_COUNT_1_BIT : vkCtx_->getMsaaSamples();
|
||||
|
||||
// --- Vertex input ---
|
||||
VkVertexInputBindingDescription charBinding{};
|
||||
|
|
@ -2610,13 +2632,17 @@ void CharacterRenderer::recreatePipelines() {
|
|||
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
||||
.setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL)
|
||||
.setColorBlendAttachment(blendState)
|
||||
.setMultisample(vkCtx_->getMsaaSamples())
|
||||
.setMultisample(samples)
|
||||
.setLayout(pipelineLayout_)
|
||||
.setRenderPass(mainPass)
|
||||
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
|
||||
.build(device);
|
||||
};
|
||||
|
||||
LOG_INFO("CharacterRenderer::recreatePipelines: renderPass=", (void*)mainPass,
|
||||
" samples=", static_cast<int>(samples),
|
||||
" pipelineLayout=", (void*)pipelineLayout_);
|
||||
|
||||
opaquePipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true);
|
||||
alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), true);
|
||||
alphaPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), false);
|
||||
|
|
@ -2625,7 +2651,16 @@ void CharacterRenderer::recreatePipelines() {
|
|||
charVert.destroy();
|
||||
charFrag.destroy();
|
||||
|
||||
core::Logger::getInstance().info("CharacterRenderer: pipelines recreated");
|
||||
if (!opaquePipeline_ || !alphaTestPipeline_ || !alphaPipeline_ || !additivePipeline_) {
|
||||
LOG_ERROR("CharacterRenderer::recreatePipelines FAILED: opaque=", (void*)opaquePipeline_,
|
||||
" alphaTest=", (void*)alphaTestPipeline_,
|
||||
" alpha=", (void*)alphaPipeline_,
|
||||
" additive=", (void*)additivePipeline_,
|
||||
" renderPass=", (void*)mainPass, " samples=", static_cast<int>(samples));
|
||||
} else {
|
||||
LOG_INFO("CharacterRenderer: pipelines recreated successfully (samples=",
|
||||
static_cast<int>(samples), ")");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
|
|
|
|||
|
|
@ -5,45 +5,56 @@ namespace wowee {
|
|||
namespace rendering {
|
||||
|
||||
void Frustum::extractFromMatrix(const glm::mat4& vp) {
|
||||
// Extract planes from view-projection matrix
|
||||
// Based on Gribb & Hartmann method (Fast Extraction of Viewing Frustum Planes)
|
||||
// Extract frustum planes from view-projection matrix.
|
||||
// Vulkan clip-space conventions (GLM_FORCE_DEPTH_ZERO_TO_ONE + Y-flip):
|
||||
// x_clip ∈ [-w, w], y_clip ∈ [-w, w] (Y flipped in proj), z_clip ∈ [0, w]
|
||||
//
|
||||
// Gribb & Hartmann method adapted for Vulkan depth [0,1].
|
||||
// Left/Right/Top/Bottom use the standard row4 ± row1/row2 formulas
|
||||
// (the Y-flip swaps the TOP/BOTTOM row2 sign, but the extracted half-spaces
|
||||
// are still correct — they just get each other's label. We swap the
|
||||
// assignments so the enum names match the geometric meaning.)
|
||||
|
||||
// Left plane: row4 + row1
|
||||
// Left plane: row4 + row1 (x_clip >= -w_clip)
|
||||
planes[LEFT].normal.x = vp[0][3] + vp[0][0];
|
||||
planes[LEFT].normal.y = vp[1][3] + vp[1][0];
|
||||
planes[LEFT].normal.z = vp[2][3] + vp[2][0];
|
||||
planes[LEFT].distance = vp[3][3] + vp[3][0];
|
||||
normalizePlane(planes[LEFT]);
|
||||
|
||||
// Right plane: row4 - row1
|
||||
// Right plane: row4 - row1 (x_clip <= w_clip)
|
||||
planes[RIGHT].normal.x = vp[0][3] - vp[0][0];
|
||||
planes[RIGHT].normal.y = vp[1][3] - vp[1][0];
|
||||
planes[RIGHT].normal.z = vp[2][3] - vp[2][0];
|
||||
planes[RIGHT].distance = vp[3][3] - vp[3][0];
|
||||
normalizePlane(planes[RIGHT]);
|
||||
|
||||
// Bottom plane: row4 + row2
|
||||
planes[BOTTOM].normal.x = vp[0][3] + vp[0][1];
|
||||
planes[BOTTOM].normal.y = vp[1][3] + vp[1][1];
|
||||
planes[BOTTOM].normal.z = vp[2][3] + vp[2][1];
|
||||
planes[BOTTOM].distance = vp[3][3] + vp[3][1];
|
||||
normalizePlane(planes[BOTTOM]);
|
||||
// With the Vulkan Y-flip (proj[1][1] negated), row4+row2 extracts
|
||||
// what is geometrically the TOP plane and row4-row2 extracts BOTTOM.
|
||||
// Swap the assignments so enum labels match geometry.
|
||||
|
||||
// Top plane: row4 - row2
|
||||
planes[TOP].normal.x = vp[0][3] - vp[0][1];
|
||||
planes[TOP].normal.y = vp[1][3] - vp[1][1];
|
||||
planes[TOP].normal.z = vp[2][3] - vp[2][1];
|
||||
planes[TOP].distance = vp[3][3] - vp[3][1];
|
||||
// Top plane (geometric): row4 - row2 after Y-flip
|
||||
planes[TOP].normal.x = vp[0][3] + vp[0][1];
|
||||
planes[TOP].normal.y = vp[1][3] + vp[1][1];
|
||||
planes[TOP].normal.z = vp[2][3] + vp[2][1];
|
||||
planes[TOP].distance = vp[3][3] + vp[3][1];
|
||||
normalizePlane(planes[TOP]);
|
||||
|
||||
// Near plane: row4 + row3
|
||||
planes[NEAR].normal.x = vp[0][3] + vp[0][2];
|
||||
planes[NEAR].normal.y = vp[1][3] + vp[1][2];
|
||||
planes[NEAR].normal.z = vp[2][3] + vp[2][2];
|
||||
planes[NEAR].distance = vp[3][3] + vp[3][2];
|
||||
// Bottom plane (geometric): row4 + row2 after Y-flip
|
||||
planes[BOTTOM].normal.x = vp[0][3] - vp[0][1];
|
||||
planes[BOTTOM].normal.y = vp[1][3] - vp[1][1];
|
||||
planes[BOTTOM].normal.z = vp[2][3] - vp[2][1];
|
||||
planes[BOTTOM].distance = vp[3][3] - vp[3][1];
|
||||
normalizePlane(planes[BOTTOM]);
|
||||
|
||||
// Near plane: row3 (z_clip >= 0 in Vulkan depth [0,1])
|
||||
planes[NEAR].normal.x = vp[0][2];
|
||||
planes[NEAR].normal.y = vp[1][2];
|
||||
planes[NEAR].normal.z = vp[2][2];
|
||||
planes[NEAR].distance = vp[3][2];
|
||||
normalizePlane(planes[NEAR]);
|
||||
|
||||
// Far plane: row4 - row3
|
||||
// Far plane: row4 - row3 (z_clip <= w_clip)
|
||||
planes[FAR].normal.x = vp[0][3] - vp[0][2];
|
||||
planes[FAR].normal.y = vp[1][3] - vp[1][2];
|
||||
planes[FAR].normal.z = vp[2][3] - vp[2][2];
|
||||
|
|
|
|||
|
|
@ -412,32 +412,37 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) {
|
|||
if (showControls) {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "CONTROLS");
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F1: Toggle HUD");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F2: Wireframe");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F3: Single tile");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F4: Culling");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F5: Stats");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F6: Multi-tile");
|
||||
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "Movement");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "WASD: Move/Strafe");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Q/E: Turn left/right");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Space: Jump");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "X: Sit/Stand");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "~: Auto-run");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Z: Sheathe weapons");
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "UI Panels");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "B: Bags/Inventory");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "C: Character sheet");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "L: Quest log");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "N: Talents");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "P: Spellbook");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "M: World map");
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "Combat & Chat");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "1-0,-,=: Action bar");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Tab: Target cycle");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Enter: Chat");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "/: Chat command");
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "Debug");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F1: Toggle this HUD");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F4: Toggle shadows");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F7: Level-up FX");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F8: Water");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F9: Time");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F10: Sun/Moon");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F11: Stars");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F12: Fog");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "+/-: Change time");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "C: Clouds");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "[/]: Density");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "L: Lens Flare");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ",/.: Intensity");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "M: Moon Cycle");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ";/': Moon Phase");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "W: Weather");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "</>: Wx Intensity");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "K: Spawn Character");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "J: Remove Chars");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "O: Spawn Test WMO");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Shift+O: Real WMO");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "P: Clear WMOs");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Esc: Settings/Close");
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include "rendering/charge_effect.hpp"
|
||||
#include "rendering/levelup_effect.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/character_preview.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
#include "rendering/minimap.hpp"
|
||||
|
|
@ -723,6 +724,21 @@ void Renderer::shutdown() {
|
|||
LOG_INFO("Renderer shutdown");
|
||||
}
|
||||
|
||||
void Renderer::registerPreview(CharacterPreview* preview) {
|
||||
if (!preview) return;
|
||||
auto it = std::find(activePreviews_.begin(), activePreviews_.end(), preview);
|
||||
if (it == activePreviews_.end()) {
|
||||
activePreviews_.push_back(preview);
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::unregisterPreview(CharacterPreview* preview) {
|
||||
auto it = std::find(activePreviews_.begin(), activePreviews_.end(), preview);
|
||||
if (it != activePreviews_.end()) {
|
||||
activePreviews_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::setMsaaSamples(VkSampleCountFlagBits samples) {
|
||||
if (!vkCtx) return;
|
||||
|
||||
|
|
@ -840,6 +856,13 @@ void Renderer::beginFrame() {
|
|||
worldMap->compositePass(currentCmd);
|
||||
}
|
||||
|
||||
// Character preview composite passes
|
||||
for (auto* preview : activePreviews_) {
|
||||
if (preview && preview->isModelLoaded()) {
|
||||
preview->compositePass(currentCmd, vkCtx->getCurrentFrame());
|
||||
}
|
||||
}
|
||||
|
||||
// Shadow pre-pass (before main render pass)
|
||||
if (shadowsEnabled && shadowDepthImage != VK_NULL_HANDLE) {
|
||||
renderShadowPass();
|
||||
|
|
@ -3007,6 +3030,15 @@ void Renderer::renderOverlay(const glm::vec4& color) {
|
|||
void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
|
||||
(void)world;
|
||||
|
||||
{
|
||||
static int rwLogCounter = 0;
|
||||
if (++rwLogCounter % 300 == 1) {
|
||||
LOG_INFO("Renderer::renderWorld frame=", rwLogCounter,
|
||||
" cmd=", (void*)currentCmd,
|
||||
" charRenderer=", (void*)characterRenderer.get());
|
||||
}
|
||||
}
|
||||
|
||||
auto renderStart = std::chrono::steady_clock::now();
|
||||
lastTerrainRenderMs = 0.0;
|
||||
lastWMORenderMs = 0.0;
|
||||
|
|
|
|||
|
|
@ -1041,8 +1041,18 @@ bool VkContext::recreateSwapchain(int width, int height) {
|
|||
VkCommandBuffer VkContext::beginFrame(uint32_t& imageIndex) {
|
||||
auto& frame = frames[currentFrame];
|
||||
|
||||
// Wait for this frame's fence
|
||||
vkWaitForFences(device, 1, &frame.inFlightFence, VK_TRUE, UINT64_MAX);
|
||||
// Wait for this frame's fence (with timeout to detect GPU hangs)
|
||||
static int beginFrameCounter = 0;
|
||||
beginFrameCounter++;
|
||||
VkResult fenceResult = vkWaitForFences(device, 1, &frame.inFlightFence, VK_TRUE, 5000000000ULL); // 5 second timeout
|
||||
if (fenceResult == VK_TIMEOUT) {
|
||||
LOG_ERROR("beginFrame[", beginFrameCounter, "] FENCE TIMEOUT (5s) on frame slot ", currentFrame, " — GPU hang detected!");
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
if (fenceResult != VK_SUCCESS) {
|
||||
LOG_ERROR("beginFrame[", beginFrameCounter, "] fence wait failed: ", (int)fenceResult);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
// Acquire next swapchain image
|
||||
VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX,
|
||||
|
|
@ -1070,7 +1080,13 @@ VkCommandBuffer VkContext::beginFrame(uint32_t& imageIndex) {
|
|||
}
|
||||
|
||||
void VkContext::endFrame(VkCommandBuffer cmd, uint32_t imageIndex) {
|
||||
vkEndCommandBuffer(cmd);
|
||||
static int endFrameCounter = 0;
|
||||
endFrameCounter++;
|
||||
|
||||
VkResult endResult = vkEndCommandBuffer(cmd);
|
||||
if (endResult != VK_SUCCESS) {
|
||||
LOG_ERROR("endFrame[", endFrameCounter, "] vkEndCommandBuffer FAILED: ", (int)endResult);
|
||||
}
|
||||
|
||||
auto& frame = frames[currentFrame];
|
||||
|
||||
|
|
@ -1086,8 +1102,9 @@ void VkContext::endFrame(VkCommandBuffer cmd, uint32_t imageIndex) {
|
|||
submitInfo.signalSemaphoreCount = 1;
|
||||
submitInfo.pSignalSemaphores = &frame.renderFinishedSemaphore;
|
||||
|
||||
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, frame.inFlightFence) != VK_SUCCESS) {
|
||||
LOG_ERROR("Failed to submit draw command buffer");
|
||||
VkResult submitResult = vkQueueSubmit(graphicsQueue, 1, &submitInfo, frame.inFlightFence);
|
||||
if (submitResult != VK_SUCCESS) {
|
||||
LOG_ERROR("endFrame[", endFrameCounter, "] vkQueueSubmit FAILED: ", (int)submitResult);
|
||||
}
|
||||
|
||||
VkPresentInfoKHR presentInfo{};
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ VkRenderTarget::~VkRenderTarget() {
|
|||
// Must call destroy() explicitly with device/allocator before destruction
|
||||
}
|
||||
|
||||
bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkFormat format) {
|
||||
bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkFormat format, bool withDepth) {
|
||||
VkDevice device = ctx.getDevice();
|
||||
VmaAllocator allocator = ctx.getAllocator();
|
||||
hasDepth_ = withDepth;
|
||||
|
||||
// Create color image (COLOR_ATTACHMENT + SAMPLED for reading in subsequent passes)
|
||||
colorImage_ = createImage(device, allocator, width, height, format,
|
||||
|
|
@ -22,6 +23,17 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkF
|
|||
return false;
|
||||
}
|
||||
|
||||
// Create depth image if requested
|
||||
if (withDepth) {
|
||||
depthImage_ = createImage(device, allocator, width, height,
|
||||
VK_FORMAT_D32_SFLOAT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT);
|
||||
if (!depthImage_.image) {
|
||||
LOG_ERROR("VkRenderTarget: failed to create depth image (", width, "x", height, ")");
|
||||
destroy(device, allocator);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create sampler (linear filtering, clamp to edge)
|
||||
VkSamplerCreateInfo samplerInfo{};
|
||||
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||
|
|
@ -41,44 +53,77 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkF
|
|||
}
|
||||
|
||||
// Create render pass
|
||||
// Color attachment: UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL (during pass)
|
||||
// -> SHADER_READ_ONLY_OPTIMAL (final layout, ready for sampling)
|
||||
VkAttachmentDescription colorAttachment{};
|
||||
colorAttachment.format = format;
|
||||
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkAttachmentDescription attachments[2]{};
|
||||
// Color attachment: UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL -> SHADER_READ_ONLY_OPTIMAL
|
||||
attachments[0].format = format;
|
||||
attachments[0].samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
attachments[0].finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
|
||||
// Depth attachment (only used when withDepth)
|
||||
attachments[1].format = VK_FORMAT_D32_SFLOAT;
|
||||
attachments[1].samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
||||
attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
|
||||
VkAttachmentReference colorRef{};
|
||||
colorRef.attachment = 0;
|
||||
colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
|
||||
VkAttachmentReference depthRef{};
|
||||
depthRef.attachment = 1;
|
||||
depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
|
||||
VkSubpassDescription subpass{};
|
||||
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
||||
subpass.colorAttachmentCount = 1;
|
||||
subpass.pColorAttachments = &colorRef;
|
||||
if (withDepth) subpass.pDepthStencilAttachment = &depthRef;
|
||||
|
||||
// Dependency: external -> subpass 0 (wait for previous reads to finish)
|
||||
VkSubpassDependency dependency{};
|
||||
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
|
||||
dependency.dstSubpass = 0;
|
||||
dependency.srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
||||
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
dependency.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
// Dependencies
|
||||
VkSubpassDependency dependencies[2]{};
|
||||
uint32_t depCount = 1;
|
||||
|
||||
// Input dependency: wait for previous fragment shader reads before writing
|
||||
dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
|
||||
dependencies[0].dstSubpass = 0;
|
||||
dependencies[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
||||
dependencies[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
||||
dependencies[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
dependencies[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
|
||||
if (withDepth) {
|
||||
dependencies[0].dstStageMask |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
|
||||
dependencies[0].dstAccessMask |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||||
|
||||
// Output dependency (depth targets only): ensure writes complete before fragment reads
|
||||
dependencies[1].srcSubpass = 0;
|
||||
dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
|
||||
dependencies[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
|
||||
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
|
||||
dependencies[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
||||
dependencies[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
|
||||
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||||
dependencies[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
depCount = 2;
|
||||
}
|
||||
|
||||
VkRenderPassCreateInfo rpInfo{};
|
||||
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
|
||||
rpInfo.attachmentCount = 1;
|
||||
rpInfo.pAttachments = &colorAttachment;
|
||||
rpInfo.attachmentCount = withDepth ? 2u : 1u;
|
||||
rpInfo.pAttachments = attachments;
|
||||
rpInfo.subpassCount = 1;
|
||||
rpInfo.pSubpasses = &subpass;
|
||||
rpInfo.dependencyCount = 1;
|
||||
rpInfo.pDependencies = &dependency;
|
||||
rpInfo.dependencyCount = depCount;
|
||||
rpInfo.pDependencies = dependencies;
|
||||
|
||||
if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderPass_) != VK_SUCCESS) {
|
||||
LOG_ERROR("VkRenderTarget: failed to create render pass");
|
||||
|
|
@ -87,11 +132,12 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkF
|
|||
}
|
||||
|
||||
// Create framebuffer
|
||||
VkImageView fbAttachments[2] = { colorImage_.imageView, depthImage_.imageView };
|
||||
VkFramebufferCreateInfo fbInfo{};
|
||||
fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
|
||||
fbInfo.renderPass = renderPass_;
|
||||
fbInfo.attachmentCount = 1;
|
||||
fbInfo.pAttachments = &colorImage_.imageView;
|
||||
fbInfo.attachmentCount = withDepth ? 2u : 1u;
|
||||
fbInfo.pAttachments = fbAttachments;
|
||||
fbInfo.width = width;
|
||||
fbInfo.height = height;
|
||||
fbInfo.layers = 1;
|
||||
|
|
@ -102,7 +148,7 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkF
|
|||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("VkRenderTarget created (", width, "x", height, ")");
|
||||
LOG_INFO("VkRenderTarget created (", width, "x", height, withDepth ? ", with depth)" : ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +165,9 @@ void VkRenderTarget::destroy(VkDevice device, VmaAllocator allocator) {
|
|||
vkDestroySampler(device, sampler_, nullptr);
|
||||
sampler_ = VK_NULL_HANDLE;
|
||||
}
|
||||
destroyImage(device, allocator, depthImage_);
|
||||
destroyImage(device, allocator, colorImage_);
|
||||
hasDepth_ = false;
|
||||
}
|
||||
|
||||
void VkRenderTarget::beginPass(VkCommandBuffer cmd, const VkClearColorValue& clear) {
|
||||
|
|
@ -130,10 +178,11 @@ void VkRenderTarget::beginPass(VkCommandBuffer cmd, const VkClearColorValue& cle
|
|||
rpBegin.renderArea.offset = {0, 0};
|
||||
rpBegin.renderArea.extent = getExtent();
|
||||
|
||||
VkClearValue clearValue{};
|
||||
clearValue.color = clear;
|
||||
rpBegin.clearValueCount = 1;
|
||||
rpBegin.pClearValues = &clearValue;
|
||||
VkClearValue clearValues[2]{};
|
||||
clearValues[0].color = clear;
|
||||
clearValues[1].depthStencil = {1.0f, 0};
|
||||
rpBegin.clearValueCount = hasDepth_ ? 2u : 1u;
|
||||
rpBegin.pClearValues = clearValues;
|
||||
|
||||
vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#include "ui/character_create_screen.hpp"
|
||||
#include "rendering/character_preview.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
|
|
@ -128,7 +130,10 @@ void CharacterCreateScreen::initializePreview(pipeline::AssetManager* am) {
|
|||
assetManager_ = am;
|
||||
if (!preview_) {
|
||||
preview_ = std::make_unique<rendering::CharacterPreview>();
|
||||
preview_->initialize(am);
|
||||
if (preview_->initialize(am)) {
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
if (renderer) renderer->registerPreview(preview_.get());
|
||||
}
|
||||
}
|
||||
// Force model reload
|
||||
prevRaceIndex_ = -1;
|
||||
|
|
@ -332,6 +337,7 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
|
|||
if (preview_) {
|
||||
updatePreviewIfNeeded();
|
||||
preview_->render();
|
||||
preview_->requestComposite();
|
||||
}
|
||||
|
||||
ImVec2 displaySize = ImGui::GetIO().DisplaySize;
|
||||
|
|
@ -363,13 +369,10 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) {
|
|||
static_cast<float>(preview_->getHeight()));
|
||||
}
|
||||
|
||||
// TODO: Vulkan offscreen preview render target
|
||||
if (preview_->getTextureId()) {
|
||||
ImGui::Image(
|
||||
static_cast<ImTextureID>(0),
|
||||
ImVec2(imgW, imgH),
|
||||
ImVec2(0.0f, 1.0f),
|
||||
ImVec2(1.0f, 0.0f));
|
||||
reinterpret_cast<ImTextureID>(preview_->getTextureId()),
|
||||
ImVec2(imgW, imgH));
|
||||
}
|
||||
|
||||
// Mouse drag rotation on the preview image
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "ui/character_screen.hpp"
|
||||
#include "rendering/character_preview.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
|
@ -250,6 +251,9 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
if (!previewInitialized_) {
|
||||
LOG_WARNING("CharacterScreen: failed to init CharacterPreview");
|
||||
preview_.reset();
|
||||
} else {
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
if (renderer) renderer->registerPreview(preview_.get());
|
||||
}
|
||||
}
|
||||
if (preview_) {
|
||||
|
|
@ -280,9 +284,10 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
previewEquipHash_ = equipHash;
|
||||
}
|
||||
|
||||
// Drive preview animation and render to its FBO.
|
||||
// Drive preview animation and request composite for next beginFrame.
|
||||
preview_->update(ImGui::GetIO().DeltaTime);
|
||||
preview_->render();
|
||||
preview_->requestComposite();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,7 +295,7 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
ImGui::BeginChild("CharDetails", ImVec2(detailPanelW, listH), true);
|
||||
|
||||
// 3D preview portrait
|
||||
if (preview_ && preview_->getTextureId() != 0) {
|
||||
if (preview_ && preview_->getTextureId()) {
|
||||
float imgW = ImGui::GetContentRegionAvail().x;
|
||||
float imgH = imgW * (static_cast<float>(preview_->getHeight()) /
|
||||
static_cast<float>(preview_->getWidth()));
|
||||
|
|
@ -301,14 +306,9 @@ void CharacterScreen::render(game::GameHandler& gameHandler) {
|
|||
imgW = imgH * (static_cast<float>(preview_->getWidth()) /
|
||||
static_cast<float>(preview_->getHeight()));
|
||||
}
|
||||
// TODO: Vulkan offscreen preview render target
|
||||
if (preview_->getTextureId()) {
|
||||
ImGui::Image(
|
||||
static_cast<ImTextureID>(0),
|
||||
ImVec2(imgW, imgH),
|
||||
ImVec2(0.0f, 1.0f),
|
||||
ImVec2(1.0f, 0.0f));
|
||||
}
|
||||
ImGui::Image(
|
||||
reinterpret_cast<ImTextureID>(preview_->getTextureId()),
|
||||
ImVec2(imgW, imgH));
|
||||
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
||||
preview_->rotate(ImGui::GetIO().MouseDelta.x * 0.2f);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "core/input.hpp"
|
||||
#include "rendering/character_preview.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/blp_loader.hpp"
|
||||
|
|
@ -175,6 +176,8 @@ void InventoryScreen::initPreview() {
|
|||
charPreview_.reset();
|
||||
return;
|
||||
}
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
if (renderer) renderer->registerPreview(charPreview_.get());
|
||||
}
|
||||
|
||||
charPreview_->loadCharacter(playerRace_, playerGender_,
|
||||
|
|
@ -925,6 +928,7 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
|
|||
if (charPreview_ && previewInitialized_) {
|
||||
charPreview_->update(ImGui::GetIO().DeltaTime);
|
||||
charPreview_->render();
|
||||
charPreview_->requestComposite();
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(20.0f, 80.0f), ImGuiCond_FirstUseEver);
|
||||
|
|
@ -1120,9 +1124,8 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
|
|||
// Background for preview area
|
||||
drawList->AddRectFilled(pMin, pMax, IM_COL32(13, 13, 25, 255));
|
||||
drawList->AddImage(
|
||||
(ImTextureID)(uintptr_t)charPreview_->getTextureId(),
|
||||
pMin, pMax,
|
||||
ImVec2(0, 1), ImVec2(1, 0)); // flip Y for GL
|
||||
reinterpret_cast<ImTextureID>(charPreview_->getTextureId()),
|
||||
pMin, pMax);
|
||||
drawList->AddRect(pMin, pMax, IM_COL32(60, 60, 80, 200));
|
||||
|
||||
// Drag-to-rotate: detect mouse drag over the preview image
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue