ADD: Selection

This commit is contained in:
2025-11-18 02:18:16 +09:00
parent 24089dc325
commit 94ba704f99
37 changed files with 7495 additions and 35 deletions

View File

@@ -99,7 +99,7 @@ target_include_directories(vulkan_engine PUBLIC
option(ENABLE_MIKKTS "Use MikkTSpace for tangent generation" ON)
target_link_libraries(vulkan_engine PUBLIC vma glm Vulkan::Vulkan fmt::fmt stb_image SDL2::SDL2 vkbootstrap imgui fastgltf::fastgltf)
target_link_libraries(vulkan_engine PUBLIC vma glm Vulkan::Vulkan fmt::fmt stb_image SDL2::SDL2 vkbootstrap imgui fastgltf::fastgltf ImGuizmo)
if (ENABLE_MIKKTS)
target_link_libraries(vulkan_engine PUBLIC mikktspace)
target_compile_definitions(vulkan_engine PUBLIC MIKKTS_ENABLE=1)

View File

@@ -14,10 +14,8 @@
//
//> includes
#include "vk_engine.h"
#include <core/vk_images.h>
#include "SDL2/SDL.h"
#include "SDL2/SDL_vulkan.h"
#include <core/vk_initializers.h>
#include <core/vk_types.h>
@@ -29,7 +27,6 @@
#include <span>
#include <array>
#include "render/vk_pipelines.h"
#include <iostream>
#include <glm/gtx/transform.hpp>
@@ -49,7 +46,6 @@
#include "vk_resource.h"
#include "engine_context.h"
#include "core/vk_pipeline_manager.h"
#include "core/config.h"
#include "core/texture_cache.h"
#include "core/ibl_manager.h"
@@ -540,6 +536,44 @@ namespace {
const DrawContext &dc = eng->_context->getMainDrawContext();
ImGui::Text("Opaque draws: %zu", dc.OpaqueSurfaces.size());
ImGui::Text("Transp draws: %zu", dc.TransparentSurfaces.size());
ImGui::Checkbox("Use ID-buffer picking", &eng->_useIdBufferPicking);
ImGui::Separator();
if (eng->_lastPick.valid)
{
const char *meshName = eng->_lastPick.mesh ? eng->_lastPick.mesh->name.c_str() : "<unknown>";
const char *sceneName = "<none>";
if (eng->_lastPick.scene && !eng->_lastPick.scene->debugName.empty())
{
sceneName = eng->_lastPick.scene->debugName.c_str();
}
ImGui::Text("Last pick scene: %s", sceneName);
ImGui::Text("Last pick mesh: %s (surface %u)", meshName, eng->_lastPick.surfaceIndex);
ImGui::Text("World pos: (%.3f, %.3f, %.3f)",
eng->_lastPick.worldPos.x,
eng->_lastPick.worldPos.y,
eng->_lastPick.worldPos.z);
ImGui::Text("Indices: first=%u count=%u",
eng->_lastPick.firstIndex,
eng->_lastPick.indexCount);
}
else
{
ImGui::TextUnformatted("Last pick: <none>");
}
ImGui::Separator();
if (eng->_hoverPick.valid)
{
const char *meshName = eng->_hoverPick.mesh ? eng->_hoverPick.mesh->name.c_str() : "<unknown>";
ImGui::Text("Hover mesh: %s (surface %u)", meshName, eng->_hoverPick.surfaceIndex);
}
else
{
ImGui::TextUnformatted("Hover: <none>");
}
if (!eng->_dragSelection.empty())
{
ImGui::Text("Drag selection: %zu objects", eng->_dragSelection.size());
}
}
} // namespace
@@ -708,7 +742,7 @@ void VulkanEngine::init()
auto imguiPass = std::make_unique<ImGuiPass>();
_renderPassManager->setImGuiPass(std::move(imguiPass));
const std::string structurePath = _assetManager->modelPath("sponza_2/scene.gltf");
const std::string structurePath = _assetManager->modelPath("mirage2000/scene.gltf");
const auto structureFile = _assetManager->loadGLTF(structurePath);
assert(structureFile.has_value());
@@ -857,6 +891,13 @@ void VulkanEngine::cleanup()
print_vma_stats(_deviceManager.get(), "after RTManager");
dump_vma_json(_deviceManager.get(), "after_RTManager");
// Destroy pick readback buffer before resource manager cleanup
if (_pickReadbackBuffer.buffer)
{
_resourceManager->destroy_buffer(_pickReadbackBuffer);
_pickReadbackBuffer = {};
}
_resourceManager->cleanup();
print_vma_stats(_deviceManager.get(), "after ResourceManager");
dump_vma_json(_deviceManager.get(), "after_ResourceManager");
@@ -884,11 +925,43 @@ void VulkanEngine::cleanup()
void VulkanEngine::draw()
{
_sceneManager->update_scene();
//> frame_clear
//wait until the gpu has finished rendering the last frame. Timeout of 1 second
VK_CHECK(vkWaitForFences(_deviceManager->device(), 1, &get_current_frame()._renderFence, true, 1000000000));
// If we scheduled an ID-buffer readback in the previous frame, resolve it now.
if (_pickResultPending && _pickReadbackBuffer.buffer && _sceneManager)
{
vmaInvalidateAllocation(_deviceManager->allocator(), _pickReadbackBuffer.allocation, 0, sizeof(uint32_t));
uint32_t pickedID = 0;
if (_pickReadbackBuffer.info.pMappedData)
{
pickedID = *reinterpret_cast<const uint32_t *>(_pickReadbackBuffer.info.pMappedData);
}
if (pickedID == 0)
{
// Do not override existing raycast pick when ID buffer reports "no object".
}
else
{
RenderObject picked{};
if (_sceneManager->resolveObjectID(pickedID, picked))
{
// Fallback hit position: object origin in world space (can refine later)
glm::vec3 fallbackPos = glm::vec3(picked.transform[3]);
_lastPick.mesh = picked.sourceMesh;
_lastPick.scene = picked.sourceScene;
_lastPick.worldPos = fallbackPos;
_lastPick.firstIndex = picked.firstIndex;
_lastPick.indexCount = picked.indexCount;
_lastPick.surfaceIndex = picked.surfaceIndex;
_lastPick.valid = true;
}
}
_pickResultPending = false;
}
get_current_frame()._deletionQueue.flush();
// Resolve last frame's pass timings before we clear and rebuild the graph
if (_renderGraph)
@@ -898,6 +971,29 @@ void VulkanEngine::draw()
get_current_frame()._frameDescriptors.clear_pools(_deviceManager->device());
//< frame_clear
_sceneManager->update_scene();
// Per-frame hover raycast based on last mouse position.
if (_sceneManager && _mousePosPixels.x >= 0.0f && _mousePosPixels.y >= 0.0f)
{
RenderObject hoverObj{};
glm::vec3 hoverPos{};
if (_sceneManager->pick(_mousePosPixels, hoverObj, hoverPos))
{
_hoverPick.mesh = hoverObj.sourceMesh;
_hoverPick.scene = hoverObj.sourceScene;
_hoverPick.worldPos = hoverPos;
_hoverPick.firstIndex = hoverObj.firstIndex;
_hoverPick.indexCount = hoverObj.indexCount;
_hoverPick.surfaceIndex = hoverObj.surfaceIndex;
_hoverPick.valid = true;
}
else
{
_hoverPick.valid = false;
}
}
uint32_t swapchainImageIndex;
VkResult e = vkAcquireNextImageKHR(_deviceManager->device(), _swapchainManager->swapchain(), 1000000000,
@@ -999,7 +1095,67 @@ void VulkanEngine::draw()
}
if (auto *geometry = _renderPassManager->getPass<GeometryPass>())
{
geometry->register_graph(_renderGraph.get(), hGBufferPosition, hGBufferNormal, hGBufferAlbedo, hDepth);
RGImageHandle hID = _renderGraph->import_id_buffer();
geometry->register_graph(_renderGraph.get(), hGBufferPosition, hGBufferNormal, hGBufferAlbedo, hID, hDepth);
// If ID-buffer picking is enabled and a pick was requested this frame,
// add a small transfer pass to read back 1 pixel from the ID buffer.
if (_useIdBufferPicking && _pendingPick.active && hID.valid() && _pickReadbackBuffer.buffer)
{
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));
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);
_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<int32_t>(_pendingPick.idCoords.x),
static_cast<int32_t>(_pendingPick.idCoords.y),
0 };
region.imageExtent = {1, 1, 1};
vkCmdCopyImageToBuffer(cmd,
idImage,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dst,
1,
&region);
});
_pickResultPending = true;
_pendingPick.active = false;
}
}
if (auto *lighting = _renderPassManager->getPass<LightingPass>())
{
@@ -1104,6 +1260,95 @@ void VulkanEngine::run()
freeze_rendering = false;
}
}
if (e.type == SDL_MOUSEMOTION)
{
_mousePosPixels = glm::vec2{static_cast<float>(e.motion.x),
static_cast<float>(e.motion.y)};
if (_dragState.buttonDown)
{
_dragState.current = _mousePosPixels;
// Consider any motion as dragging for now; can add threshold if desired.
_dragState.dragging = true;
}
}
if (e.type == SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_LEFT)
{
_dragState.buttonDown = true;
_dragState.dragging = false;
_dragState.start = glm::vec2{static_cast<float>(e.button.x),
static_cast<float>(e.button.y)};
_dragState.current = _dragState.start;
}
if (e.type == SDL_MOUSEBUTTONUP && e.button.button == SDL_BUTTON_LEFT)
{
glm::vec2 releasePos{static_cast<float>(e.button.x),
static_cast<float>(e.button.y)};
_dragState.buttonDown = false;
constexpr float clickThreshold = 3.0f;
glm::vec2 delta = releasePos - _dragState.start;
bool treatAsClick = !_dragState.dragging &&
std::abs(delta.x) < clickThreshold &&
std::abs(delta.y) < clickThreshold;
if (treatAsClick)
{
// Raycast click selection
if (_sceneManager)
{
RenderObject hitObject{};
glm::vec3 hitPos{};
if (_sceneManager->pick(releasePos, hitObject, hitPos))
{
_lastPick.mesh = hitObject.sourceMesh;
_lastPick.scene = hitObject.sourceScene;
_lastPick.worldPos = hitPos;
_lastPick.firstIndex = hitObject.firstIndex;
_lastPick.indexCount = hitObject.indexCount;
_lastPick.surfaceIndex = hitObject.surfaceIndex;
_lastPick.valid = true;
}
else
{
_lastPick.valid = false;
}
}
// Optionally queue an ID-buffer pick at this position
if (_useIdBufferPicking)
{
_pendingPick.active = true;
_pendingPick.windowPos = releasePos;
}
}
else
{
// Drag selection completed; compute selection based on screen-space rectangle.
_dragSelection.clear();
if (_sceneManager)
{
std::vector<RenderObject> selected;
_sceneManager->selectRect(_dragState.start, releasePos, selected);
_dragSelection.reserve(selected.size());
for (const RenderObject &obj : selected)
{
PickInfo info{};
info.mesh = obj.sourceMesh;
info.scene = obj.sourceScene;
// Use bounds origin transformed to world as a representative point.
glm::vec3 centerWorld = glm::vec3(obj.transform * glm::vec4(obj.bounds.origin, 1.0f));
info.worldPos = centerWorld;
info.firstIndex = obj.firstIndex;
info.indexCount = obj.indexCount;
info.surfaceIndex = obj.surfaceIndex;
info.valid = true;
_dragSelection.push_back(info);
}
}
}
_dragState.dragging = false;
}
_sceneManager->getMainCamera().processSDLEvent(e);
ImGui_ImplSDL2_ProcessEvent(&e);
}
@@ -1207,6 +1452,12 @@ void VulkanEngine::init_frame_resources()
{
_frames[i].init(_deviceManager.get(), frame_sizes);
}
// Allocate a small readback buffer for ID-buffer picking (single uint32 pixel)
_pickReadbackBuffer = _resourceManager->create_buffer(
sizeof(uint32_t),
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VMA_MEMORY_USAGE_CPU_TO_GPU);
}
void VulkanEngine::init_pipelines()
@@ -1218,8 +1469,16 @@ void MeshNode::Draw(const glm::mat4 &topMatrix, DrawContext &ctx)
{
glm::mat4 nodeMatrix = topMatrix * worldTransform;
for (auto &s: mesh->surfaces)
if (!mesh)
{
Node::Draw(topMatrix, ctx);
return;
}
for (uint32_t i = 0; i < mesh->surfaces.size(); ++i)
{
const auto &s = mesh->surfaces[i];
RenderObject def{};
def.indexCount = s.count;
def.firstIndex = s.startIndex;
@@ -1230,6 +1489,10 @@ void MeshNode::Draw(const glm::mat4 &topMatrix, DrawContext &ctx)
def.transform = nodeMatrix;
def.vertexBufferAddress = mesh->meshBuffers.vertexBufferAddress;
def.sourceMesh = mesh.get();
def.surfaceIndex = i;
def.objectID = ctx.nextID++;
def.sourceScene = scene;
if (s.material->data.passType == MaterialPass::Transparent)
{

View File

@@ -49,9 +49,11 @@ struct RenderPass
struct MeshNode : public Node
{
std::shared_ptr<MeshAsset> mesh;
std::shared_ptr<MeshAsset> mesh;
// Owning glTF scene (for picking/debug); may be null for non-gltf meshes.
LoadedGLTF *scene = nullptr;
virtual void Draw(const glm::mat4 &topMatrix, DrawContext &ctx) override;
virtual void Draw(const glm::mat4 &topMatrix, DrawContext &ctx) override;
};
class VulkanEngine
@@ -114,6 +116,42 @@ public:
// Debug helpers: track spawned IBL test meshes to remove them easily
std::vector<std::string> _iblTestNames;
struct PickInfo
{
MeshAsset *mesh = nullptr;
LoadedGLTF *scene = nullptr;
glm::vec3 worldPos{0.0f};
uint32_t indexCount = 0;
uint32_t firstIndex = 0;
uint32_t surfaceIndex = 0;
bool valid = false;
} _lastPick;
struct PickRequest
{
bool active = false;
glm::vec2 windowPos{0.0f};
glm::uvec2 idCoords{0, 0};
} _pendingPick;
bool _pickResultPending = false;
AllocatedBuffer _pickReadbackBuffer{};
// Hover and drag-selection state (raycast-based)
PickInfo _hoverPick{};
glm::vec2 _mousePosPixels{-1.0f, -1.0f};
struct DragState
{
bool dragging = false;
bool buttonDown = false;
glm::vec2 start{0.0f};
glm::vec2 current{0.0f};
} _dragState;
// Optional list of last drag-selected objects (for future editing UI)
std::vector<PickInfo> _dragSelection;
// Toggle to enable/disable ID-buffer picking in addition to raycast
bool _useIdBufferPicking = false;
// Debug: persistent pass enable overrides (by pass name)
std::unordered_map<std::string, bool> _rgPassToggles;

View File

@@ -70,6 +70,10 @@ void SwapchainManager::init_swapchain()
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT);
_gBufferAlbedo = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT);
_idBuffer = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R32_UINT,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT);
_deletionQueue.push_function([=]() {
vkDestroyImageView(_deviceManager->device(), _drawImage.imageView, nullptr);
@@ -81,6 +85,7 @@ void SwapchainManager::init_swapchain()
_resourceManager->destroy_image(_gBufferPosition);
_resourceManager->destroy_image(_gBufferNormal);
_resourceManager->destroy_image(_gBufferAlbedo);
_resourceManager->destroy_image(_idBuffer);
});
};
@@ -191,6 +196,10 @@ void SwapchainManager::resize_swapchain(struct SDL_Window *window)
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT);
_gBufferAlbedo = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT);
_idBuffer = _resourceManager->create_image(drawImageExtent, VK_FORMAT_R32_UINT,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT);
_deletionQueue.push_function([=]() {
vkDestroyImageView(_deviceManager->device(), _drawImage.imageView, nullptr);
@@ -202,6 +211,7 @@ void SwapchainManager::resize_swapchain(struct SDL_Window *window)
_resourceManager->destroy_image(_gBufferPosition);
_resourceManager->destroy_image(_gBufferNormal);
_resourceManager->destroy_image(_gBufferAlbedo);
_resourceManager->destroy_image(_idBuffer);
});
resize_requested = false;

