From d3ab4d18a8ee48fd67efd4b63dfc2652743a449b Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Fri, 12 Dec 2025 20:45:02 +0900 Subject: [PATCH] ADD: aspect ratio preserving picking --- src/core/device/images.cpp | 35 ++++++++++++ src/core/device/images.h | 8 +++ src/core/engine.cpp | 97 ++++++++++++++++++---------------- src/core/engine_ui.cpp | 33 +++++++++--- src/scene/vk_scene_picking.cpp | 87 +++++++++++++++++++++++++----- 5 files changed, 194 insertions(+), 66 deletions(-) diff --git a/src/core/device/images.cpp b/src/core/device/images.cpp index 1131895..db015d2 100644 --- a/src/core/device/images.cpp +++ b/src/core/device/images.cpp @@ -178,6 +178,41 @@ VkRect2D vkutil::compute_letterbox_rect(VkExtent2D srcSize, VkExtent2D dstSize) return rect; } +bool vkutil::map_window_to_letterbox_src(const glm::vec2 &windowPosPixels, + VkExtent2D srcSize, + VkExtent2D dstSize, + glm::vec2 &outSrcPosPixels) +{ + outSrcPosPixels = glm::vec2{0.0f, 0.0f}; + if (srcSize.width == 0 || srcSize.height == 0 || dstSize.width == 0 || dstSize.height == 0) + { + return false; + } + + VkRect2D rect = compute_letterbox_rect(srcSize, dstSize); + if (rect.extent.width == 0 || rect.extent.height == 0) + { + return false; + } + + const float localX = windowPosPixels.x - static_cast(rect.offset.x); + const float localY = windowPosPixels.y - static_cast(rect.offset.y); + + if (localX < 0.0f || localY < 0.0f || + localX >= static_cast(rect.extent.width) || + localY >= static_cast(rect.extent.height)) + { + return false; + } + + const float u = localX / static_cast(rect.extent.width); + const float v = localY / static_cast(rect.extent.height); + + outSrcPosPixels.x = u * static_cast(srcSize.width); + outSrcPosPixels.y = v * static_cast(srcSize.height); + return true; +} + void vkutil::copy_image_to_image_letterboxed(VkCommandBuffer cmd, VkImage source, VkImage destination, diff --git a/src/core/device/images.h b/src/core/device/images.h index da0c2fd..c6117ad 100644 --- a/src/core/device/images.h +++ b/src/core/device/images.h @@ -1,5 +1,6 @@ #pragma once #include +#include namespace vkutil { @@ -8,6 +9,13 @@ namespace vkutil { // Compute a letterboxed destination rect inside dstSize that preserves srcSize aspect ratio. VkRect2D compute_letterbox_rect(VkExtent2D srcSize, VkExtent2D dstSize); + // Map a window-space pixel position (in dstSize, top-left origin) into pixel coordinates + // inside the letterboxed srcSize view. Returns false if the position lies in black bars. + bool map_window_to_letterbox_src(const glm::vec2 &windowPosPixels, + VkExtent2D srcSize, + VkExtent2D dstSize, + glm::vec2 &outSrcPosPixels); + void copy_image_to_image(VkCommandBuffer cmd, VkImage source, VkImage destination, VkExtent2D srcSize, VkExtent2D dstSize); // Blit source into a letterboxed rect in destination (preserves aspect ratio). void copy_image_to_image_letterboxed(VkCommandBuffer cmd, diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 66c6f55..0a4f16d 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -47,6 +47,7 @@ #include "render/passes/tonemap.h" #include "render/passes/shadow.h" #include "device/resource.h" +#include "device/images.h" #include "context.h" #include "core/pipeline/manager.h" #include "core/assets/texture_cache.h" @@ -893,56 +894,62 @@ void VulkanEngine::draw() VkExtent2D swapExt = _swapchainManager->swapchainExtent(); VkExtent2D drawExt = _drawExtent; - float sx = _pendingPick.windowPos.x / float(std::max(1u, swapExt.width)); - float sy = _pendingPick.windowPos.y / float(std::max(1u, swapExt.height)); + glm::vec2 logicalPos{}; + if (!vkutil::map_window_to_letterbox_src(_pendingPick.windowPos, drawExt, swapExt, logicalPos)) + { + // Click landed outside the active letterboxed region. + _pendingPick.active = false; + } + else + { + uint32_t idX = uint32_t(glm::clamp(logicalPos.x, 0.0f, float(drawExt.width - 1))); + uint32_t idY = uint32_t(glm::clamp(logicalPos.y, 0.0f, float(drawExt.height - 1))); + _pendingPick.idCoords = {idX, idY}; - uint32_t idX = uint32_t(glm::clamp(sx * float(drawExt.width), 0.0f, float(drawExt.width - 1))); - uint32_t idY = uint32_t(glm::clamp(sy * float(drawExt.height), 0.0f, float(drawExt.height - 1))); - _pendingPick.idCoords = {idX, idY}; + RGImportedBufferDesc bd{}; + bd.name = "pick.readback"; + bd.buffer = _pickReadbackBuffer.buffer; + bd.size = sizeof(uint32_t); + bd.currentStage = VK_PIPELINE_STAGE_2_NONE; + bd.currentAccess = 0; + RGBufferHandle hPickBuf = _renderGraph->import_buffer(bd); - RGImportedBufferDesc bd{}; - bd.name = "pick.readback"; - bd.buffer = _pickReadbackBuffer.buffer; - bd.size = sizeof(uint32_t); - bd.currentStage = VK_PIPELINE_STAGE_2_NONE; - bd.currentAccess = 0; - RGBufferHandle hPickBuf = _renderGraph->import_buffer(bd); + _renderGraph->add_pass( + "PickReadback", + RGPassType::Transfer, + [hID, hPickBuf](RGPassBuilder &builder, EngineContext *) + { + builder.read(hID, RGImageUsage::TransferSrc); + builder.write_buffer(hPickBuf, RGBufferUsage::TransferDst); + }, + [this, hID, hPickBuf](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *) + { + VkImage idImage = res.image(hID); + VkBuffer dst = res.buffer(hPickBuf); + if (idImage == VK_NULL_HANDLE || dst == VK_NULL_HANDLE) return; - _renderGraph->add_pass( - "PickReadback", - RGPassType::Transfer, - [hID, hPickBuf](RGPassBuilder &builder, EngineContext *) - { - builder.read(hID, RGImageUsage::TransferSrc); - builder.write_buffer(hPickBuf, RGBufferUsage::TransferDst); - }, - [this, hID, hPickBuf](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *) - { - VkImage idImage = res.image(hID); - VkBuffer dst = res.buffer(hPickBuf); - if (idImage == VK_NULL_HANDLE || dst == VK_NULL_HANDLE) return; + VkBufferImageCopy region{}; + region.bufferOffset = 0; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.mipLevel = 0; + region.imageSubresource.baseArrayLayer = 0; + region.imageSubresource.layerCount = 1; + region.imageOffset = { static_cast(_pendingPick.idCoords.x), + static_cast(_pendingPick.idCoords.y), + 0 }; + region.imageExtent = {1, 1, 1}; - VkBufferImageCopy region{}; - region.bufferOffset = 0; - region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - region.imageSubresource.mipLevel = 0; - region.imageSubresource.baseArrayLayer = 0; - region.imageSubresource.layerCount = 1; - region.imageOffset = { static_cast(_pendingPick.idCoords.x), - static_cast(_pendingPick.idCoords.y), - 0 }; - region.imageExtent = {1, 1, 1}; + vkCmdCopyImageToBuffer(cmd, + idImage, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + dst, + 1, + ®ion); + }); - vkCmdCopyImageToBuffer(cmd, - idImage, - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - dst, - 1, - ®ion); - }); - - _pickResultPending = true; - _pendingPick.active = false; + _pickResultPending = true; + _pendingPick.active = false; + } } } if (auto *lighting = _renderPassManager->getPass()) diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index 0e03e1e..0ed5a25 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -20,6 +20,7 @@ #include "core/pipeline/manager.h" #include "core/assets/texture_cache.h" #include "core/assets/ibl_manager.h" +#include "device/images.h" #include "context.h" #include #include @@ -1186,18 +1187,36 @@ namespace ImGuiIO &io = ImGui::GetIO(); ImGuizmo::SetOrthographic(false); + + VkExtent2D swapExtent = eng->_swapchainManager + ? eng->_swapchainManager->swapchainExtent() + : VkExtent2D{1, 1}; + VkExtent2D drawExtent{ + static_cast(static_cast(eng->_logicalRenderExtent.width) * eng->renderScale), + static_cast(static_cast(eng->_logicalRenderExtent.height) * eng->renderScale) + }; + if (drawExtent.width == 0 || drawExtent.height == 0) + { + drawExtent = VkExtent2D{1, 1}; + } + + VkRect2D activeRect = vkutil::compute_letterbox_rect(drawExtent, swapExtent); + const float fbScaleX = (io.DisplayFramebufferScale.x > 0.0f) ? io.DisplayFramebufferScale.x : 1.0f; + const float fbScaleY = (io.DisplayFramebufferScale.y > 0.0f) ? io.DisplayFramebufferScale.y : 1.0f; + const float rectX = static_cast(activeRect.offset.x) / fbScaleX; + const float rectY = static_cast(activeRect.offset.y) / fbScaleY; + const float rectW = static_cast(activeRect.extent.width) / fbScaleX; + const float rectH = static_cast(activeRect.extent.height) / fbScaleY; + ImGuizmo::SetDrawlist(); - ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); + ImGuizmo::SetRect(rectX, rectY, rectW, rectH); // Build a distance-based perspective projection for ImGuizmo instead of // using the engine's reversed-Z Vulkan projection. Camera &cam = sceneMgr->getMainCamera(); float fovRad = glm::radians(cam.fovDegrees); - VkExtent2D extent = eng->_swapchainManager - ? eng->_swapchainManager->swapchainExtent() - : VkExtent2D{1, 1}; - float aspect = extent.height > 0 - ? static_cast(extent.width) / static_cast(extent.height) + float aspect = drawExtent.height > 0 + ? static_cast(drawExtent.width) / static_cast(drawExtent.height) : 1.0f; // Distance from camera to object; clamp to avoid degenerate planes. @@ -1221,7 +1240,7 @@ namespace ImDrawList* dl = ImGui::GetForegroundDrawList(); ImGuizmo::SetDrawlist(dl); - ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); + ImGuizmo::SetRect(rectX, rectY, rectW, rectH); ImGuizmo::Manipulate(&view[0][0], &proj[0][0], op, mode, &targetTransform[0][0]); diff --git a/src/scene/vk_scene_picking.cpp b/src/scene/vk_scene_picking.cpp index 8949281..19bd96d 100644 --- a/src/scene/vk_scene_picking.cpp +++ b/src/scene/vk_scene_picking.cpp @@ -1,6 +1,7 @@ #include "vk_scene.h" #include "core/device/swapchain.h" +#include "core/device/images.h" #include "core/context.h" #include "mesh_bvh.h" @@ -433,18 +434,34 @@ bool SceneManager::pick(const glm::vec2 &mousePosPixels, RenderObject &outObject return false; } - VkExtent2D extent = swapchain->windowExtent(); - if (extent.width == 0 || extent.height == 0) + VkExtent2D dstExtent = swapchain->windowExtent(); + if (dstExtent.width == 0 || dstExtent.height == 0) { return false; } - float width = static_cast(extent.width); - float height = static_cast(extent.height); + VkExtent2D logicalExtent{ kRenderWidth, kRenderHeight }; + if (_context) + { + VkExtent2D ctxLogical = _context->getLogicalRenderExtent(); + if (ctxLogical.width > 0 && ctxLogical.height > 0) + { + logicalExtent = ctxLogical; + } + } - // Convert from window coordinates (top-left origin) to NDC in [-1, 1]. - float ndcX = (2.0f * mousePosPixels.x / width) - 1.0f; - float ndcY = 1.0f - (2.0f * mousePosPixels.y / height); + glm::vec2 logicalPos{}; + if (!vkutil::map_window_to_letterbox_src(mousePosPixels, logicalExtent, dstExtent, logicalPos)) + { + return false; + } + + float width = static_cast(logicalExtent.width); + float height = static_cast(logicalExtent.height); + + // Convert from logical view coordinates (top-left origin) to NDC in [-1, 1]. + float ndcX = (2.0f * logicalPos.x / width) - 1.0f; + float ndcY = 1.0f - (2.0f * logicalPos.y / height); float fovRad = glm::radians(mainCamera.fovDegrees); float tanHalfFov = std::tan(fovRad * 0.5f); @@ -566,16 +583,58 @@ void SceneManager::selectRect(const glm::vec2 &p0, const glm::vec2 &p1, std::vec return; } - VkExtent2D extent = _context->getSwapchain()->windowExtent(); - if (extent.width == 0 || extent.height == 0) + SwapchainManager *swapchain = _context->getSwapchain(); + VkExtent2D dstExtent = swapchain->windowExtent(); + if (dstExtent.width == 0 || dstExtent.height == 0) { return; } - float width = static_cast(extent.width); - float height = static_cast(extent.height); + VkExtent2D logicalExtent{ kRenderWidth, kRenderHeight }; + VkExtent2D ctxLogical = _context->getLogicalRenderExtent(); + if (ctxLogical.width > 0 && ctxLogical.height > 0) + { + logicalExtent = ctxLogical; + } - // Convert from window coordinates (top-left origin) to NDC in [-1, 1]. + VkRect2D activeRect = vkutil::compute_letterbox_rect(logicalExtent, dstExtent); + if (activeRect.extent.width == 0 || activeRect.extent.height == 0) + { + return; + } + + glm::vec2 selMin = glm::min(p0, p1); + glm::vec2 selMax = glm::max(p0, p1); + + glm::vec2 activeMin{static_cast(activeRect.offset.x), + static_cast(activeRect.offset.y)}; + glm::vec2 activeMax{activeMin.x + static_cast(activeRect.extent.width), + activeMin.y + static_cast(activeRect.extent.height)}; + + glm::vec2 clipMin = glm::max(selMin, activeMin); + glm::vec2 clipMax = glm::min(selMax, activeMax); + if (clipMax.x <= clipMin.x || clipMax.y <= clipMin.y) + { + return; + } + + auto toLogical = [&](const glm::vec2 &p) -> glm::vec2 + { + float localX = p.x - activeMin.x; + float localY = p.y - activeMin.y; + float u = localX / static_cast(activeRect.extent.width); + float v = localY / static_cast(activeRect.extent.height); + return glm::vec2{u * static_cast(logicalExtent.width), + v * static_cast(logicalExtent.height)}; + }; + + glm::vec2 logical0 = toLogical(clipMin); + glm::vec2 logical1 = toLogical(clipMax); + + float width = static_cast(logicalExtent.width); + float height = static_cast(logicalExtent.height); + + // Convert from logical view coordinates (top-left origin) to NDC in [-1, 1]. auto toNdc = [&](const glm::vec2 &p) -> glm::vec2 { float ndcX = (2.0f * p.x / width) - 1.0f; @@ -583,8 +642,8 @@ void SceneManager::selectRect(const glm::vec2 &p0, const glm::vec2 &p1, std::vec return glm::vec2{ndcX, ndcY}; }; - glm::vec2 ndc0 = toNdc(p0); - glm::vec2 ndc1 = toNdc(p1); + glm::vec2 ndc0 = toNdc(logical0); + glm::vec2 ndc1 = toNdc(logical1); glm::vec2 ndcMin = glm::min(ndc0, ndc1); glm::vec2 ndcMax = glm::max(ndc0, ndc1);