diff --git a/conanfile.txt b/conanfile.txt deleted file mode 100644 index aed0f41..0000000 --- a/conanfile.txt +++ /dev/null @@ -1,7 +0,0 @@ -[requires] -taskflow/3.10.0 -[generators] -CMakeDeps -CMakeToolchain -[layout] -cmake_layout \ No newline at end of file diff --git a/shaders/background_env.frag b/shaders/background_env.frag index 885c4ec..fc0e2b3 100644 --- a/shaders/background_env.frag +++ b/shaders/background_env.frag @@ -16,6 +16,8 @@ void main() vec3 worldDir = normalize((inverse(sceneData.view) * vec4(viewDir, 0.0)).xyz); vec2 uv = dir_to_equirect(worldDir); - vec3 col = textureLod(iblSpec2D, uv, 0.0).rgb; + // Sample a dedicated background environment map when available. + // The engine binds iblBackground2D to a texture that may differ from the IBL specular map. + vec3 col = textureLod(iblBackground2D, uv, 0.0).rgb; outColor = vec4(col, 1.0); } diff --git a/shaders/ibl_common.glsl b/shaders/ibl_common.glsl index 23873b8..452e3bd 100644 --- a/shaders/ibl_common.glsl +++ b/shaders/ibl_common.glsl @@ -1,10 +1,11 @@ #ifndef IBL_COMMON_GLSL #define IBL_COMMON_GLSL -// IBL bindings (set=3): specular equirect 2D, BRDF LUT, SH UBO. +// IBL bindings (set=3): specular equirect 2D, BRDF LUT, SH UBO, optional background map. layout(set=3, binding=0) uniform sampler2D iblSpec2D; layout(set=3, binding=1) uniform sampler2D iblBRDF; layout(std140, set=3, binding=2) uniform IBL_SH { vec4 sh[9]; } iblSH; +layout(set=3, binding=3) uniform sampler2D iblBackground2D; // Evaluate diffuse irradiance from 2nd-order SH coefficients (9 coeffs). // Coefficients are pre-convolved with the Lambert kernel on the CPU. diff --git a/src/core/assets/ibl_manager.cpp b/src/core/assets/ibl_manager.cpp index d707112..1965139 100644 --- a/src/core/assets/ibl_manager.cpp +++ b/src/core/assets/ibl_manager.cpp @@ -10,14 +10,29 @@ #include #include "core/device/device.h" +#include "core/assets/texture_cache.h" bool IBLManager::load(const IBLPaths &paths) { if (_ctx == nullptr || _ctx->getResources() == nullptr) return false; - ensureLayout(); ResourceManager *rm = _ctx->getResources(); - // Load specular environment: prefer cubemap; fallback to 2D equirect with mips + // When uploads are deferred into the RenderGraph, any previously queued + // image uploads might still reference VkImage handles owned by this + // manager. Before destroying or recreating IBL images, flush those + // uploads via the immediate path so we never record barriers or copies + // for images that have been destroyed. + if (rm->deferred_uploads() && rm->has_pending_uploads()) + { + rm->process_queued_uploads_immediate(); + } + + // Allow reloading at runtime: destroy previous images/SH but keep layout. + destroy_images_and_sh(); + ensureLayout(); + + // Load specular environment: prefer cubemap; fallback to 2D equirect with mips. + // Also hint the TextureCache (if present) so future switches are cheap. if (!paths.specularCube.empty()) { // Try as cubemap first @@ -222,6 +237,12 @@ bool IBLManager::load(const IBLPaths &paths) _diff = _spec; } + // If background is still missing but specular is valid, reuse the specular environment. + if (_background.image == VK_NULL_HANDLE && _spec.image != VK_NULL_HANDLE) + { + _background = _spec; + } + // BRDF LUT if (!paths.brdfLut2D.empty()) { @@ -251,34 +272,16 @@ bool IBLManager::load(const IBLPaths &paths) void IBLManager::unload() { if (_ctx == nullptr || _ctx->getResources() == nullptr) return; - auto *rm = _ctx->getResources(); - if (_spec.image) - { - rm->destroy_image(_spec); - } - // Handle potential aliasing: _diff may have been set to _spec in load(). - if (_diff.image && _diff.image != _spec.image) - { - rm->destroy_image(_diff); - } - if (_brdf.image) - { - rm->destroy_image(_brdf); - } - _spec = {}; - _diff = {}; - _brdf = {}; + // Destroy images and SH buffer first. + destroy_images_and_sh(); + + // Then release descriptor layout. if (_iblSetLayout && _ctx && _ctx->getDevice()) { vkDestroyDescriptorSetLayout(_ctx->getDevice()->device(), _iblSetLayout, nullptr); _iblSetLayout = VK_NULL_HANDLE; } - if (_shBuffer.buffer) - { - rm->destroy_buffer(_shBuffer); - _shBuffer = {}; - } } bool IBLManager::ensureLayout() @@ -293,8 +296,48 @@ bool IBLManager::ensureLayout() builder.add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); // binding 2: SH coefficients UBO (vec4[9]) builder.add_binding(2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); + // binding 3: optional background environment texture (2D equirect) + builder.add_binding(3, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); _iblSetLayout = builder.build( _ctx->getDevice()->device(), VK_SHADER_STAGE_FRAGMENT_BIT, nullptr, VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); return _iblSetLayout != VK_NULL_HANDLE; } + +void IBLManager::destroy_images_and_sh() +{ + if (_ctx == nullptr || _ctx->getResources() == nullptr) return; + auto *rm = _ctx->getResources(); + + if (_spec.image) + { + rm->destroy_image(_spec); + } + // Handle potential aliasing: _diff may have been set to _spec in load(). + if (_diff.image && _diff.image != _spec.image) + { + rm->destroy_image(_diff); + } + // _background may alias _spec or _diff; only destroy when unique. + if (_background.image && + _background.image != _spec.image && + _background.image != _diff.image) + { + rm->destroy_image(_background); + } + if (_brdf.image) + { + rm->destroy_image(_brdf); + } + + if (_shBuffer.buffer) + { + rm->destroy_buffer(_shBuffer); + _shBuffer = {}; + } + + _spec = {}; + _diff = {}; + _background = {}; + _brdf = {}; +} diff --git a/src/core/assets/ibl_manager.h b/src/core/assets/ibl_manager.h index 383a44f..0db490c 100644 --- a/src/core/assets/ibl_manager.h +++ b/src/core/assets/ibl_manager.h @@ -3,6 +3,8 @@ #include #include +class TextureCache; + class EngineContext; struct IBLPaths @@ -10,6 +12,9 @@ struct IBLPaths std::string specularCube; // .ktx2 (GPU-ready BC6H or R16G16B16A16) std::string diffuseCube; // .ktx2 std::string brdfLut2D; // .ktx2 (BC5 RG UNORM or similar) + // Optional separate background environment map (2D equirect .ktx2). + // When empty, the IBL system falls back to using specularCube for the background. + std::string background2D; }; class IBLManager @@ -17,6 +22,8 @@ class IBLManager public: void init(EngineContext *ctx) { _ctx = ctx; } + void set_texture_cache(TextureCache *cache) { _cache = cache; } + // Load all three textures. Returns true when specular+diffuse (and optional LUT) are resident. bool load(const IBLPaths &paths); @@ -28,6 +35,9 @@ public: AllocatedImage specular() const { return _spec; } AllocatedImage diffuse() const { return _diff; } AllocatedImage brdf() const { return _brdf; } + // Background environment texture used by the background pass. + // May alias specular() when a dedicated background is not provided. + AllocatedImage background() const { return _background; } AllocatedBuffer shBuffer() const { return _shBuffer; } bool hasSH() const { return _shBuffer.buffer != VK_NULL_HANDLE; } @@ -39,9 +49,14 @@ public: private: EngineContext *_ctx{nullptr}; + TextureCache *_cache{nullptr}; AllocatedImage _spec{}; AllocatedImage _diff{}; AllocatedImage _brdf{}; + AllocatedImage _background{}; VkDescriptorSetLayout _iblSetLayout = VK_NULL_HANDLE; AllocatedBuffer _shBuffer{}; // 9*vec4 coefficients (RGB in .xyz) + + // Destroy current GPU images/SH buffer but keep descriptor layout alive. + void destroy_images_and_sh(); }; diff --git a/src/core/engine.cpp b/src/core/engine.cpp index b03986f..58918d5 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -28,6 +28,7 @@ #include #include +#include #include #include "config.h" @@ -229,6 +230,10 @@ void VulkanEngine::init() // Create IBL manager early so set=3 layout exists before pipelines are built _iblManager = std::make_unique(); _iblManager->init(_context.get()); + if (_textureCache) + { + _iblManager->set_texture_cache(_textureCache.get()); + } // Publish to context for passes and pipeline layout assembly _context->ibl = _iblManager.get(); @@ -238,6 +243,13 @@ void VulkanEngine::init() ibl.specularCube = _assetManager->assetPath("ibl/docklands.ktx2"); ibl.diffuseCube = _assetManager->assetPath("ibl/docklands.ktx2"); // temporary: reuse if separate diffuse not provided ibl.brdfLut2D = _assetManager->assetPath("ibl/brdf_lut.ktx2"); + // By default, use the same texture for lighting and background; users can point background2D + // at a different .ktx2 to decouple them. + ibl.background2D = ibl.specularCube; + // Treat this as the global/fallback IBL used outside any local volume. + _globalIBLPaths = ibl; + _hasGlobalIBL = true; + _activeIBLVolume = -1; _iblManager->load(ibl); } @@ -469,6 +481,44 @@ void VulkanEngine::draw() { _sceneManager->update_scene(); + // Update IBL based on camera position and user-defined reflection volumes. + if (_iblManager && _sceneManager) + { + glm::vec3 camPos = _sceneManager->getMainCamera().position; + int newVolume = -1; + for (size_t i = 0; i < _iblVolumes.size(); ++i) + { + const IBLVolume &v = _iblVolumes[i]; + if (!v.enabled) continue; + glm::vec3 local = camPos - v.center; + if (std::abs(local.x) <= v.halfExtents.x && + std::abs(local.y) <= v.halfExtents.y && + std::abs(local.z) <= v.halfExtents.z) + { + newVolume = static_cast(i); + break; + } + } + if (newVolume != _activeIBLVolume) + { + const IBLPaths *paths = nullptr; + if (newVolume >= 0) + { + paths = &_iblVolumes[newVolume].paths; + } + else if (_hasGlobalIBL) + { + paths = &_globalIBLPaths; + } + + if (paths) + { + _iblManager->load(*paths); + } + _activeIBLVolume = newVolume; + } + } + // Per-frame hover raycast based on last mouse position. if (_sceneManager && _mousePosPixels.x >= 0.0f && _mousePosPixels.y >= 0.0f) { @@ -558,6 +608,8 @@ void VulkanEngine::draw() RGImageHandle hGBufferNormal = _renderGraph->import_gbuffer_normal(); RGImageHandle hGBufferAlbedo = _renderGraph->import_gbuffer_albedo(); RGImageHandle hSwapchain = _renderGraph->import_swapchain_image(swapchainImageIndex); + // For debug overlays (IBL volumes), re-use HDR draw image as a color target. + RGImageHandle hDebugColor = hDraw; // Create transient depth targets for cascaded shadow maps (even if RT-only, to keep descriptors stable) const VkExtent2D shadowExtent{2048, 2048}; diff --git a/src/core/engine.h b/src/core/engine.h index 70d7349..03fcfd5 100644 --- a/src/core/engine.h +++ b/src/core/engine.h @@ -116,6 +116,21 @@ public: // Debug helpers: track spawned IBL test meshes to remove them easily std::vector _iblTestNames; + // Simple world-space IBL reflection volumes (axis-aligned boxes). + struct IBLVolume + { + glm::vec3 center{0.0f, 0.0f, 0.0f}; + glm::vec3 halfExtents{10.0f, 10.0f, 10.0f}; + IBLPaths paths{}; // HDRI paths for this volume + bool enabled{true}; + }; + // Global/default IBL used when no volume contains the camera. + IBLPaths _globalIBLPaths{}; + bool _hasGlobalIBL{false}; + // User-defined local IBL volumes and currently active index (-1 = global). + std::vector _iblVolumes; + int _activeIBLVolume{-1}; + struct PickInfo { MeshAsset *mesh = nullptr; diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index 823e939..c6696bc 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -21,6 +21,7 @@ #include "core/assets/ibl_manager.h" #include "context.h" #include +#include #include "mesh_bvh.h" @@ -138,11 +139,102 @@ namespace static void ui_ibl(VulkanEngine *eng) { if (!eng) return; + if (ImGui::Button("Spawn IBL Test Grid")) { spawn_ibl_test(eng); } ImGui::SameLine(); if (ImGui::Button("Clear IBL Test")) { clear_ibl_test(eng); } ImGui::TextUnformatted( "5x5 spheres: metallic across columns, roughness across rows.\nExtra: chrome + glass."); + + ImGui::Separator(); + ImGui::TextUnformatted("IBL Volumes (reflection probes)"); + + if (!eng->_iblManager) + { + ImGui::TextUnformatted("IBLManager not available"); + return; + } + + if (eng->_activeIBLVolume < 0) + { + ImGui::TextUnformatted("Active IBL: Global"); + } + else + { + ImGui::Text("Active IBL: Volume %d", eng->_activeIBLVolume); + } + + if (ImGui::Button("Add IBL Volume")) + { + VulkanEngine::IBLVolume vol{}; + if (eng->_sceneManager) + { + vol.center = eng->_sceneManager->getMainCamera().position; + } + vol.halfExtents = glm::vec3(10.0f, 10.0f, 10.0f); + vol.paths = eng->_globalIBLPaths; + eng->_iblVolumes.push_back(vol); + } + + for (size_t i = 0; i < eng->_iblVolumes.size(); ++i) + { + auto &vol = eng->_iblVolumes[i]; + ImGui::PushID(static_cast(i)); + ImGui::Separator(); + ImGui::Text("Volume %zu", i); + ImGui::Checkbox("Enabled", &vol.enabled); + ImGui::InputFloat3("Center", &vol.center.x); + ImGui::InputFloat3("Half Extents", &vol.halfExtents.x); + + // Simple path editors; store absolute or engine-local paths. + char specBuf[256]{}; + char diffBuf[256]{}; + char bgBuf[256]{}; + char brdfBuf[256]{}; + std::strncpy(specBuf, vol.paths.specularCube.c_str(), sizeof(specBuf) - 1); + std::strncpy(diffBuf, vol.paths.diffuseCube.c_str(), sizeof(diffBuf) - 1); + std::strncpy(bgBuf, vol.paths.background2D.c_str(), sizeof(bgBuf) - 1); + std::strncpy(brdfBuf, vol.paths.brdfLut2D.c_str(), sizeof(brdfBuf) - 1); + + if (ImGui::InputText("Specular path", specBuf, IM_ARRAYSIZE(specBuf))) + { + vol.paths.specularCube = specBuf; + } + if (ImGui::InputText("Diffuse path", diffBuf, IM_ARRAYSIZE(diffBuf))) + { + vol.paths.diffuseCube = diffBuf; + } + if (ImGui::InputText("Background path", bgBuf, IM_ARRAYSIZE(bgBuf))) + { + vol.paths.background2D = bgBuf; + } + if (ImGui::InputText("BRDF LUT path", brdfBuf, IM_ARRAYSIZE(brdfBuf))) + { + vol.paths.brdfLut2D = brdfBuf; + } + + if (ImGui::Button("Reload This Volume IBL")) + { + if (eng->_iblManager && vol.enabled) + { + eng->_iblManager->load(vol.paths); + eng->_activeIBLVolume = static_cast(i); + } + } + ImGui::SameLine(); + if (ImGui::Button("Set As Global IBL")) + { + eng->_globalIBLPaths = vol.paths; + eng->_hasGlobalIBL = true; + eng->_activeIBLVolume = -1; + if (eng->_iblManager) + { + eng->_iblManager->load(eng->_globalIBLPaths); + } + } + + ImGui::PopID(); + } } // Quick stats & targets overview @@ -600,6 +692,99 @@ namespace } } + // Point light editor + if (eng->_sceneManager) + { + ImGui::Separator(); + ImGui::TextUnformatted("Point lights"); + + SceneManager *sceneMgr = eng->_sceneManager.get(); + const auto &lights = sceneMgr->getPointLights(); + ImGui::Text("Active lights: %zu", lights.size()); + + static int selectedLight = -1; + if (selectedLight >= static_cast(lights.size())) + { + selectedLight = static_cast(lights.size()) - 1; + } + + if (ImGui::BeginListBox("Light list")) + { + for (size_t i = 0; i < lights.size(); ++i) + { + std::string label = fmt::format("Light {}", i); + const bool isSelected = (selectedLight == static_cast(i)); + if (ImGui::Selectable(label.c_str(), isSelected)) + { + selectedLight = static_cast(i); + } + } + ImGui::EndListBox(); + } + + // Controls for the selected light + if (selectedLight >= 0 && selectedLight < static_cast(lights.size())) + { + SceneManager::PointLight pl{}; + if (sceneMgr->getPointLight(static_cast(selectedLight), pl)) + { + float pos[3] = {pl.position.x, pl.position.y, pl.position.z}; + float col[3] = {pl.color.r, pl.color.g, pl.color.b}; + bool changed = false; + + changed |= ImGui::InputFloat3("Position", pos); + changed |= ImGui::SliderFloat("Radius", &pl.radius, 0.1f, 1000.0f); + changed |= ImGui::ColorEdit3("Color", col); + changed |= ImGui::SliderFloat("Intensity", &pl.intensity, 0.0f, 100.0f); + + if (changed) + { + pl.position = glm::vec3(pos[0], pos[1], pos[2]); + pl.color = glm::vec3(col[0], col[1], col[2]); + sceneMgr->setPointLight(static_cast(selectedLight), pl); + } + + if (ImGui::Button("Remove selected light")) + { + sceneMgr->removePointLight(static_cast(selectedLight)); + selectedLight = -1; + } + } + } + + // Controls for adding a new light + ImGui::Separator(); + ImGui::TextUnformatted("Add point light"); + static float newPos[3] = {0.0f, 1.0f, 0.0f}; + static float newRadius = 10.0f; + static float newColor[3] = {1.0f, 1.0f, 1.0f}; + static float newIntensity = 5.0f; + + ImGui::InputFloat3("New position", newPos); + ImGui::SliderFloat("New radius", &newRadius, 0.1f, 1000.0f); + ImGui::ColorEdit3("New color", newColor); + ImGui::SliderFloat("New intensity", &newIntensity, 0.0f, 100.0f); + + if (ImGui::Button("Add point light")) + { + SceneManager::PointLight pl{}; + pl.position = glm::vec3(newPos[0], newPos[1], newPos[2]); + pl.radius = newRadius; + pl.color = glm::vec3(newColor[0], newColor[1], newColor[2]); + pl.intensity = newIntensity; + + const size_t oldCount = sceneMgr->getPointLightCount(); + sceneMgr->addPointLight(pl); + selectedLight = static_cast(oldCount); + } + + if (ImGui::Button("Clear all lights")) + { + sceneMgr->clearPointLights(); + selectedLight = -1; + } + } + ImGui::Separator(); // Delete selected model/primitive (uses last pick if valid, otherwise hover) static std::string deleteStatus; diff --git a/src/render/passes/background.cpp b/src/render/passes/background.cpp index 9570f78..cc8730c 100644 --- a/src/render/passes/background.cpp +++ b/src/render/passes/background.cpp @@ -151,21 +151,37 @@ void BackgroundPass::register_graph(RenderGraph *graph, RGImageHandle drawHandle DescriptorWriter w0; w0.write_buffer(0, ubo.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); w0.update_set(ctx->getDevice()->device(), global); - // IBL set - VkImageView specView = _fallbackIblCube.imageView; - if (ctx->ibl && ctx->ibl->specular().imageView) specView = ctx->ibl->specular().imageView; - VkDescriptorSetLayout iblLayout = (ctx->ibl ? ctx->ibl->descriptorLayout() : _emptySetLayout); - VkDescriptorSet ibl = ctx->currentFrame->_frameDescriptors.allocate( - ctx->getDevice()->device(), iblLayout); - DescriptorWriter w3; - // Bind only specular at binding 0; other bindings are unused in this shader - w3.write_image(0, specView, ctx->getSamplers()->defaultLinear(), - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); - w3.update_set(ctx->getDevice()->device(), ibl); + // IBL/background set (set = 3) + VkDescriptorSet ibl = VK_NULL_HANDLE; + if (ctx->ibl) + { + VkImageView envView = _fallbackIblCube.imageView; + // Prefer a dedicated background texture when available, otherwise reuse specular. + if (ctx->ibl->background().imageView) + { + envView = ctx->ibl->background().imageView; + } + else if (ctx->ibl->specular().imageView) + { + envView = ctx->ibl->specular().imageView; + } + + VkDescriptorSetLayout iblLayout = ctx->ibl->descriptorLayout(); + ibl = ctx->currentFrame->_frameDescriptors.allocate( + ctx->getDevice()->device(), iblLayout); + DescriptorWriter w3; + // Bind background map at binding 3; other bindings are unused in this shader. + w3.write_image(3, envView, ctx->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + w3.update_set(ctx->getDevice()->device(), ibl); + } vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _envPipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _envPipelineLayout, 0, 1, &global, 0, nullptr); - vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _envPipelineLayout, 3, 1, &ibl, 0, nullptr); + if (ibl != VK_NULL_HANDLE) + { + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _envPipelineLayout, 3, 1, &ibl, 0, nullptr); + } VkExtent2D extent = ctx->getDrawExtent(); VkViewport vp{0.f, 0.f, float(extent.width), float(extent.height), 0.f, 1.f}; diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 6a65041..67d62df 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -37,6 +37,36 @@ void SceneManager::clearPointLights() pointLights.clear(); } +bool SceneManager::getPointLight(size_t index, PointLight &outLight) const +{ + if (index >= pointLights.size()) + { + return false; + } + outLight = pointLights[index]; + return true; +} + +bool SceneManager::setPointLight(size_t index, const PointLight &light) +{ + if (index >= pointLights.size()) + { + return false; + } + pointLights[index] = light; + return true; +} + +bool SceneManager::removePointLight(size_t index) +{ + if (index >= pointLights.size()) + { + return false; + } + pointLights.erase(pointLights.begin() + index); + return true; +} + void SceneManager::init(EngineContext *context) { _context = context; diff --git a/src/scene/vk_scene.h b/src/scene/vk_scene.h index 90980d1..bc9ffe5 100644 --- a/src/scene/vk_scene.h +++ b/src/scene/vk_scene.h @@ -136,6 +136,10 @@ public: void addPointLight(const PointLight &light); void clearPointLights(); + size_t getPointLightCount() const { return pointLights.size(); } + bool getPointLight(size_t index, PointLight &outLight) const; + bool setPointLight(size_t index, const PointLight &light); + bool removePointLight(size_t index); const std::vector &getPointLights() const { return pointLights; } struct SceneStats