View File

@@ -29,6 +29,7 @@ public:
AllocatedImage gBufferPosition() const { return _gBufferPosition; }
AllocatedImage gBufferNormal() const { return _gBufferNormal; }
AllocatedImage gBufferAlbedo() const { return _gBufferAlbedo; }
AllocatedImage idBuffer() const { return _idBuffer; }
VkExtent2D windowExtent() const { return _windowExtent; }
bool resize_requested{false};
@@ -50,6 +51,7 @@ private:
AllocatedImage _gBufferPosition = {};
AllocatedImage _gBufferNormal = {};
AllocatedImage _gBufferAlbedo = {};
AllocatedImage _idBuffer = {};
DeletionQueue _deletionQueue;
};

View File

@@ -129,6 +129,7 @@ struct GPUMeshBuffers {
struct GPUDrawPushConstants {
glm::mat4 worldMatrix;
VkDeviceAddress vertexBuffer;
uint32_t objectID;
};
struct DrawContext;

View File

@@ -947,6 +947,18 @@ RGImageHandle RenderGraph::import_gbuffer_albedo()
return import_image(d);
}
RGImageHandle RenderGraph::import_id_buffer()
{
RGImportedImageDesc d{};
d.name = "idBuffer.objectID";
d.image = _context->getSwapchain()->idBuffer().image;
d.imageView = _context->getSwapchain()->idBuffer().imageView;
d.format = _context->getSwapchain()->idBuffer().imageFormat;
d.extent = _context->getDrawExtent();
d.currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
return import_image(d);
}
RGImageHandle RenderGraph::import_swapchain_image(uint32_t index)
{
RGImportedImageDesc d{};

View File

@@ -56,6 +56,7 @@ struct Pass; // fwd
RGImageHandle import_gbuffer_position();
RGImageHandle import_gbuffer_normal();
RGImageHandle import_gbuffer_albedo();
RGImageHandle import_id_buffer();
RGImageHandle import_swapchain_image(uint32_t index);
void add_present_chain(RGImageHandle sourceDraw,
RGImageHandle targetSwapchain,

View File

@@ -13,7 +13,7 @@ void GLTFMetallic_Roughness::build_pipelines(VulkanEngine *engine)
VkPushConstantRange matrixRange{};
matrixRange.offset = 0;
matrixRange.size = sizeof(GPUDrawPushConstants);
matrixRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
matrixRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
DescriptorLayoutBuilder layoutBuilder;
layoutBuilder.add_binding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
@@ -92,9 +92,10 @@ void GLTFMetallic_Roughness::build_pipelines(VulkanEngine *engine)
VkFormat gFormats[] = {
engine->_swapchainManager->gBufferPosition().imageFormat,
engine->_swapchainManager->gBufferNormal().imageFormat,
engine->_swapchainManager->gBufferAlbedo().imageFormat
engine->_swapchainManager->gBufferAlbedo().imageFormat,
engine->_swapchainManager->idBuffer().imageFormat
};
b.set_color_attachment_formats(std::span<VkFormat>(gFormats, 3));
b.set_color_attachment_formats(std::span<VkFormat>(gFormats, 4));
b.set_depth_format(engine->_swapchainManager->depthImage().imageFormat);
};
engine->_pipelineManager->registerGraphics("mesh.gbuffer", gbufferInfo);

View File

@@ -69,9 +69,11 @@ void GeometryPass::register_graph(RenderGraph *graph,
RGImageHandle gbufferPosition,
RGImageHandle gbufferNormal,
RGImageHandle gbufferAlbedo,
RGImageHandle idHandle,
RGImageHandle depthHandle)
{
if (!graph || !gbufferPosition.valid() || !gbufferNormal.valid() || !gbufferAlbedo.valid() || !depthHandle.valid())
if (!graph || !gbufferPosition.valid() || !gbufferNormal.valid() || !gbufferAlbedo.valid() ||
!idHandle.valid() || !depthHandle.valid())
{
return;
}
@@ -79,7 +81,7 @@ void GeometryPass::register_graph(RenderGraph *graph,
graph->add_pass(
"Geometry",
RGPassType::Graphics,
[gbufferPosition, gbufferNormal, gbufferAlbedo, depthHandle](RGPassBuilder &builder, EngineContext *ctx)
[gbufferPosition, gbufferNormal, gbufferAlbedo, idHandle, depthHandle](RGPassBuilder &builder, EngineContext *ctx)
{
VkClearValue clear{};
clear.color = {{0.f, 0.f, 0.f, 0.f}};
@@ -87,6 +89,9 @@ void GeometryPass::register_graph(RenderGraph *graph,
builder.write_color(gbufferPosition, true, clear);
builder.write_color(gbufferNormal, true, clear);
builder.write_color(gbufferAlbedo, true, clear);
VkClearValue clearID{};
clearID.color.uint32[0] = 0u;
builder.write_color(idHandle, true, clearID);
// Reverse-Z: clear depth to 0.0
VkClearValue depthClear{};
@@ -118,11 +123,11 @@ void GeometryPass::register_graph(RenderGraph *graph,
builder.read_buffer(b, RGBufferUsage::StorageRead, 0, "geom.vertex");
}
},
[this, gbufferPosition, gbufferNormal, gbufferAlbedo, depthHandle](VkCommandBuffer cmd,
const RGPassResources &res,
EngineContext *ctx)
[this, gbufferPosition, gbufferNormal, gbufferAlbedo, idHandle, depthHandle](VkCommandBuffer cmd,
const RGPassResources &res,
EngineContext *ctx)
{
draw_geometry(cmd, ctx, res, gbufferPosition, gbufferNormal, gbufferAlbedo, depthHandle);
draw_geometry(cmd, ctx, res, gbufferPosition, gbufferNormal, gbufferAlbedo, idHandle, depthHandle);
});
}
@@ -132,6 +137,7 @@ void GeometryPass::draw_geometry(VkCommandBuffer cmd,
RGImageHandle gbufferPosition,
RGImageHandle gbufferNormal,
RGImageHandle gbufferAlbedo,
RGImageHandle /*idHandle*/,
RGImageHandle depthHandle) const
{
EngineContext *ctxLocal = context ? context : _context;
@@ -270,6 +276,7 @@ void GeometryPass::draw_geometry(VkCommandBuffer cmd,
GPUDrawPushConstants push_constants{};
push_constants.worldMatrix = r.transform;
push_constants.vertexBuffer = r.vertexBufferAddress;
push_constants.objectID = r.objectID;
vkCmdPushConstants(cmd, r.material->pipeline->layout, VK_SHADER_STAGE_VERTEX_BIT, 0,
sizeof(GPUDrawPushConstants), &push_constants);

View File

@@ -18,6 +18,7 @@ public:
RGImageHandle gbufferPosition,
RGImageHandle gbufferNormal,
RGImageHandle gbufferAlbedo,
RGImageHandle idHandle,
RGImageHandle depthHandle);
private:
@@ -29,5 +30,6 @@ private:
RGImageHandle gbufferPosition,
RGImageHandle gbufferNormal,
RGImageHandle gbufferAlbedo,
RGImageHandle idHandle,
RGImageHandle depthHandle) const;
};

View File

@@ -29,7 +29,7 @@ void ShadowPass::init(EngineContext *context)
// Keep push constants matching current shader layout for now
VkPushConstantRange pc{};
pc.offset = 0;
// Push constants layout in shadow.vert is mat4 + device address + uint, rounded to 16 bytes
// Push constants layout in shadow.vert is GPUDrawPushConstants + cascade index, rounded to 16 bytes
const uint32_t pcRaw = static_cast<uint32_t>(sizeof(GPUDrawPushConstants) + sizeof(uint32_t));
const uint32_t pcAligned = (pcRaw + 15u) & ~15u; // 16-byte alignment to match std430 expectations
pc.size = pcAligned;
@@ -197,6 +197,7 @@ void ShadowPass::draw_shadow(VkCommandBuffer cmd,
ShadowPC spc{};
spc.draw.worldMatrix = r.transform;
spc.draw.vertexBuffer = r.vertexBufferAddress;
spc.draw.objectID = r.objectID;
spc.cascadeIndex = cascadeIndex;
vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(ShadowPC), &spc);
vkCmdDrawIndexed(cmd, r.indexCount, 1, r.firstIndex, 0, 0);

View File

@@ -196,6 +196,7 @@ void TransparentPass::draw_transparent(VkCommandBuffer cmd,
GPUDrawPushConstants push{};
push.worldMatrix = r.transform;
push.vertexBuffer = r.vertexBufferAddress;
push.objectID = r.objectID;
vkCmdPushConstants(cmd, r.material->pipeline->layout, VK_SHADER_STAGE_VERTEX_BIT, 0,
sizeof(GPUDrawPushConstants), &push);
vkCmdDrawIndexed(cmd, r.indexCount, 1, r.firstIndex, 0, 0);

View File

@@ -574,17 +574,31 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
newSurface.material = materials[0];
}
glm::vec3 minpos = vertices[initial_vtx].position;
glm::vec3 maxpos = vertices[initial_vtx].position;
for (int i = initial_vtx; i < vertices.size(); i++)
// Compute per-surface bounds using only the indices referenced by this primitive.
if (newSurface.count > 0)
{
minpos = glm::min(minpos, vertices[i].position);
maxpos = glm::max(maxpos, vertices[i].position);
uint32_t firstIndex = newSurface.startIndex;
uint32_t lastIndex = newSurface.startIndex + newSurface.count;
uint32_t baseVertex = indices[firstIndex];
glm::vec3 minpos = vertices[baseVertex].position;
glm::vec3 maxpos = vertices[baseVertex].position;
for (uint32_t i = firstIndex + 1; i < lastIndex; i++)
{
uint32_t vi = indices[i];
const glm::vec3 &p = vertices[vi].position;
minpos = glm::min(minpos, p);
maxpos = glm::max(maxpos, p);
}
newSurface.bounds.origin = (maxpos + minpos) / 2.f;
newSurface.bounds.extents = (maxpos - minpos) / 2.f;
newSurface.bounds.sphereRadius = glm::length(newSurface.bounds.extents);
}
else
{
newSurface.bounds.origin = glm::vec3(0.0f);
newSurface.bounds.extents = glm::vec3(0.5f);
newSurface.bounds.sphereRadius = glm::length(newSurface.bounds.extents);
}
newSurface.bounds.origin = (maxpos + minpos) / 2.f;
newSurface.bounds.extents = (maxpos - minpos) / 2.f;
newSurface.bounds.sphereRadius = glm::length(newSurface.bounds.extents);
newmesh->surfaces.push_back(newSurface);
}
@@ -616,8 +630,10 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
// find if the node has a mesh, and if it does hook it to the mesh pointer and allocate it with the meshnode class
if (node.meshIndex.has_value())
{
newNode = std::make_shared<MeshNode>();
static_cast<MeshNode *>(newNode.get())->mesh = meshes[*node.meshIndex];
auto meshNode = std::make_shared<MeshNode>();
meshNode->mesh = meshes[*node.meshIndex];
meshNode->scene = &file;
newNode = meshNode;
}
else
{

View File

@@ -84,6 +84,9 @@ struct LoadedGLTF : public IRenderable
float animationTime = 0.f;
bool animationLoop = true;
// Optional debug name (e.g., key used when loaded into SceneManager)
std::string debugName;
// Animation helpers
void updateAnimation(float dt);
void refreshAllTransforms();

View File

@@ -9,12 +9,187 @@
#include "core/config.h"
#include "glm/gtx/transform.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include "glm/gtx/norm.inl"
#include "glm/gtx/compatibility.hpp"
#include <algorithm>
#include <limits>
#include <cmath>
#include "core/config.h"
namespace
{
// Quick conservative ray / bounding-sphere test in world space.
// Returns false when the ray misses the sphere; on hit, outT is the
// closest positive intersection distance along the ray direction.
bool intersect_ray_sphere(const glm::vec3 &rayOrigin,
const glm::vec3 &rayDir,
const Bounds &bounds,
const glm::mat4 &worldTransform,
float &outT)
{
// Sphere center is bounds.origin transformed to world.
glm::vec3 centerWorld = glm::vec3(worldTransform * glm::vec4(bounds.origin, 1.0f));
// Approximate world-space radius by scaling with the maximum axis scale.
glm::vec3 sx = glm::vec3(worldTransform[0]);
glm::vec3 sy = glm::vec3(worldTransform[1]);
glm::vec3 sz = glm::vec3(worldTransform[2]);
float maxScale = std::max({glm::length(sx), glm::length(sy), glm::length(sz)});
float radiusWorld = bounds.sphereRadius * maxScale;
if (radiusWorld <= 0.0f)
{
return false;
}
glm::vec3 oc = rayOrigin - centerWorld;
float b = glm::dot(oc, rayDir);
float c = glm::dot(oc, oc) - radiusWorld * radiusWorld;
float disc = b * b - c;
if (disc < 0.0f)
{
return false;
}
float s = std::sqrt(disc);
float t0 = -b - s;
float t1 = -b + s;
float t = t0 >= 0.0f ? t0 : t1;
if (t < 0.0f)
{
return false;
}
outT = t;
return true;
}
// Ray / oriented-bounds intersection in world space using object-local AABB.
// Uses a quick sphere test first; on success refines with OBB slabs.
// Returns true when hit; outWorldHit is the closest hit point in world space.
bool intersect_ray_bounds(const glm::vec3 &rayOrigin,
const glm::vec3 &rayDir,
const Bounds &bounds,
const glm::mat4 &worldTransform,
glm::vec3 &outWorldHit)
{
if (glm::length2(rayDir) < 1e-8f)
{
return false;
}
// Early reject using bounding sphere in world space.
float sphereT = 0.0f;
if (!intersect_ray_sphere(rayOrigin, rayDir, bounds, worldTransform, sphereT))
{
return false;
}
// Transform ray into local space of the bounds for precise box test.
glm::mat4 invM = glm::inverse(worldTransform);
glm::vec3 localOrigin = glm::vec3(invM * glm::vec4(rayOrigin, 1.0f));
glm::vec3 localDir = glm::vec3(invM * glm::vec4(rayDir, 0.0f));
if (glm::length2(localDir) < 1e-8f)
{
return false;
}
localDir = glm::normalize(localDir);
glm::vec3 minB = bounds.origin - bounds.extents;
glm::vec3 maxB = bounds.origin + bounds.extents;
float tMin = 0.0f;
float tMax = std::numeric_limits<float>::max();
for (int axis = 0; axis < 3; ++axis)
{
float o = localOrigin[axis];
float d = localDir[axis];
if (std::abs(d) < 1e-8f)
{
// Ray parallel to slab: must be inside to intersect.
if (o < minB[axis] || o > maxB[axis])
{
return false;
}
}
else
{
float invD = 1.0f / d;
float t1 = (minB[axis] - o) * invD;
float t2 = (maxB[axis] - o) * invD;
if (t1 > t2)
{
std::swap(t1, t2);
}
tMin = std::max(tMin, t1);
tMax = std::min(tMax, t2);
if (tMax < tMin)
{
return false;
}
}
}
if (tMax < 0.0f)
{
return false;
}
float tHit = (tMin >= 0.0f) ? tMin : tMax;
glm::vec3 localHit = localOrigin + tHit * localDir;
glm::vec3 worldHit = glm::vec3(worldTransform * glm::vec4(localHit, 1.0f));
if (glm::dot(worldHit - rayOrigin, rayDir) <= 0.0f)
{
return false;
}
outWorldHit = worldHit;
return true;
}
// Test whether the clip-space box corners of an object intersect a 2D NDC rectangle.
// ndcMin/ndcMax are in [-1,1]x[-1,1]. Returns true if any visible corner projects inside.
bool box_overlaps_ndc_rect(const RenderObject &obj,
const glm::mat4 &viewproj,
const glm::vec2 &ndcMin,
const glm::vec2 &ndcMax)
{
const glm::vec3 o = obj.bounds.origin;
const glm::vec3 e = obj.bounds.extents;
const glm::mat4 m = viewproj * obj.transform; // world -> clip
const std::array<glm::vec3, 8> corners{
glm::vec3{+1, +1, +1}, glm::vec3{+1, +1, -1}, glm::vec3{+1, -1, +1}, glm::vec3{+1, -1, -1},
glm::vec3{-1, +1, +1}, glm::vec3{-1, +1, -1}, glm::vec3{-1, -1, +1}, glm::vec3{-1, -1, -1},
};
for (const glm::vec3 &c : corners)
{
glm::vec3 pLocal = o + c * e;
glm::vec4 clip = m * glm::vec4(pLocal, 1.f);
if (clip.w <= 0.0f)
{
continue;
}
float x = clip.x / clip.w;
float y = clip.y / clip.w;
float z = clip.z / clip.w; // Vulkan Z0: 0..1
if (z < 0.0f || z > 1.0f)
{
continue;
}
if (x >= ndcMin.x && x <= ndcMax.x &&
y >= ndcMin.y && y <= ndcMax.y)
{
return true;
}
}
return false;
}
} // namespace
void SceneManager::init(EngineContext *context)
{
_context = context;
@@ -35,6 +210,7 @@ void SceneManager::update_scene()
mainDrawContext.OpaqueSurfaces.clear();
mainDrawContext.TransparentSurfaces.clear();
mainDrawContext.nextID = 1;
mainCamera.update();
@@ -102,6 +278,7 @@ void SceneManager::update_scene()
{
const MeshInstance &inst = kv.second;
if (!inst.mesh || inst.mesh->surfaces.empty()) continue;
uint32_t surfaceIndex = 0;
for (const auto &surf: inst.mesh->surfaces)
{
RenderObject obj{};
@@ -113,6 +290,9 @@ void SceneManager::update_scene()
obj.material = &surf.material->data;
obj.bounds = surf.bounds;
obj.transform = inst.transform;
obj.sourceMesh = inst.mesh.get();
obj.surfaceIndex = surfaceIndex++;
obj.objectID = mainDrawContext.nextID++;
if (obj.material->passType == MaterialPass::Transparent)
{
mainDrawContext.TransparentSurfaces.push_back(obj);
@@ -229,8 +409,165 @@ void SceneManager::update_scene()
stats.scene_update_time = elapsed.count() / 1000.f;
}
bool SceneManager::pick(const glm::vec2 &mousePosPixels, RenderObject &outObject, glm::vec3 &outWorldPos)
{
if (_context == nullptr)
{
return false;
}
SwapchainManager *swapchain = _context->getSwapchain();
if (swapchain == nullptr)
{
return false;
}
VkExtent2D extent = swapchain->windowExtent();
if (extent.width == 0 || extent.height == 0)
{
return false;
}
float width = static_cast<float>(extent.width);
float height = static_cast<float>(extent.height);
// 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);
float fovRad = glm::radians(mainCamera.fovDegrees);
float tanHalfFov = std::tan(fovRad * 0.5f);
float aspect = width / height;
// Build ray in camera space using -Z forward convention.
glm::vec3 dirCamera(ndcX * aspect * tanHalfFov,
ndcY * tanHalfFov,
-1.0f);
dirCamera = glm::normalize(dirCamera);
glm::vec3 rayOrigin = mainCamera.position;
glm::mat4 camRotation = mainCamera.getRotationMatrix();
glm::vec3 rayDir = glm::normalize(glm::vec3(camRotation * glm::vec4(dirCamera, 0.0f)));
bool anyHit = false;
float bestDist2 = std::numeric_limits<float>::max();
glm::vec3 bestHitPos{};
auto testList = [&](const std::vector<RenderObject> &list)
{
for (const RenderObject &obj: list)
{
glm::vec3 hitPos{};
if (!intersect_ray_bounds(rayOrigin, rayDir, obj.bounds, obj.transform, hitPos))
{
continue;
}
float d2 = glm::length2(hitPos - rayOrigin);
if (d2 < bestDist2)
{
bestDist2 = d2;
bestHitPos = hitPos;
outObject = obj;
anyHit = true;
}
}
};
testList(mainDrawContext.OpaqueSurfaces);
testList(mainDrawContext.TransparentSurfaces);
if (anyHit)
{
outWorldPos = bestHitPos;
}
return anyHit;
}
bool SceneManager::resolveObjectID(uint32_t id, RenderObject &outObject) const
{
if (id == 0)
{
return false;
}
auto findIn = [&](const std::vector<RenderObject> &list) -> bool
{
for (const RenderObject &obj : list)
{
if (obj.objectID == id)
{
outObject = obj;
return true;
}
}
return false;
};
if (findIn(mainDrawContext.OpaqueSurfaces))
{
return true;
}
if (findIn(mainDrawContext.TransparentSurfaces))
{
return true;
}
return false;
}
void SceneManager::selectRect(const glm::vec2 &p0, const glm::vec2 &p1, std::vector<RenderObject> &outObjects) const
{
if (!_context || !_context->getSwapchain())
{
return;
}
VkExtent2D extent = _context->getSwapchain()->windowExtent();
if (extent.width == 0 || extent.height == 0)
{
return;
}
float width = static_cast<float>(extent.width);
float height = static_cast<float>(extent.height);
// Convert from window 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;
float ndcY = 1.0f - (2.0f * p.y / height);
return glm::vec2{ndcX, ndcY};
};
glm::vec2 ndc0 = toNdc(p0);
glm::vec2 ndc1 = toNdc(p1);
glm::vec2 ndcMin = glm::min(ndc0, ndc1);
glm::vec2 ndcMax = glm::max(ndc0, ndc1);
const glm::mat4 vp = sceneData.viewproj;
auto testList = [&](const std::vector<RenderObject> &list)
{
for (const RenderObject &obj : list)
{
if (box_overlaps_ndc_rect(obj, vp, ndcMin, ndcMax))
{
outObjects.push_back(obj);
}
}
};
testList(mainDrawContext.OpaqueSurfaces);
testList(mainDrawContext.TransparentSurfaces);
}
void SceneManager::loadScene(const std::string &name, std::shared_ptr<LoadedGLTF> scene)
{
if (scene)
{
scene->debugName = name;
}
loadedScenes[name] = std::move(scene);
}

View File

@@ -3,12 +3,14 @@
#include <scene/camera.h>
#include <unordered_map>
#include <memory>
#include <glm/vec2.hpp>
#include "scene/vk_loader.h"
class EngineContext;
struct RenderObject
{
// Geometry and material binding
uint32_t indexCount;
uint32_t firstIndex;
VkBuffer indexBuffer;
@@ -19,12 +21,22 @@ struct RenderObject
glm::mat4 transform;
VkDeviceAddress vertexBufferAddress;
// Optional debug/source information (may be null/unused for some objects).
MeshAsset *sourceMesh = nullptr;
uint32_t surfaceIndex = 0;
// Unique per-draw identifier for ID-buffer picking (0 = none).
uint32_t objectID = 0;
// Optional owning glTF scene for this draw (null for procedural/dynamic meshes).
LoadedGLTF *sourceScene = nullptr;
};
struct DrawContext
{
std::vector<RenderObject> OpaqueSurfaces;
std::vector<RenderObject> TransparentSurfaces;
// Monotonic counter used to assign stable per-frame object IDs.
uint32_t nextID = 1;
};
class SceneManager
@@ -37,6 +49,20 @@ public:
void update_scene();
Camera &getMainCamera() { return mainCamera; }
// Ray-pick against current DrawContext using per-surface Bounds.
// mousePosPixels is in window coordinates (SDL), origin at top-left.
// Returns true if any object was hit, filling outObject and outWorldPos.
bool pick(const glm::vec2 &mousePosPixels, RenderObject &outObject, glm::vec3 &outWorldPos);
// Resolve an object ID (from ID buffer) back to the RenderObject for
// the most recently built DrawContext. Returns false if not found or id==0.
bool resolveObjectID(uint32_t id, RenderObject &outObject) const;
// Select all objects whose projected bounds intersect the given screen-space
// rectangle (window coordinates, origin top-left). Results are appended to outObjects.
void selectRect(const glm::vec2 &p0, const glm::vec2 &p1, std::vector<RenderObject> &outObjects) const;
const GPUSceneData &getSceneData() const { return sceneData; }
DrawContext &getMainDrawContext() { return mainDrawContext; }