From 6ea6b11096fb1a91a18f09d1fe33f60fddcb09b1 Mon Sep 17 00:00:00 2001 From: jyxiong Date: Sat, 28 Feb 2026 18:06:11 +0800 Subject: [PATCH 1/5] feat: add IBL panel --- example/damaged_helmet/main.cpp | 8 -- source/paimon/app/panel/editor_layer.cpp | 18 ++-- source/paimon/app/panel/editor_layer.h | 2 + source/paimon/app/panel/ibl_panel.cpp | 119 +++++++++++++++++++++++ source/paimon/app/panel/ibl_panel.h | 44 +++++++++ source/paimon/app/panel/scene_panel.cpp | 20 ++++ source/paimon/core/ecs/components.h | 3 + source/paimon/core/ecs/scene.cpp | 8 ++ 8 files changed, 208 insertions(+), 14 deletions(-) create mode 100644 source/paimon/app/panel/ibl_panel.cpp create mode 100644 source/paimon/app/panel/ibl_panel.h diff --git a/example/damaged_helmet/main.cpp b/example/damaged_helmet/main.cpp index 5ea9a5b..a9eb3ca 100644 --- a/example/damaged_helmet/main.cpp +++ b/example/damaged_helmet/main.cpp @@ -1,5 +1,4 @@ #include "paimon/app/application.h" -#include "paimon/core/ecs/components.h" #include "paimon/core/log_system.h" using namespace paimon; @@ -9,13 +8,6 @@ class GltfViewerApp : public Application { GltfViewerApp() : Application() { auto &scene = getScene(); - - { - // Initialize environment entity - auto environment = scene.createEntity("Environment"); - environment.addComponent(); - scene.setEnvironment(environment); - } } ~GltfViewerApp() override = default; diff --git a/source/paimon/app/panel/editor_layer.cpp b/source/paimon/app/panel/editor_layer.cpp index e9f4ed3..d796843 100644 --- a/source/paimon/app/panel/editor_layer.cpp +++ b/source/paimon/app/panel/editor_layer.cpp @@ -30,6 +30,7 @@ void EditorLayer::onImGuiRender() { m_menuPanel.onImGuiRender(); m_viewportPanel.onImGuiRender(); m_scenePanel.onImGuiRender(); + m_iblPanel.onImGuiRender(); } void EditorLayer::setupDockingLayout() { @@ -44,15 +45,20 @@ void EditorLayer::setupDockingLayout() { // Split the dockspace into left and right ImGuiID dock_left, dock_right; ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.2f, &dock_left, &dock_right); - - // Split right into center and bottom-right + + // Split right into center and properties ImGuiID dock_center, dock_bottom_right; ImGui::DockBuilderSplitNode(dock_right, ImGuiDir_Right, 0.25f, &dock_bottom_right, &dock_center); - + + // Split left into top (scene hierarchy) and bottom (IBL) + ImGuiID dock_left_top, dock_left_bottom; + ImGui::DockBuilderSplitNode(dock_left, ImGuiDir_Down, 0.45f, &dock_left_bottom, &dock_left_top); + // Dock windows to specific locations - ImGui::DockBuilderDockWindow("Scene Hierarchy", dock_left); - ImGui::DockBuilderDockWindow("Viewport", dock_center); - ImGui::DockBuilderDockWindow("Properties", dock_bottom_right); + ImGui::DockBuilderDockWindow("Scene Hierarchy", dock_left_top); + ImGui::DockBuilderDockWindow("IBL", dock_left_bottom); + ImGui::DockBuilderDockWindow("Viewport", dock_center); + ImGui::DockBuilderDockWindow("Properties", dock_bottom_right); // Finish building ImGui::DockBuilderFinish(dockspace_id); diff --git a/source/paimon/app/panel/editor_layer.h b/source/paimon/app/panel/editor_layer.h index b245723..7a9748e 100644 --- a/source/paimon/app/panel/editor_layer.h +++ b/source/paimon/app/panel/editor_layer.h @@ -1,6 +1,7 @@ #pragma once #include "paimon/app/layer.h" +#include "paimon/app/panel/ibl_panel.h" #include "paimon/app/panel/menu_panel.h" #include "paimon/app/panel/scene_panel.h" #include "paimon/app/panel/viewport_panel.h" @@ -24,6 +25,7 @@ class EditorLayer : public Layer { MenuPanel m_menuPanel; ScenePanel m_scenePanel; ViewportPanel m_viewportPanel; + IBLPanel m_iblPanel; bool m_firstTime = true; }; } // namespace paimon \ No newline at end of file diff --git a/source/paimon/app/panel/ibl_panel.cpp b/source/paimon/app/panel/ibl_panel.cpp new file mode 100644 index 0000000..731e2d9 --- /dev/null +++ b/source/paimon/app/panel/ibl_panel.cpp @@ -0,0 +1,119 @@ +#include "paimon/app/panel/ibl_panel.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "paimon/app/application.h" +#include "paimon/core/ecs/components.h" +#include "paimon/core/ecs/scene.h" +#include "paimon/core/log_system.h" + +namespace paimon { + +IBLPanel::IBLPanel() {} + +IBLPanel::~IBLPanel() {} + +void IBLPanel::onImGuiRender() { + ImGui::Begin("IBL"); + + // ---- Load HDR section ------------------------------------------------ + ImGui::SeparatorText("Equirectangular Map"); + + if (ImGui::Button("Load HDR...")) { + nfdu8char_t *outPath = nullptr; + nfdu8filteritem_t filters[2] = {{"HDR Image", "hdr"}, {"All Files", "*"}}; + nfdresult_t result = NFD_OpenDialogU8(&outPath, filters, 2, nullptr); + + if (result == NFD_OKAY) { + if (loadHDR(outPath)) { + m_hdrPath = outPath; + LOG_INFO("IBL: loaded HDR '{}'", m_hdrPath); + generateIBL(); // run immediately after loading + } + NFD_FreePathU8(outPath); + } else if (result == NFD_ERROR) { + LOG_ERROR("IBL: NFD error – {}", NFD_GetError()); + } + } + + // Show the loaded file path + if (!m_hdrPath.empty()) { + ImGui::SameLine(); + ImGui::TextUnformatted(m_hdrPath.c_str()); + } + + // ---- Preview --------------------------------------------------------- + if (m_equirectTexture) { + ImVec2 avail = ImGui::GetContentRegionAvail(); + float maxW = avail.x; + float ratio = m_previewHeight / m_previewWidth; + float dispW = maxW; + float dispH = dispW * ratio; + + ImGui::Image((ImTextureID)(uintptr_t)m_equirectTexture->get_name(), + ImVec2(dispW, dispH), + ImVec2(0, 1), // UV top-left (flip vertically) + ImVec2(1, 0) // UV bottom-right + ); + } + + ImGui::End(); +} + +bool IBLPanel::loadHDR(const std::string &path) { + stbi_set_flip_vertically_on_load(true); + + int width, height, nrChannels; + float *data = stbi_loadf(path.c_str(), &width, &height, &nrChannels, 0); + + if (!data) { + LOG_ERROR("IBL: failed to load HDR image '{}': {}", path, + stbi_failure_reason()); + return false; + } + + GLenum format = GL_RGB; + if (nrChannels == 1) + format = GL_RED; + else if (nrChannels == 4) + format = GL_RGBA; + + m_equirectTexture = std::make_unique(GL_TEXTURE_2D); + m_equirectTexture->set_storage_2d(1, GL_RGB32F, width, height); + m_equirectTexture->set_sub_image_2d(0, 0, 0, width, height, format, GL_FLOAT, + data); + m_equirectTexture->set(GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + m_equirectTexture->set(GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + m_equirectTexture->set(GL_TEXTURE_MIN_FILTER, GL_LINEAR); + m_equirectTexture->set(GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + m_previewWidth = static_cast(width); + m_previewHeight = static_cast(height); + + stbi_image_free(data); + return true; +} + +void IBLPanel::generateIBL() { + + m_iblSampler = std::make_unique(); + m_iblSampler->execute(*m_equirectTexture); + + // Wire the textures into the scene Environment entity. + auto &scene = Application::getInstance().getScene(); + auto envEntity = scene.getEnvironment(); + + auto &envComp = envEntity.getComponent(); + envComp.irradianceMap = m_iblSampler->getIrradianceMap(); + envComp.prefilteredMap = m_iblSampler->getPrefilteredMap(); + envComp.brdfLUT = m_iblSampler->getBRDFLUT(); +} + +} // namespace paimon diff --git a/source/paimon/app/panel/ibl_panel.h b/source/paimon/app/panel/ibl_panel.h new file mode 100644 index 0000000..4bd3833 --- /dev/null +++ b/source/paimon/app/panel/ibl_panel.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +#include "paimon/opengl/texture.h" +#include "paimon/utility/ibl_sampler.h" + +namespace paimon { + +/// Panel for Image-Based Lighting (IBL) operations. +/// Allows loading an equirectangular HDR map, generating IBL textures via +/// IBLSampler, and wiring the results into a scene Environment entity. +class IBLPanel { +public: + IBLPanel(); + ~IBLPanel(); + + void onImGuiRender(); + +private: + /// Load an HDR equirectangular image from disk and upload it as a GL texture. + bool loadHDR(const std::string &path); + + /// Run IBLSampler on the currently loaded equirectangular texture + /// and wire the results into the scene Environment entity. + void generateIBL(); + +private: + // The loaded equirectangular texture (used as input to IBLSampler) + std::unique_ptr m_equirectTexture; + + // The IBL sampler that generates irradiance / prefiltered / BRDF LUT maps + std::unique_ptr m_iblSampler; + + // Cached path of the last loaded HDR file + std::string m_hdrPath; + + // Image display size inside the panel + float m_previewWidth = 320.0f; + float m_previewHeight = 160.0f; +}; + +} // namespace paimon diff --git a/source/paimon/app/panel/scene_panel.cpp b/source/paimon/app/panel/scene_panel.cpp index 0463a62..f0d1546 100644 --- a/source/paimon/app/panel/scene_panel.cpp +++ b/source/paimon/app/panel/scene_panel.cpp @@ -433,6 +433,20 @@ void ScenePanel::drawComponents(ecs::Entity entity) { ImGui::TextWrapped("Tip: Position and direction are controlled by Transform"); } } + + // Environment (IBL) component + if (entity.hasComponent()) { + if (ImGui::CollapsingHeader("Environment (IBL)", ImGuiTreeNodeFlags_DefaultOpen)) { + auto &env = entity.getComponent(); + + ImGui::DragFloat("Intensity", &env.intensity, 0.05f, 0.0f, 10.0f); + + glm::vec3 rotDeg = glm::degrees(glm::eulerAngles(env.rotation)); + if (ImGui::DragFloat3("Rotation##IBL", glm::value_ptr(rotDeg), 1.0f)) { + env.rotation = glm::quat(glm::radians(rotDeg)); + } + } + } } void ScenePanel::drawAddComponentButton(ecs::Entity entity) { @@ -467,6 +481,12 @@ void ScenePanel::drawAddComponentButton(ecs::Entity entity) { } } + if (!entity.hasComponent()) { + if (ImGui::MenuItem("Environment")) { + entity.addComponent(); + } + } + // Light submenu if (ImGui::BeginMenu("Light")) { if (!entity.hasComponent()) { diff --git a/source/paimon/core/ecs/components.h b/source/paimon/core/ecs/components.h index eb0022b..b090930 100644 --- a/source/paimon/core/ecs/components.h +++ b/source/paimon/core/ecs/components.h @@ -102,10 +102,13 @@ struct Renderable { /// Image based lighting component struct Environment { float intensity = 1.0f; + glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); std::shared_ptr irradianceMap; // Diffuse IBL + std::shared_ptr prefilteredMap; // Specular IBL + std::shared_ptr brdfLUT; // BRDF lookup texture // std::array irradianceCoefficients; // Spherical Harmonics coefficients diff --git a/source/paimon/core/ecs/scene.cpp b/source/paimon/core/ecs/scene.cpp index 251f566..39c08f9 100644 --- a/source/paimon/core/ecs/scene.cpp +++ b/source/paimon/core/ecs/scene.cpp @@ -69,6 +69,14 @@ std::unique_ptr Scene::create() { scene->setDirectionalLight(directionalLight); } + { + // Initialize environment entity + auto environment = scene->createEntity("Environment"); + environment.addComponent(); + + scene->setEnvironment(environment); + } + return scene; } From 72bf2573839147397f787ffe2017f944b66905eb Mon Sep 17 00:00:00 2001 From: jyxiong Date: Mon, 2 Mar 2026 10:49:04 +0800 Subject: [PATCH 2/5] refactor --- example/CMakeLists.txt | 1 - example/ibl/main.cpp | 83 ------------ source/paimon/app/panel/editor_layer.cpp | 4 +- source/paimon/app/panel/editor_layer.h | 2 - source/paimon/app/panel/ibl_panel.cpp | 119 ----------------- source/paimon/app/panel/ibl_panel.h | 44 ------- source/paimon/app/panel/menu_panel.cpp | 17 +++ source/paimon/app/panel/scene_panel.cpp | 44 ++----- source/paimon/app/panel/scene_panel.h | 1 - source/paimon/core/ecs/components.h | 2 + source/paimon/core/io/ibl.cpp | 107 +++++++++++++++ source/paimon/core/io/ibl.h | 52 ++++++++ source/paimon/utility/ibl_sampler.cpp | 159 ----------------------- source/paimon/utility/ibl_sampler.h | 42 ------ 14 files changed, 192 insertions(+), 485 deletions(-) delete mode 100644 example/ibl/main.cpp delete mode 100644 source/paimon/app/panel/ibl_panel.cpp delete mode 100644 source/paimon/app/panel/ibl_panel.h create mode 100644 source/paimon/core/io/ibl.cpp create mode 100644 source/paimon/core/io/ibl.h delete mode 100644 source/paimon/utility/ibl_sampler.cpp delete mode 100644 source/paimon/utility/ibl_sampler.h diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 5872b01..27ca833 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -47,5 +47,4 @@ add_example(damaged_helmet) add_example(debug_message) add_example(frame_graph) add_example(geometry) -add_example(ibl) add_example(query) diff --git a/example/ibl/main.cpp b/example/ibl/main.cpp deleted file mode 100644 index d88e439..0000000 --- a/example/ibl/main.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "paimon/app/application.h" -#include "paimon/config.h" -#include "paimon/core/log_system.h" -#include "paimon/opengl/texture.h" -#include "paimon/utility/ibl_sampler.h" - -using namespace paimon; - -// Window dimensions -const int WIDTH = 800; -const int HEIGHT = 600; - -// Load HDR texture from file -std::unique_ptr loadHDRTexture(const std::filesystem::path &path) { - stbi_set_flip_vertically_on_load(true); - - int width, height, nrComponents; - float *data = stbi_loadf(path.string().c_str(), &width, &height, &nrComponents, 0); - - if (!data) { - std::cerr << "Failed to load HDR image: " << path << std::endl; - return nullptr; - } - - auto texture = std::make_unique(GL_TEXTURE_2D); - - GLenum format = GL_RGB; - if (nrComponents == 1) - format = GL_RED; - else if (nrComponents == 3) - format = GL_RGB; - else if (nrComponents == 4) - format = GL_RGBA; - - texture->set_storage_2d(1, GL_RGB32F, width, height); - texture->set_sub_image_2d(0, 0, 0, width, height, format, GL_FLOAT, data); - - texture->set(GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - texture->set(GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - texture->set(GL_TEXTURE_MIN_FILTER, GL_LINEAR); - texture->set(GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - stbi_image_free(data); - - return texture; -} - -class IBLApp : public Application { -public: - IBLApp() : Application() {} - - ~IBLApp() override = default; -}; - -int main() { - - LogSystem::init(); - - IBLApp app; - - // Load HDR environment map - auto hdrTexture = loadHDRTexture(std::filesystem::path(PAIMON_TEXTURE_DIR) / - "belfast_sunset_puresky_2k.hdr"); - - // Create IBL sampler - IBLSampler iblSampler; - iblSampler.execute(*hdrTexture); - iblSampler.save(); - - return 0; -} diff --git a/source/paimon/app/panel/editor_layer.cpp b/source/paimon/app/panel/editor_layer.cpp index d796843..580025b 100644 --- a/source/paimon/app/panel/editor_layer.cpp +++ b/source/paimon/app/panel/editor_layer.cpp @@ -30,7 +30,6 @@ void EditorLayer::onImGuiRender() { m_menuPanel.onImGuiRender(); m_viewportPanel.onImGuiRender(); m_scenePanel.onImGuiRender(); - m_iblPanel.onImGuiRender(); } void EditorLayer::setupDockingLayout() { @@ -50,13 +49,12 @@ void EditorLayer::setupDockingLayout() { ImGuiID dock_center, dock_bottom_right; ImGui::DockBuilderSplitNode(dock_right, ImGuiDir_Right, 0.25f, &dock_bottom_right, &dock_center); - // Split left into top (scene hierarchy) and bottom (IBL) + // Split left into top (scene hierarchy) ImGuiID dock_left_top, dock_left_bottom; ImGui::DockBuilderSplitNode(dock_left, ImGuiDir_Down, 0.45f, &dock_left_bottom, &dock_left_top); // Dock windows to specific locations ImGui::DockBuilderDockWindow("Scene Hierarchy", dock_left_top); - ImGui::DockBuilderDockWindow("IBL", dock_left_bottom); ImGui::DockBuilderDockWindow("Viewport", dock_center); ImGui::DockBuilderDockWindow("Properties", dock_bottom_right); diff --git a/source/paimon/app/panel/editor_layer.h b/source/paimon/app/panel/editor_layer.h index 7a9748e..b245723 100644 --- a/source/paimon/app/panel/editor_layer.h +++ b/source/paimon/app/panel/editor_layer.h @@ -1,7 +1,6 @@ #pragma once #include "paimon/app/layer.h" -#include "paimon/app/panel/ibl_panel.h" #include "paimon/app/panel/menu_panel.h" #include "paimon/app/panel/scene_panel.h" #include "paimon/app/panel/viewport_panel.h" @@ -25,7 +24,6 @@ class EditorLayer : public Layer { MenuPanel m_menuPanel; ScenePanel m_scenePanel; ViewportPanel m_viewportPanel; - IBLPanel m_iblPanel; bool m_firstTime = true; }; } // namespace paimon \ No newline at end of file diff --git a/source/paimon/app/panel/ibl_panel.cpp b/source/paimon/app/panel/ibl_panel.cpp deleted file mode 100644 index 731e2d9..0000000 --- a/source/paimon/app/panel/ibl_panel.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "paimon/app/panel/ibl_panel.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "paimon/app/application.h" -#include "paimon/core/ecs/components.h" -#include "paimon/core/ecs/scene.h" -#include "paimon/core/log_system.h" - -namespace paimon { - -IBLPanel::IBLPanel() {} - -IBLPanel::~IBLPanel() {} - -void IBLPanel::onImGuiRender() { - ImGui::Begin("IBL"); - - // ---- Load HDR section ------------------------------------------------ - ImGui::SeparatorText("Equirectangular Map"); - - if (ImGui::Button("Load HDR...")) { - nfdu8char_t *outPath = nullptr; - nfdu8filteritem_t filters[2] = {{"HDR Image", "hdr"}, {"All Files", "*"}}; - nfdresult_t result = NFD_OpenDialogU8(&outPath, filters, 2, nullptr); - - if (result == NFD_OKAY) { - if (loadHDR(outPath)) { - m_hdrPath = outPath; - LOG_INFO("IBL: loaded HDR '{}'", m_hdrPath); - generateIBL(); // run immediately after loading - } - NFD_FreePathU8(outPath); - } else if (result == NFD_ERROR) { - LOG_ERROR("IBL: NFD error – {}", NFD_GetError()); - } - } - - // Show the loaded file path - if (!m_hdrPath.empty()) { - ImGui::SameLine(); - ImGui::TextUnformatted(m_hdrPath.c_str()); - } - - // ---- Preview --------------------------------------------------------- - if (m_equirectTexture) { - ImVec2 avail = ImGui::GetContentRegionAvail(); - float maxW = avail.x; - float ratio = m_previewHeight / m_previewWidth; - float dispW = maxW; - float dispH = dispW * ratio; - - ImGui::Image((ImTextureID)(uintptr_t)m_equirectTexture->get_name(), - ImVec2(dispW, dispH), - ImVec2(0, 1), // UV top-left (flip vertically) - ImVec2(1, 0) // UV bottom-right - ); - } - - ImGui::End(); -} - -bool IBLPanel::loadHDR(const std::string &path) { - stbi_set_flip_vertically_on_load(true); - - int width, height, nrChannels; - float *data = stbi_loadf(path.c_str(), &width, &height, &nrChannels, 0); - - if (!data) { - LOG_ERROR("IBL: failed to load HDR image '{}': {}", path, - stbi_failure_reason()); - return false; - } - - GLenum format = GL_RGB; - if (nrChannels == 1) - format = GL_RED; - else if (nrChannels == 4) - format = GL_RGBA; - - m_equirectTexture = std::make_unique(GL_TEXTURE_2D); - m_equirectTexture->set_storage_2d(1, GL_RGB32F, width, height); - m_equirectTexture->set_sub_image_2d(0, 0, 0, width, height, format, GL_FLOAT, - data); - m_equirectTexture->set(GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - m_equirectTexture->set(GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - m_equirectTexture->set(GL_TEXTURE_MIN_FILTER, GL_LINEAR); - m_equirectTexture->set(GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - m_previewWidth = static_cast(width); - m_previewHeight = static_cast(height); - - stbi_image_free(data); - return true; -} - -void IBLPanel::generateIBL() { - - m_iblSampler = std::make_unique(); - m_iblSampler->execute(*m_equirectTexture); - - // Wire the textures into the scene Environment entity. - auto &scene = Application::getInstance().getScene(); - auto envEntity = scene.getEnvironment(); - - auto &envComp = envEntity.getComponent(); - envComp.irradianceMap = m_iblSampler->getIrradianceMap(); - envComp.prefilteredMap = m_iblSampler->getPrefilteredMap(); - envComp.brdfLUT = m_iblSampler->getBRDFLUT(); -} - -} // namespace paimon diff --git a/source/paimon/app/panel/ibl_panel.h b/source/paimon/app/panel/ibl_panel.h deleted file mode 100644 index 4bd3833..0000000 --- a/source/paimon/app/panel/ibl_panel.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include - -#include "paimon/opengl/texture.h" -#include "paimon/utility/ibl_sampler.h" - -namespace paimon { - -/// Panel for Image-Based Lighting (IBL) operations. -/// Allows loading an equirectangular HDR map, generating IBL textures via -/// IBLSampler, and wiring the results into a scene Environment entity. -class IBLPanel { -public: - IBLPanel(); - ~IBLPanel(); - - void onImGuiRender(); - -private: - /// Load an HDR equirectangular image from disk and upload it as a GL texture. - bool loadHDR(const std::string &path); - - /// Run IBLSampler on the currently loaded equirectangular texture - /// and wire the results into the scene Environment entity. - void generateIBL(); - -private: - // The loaded equirectangular texture (used as input to IBLSampler) - std::unique_ptr m_equirectTexture; - - // The IBL sampler that generates irradiance / prefiltered / BRDF LUT maps - std::unique_ptr m_iblSampler; - - // Cached path of the last loaded HDR file - std::string m_hdrPath; - - // Image display size inside the panel - float m_previewWidth = 320.0f; - float m_previewHeight = 160.0f; -}; - -} // namespace paimon diff --git a/source/paimon/app/panel/menu_panel.cpp b/source/paimon/app/panel/menu_panel.cpp index 5d6919f..bb1a9c1 100644 --- a/source/paimon/app/panel/menu_panel.cpp +++ b/source/paimon/app/panel/menu_panel.cpp @@ -6,6 +6,7 @@ #include "paimon/app/application.h" #include "paimon/app/event/application_event.h" #include "paimon/core/ecs/scene.h" +#include "paimon/core/io/ibl.h" #include "paimon/core/log_system.h" namespace paimon { @@ -53,6 +54,22 @@ void MenuPanel::showFileMenu() { LOG_ERROR("Error opening file dialog: {}", NFD_GetError()); } } + + if (ImGui::MenuItem("Load Sky Box...")) { + nfdu8char_t* outPath = nullptr; + nfdu8filteritem_t filters[1] = {{"HDR Image", "hdr"}}; + nfdresult_t result = NFD_OpenDialogU8(&outPath, filters, 1, nullptr); + + if (result == NFD_OKAY) { + auto& scene = Application::getInstance().getScene(); + LOG_INFO("Loading sky box: {}", outPath); + IBLLoader loader(outPath); + loader.load(scene); + NFD_FreePathU8(outPath); + } else if (result == NFD_ERROR) { + LOG_ERROR("Error opening file dialog: {}", NFD_GetError()); + } + } ImGui::Separator(); diff --git a/source/paimon/app/panel/scene_panel.cpp b/source/paimon/app/panel/scene_panel.cpp index f0d1546..390f14b 100644 --- a/source/paimon/app/panel/scene_panel.cpp +++ b/source/paimon/app/panel/scene_panel.cpp @@ -445,6 +445,19 @@ void ScenePanel::drawComponents(ecs::Entity entity) { if (ImGui::DragFloat3("Rotation##IBL", glm::value_ptr(rotDeg), 1.0f)) { env.rotation = glm::quat(glm::radians(rotDeg)); } + + // Equirectangular map preview + if (env.equirectangularMap) { + ImGui::Spacing(); + ImGui::TextUnformatted("Equirectangular Map"); + float dispW = ImGui::GetContentRegionAvail().x; + float dispH = dispW * 0.5f; // 2:1 aspect ratio + ImGui::Image( + (ImTextureID)(uintptr_t)env.equirectangularMap->get_name(), + ImVec2(dispW, dispH), + ImVec2(0, 1), ImVec2(1, 0) // flip vertically + ); + } } } } @@ -510,35 +523,4 @@ void ScenePanel::drawAddComponentButton(ecs::Entity entity) { ImGui::EndPopup(); } } - -void ScenePanel::drawEntityTransform(ecs::Entity entity) { - if (!entity.isValid()) { - return; - } - - // Display entity name - auto &name = entity.getComponent(); - ImGui::Text("Entity: %s", name.name.c_str()); - ImGui::Separator(); - - // Display and edit transform - if (entity.hasComponent()) { - auto &transform = entity.getComponent(); - - ImGui::Text("Transform"); - - // Translation - ImGui::DragFloat3("Translation", glm::value_ptr(transform.translation), 0.1f); - - // Rotation (as Euler angles in degrees) - glm::vec3 rotation = glm::degrees(glm::eulerAngles(transform.rotation)); - if (ImGui::DragFloat3("Rotation", glm::value_ptr(rotation), 1.0f)) { - transform.rotation = glm::quat(glm::radians(rotation)); - } - - // Scale - ImGui::DragFloat3("Scale", glm::value_ptr(transform.scale), 0.1f); - } -} - } // namespace paimon diff --git a/source/paimon/app/panel/scene_panel.h b/source/paimon/app/panel/scene_panel.h index 753e233..1b62be8 100644 --- a/source/paimon/app/panel/scene_panel.h +++ b/source/paimon/app/panel/scene_panel.h @@ -13,7 +13,6 @@ class ScenePanel { private: void drawEntityNode(ecs::Entity entity); - void drawEntityTransform(ecs::Entity entity); void drawComponents(ecs::Entity entity); void drawAddComponentButton(ecs::Entity entity); diff --git a/source/paimon/core/ecs/components.h b/source/paimon/core/ecs/components.h index b090930..f64a01e 100644 --- a/source/paimon/core/ecs/components.h +++ b/source/paimon/core/ecs/components.h @@ -105,6 +105,8 @@ struct Environment { glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + std::shared_ptr equirectangularMap; // Source equirectangular HDR + std::shared_ptr irradianceMap; // Diffuse IBL std::shared_ptr prefilteredMap; // Specular IBL diff --git a/source/paimon/core/io/ibl.cpp b/source/paimon/core/io/ibl.cpp new file mode 100644 index 0000000..a1e5745 --- /dev/null +++ b/source/paimon/core/io/ibl.cpp @@ -0,0 +1,107 @@ +#include "paimon/core/io/ibl.h" + +#include +#include + +#include "paimon/core/ecs/components.h" +#include "paimon/core/log_system.h" +#include "paimon/rendering/render_context.h" +#include "paimon/utility/brdf_lut_pass.h" +#include "paimon/utility/equirectangular_to_cubemap_pass.h" +#include "paimon/utility/irradiance_map_pass.h" +#include "paimon/utility/prefiltered_map_pass.h" + +using namespace paimon; + +namespace { +constexpr uint32_t defaultCubemapSize = 1024; +constexpr uint32_t defaultIrradianceSize = 32; +constexpr uint32_t defaultPrefilteredSize = 512; +constexpr uint32_t defaultPrefilteredMipLevels = 5; +constexpr uint32_t defaultBRDFLUTSize = 512; +} // namespace + +IBLLoader::IBLLoader(const std::filesystem::path &filepath) + : m_filepath(filepath), + m_renderContext(std::make_unique()), + m_equirectToCubemapPass( + std::make_unique(*m_renderContext)), + m_irradianceMapPass( + std::make_unique(*m_renderContext)), + m_prefilteredMapPass( + std::make_unique(*m_renderContext)), + m_brdfLUTPass(std::make_unique(*m_renderContext)) {} + +IBLLoader::~IBLLoader() = default; + +void IBLLoader::load(ecs::Scene &scene) { + loadHDRTexture(); + + if (!m_equirectangularTexture) { + LOG_ERROR("IBLLoader: aborting – HDR texture could not be loaded from '{}'", + m_filepath.string()); + return; + } + + processIBL(); + + // Get the existing Environment entity, or create a new one. + auto envEntity = scene.getEnvironment(); + if (!envEntity) { + envEntity = scene.createEntity("Environment"); + envEntity.addComponent(); + scene.setEnvironment(envEntity); + LOG_INFO("IBLLoader: created new Environment entity"); + } + + auto &envComp = envEntity.getOrAddComponent(); + envComp.equirectangularMap = m_equirectangularTexture; + envComp.irradianceMap = m_irradianceMapPass->getIrradianceMap(); + envComp.prefilteredMap = m_prefilteredMapPass->getPrefilteredMap(); + envComp.brdfLUT = m_brdfLUTPass->getBRDFLUT(); + + LOG_INFO("IBLLoader: environment textures loaded from '{}'", + m_filepath.string()); +} + +void IBLLoader::loadHDRTexture() { + stbi_set_flip_vertically_on_load(true); + + int width = 0, height = 0, nrChannels = 0; + float *data = stbi_loadf(m_filepath.string().c_str(), &width, &height, + &nrChannels, 0); + + if (!data) { + LOG_ERROR("IBLLoader: failed to load HDR image '{}': {}", + m_filepath.string(), stbi_failure_reason()); + return; + } + + GLenum format = GL_RGB; + if (nrChannels == 1) + format = GL_RED; + else if (nrChannels == 4) + format = GL_RGBA; + + m_equirectangularTexture = std::make_shared(GL_TEXTURE_2D); + m_equirectangularTexture->set_storage_2d(1, GL_RGB32F, width, height); + m_equirectangularTexture->set_sub_image_2d(0, 0, 0, width, height, format, + GL_FLOAT, data); + m_equirectangularTexture->set(GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + m_equirectangularTexture->set(GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + m_equirectangularTexture->set(GL_TEXTURE_MIN_FILTER, GL_LINEAR); + m_equirectangularTexture->set(GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + stbi_image_free(data); +} + +void IBLLoader::processIBL() { + m_equirectToCubemapPass->execute(*m_equirectangularTexture, + defaultCubemapSize); + m_irradianceMapPass->execute(m_equirectToCubemapPass->getCubemap(), + defaultIrradianceSize); + m_prefilteredMapPass->execute(m_equirectToCubemapPass->getCubemap(), + defaultPrefilteredSize, + defaultPrefilteredMipLevels); + m_brdfLUTPass->execute(defaultBRDFLUTSize); +} diff --git a/source/paimon/core/io/ibl.h b/source/paimon/core/io/ibl.h new file mode 100644 index 0000000..e6ee831 --- /dev/null +++ b/source/paimon/core/io/ibl.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include "paimon/core/ecs/scene.h" +#include "paimon/opengl/texture.h" + +// Forward declarations +namespace paimon { +class RenderContext; +class EquirectangularToCubemapPass; +class IrradianceMapPass; +class PrefilteredMapPass; +class BRDFLUTPass; +} // namespace paimon + +namespace paimon { + +/// IBL (Image-Based Lighting) loader. +/// Loads an HDR equirectangular image from disk, pre-computes the IBL textures +/// (irradiance map, pre-filtered map, BRDF LUT), and stores the results in the +/// scene's Environment entity. If the scene does not yet have an Environment +/// entity one is created automatically. +class IBLLoader { +public: + IBLLoader(const std::filesystem::path &filepath); + ~IBLLoader(); + + IBLLoader(const IBLLoader &) = delete; + IBLLoader &operator=(const IBLLoader &) = delete; + + void load(ecs::Scene &scene); + +private: + void loadHDRTexture(); + void processIBL(); + +private: + std::filesystem::path m_filepath; + + std::unique_ptr m_renderContext; + + std::shared_ptr m_equirectangularTexture; + + std::unique_ptr m_equirectToCubemapPass; + std::unique_ptr m_irradianceMapPass; + std::unique_ptr m_prefilteredMapPass; + std::unique_ptr m_brdfLUTPass; +}; + +} // namespace paimon diff --git a/source/paimon/utility/ibl_sampler.cpp b/source/paimon/utility/ibl_sampler.cpp deleted file mode 100644 index 4d79836..0000000 --- a/source/paimon/utility/ibl_sampler.cpp +++ /dev/null @@ -1,159 +0,0 @@ -#include "paimon/utility/ibl_sampler.h" - -#include -#include -#include - -#include "paimon/config.h" -#include "paimon/opengl/texture.h" -#include "paimon/rendering/render_context.h" -#include "paimon/utility/brdf_lut_pass.h" -#include "paimon/utility/equirectangular_to_cubemap_pass.h" -#include "paimon/utility/irradiance_map_pass.h" -#include "paimon/utility/prefiltered_map_pass.h" -#include "paimon/core/log_system.h" - -using namespace paimon; - -std::unique_ptr loadHDRTexture(const std::string &path); -void saveCubemapToFiles(const Texture &cubemap, const std::string &basePath, - int size, int mipLevel = 0); -void save2DTextureToFile(const Texture &texture, const std::string &path, - int width, int height); - -constexpr uint32_t defaultCubemapSize = 1024; -constexpr uint32_t defaultIrradianceSize = 32; -constexpr uint32_t defaultPrefilteredSize = 512; -constexpr uint32_t defaultPrefilteredMipLevels = 5; -constexpr uint32_t defaultBRDFLUTSize = 512; - -IBLSampler::IBLSampler() - : m_renderContext(std::make_unique()), - m_equirectToCubemapPass( - std::make_unique(*m_renderContext)), - m_irradianceMapPass( - std::make_unique(*m_renderContext)), - m_prefilteredMapPass( - std::make_unique(*m_renderContext)), - m_brdfLUTPass(std::make_unique(*m_renderContext)) {} - -IBLSampler::~IBLSampler() = default; - -void IBLSampler::execute(const Texture &equirectangular) { - m_equirectToCubemapPass->execute(equirectangular, defaultCubemapSize); - m_irradianceMapPass->execute(m_equirectToCubemapPass->getCubemap(), - defaultIrradianceSize); - m_prefilteredMapPass->execute(m_equirectToCubemapPass->getCubemap(), - defaultPrefilteredSize, - defaultPrefilteredMipLevels); - m_brdfLUTPass->execute(defaultBRDFLUTSize); -} - -void IBLSampler::save() { - // Save cubemap faces - saveCubemapToFiles(m_equirectToCubemapPass->getCubemap(), - (std::filesystem::path(PAIMON_TEXTURE_DIR) / "env_cubemap").string(), defaultCubemapSize, 0); - saveCubemapToFiles(*m_irradianceMapPass->getIrradianceMap(), - (std::filesystem::path(PAIMON_TEXTURE_DIR) / "irradiance_map").string(), defaultIrradianceSize, 0); - - for (uint32_t mip = 0; mip < defaultPrefilteredMipLevels; ++mip) { - uint32_t mipSize = defaultPrefilteredSize >> mip; - saveCubemapToFiles(*m_prefilteredMapPass->getPrefilteredMap(), - (std::filesystem::path(PAIMON_TEXTURE_DIR) / "prefiltered_map").string(), mipSize, mip); - } - - // Save BRDF LUT - save2DTextureToFile(*m_brdfLUTPass->getBRDFLUT(), (std::filesystem::path(PAIMON_TEXTURE_DIR) / "brdf_lut.hdr").string(), - defaultBRDFLUTSize, defaultBRDFLUTSize); -} - -std::shared_ptr IBLSampler::getIrradianceMap() const { - return m_irradianceMapPass->getIrradianceMap(); -} - -std::shared_ptr IBLSampler::getPrefilteredMap() const { - return m_prefilteredMapPass->getPrefilteredMap(); -} - -std::shared_ptr IBLSampler::getBRDFLUT() const { - return m_brdfLUTPass->getBRDFLUT(); -} - -// Save cubemap faces to HDR files -void saveCubemapToFiles(const Texture &cubemap, const std::string &basePath, - int size, int mipLevel) { - const char *faceNames[6] = { - "px", "nx", // +X, -X - "py", "ny", // +Y, -Y - "pz", "nz" // +Z, -Z - }; - - std::vector pixels(size * size * 3); - - for (int face = 0; face < 6; ++face) { - // For cubemap textures, glGetTextureSubImage treats them as an array of 6 - // slices zoffset is the face index (0-5), depth is the number of faces to - // access (1) - glGetTextureSubImage(cubemap.get_name(), - mipLevel, // level - 0, 0, - face, // xoffset, yoffset, zoffset (face index: 0-5) - size, size, 1, // width, height, depth (1 face) - GL_RGB, // format - GL_FLOAT, // type - pixels.size() * sizeof(float), // bufSize - pixels.data() // pixels - ); - - // Save to HDR file (no tone mapping, preserve full HDR range) - std::string filename = basePath; - if (mipLevel > 0) { - filename += "_mip" + std::to_string(mipLevel); - } - filename += "_"; - filename += faceNames[face]; - filename += ".hdr"; - - stbi_flip_vertically_on_write(1); - if (!stbi_write_hdr(filename.c_str(), size, size, 3, pixels.data())) { - LOG_ERROR(" Failed to save: {}", filename); - } - } -} - -// Save 2D texture to HDR file -void save2DTextureToFile(const Texture &texture, const std::string &filepath, - int width, int height) { - std::vector pixels(width * height * 2); // RG format - - // Read texture data - glGetTextureSubImage(texture.get_name(), - 0, // level - 0, 0, 0, // xoffset, yoffset, zoffset - width, height, 1, // width, height, depth - GL_RG, // format (BRDF LUT is RG16F) - GL_FLOAT, // type - pixels.size() * sizeof(float), // bufSize - pixels.data() // pixels - ); - - // Convert RG to RGB for HDR file (add blue channel = 0) - std::vector pixelsRGB(width * height * 3); - for (int i = 0; i < width * height; ++i) { - pixelsRGB[i * 3 + 0] = pixels[i * 2 + 0]; // R - pixelsRGB[i * 3 + 1] = pixels[i * 2 + 1]; // G - pixelsRGB[i * 3 + 2] = 0.0f; // B = 0 - } - - // Check data range - float maxR = 0.0f, maxG = 0.0f; - for (int i = 0; i < width * height; ++i) { - maxR = std::max(maxR, pixels[i * 2 + 0]); - maxG = std::max(maxG, pixels[i * 2 + 1]); - } - - stbi_flip_vertically_on_write(1); - if (!stbi_write_hdr(filepath.c_str(), width, height, 3, pixelsRGB.data())) { - LOG_ERROR(" Failed to save: {}", filepath); - } -} \ No newline at end of file diff --git a/source/paimon/utility/ibl_sampler.h b/source/paimon/utility/ibl_sampler.h deleted file mode 100644 index e6277f5..0000000 --- a/source/paimon/utility/ibl_sampler.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include -#include "paimon/opengl/texture.h" - -namespace paimon { - -class RenderContext; -class EquirectangularToCubemapPass; -class IrradianceMapPass; -class PrefilteredMapPass; -class BRDFLUTPass; - -/// IBL (Image-Based Lighting) sampler for pre-computing environment maps -class IBLSampler { -public: - IBLSampler(); - ~IBLSampler(); - - IBLSampler(const IBLSampler &other) = delete; - IBLSampler &operator=(const IBLSampler &other) = delete; - - void execute(const Texture &equirectangular); - - void save(); - - std::shared_ptr getIrradianceMap() const; - std::shared_ptr getPrefilteredMap() const; - std::shared_ptr getBRDFLUT() const; - -private: - std::unique_ptr m_renderContext; - - std::unique_ptr m_equirectangularTexture; - - std::unique_ptr m_equirectToCubemapPass; - std::unique_ptr m_irradianceMapPass; - std::unique_ptr m_prefilteredMapPass; - std::unique_ptr m_brdfLUTPass; -}; - -} // namespace paimon \ No newline at end of file From acd3436244466eed43e169c8be7fdecfbfa33b4f Mon Sep 17 00:00:00 2001 From: jyxiong Date: Mon, 2 Mar 2026 11:05:05 +0800 Subject: [PATCH 3/5] refactor shader --- asset/shader/{ => common}/brdf.glsl | 0 asset/shader/{ => common}/punctual.glsl | 0 asset/shader/common/sampling.glsl | 39 ++++++++++++++ .../{ibl_brdf_lut.frag => ibl/brdf_lut.frag} | 39 ++------------ .../{ibl_brdf_lut.vert => ibl/brdf_lut.vert} | 0 .../{ibl_cubemap.vert => ibl/cubemap.vert} | 0 .../equirect_to_cubemap.frag} | 0 .../irradiance.frag} | 0 .../prefilter.frag} | 53 ++----------------- source/paimon/utility/brdf_lut_pass.cpp | 4 +- .../equirectangular_to_cubemap_pass.cpp | 4 +- source/paimon/utility/irradiance_map_pass.cpp | 4 +- .../paimon/utility/prefiltered_map_pass.cpp | 4 +- 13 files changed, 54 insertions(+), 93 deletions(-) rename asset/shader/{ => common}/brdf.glsl (100%) rename asset/shader/{ => common}/punctual.glsl (100%) create mode 100644 asset/shader/common/sampling.glsl rename asset/shader/{ibl_brdf_lut.frag => ibl/brdf_lut.frag} (58%) rename asset/shader/{ibl_brdf_lut.vert => ibl/brdf_lut.vert} (100%) rename asset/shader/{ibl_cubemap.vert => ibl/cubemap.vert} (100%) rename asset/shader/{ibl_equirect_to_cubemap.frag => ibl/equirect_to_cubemap.frag} (100%) rename asset/shader/{ibl_irradiance.frag => ibl/irradiance.frag} (100%) rename asset/shader/{ibl_prefilter.frag => ibl/prefilter.frag} (54%) diff --git a/asset/shader/brdf.glsl b/asset/shader/common/brdf.glsl similarity index 100% rename from asset/shader/brdf.glsl rename to asset/shader/common/brdf.glsl diff --git a/asset/shader/punctual.glsl b/asset/shader/common/punctual.glsl similarity index 100% rename from asset/shader/punctual.glsl rename to asset/shader/common/punctual.glsl diff --git a/asset/shader/common/sampling.glsl b/asset/shader/common/sampling.glsl new file mode 100644 index 0000000..a4d096d --- /dev/null +++ b/asset/shader/common/sampling.glsl @@ -0,0 +1,39 @@ +// IBL precomputation sampling utilities +// Requires: PI (defined in common/brdf.glsl) + +// Hammersley quasi-random low-discrepancy sequence +float RadicalInverse_VdC(uint bits) { + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; +} + +vec2 Hammersley(uint i, uint N) { + return vec2(float(i)/float(N), RadicalInverse_VdC(i)); +} + +// GGX importance sampling for IBL precomputation +vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) { + float a = roughness * roughness; + + float phi = 2.0 * PI * Xi.x; + float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y)); + float sinTheta = sqrt(1.0 - cosTheta*cosTheta); + + // From spherical coordinates to cartesian coordinates + vec3 H; + H.x = cos(phi) * sinTheta; + H.y = sin(phi) * sinTheta; + H.z = cosTheta; + + // From tangent-space vector to world-space sample vector + vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangent = normalize(cross(up, N)); + vec3 bitangent = cross(N, tangent); + + vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z; + return normalize(sampleVec); +} diff --git a/asset/shader/ibl_brdf_lut.frag b/asset/shader/ibl/brdf_lut.frag similarity index 58% rename from asset/shader/ibl_brdf_lut.frag rename to asset/shader/ibl/brdf_lut.frag index 76f6fbd..8465923 100644 --- a/asset/shader/ibl_brdf_lut.frag +++ b/asset/shader/ibl/brdf_lut.frag @@ -1,43 +1,12 @@ #version 460 core +#include +#include + layout(location = 0) in vec2 v_texcoord; layout(location = 0) out vec2 FragColor; -const float PI = 3.14159265359; - -float RadicalInverse_VdC(uint bits) { - bits = (bits << 16u) | (bits >> 16u); - bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); - bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); - bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); - bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); - return float(bits) * 2.3283064365386963e-10; -} - -vec2 Hammersley(uint i, uint N) { - return vec2(float(i)/float(N), RadicalInverse_VdC(i)); -} - -vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) { - float a = roughness*roughness; - - float phi = 2.0 * PI * Xi.x; - float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y)); - float sinTheta = sqrt(1.0 - cosTheta*cosTheta); - - vec3 H; - H.x = cos(phi) * sinTheta; - H.y = sin(phi) * sinTheta; - H.z = cosTheta; - - vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); - vec3 tangent = normalize(cross(up, N)); - vec3 bitangent = cross(N, tangent); - - vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z; - return normalize(sampleVec); -} - +// IBL-specific geometry term: k = roughness^2 / 2 (vs. (r+1)^2/8 for direct lighting) float GeometrySchlickGGX(float NdotV, float roughness) { float a = roughness; float k = (a * a) / 2.0; diff --git a/asset/shader/ibl_brdf_lut.vert b/asset/shader/ibl/brdf_lut.vert similarity index 100% rename from asset/shader/ibl_brdf_lut.vert rename to asset/shader/ibl/brdf_lut.vert diff --git a/asset/shader/ibl_cubemap.vert b/asset/shader/ibl/cubemap.vert similarity index 100% rename from asset/shader/ibl_cubemap.vert rename to asset/shader/ibl/cubemap.vert diff --git a/asset/shader/ibl_equirect_to_cubemap.frag b/asset/shader/ibl/equirect_to_cubemap.frag similarity index 100% rename from asset/shader/ibl_equirect_to_cubemap.frag rename to asset/shader/ibl/equirect_to_cubemap.frag diff --git a/asset/shader/ibl_irradiance.frag b/asset/shader/ibl/irradiance.frag similarity index 100% rename from asset/shader/ibl_irradiance.frag rename to asset/shader/ibl/irradiance.frag diff --git a/asset/shader/ibl_prefilter.frag b/asset/shader/ibl/prefilter.frag similarity index 54% rename from asset/shader/ibl_prefilter.frag rename to asset/shader/ibl/prefilter.frag index 6edbd1d..1546ad0 100644 --- a/asset/shader/ibl_prefilter.frag +++ b/asset/shader/ibl/prefilter.frag @@ -1,5 +1,8 @@ #version 460 core +#include +#include + layout(location = 0) in vec3 v_localPos; layout(location = 0) out vec4 FragColor; @@ -10,56 +13,6 @@ layout(binding = 1, std140) uniform PrefilteredParams { float u_envMapResolution; }; -const float PI = 3.14159265359; - -float DistributionGGX(vec3 N, vec3 H, float roughness) { - float a = roughness * roughness; - float a2 = a * a; - float NdotH = max(dot(N, H), 0.0); - float NdotH2 = NdotH * NdotH; - - float nom = a2; - float denom = (NdotH2 * (a2 - 1.0) + 1.0); - denom = PI * denom * denom; - - return nom / denom; -} - -float RadicalInverse_VdC(uint bits) { - bits = (bits << 16u) | (bits >> 16u); - bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); - bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); - bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); - bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); - return float(bits) * 2.3283064365386963e-10; // / 0x100000000 -} - -vec2 Hammersley(uint i, uint N) { - return vec2(float(i)/float(N), RadicalInverse_VdC(i)); -} - -vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) { - float a = roughness * roughness; - - float phi = 2.0 * PI * Xi.x; - float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y)); - float sinTheta = sqrt(1.0 - cosTheta*cosTheta); - - // From spherical coordinates to cartesian coordinates - vec3 H; - H.x = cos(phi) * sinTheta; - H.y = sin(phi) * sinTheta; - H.z = cosTheta; - - // From tangent-space vector to world-space sample vector - vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); - vec3 tangent = normalize(cross(up, N)); - vec3 bitangent = cross(N, tangent); - - vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z; - return normalize(sampleVec); -} - void main() { vec3 N = normalize(v_localPos); vec3 R = N; diff --git a/source/paimon/utility/brdf_lut_pass.cpp b/source/paimon/utility/brdf_lut_pass.cpp index bf9e698..63e023a 100644 --- a/source/paimon/utility/brdf_lut_pass.cpp +++ b/source/paimon/utility/brdf_lut_pass.cpp @@ -18,8 +18,8 @@ BRDFLUTPass::BRDFLUTPass(RenderContext &renderContext) // Get shader programs auto &shaderManager = Application::getInstance().getShaderManager(); - auto *vertex_program = shaderManager.createShaderProgram("ibl_brdf_lut.vert"); - auto *fragment_program = shaderManager.createShaderProgram("ibl_brdf_lut.frag"); + auto *vertex_program = shaderManager.createShaderProgram("brdf_lut.vert"); + auto *fragment_program = shaderManager.createShaderProgram("brdf_lut.frag"); if (!vertex_program || !fragment_program) { LOG_ERROR("Failed to load BRDF LUT shader programs"); diff --git a/source/paimon/utility/equirectangular_to_cubemap_pass.cpp b/source/paimon/utility/equirectangular_to_cubemap_pass.cpp index 9dc018b..4821d76 100644 --- a/source/paimon/utility/equirectangular_to_cubemap_pass.cpp +++ b/source/paimon/utility/equirectangular_to_cubemap_pass.cpp @@ -18,8 +18,8 @@ EquirectangularToCubemapPass::EquirectangularToCubemapPass(RenderContext &render // Get shader programs auto &shaderManager = Application::getInstance().getShaderManager(); - auto *vertex_program = shaderManager.createShaderProgram("ibl_cubemap.vert"); - auto *fragment_program = shaderManager.createShaderProgram("ibl_equirect_to_cubemap.frag"); + auto *vertex_program = shaderManager.createShaderProgram("cubemap.vert"); + auto *fragment_program = shaderManager.createShaderProgram("equirect_to_cubemap.frag"); if (!vertex_program || !fragment_program) { LOG_ERROR("Failed to load equirectangular to cubemap shader programs"); diff --git a/source/paimon/utility/irradiance_map_pass.cpp b/source/paimon/utility/irradiance_map_pass.cpp index ce885ea..4d5294e 100644 --- a/source/paimon/utility/irradiance_map_pass.cpp +++ b/source/paimon/utility/irradiance_map_pass.cpp @@ -25,8 +25,8 @@ IrradianceMapPass::IrradianceMapPass(RenderContext &renderContext) // Get shader programs auto &shaderManager = Application::getInstance().getShaderManager(); - auto *vertex_program = shaderManager.createShaderProgram("ibl_cubemap.vert"); - auto *fragment_program = shaderManager.createShaderProgram("ibl_irradiance.frag"); + auto *vertex_program = shaderManager.createShaderProgram("cubemap.vert"); + auto *fragment_program = shaderManager.createShaderProgram("irradiance.frag"); if (!vertex_program || !fragment_program) { LOG_ERROR("Failed to load irradiance map shader programs"); diff --git a/source/paimon/utility/prefiltered_map_pass.cpp b/source/paimon/utility/prefiltered_map_pass.cpp index f309a9f..30fe512 100644 --- a/source/paimon/utility/prefiltered_map_pass.cpp +++ b/source/paimon/utility/prefiltered_map_pass.cpp @@ -30,9 +30,9 @@ PrefilteredMapPass::PrefilteredMapPass(RenderContext &renderContext) // Get shader programs auto &shaderManager = Application::getInstance().getShaderManager(); - auto *vertex_program = shaderManager.createShaderProgram("ibl_cubemap.vert"); + auto *vertex_program = shaderManager.createShaderProgram("cubemap.vert"); auto *fragment_program = - shaderManager.createShaderProgram("ibl_prefilter.frag"); + shaderManager.createShaderProgram("prefilter.frag"); if (!vertex_program || !fragment_program) { LOG_ERROR("Failed to load prefiltered map shader programs"); From c39fff5361fddb68d40fa3ad390f07bb6a2fca7e Mon Sep 17 00:00:00 2001 From: jyxiong Date: Mon, 2 Mar 2026 14:51:10 +0800 Subject: [PATCH 4/5] feat: add IBL --- asset/shader/common/brdf.glsl | 75 ++++--------------- asset/shader/common/ibl.glsl | 41 ++++++++++ asset/shader/common/lighting.glsl | 26 ------- asset/shader/common/pbr.glsl | 37 +++++++++ asset/shader/common/punctual.glsl | 40 ++++++++++ asset/shader/damaged_helmet.frag | 26 +++++-- asset/shader/ibl/brdf_lut.frag | 24 +----- .../rendering/render_pass/color_pass.cpp | 30 ++++++++ .../paimon/rendering/render_pass/color_pass.h | 9 ++- 9 files changed, 195 insertions(+), 113 deletions(-) create mode 100644 asset/shader/common/ibl.glsl delete mode 100644 asset/shader/common/lighting.glsl create mode 100644 asset/shader/common/pbr.glsl diff --git a/asset/shader/common/brdf.glsl b/asset/shader/common/brdf.glsl index 804f53c..455a4f2 100644 --- a/asset/shader/common/brdf.glsl +++ b/asset/shader/common/brdf.glsl @@ -3,10 +3,17 @@ const float PI = 3.14159265359; -// Fresnel-Schlick approximation +// Fresnel-Schlick (general form, roughness clamps the max reflectance for IBL) +vec3 fresnelSchlick(float cosTheta, vec3 F0, float roughness) +{ + vec3 Fmax = max(vec3(1.0 - roughness), F0); + return F0 + (Fmax - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); +} + +// Convenience overload for direct lighting (roughness = 0) vec3 fresnelSchlick(float cosTheta, vec3 F0) { - return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); + return fresnelSchlick(cosTheta, F0, 0.0); } // GGX/Trowbridge-Reitz normal distribution function @@ -24,62 +31,12 @@ float DistributionGGX(vec3 N, vec3 H, float roughness) return nom / max(denom, 0.0001); } -// Schlick-GGX geometry function (single direction) -float GeometrySchlickGGX(float NdotV, float roughness) -{ - float r = (roughness + 1.0); - float k = (r * r) / 8.0; - - float nom = NdotV; - float denom = NdotV * (1.0 - k) + k; - - return nom / max(denom, 0.0001); -} - -// Smith's method for geometry obstruction -float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) +// Height-Correlated Smith GGX visibility term (Heitz 2014, used by Filament/Unity HDRP) +// Returns V = G / (4 * NdotV * NdotL) — denominator already incorporated +float V_SmithGGXCorrelated(float NdotV, float NdotL, float roughness) { - float NdotV = max(dot(N, V), 0.0); - float NdotL = max(dot(N, L), 0.0); - float ggx2 = GeometrySchlickGGX(NdotV, roughness); - float ggx1 = GeometrySchlickGGX(NdotL, roughness); - - return ggx1 * ggx2; -} - -// Calculate PBR lighting for a single light source -// Returns the contribution from this light -vec3 calculatePBRLighting( - vec3 N, // Normal - vec3 V, // View direction - vec3 L, // Light direction - vec3 albedo, // Base color - float metallic, // Metallic value - float roughness, // Roughness value - vec3 radiance // Light color/intensity -) -{ - vec3 H = normalize(V + L); - - // Calculate reflectance at normal incidence - vec3 F0 = vec3(0.04); - F0 = mix(F0, albedo, metallic); - - // Cook-Torrance BRDF - float NDF = DistributionGGX(N, H, roughness); - float G = GeometrySmith(N, V, L, roughness); - vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); - - vec3 numerator = NDF * G * F; - float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001; - vec3 specular = numerator / denominator; - - // Energy conservation: kS + kD = 1.0 - vec3 kS = F; - vec3 kD = vec3(1.0) - kS; - kD *= 1.0 - metallic; // Metallic surfaces don't have diffuse - - float NdotL = max(dot(N, L), 0.0); - - return (kD * albedo / PI + specular) * radiance * NdotL; + float a2 = roughness * roughness * roughness * roughness; + float GGXV = NdotL * sqrt(NdotV * NdotV * (1.0 - a2) + a2); + float GGXL = NdotV * sqrt(NdotL * NdotL * (1.0 - a2) + a2); + return 0.5 / max(GGXV + GGXL, 0.0001); } diff --git a/asset/shader/common/ibl.glsl b/asset/shader/common/ibl.glsl new file mode 100644 index 0000000..8a00019 --- /dev/null +++ b/asset/shader/common/ibl.glsl @@ -0,0 +1,41 @@ +// IBL (Image-Based Lighting) ambient contribution +// Split-sum approximation (Karis 2013) + +#include + +// Calculate IBL ambient contribution (diffuse + specular) +// irradianceMap : convolved diffuse irradiance cubemap +// prefilteredMap : prefiltered specular cubemap (mips = roughness levels) +// brdfLUT : precomputed split-sum BRDF LUT (R=scale, G=bias) +vec3 calculateIBLLighting( + vec3 N, + vec3 V, + vec3 albedo, + float metallic, + float roughness, + float ao, + samplerCube irradianceMap, + samplerCube prefilteredMap, + sampler2D brdfLUT +) +{ + float NdotV = max(dot(N, V), 0.0); + + vec3 F0 = mix(vec3(0.04), albedo, metallic); + vec3 F = fresnelSchlick(NdotV, F0, roughness); + + vec3 kD = (1.0 - F) * (1.0 - metallic); + + // Diffuse IBL + vec3 irradiance = texture(irradianceMap, N).rgb; + vec3 diffuse = kD * irradiance * albedo; + + // Specular IBL (split-sum) + vec3 R = reflect(-V, N); + const float MAX_LOD = 4.0; // must match prefilter mip count + vec3 prefilteredColor = textureLod(prefilteredMap, R, roughness * MAX_LOD).rgb; + vec2 brdf = texture(brdfLUT, vec2(NdotV, roughness)).rg; + vec3 specular = prefilteredColor * (F * brdf.x + brdf.y); + + return (diffuse + specular) * ao; +} diff --git a/asset/shader/common/lighting.glsl b/asset/shader/common/lighting.glsl deleted file mode 100644 index e7531b6..0000000 --- a/asset/shader/common/lighting.glsl +++ /dev/null @@ -1,26 +0,0 @@ -// Common lighting functions - -#ifndef LIGHTING_GLSL -#define LIGHTING_GLSL - -struct Light { - vec3 position; - vec3 color; - float intensity; -}; - -vec3 calculateLighting(vec3 normal, vec3 fragPos, Light light) { - vec3 lightDir = normalize(light.position - fragPos); - float diff = max(dot(normal, lightDir), 0.0); - return light.color * light.intensity * diff; -} - -#ifdef ENABLE_SHADOWS -float calculateShadow(vec3 fragPos, Light light) { - // Simple shadow calculation - float distance = length(light.position - fragPos); - return smoothstep(0.0, 1.0, distance / 10.0); -} -#endif - -#endif // LIGHTING_GLSL diff --git a/asset/shader/common/pbr.glsl b/asset/shader/common/pbr.glsl new file mode 100644 index 0000000..7e3d52b --- /dev/null +++ b/asset/shader/common/pbr.glsl @@ -0,0 +1,37 @@ +// PBR lighting calculation +// Combines BRDF functions with light contribution + +#include + +// Calculate PBR lighting for a single light source +// Returns the contribution from this light +vec3 calculatePBRLighting( + vec3 N, // Normal + vec3 V, // View direction + vec3 L, // Light direction + vec3 albedo, // Base color + float metallic, // Metallic value + float roughness, // Roughness value + vec3 radiance // Light color/intensity +) +{ + vec3 H = normalize(V + L); + + vec3 F0 = mix(vec3(0.04), albedo, metallic); + + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + + // Cook-Torrance BRDF + // V_SmithGGXCorrelated already includes the 4*NdotV*NdotL denominator + float NDF = DistributionGGX(N, H, roughness); + float Vis = V_SmithGGXCorrelated(NdotV, NdotL, roughness); + vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); + + vec3 specular = NDF * Vis * F; + + // Energy conservation: kS + kD = 1.0 + vec3 kD = (vec3(1.0) - F) * (1.0 - metallic); + + return (kD * albedo / PI + specular) * radiance * NdotL; +} diff --git a/asset/shader/common/punctual.glsl b/asset/shader/common/punctual.glsl index 31d6f8b..737ff00 100644 --- a/asset/shader/common/punctual.glsl +++ b/asset/shader/common/punctual.glsl @@ -1,5 +1,7 @@ // Punctual lights support (point lights and spot lights) +#include + // Light types const int LIGHT_TYPE_DIRECTIONAL = 0; const int LIGHT_TYPE_POINT = 1; @@ -92,3 +94,41 @@ vec3 calculateRadiance(PunctualLight light, vec3 fragPosition, vec3 L) return vec3(0.0); // Unknown light type } + +// Calculate PBR lighting for a single light source +// Returns the contribution from this light +vec3 calculatePBRLighting( + vec3 N, // Normal + vec3 V, // View direction + vec3 L, // Light direction + vec3 albedo, // Base color + float metallic, // Metallic value + float roughness, // Roughness value + vec3 radiance // Light color/intensity +) +{ + vec3 H = normalize(V + L); + + // Calculate reflectance at normal incidence + vec3 F0 = vec3(0.04); + F0 = mix(F0, albedo, metallic); + + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + + // Cook-Torrance BRDF + // V_SmithGGXCorrelated already includes the 4*NdotV*NdotL denominator + float NDF = DistributionGGX(N, H, roughness); + float Vis = V_SmithGGXCorrelated(NdotV, NdotL, roughness); + vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); + + vec3 specular = NDF * Vis * F; + + // Energy conservation: kS + kD = 1.0 + vec3 kS = F; + vec3 kD = vec3(1.0) - kS; + kD *= 1.0 - metallic; // Metallic surfaces don't have diffuse + + return (kD * albedo / PI + specular) * radiance * NdotL; +} + diff --git a/asset/shader/damaged_helmet.frag b/asset/shader/damaged_helmet.frag index 2b48ce0..51a1981 100644 --- a/asset/shader/damaged_helmet.frag +++ b/asset/shader/damaged_helmet.frag @@ -1,7 +1,8 @@ #version 460 core -#include -#include +#include +#include +#include in vec3 v_position; in vec3 v_normal; @@ -16,6 +17,11 @@ layout(binding = 2) uniform sampler2D u_normalTexture; layout(binding = 3) uniform sampler2D u_emissiveTexture; layout(binding = 4) uniform sampler2D u_occlusionTexture; +// IBL textures +layout(binding = 5) uniform samplerCube u_irradianceMap; +layout(binding = 6) uniform samplerCube u_prefilteredMap; +layout(binding = 7) uniform sampler2D u_brdfLUT; + // UBO for camera layout(std140, binding = 1) uniform CameraUBO { @@ -42,6 +48,13 @@ layout(std140, binding = 3) uniform LightingUBO PunctualLight lights[32]; // Maximum 32 lights } u_lighting; +// IBL / tone-mapping parameters +layout(std140, binding = 4) uniform EnvironmentUBO +{ + float u_iblIntensity; // scales IBL contribution + float _padding[2]; +} u_env; + void main() { // Sample textures @@ -68,10 +81,13 @@ void main() Lo += calculatePBRLighting(N, V, L, baseColor.rgb, metallic, roughness, radiance); } - // Ambient lighting (simplified) - vec3 ambient = vec3(0.03) * baseColor.rgb * ao; + // IBL ambient + vec3 ambient = calculateIBLLighting(N, V, baseColor.rgb, metallic, roughness, ao, + u_irradianceMap, u_prefilteredMap, u_brdfLUT) + * u_env.u_iblIntensity; - vec3 color = ambient + Lo + emissive; + // vec3 color = ambient + Lo + emissive; + vec3 color = Lo; // For debugging, just show direct lighting // Tone mapping (simple Reinhard) color = color / (color + vec3(1.0)); diff --git a/asset/shader/ibl/brdf_lut.frag b/asset/shader/ibl/brdf_lut.frag index 8465923..6a7bc3d 100644 --- a/asset/shader/ibl/brdf_lut.frag +++ b/asset/shader/ibl/brdf_lut.frag @@ -6,26 +6,6 @@ layout(location = 0) in vec2 v_texcoord; layout(location = 0) out vec2 FragColor; -// IBL-specific geometry term: k = roughness^2 / 2 (vs. (r+1)^2/8 for direct lighting) -float GeometrySchlickGGX(float NdotV, float roughness) { - float a = roughness; - float k = (a * a) / 2.0; - - float nom = NdotV; - float denom = NdotV * (1.0 - k) + k; - - return nom / denom; -} - -float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { - float NdotV = max(dot(N, V), 0.0); - float NdotL = max(dot(N, L), 0.0); - float ggx2 = GeometrySchlickGGX(NdotV, roughness); - float ggx1 = GeometrySchlickGGX(NdotL, roughness); - - return ggx1 * ggx2; -} - vec2 IntegrateBRDF(float NdotV, float roughness) { vec3 V; V.x = sqrt(1.0 - NdotV*NdotV); @@ -48,8 +28,8 @@ vec2 IntegrateBRDF(float NdotV, float roughness) { float VdotH = max(dot(V, H), 0.0); if(NdotL > 0.0) { - float G = GeometrySmith(N, V, L, roughness); - float G_Vis = (G * VdotH) / (NdotH * NdotV); + float V = V_SmithGGXCorrelated(NdotV, NdotL, roughness); + float G_Vis = V * 4.0 * NdotL * VdotH / NdotH; float Fc = pow(1.0 - VdotH, 5.0); A += (1.0 - Fc) * G_Vis; diff --git a/source/paimon/rendering/render_pass/color_pass.cpp b/source/paimon/rendering/render_pass/color_pass.cpp index f842dad..b986752 100644 --- a/source/paimon/rendering/render_pass/color_pass.cpp +++ b/source/paimon/rendering/render_pass/color_pass.cpp @@ -23,6 +23,14 @@ ColorPass::ColorPass(RenderContext &renderContext) m_sampler->set(GL_TEXTURE_WRAP_S, GL_REPEAT); m_sampler->set(GL_TEXTURE_WRAP_T, GL_REPEAT); + // Setup sampler for IBL cubemaps + m_ibl_sampler = std::make_unique(); + m_ibl_sampler->set(GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + m_ibl_sampler->set(GL_TEXTURE_MAG_FILTER, GL_LINEAR); + m_ibl_sampler->set(GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + m_ibl_sampler->set(GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + m_ibl_sampler->set(GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + // Get shader programs for main rendering (separable programs for pipeline) auto &shaderManager = Application::getInstance().getShaderManager(); @@ -73,6 +81,8 @@ ColorPass::ColorPass(RenderContext &renderContext) // Allocate space for lighting UBO with fixed maximum lights m_lighting_ubo.set_storage(sizeof(LightingUBO), nullptr, GL_DYNAMIC_STORAGE_BIT); + m_environment_ubo.set_storage(sizeof(EnvironmentUBO), nullptr, + GL_DYNAMIC_STORAGE_BIT); } void ColorPass::draw(RenderContext &ctx, const glm::ivec2 &resolution, @@ -288,10 +298,30 @@ void ColorPass::draw(RenderContext &ctx, const glm::ivec2 &resolution, } } + // Bind IBL textures (bindings 5/6/7 match shader layout) + auto envView = scene.view(); + for (auto [envEntity, env] : envView.each()) { + EnvironmentUBO envData; + envData.iblIntensity = env.intensity; + m_environment_ubo.set_sub_data(0, sizeof(EnvironmentUBO), &envData); + + if (env.irradianceMap) { + ctx.bindTexture(5, *env.irradianceMap, *m_ibl_sampler); + } + if (env.prefilteredMap) { + ctx.bindTexture(6, *env.prefilteredMap, *m_ibl_sampler); + } + if (env.brdfLUT) { + ctx.bindTexture(7, *env.brdfLUT, *m_sampler); + } + break; // Only one environment + } + ctx.bindUniformBuffer(0, m_transform_ubo); ctx.bindUniformBuffer(1, m_camera_ubo); ctx.bindUniformBuffer(2, m_material_ubo); ctx.bindUniformBuffer(3, m_lighting_ubo); + ctx.bindUniformBuffer(4, m_environment_ubo); // Draw the primitive if (primitive.hasIndices()) { diff --git a/source/paimon/rendering/render_pass/color_pass.h b/source/paimon/rendering/render_pass/color_pass.h index 67ab8aa..fb38879 100644 --- a/source/paimon/rendering/render_pass/color_pass.h +++ b/source/paimon/rendering/render_pass/color_pass.h @@ -53,6 +53,11 @@ struct MaterialUBO { float _padding[3]; // alignment }; +struct EnvironmentUBO { + float iblIntensity; // scales IBL contribution (ecs::Environment::intensity) + float _padding[2]; +}; + class ColorPass { public: ColorPass(RenderContext &renderContext); @@ -68,13 +73,15 @@ class ColorPass { std::unique_ptr m_depth_texture; std::unique_ptr m_sampler; + std::unique_ptr m_ibl_sampler; // Cubemap sampler for IBL textures std::unique_ptr m_pipeline; // Uniform buffers Buffer m_transform_ubo; Buffer m_camera_ubo; - Buffer m_lighting_ubo; // UBO for lighting + Buffer m_lighting_ubo; Buffer m_material_ubo; + Buffer m_environment_ubo; }; } \ No newline at end of file From 75fcdab042db12fdae0affafb29ac73e23bdf429 Mon Sep 17 00:00:00 2001 From: jyxiong Date: Tue, 3 Mar 2026 16:23:26 +0800 Subject: [PATCH 5/5] fix --- asset/shader/common/ibl.glsl | 2 +- asset/shader/common/pbr.glsl | 37 ------- asset/shader/common/punctual.glsl | 1 - asset/shader/damaged_helmet.frag | 13 +-- source/paimon/core/io/ibl.cpp | 103 +++++++++++++++++- source/paimon/core/io/ibl.h | 5 + .../rendering/render_pass/color_pass.cpp | 3 +- .../paimon/rendering/render_pass/color_pass.h | 5 +- source/paimon/utility/irradiance_map_pass.cpp | 2 +- .../paimon/utility/prefiltered_map_pass.cpp | 2 +- 10 files changed, 119 insertions(+), 54 deletions(-) delete mode 100644 asset/shader/common/pbr.glsl diff --git a/asset/shader/common/ibl.glsl b/asset/shader/common/ibl.glsl index 8a00019..269afae 100644 --- a/asset/shader/common/ibl.glsl +++ b/asset/shader/common/ibl.glsl @@ -32,7 +32,7 @@ vec3 calculateIBLLighting( // Specular IBL (split-sum) vec3 R = reflect(-V, N); - const float MAX_LOD = 4.0; // must match prefilter mip count + const float MAX_LOD = 5.0; // must match prefilter mip count vec3 prefilteredColor = textureLod(prefilteredMap, R, roughness * MAX_LOD).rgb; vec2 brdf = texture(brdfLUT, vec2(NdotV, roughness)).rg; vec3 specular = prefilteredColor * (F * brdf.x + brdf.y); diff --git a/asset/shader/common/pbr.glsl b/asset/shader/common/pbr.glsl deleted file mode 100644 index 7e3d52b..0000000 --- a/asset/shader/common/pbr.glsl +++ /dev/null @@ -1,37 +0,0 @@ -// PBR lighting calculation -// Combines BRDF functions with light contribution - -#include - -// Calculate PBR lighting for a single light source -// Returns the contribution from this light -vec3 calculatePBRLighting( - vec3 N, // Normal - vec3 V, // View direction - vec3 L, // Light direction - vec3 albedo, // Base color - float metallic, // Metallic value - float roughness, // Roughness value - vec3 radiance // Light color/intensity -) -{ - vec3 H = normalize(V + L); - - vec3 F0 = mix(vec3(0.04), albedo, metallic); - - float NdotV = max(dot(N, V), 0.0); - float NdotL = max(dot(N, L), 0.0); - - // Cook-Torrance BRDF - // V_SmithGGXCorrelated already includes the 4*NdotV*NdotL denominator - float NDF = DistributionGGX(N, H, roughness); - float Vis = V_SmithGGXCorrelated(NdotV, NdotL, roughness); - vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); - - vec3 specular = NDF * Vis * F; - - // Energy conservation: kS + kD = 1.0 - vec3 kD = (vec3(1.0) - F) * (1.0 - metallic); - - return (kD * albedo / PI + specular) * radiance * NdotL; -} diff --git a/asset/shader/common/punctual.glsl b/asset/shader/common/punctual.glsl index 737ff00..f5d1e82 100644 --- a/asset/shader/common/punctual.glsl +++ b/asset/shader/common/punctual.glsl @@ -18,7 +18,6 @@ struct PunctualLight float intensity; float innerConeAngle; float outerConeAngle; - vec2 _padding; }; // Calculate attenuation for point lights and spot lights diff --git a/asset/shader/damaged_helmet.frag b/asset/shader/damaged_helmet.frag index 51a1981..8a9d892 100644 --- a/asset/shader/damaged_helmet.frag +++ b/asset/shader/damaged_helmet.frag @@ -1,6 +1,5 @@ #version 460 core -#include #include #include @@ -37,22 +36,20 @@ layout(std140, binding = 2) uniform MaterialUBO vec3 emissiveFactor; float metallicFactor; float roughnessFactor; - float _padding[3]; // alignment } u_material; // UBO for all lighting (fixed maximum size) layout(std140, binding = 3) uniform LightingUBO { int lightCount; - int _padding[3]; PunctualLight lights[32]; // Maximum 32 lights } u_lighting; // IBL / tone-mapping parameters layout(std140, binding = 4) uniform EnvironmentUBO { - float u_iblIntensity; // scales IBL contribution - float _padding[2]; + mat4 rotation; // for rotating the environment map + float intensity; // scales IBL contribution } u_env; void main() @@ -82,12 +79,12 @@ void main() } // IBL ambient + N = (u_env.rotation * vec4(N, 0.0)).xyz; // Rotate normal for IBL vec3 ambient = calculateIBLLighting(N, V, baseColor.rgb, metallic, roughness, ao, u_irradianceMap, u_prefilteredMap, u_brdfLUT) - * u_env.u_iblIntensity; + * u_env.intensity; - // vec3 color = ambient + Lo + emissive; - vec3 color = Lo; // For debugging, just show direct lighting + vec3 color = ambient + Lo + emissive; // Tone mapping (simple Reinhard) color = color / (color + vec3(1.0)); diff --git a/source/paimon/core/io/ibl.cpp b/source/paimon/core/io/ibl.cpp index a1e5745..ab5d61d 100644 --- a/source/paimon/core/io/ibl.cpp +++ b/source/paimon/core/io/ibl.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "paimon/core/ecs/components.h" #include "paimon/core/log_system.h" @@ -62,11 +63,32 @@ void IBLLoader::load(ecs::Scene &scene) { LOG_INFO("IBLLoader: environment textures loaded from '{}'", m_filepath.string()); + +#ifdef PAIMON_DEBUG + // save((std::filesystem::path(PAIMON_TEXTURE_DIR) / "ibl_output")); +#endif } -void IBLLoader::loadHDRTexture() { - stbi_set_flip_vertically_on_load(true); +void IBLLoader::save(const std::filesystem::path &directory) { + // Ensure output directory exists + std::filesystem::create_directories(directory); + + // Save cubemap faces + saveCubemap(m_equirectToCubemapPass->getCubemap(), + directory / "env_cubemap", defaultCubemapSize, 0); + saveCubemap(*m_irradianceMapPass->getIrradianceMap(), + directory / "irradiance_map", defaultIrradianceSize, 0); + for (uint32_t mip = 0; mip < defaultPrefilteredMipLevels; ++mip) { + uint32_t mipSize = defaultPrefilteredSize >> mip; + saveCubemap(*m_prefilteredMapPass->getPrefilteredMap(), + directory / "prefiltered_map", mipSize, mip); + } + // Save BRDF LUT + save2DTexture(*m_brdfLUTPass->getBRDFLUT(), directory / "brdf_lut.hdr", + defaultBRDFLUTSize, defaultBRDFLUTSize); +} +void IBLLoader::loadHDRTexture() { int width = 0, height = 0, nrChannels = 0; float *data = stbi_loadf(m_filepath.string().c_str(), &width, &height, &nrChannels, 0); @@ -105,3 +127,80 @@ void IBLLoader::processIBL() { defaultPrefilteredMipLevels); m_brdfLUTPass->execute(defaultBRDFLUTSize); } + +void IBLLoader::saveCubemap(const Texture &cubemap, const std::filesystem::path &basePath, + int size, int mipLevel) { + const char *faceNames[6] = { + "px", "nx", // +X, -X + "py", "ny", // +Y, -Y + "pz", "nz" // +Z, -Z + }; + + std::vector pixels(size * size * 3); + + for (int face = 0; face < 6; ++face) { + // For cubemap textures, glGetTextureSubImage treats them as an array of 6 + // slices zoffset is the face index (0-5), depth is the number of faces to + // access (1) + glGetTextureSubImage(cubemap.get_name(), + mipLevel, // level + 0, 0, + face, // xoffset, yoffset, zoffset (face index: 0-5) + size, size, 1, // width, height, depth (1 face) + GL_RGB, // format + GL_FLOAT, // type + pixels.size() * sizeof(float), // bufSize + pixels.data() // pixels + ); + + // Save to HDR file (no tone mapping, preserve full HDR range) + std::string filename = basePath.string(); + if (mipLevel > 0) { + filename += "_mip" + std::to_string(mipLevel); + } + filename += "_"; + filename += faceNames[face]; + filename += ".hdr"; + + stbi_flip_vertically_on_write(1); + if (!stbi_write_hdr(filename.c_str(), size, size, 3, pixels.data())) { + LOG_ERROR(" Failed to save: {}", filename); + } + } +} + +void IBLLoader::save2DTexture(const Texture &texture, const std::filesystem::path &filepath, + int width, int height) { + std::vector pixels(width * height * 2); // RG format + + // Read texture data + glGetTextureSubImage(texture.get_name(), + 0, // level + 0, 0, 0, // xoffset, yoffset, zoffset + width, height, 1, // width, height, depth + GL_RG, // format (BRDF LUT is RG16F) + GL_FLOAT, // type + pixels.size() * sizeof(float), // bufSize + pixels.data() // pixels + ); + + // Convert RG to RGB for HDR file (add blue channel = 0) + std::vector pixelsRGB(width * height * 3); + for (int i = 0; i < width * height; ++i) { + pixelsRGB[i * 3 + 0] = pixels[i * 2 + 0]; // R + pixelsRGB[i * 3 + 1] = pixels[i * 2 + 1]; // G + pixelsRGB[i * 3 + 2] = 0.0f; // B = 0 + } + + // Check data range + float maxR = 0.0f, maxG = 0.0f; + for (int i = 0; i < width * height; ++i) { + maxR = std::max(maxR, pixels[i * 2 + 0]); + maxG = std::max(maxG, pixels[i * 2 + 1]); + } + + stbi_flip_vertically_on_write(1); + if (!stbi_write_hdr(filepath.string().c_str(), width, height, 3, pixelsRGB.data())) { + LOG_ERROR(" Failed to save: {}", filepath.string()); + } +} diff --git a/source/paimon/core/io/ibl.h b/source/paimon/core/io/ibl.h index e6ee831..b5c31b4 100644 --- a/source/paimon/core/io/ibl.h +++ b/source/paimon/core/io/ibl.h @@ -31,10 +31,15 @@ class IBLLoader { IBLLoader &operator=(const IBLLoader &) = delete; void load(ecs::Scene &scene); + void save(const std::filesystem::path &directory); private: void loadHDRTexture(); void processIBL(); + void saveCubemap(const Texture &cubemap, const std::filesystem::path &basePath, + int size, int mipLevel = 0); + void save2DTexture(const Texture &texture, const std::filesystem::path &filepath, + int width, int height); private: std::filesystem::path m_filepath; diff --git a/source/paimon/rendering/render_pass/color_pass.cpp b/source/paimon/rendering/render_pass/color_pass.cpp index b986752..1d5cdbb 100644 --- a/source/paimon/rendering/render_pass/color_pass.cpp +++ b/source/paimon/rendering/render_pass/color_pass.cpp @@ -302,7 +302,8 @@ void ColorPass::draw(RenderContext &ctx, const glm::ivec2 &resolution, auto envView = scene.view(); for (auto [envEntity, env] : envView.each()) { EnvironmentUBO envData; - envData.iblIntensity = env.intensity; + envData.intensity = env.intensity; + envData.rotation = glm::mat4_cast(env.rotation); m_environment_ubo.set_sub_data(0, sizeof(EnvironmentUBO), &envData); if (env.irradianceMap) { diff --git a/source/paimon/rendering/render_pass/color_pass.h b/source/paimon/rendering/render_pass/color_pass.h index fb38879..0d233e9 100644 --- a/source/paimon/rendering/render_pass/color_pass.h +++ b/source/paimon/rendering/render_pass/color_pass.h @@ -54,8 +54,9 @@ struct MaterialUBO { }; struct EnvironmentUBO { - float iblIntensity; // scales IBL contribution (ecs::Environment::intensity) - float _padding[2]; + glm::mat4 rotation; // Rotation of the environment map, applied to IBL sampling + float intensity; // scales IBL contribution (ecs::Environment::intensity) + float _padding[3]; // alignment }; class ColorPass { diff --git a/source/paimon/utility/irradiance_map_pass.cpp b/source/paimon/utility/irradiance_map_pass.cpp index 4d5294e..9bab475 100644 --- a/source/paimon/utility/irradiance_map_pass.cpp +++ b/source/paimon/utility/irradiance_map_pass.cpp @@ -75,7 +75,7 @@ void IrradianceMapPass::execute( const Texture &envCubemap, uint32_t irradianceSize) { - m_irradianceMap->set_storage_2d(1, GL_RGB16F, irradianceSize, irradianceSize); + m_irradianceMap->set_storage_2d(1, GL_RGB32F, irradianceSize, irradianceSize); m_depthTexture->set_storage_2d(1, GL_DEPTH_COMPONENT24, irradianceSize, irradianceSize); // Prepare view matrices for 6 faces diff --git a/source/paimon/utility/prefiltered_map_pass.cpp b/source/paimon/utility/prefiltered_map_pass.cpp index 30fe512..94008c6 100644 --- a/source/paimon/utility/prefiltered_map_pass.cpp +++ b/source/paimon/utility/prefiltered_map_pass.cpp @@ -81,7 +81,7 @@ void PrefilteredMapPass::execute(const Texture &envCubemap, uint32_t prefilteredSize, uint32_t mipLevels) { // Create output prefiltered cubemap with mipmaps - m_prefilteredMap->set_storage_2d(mipLevels, GL_RGB16F, prefilteredSize, + m_prefilteredMap->set_storage_2d(mipLevels, GL_RGB32F, prefilteredSize, prefilteredSize); // Prepare view matrices for 6 faces