diff --git a/asset/texture/brown_photostudio_02_2k.hdr b/asset/texture/brown_photostudio_02_2k.hdr new file mode 100644 index 0000000..c1529ed Binary files /dev/null and b/asset/texture/brown_photostudio_02_2k.hdr differ diff --git a/source/paimon/app/application.cpp b/source/paimon/app/application.cpp index 4f4ff7f..5707db1 100644 --- a/source/paimon/app/application.cpp +++ b/source/paimon/app/application.cpp @@ -15,10 +15,11 @@ Application::Application(const ApplicationConfig& config) { // Create window m_window = Window::create(config.windowConfig, config.contextFormat); - m_scene = ecs::Scene::create(); - + // Load shaders m_shaderManager.load(PAIMON_SHADER_DIR); + m_scene = ecs::Scene::create(); + m_imguiLayer = pushLayer(std::make_unique()); pushLayer(std::make_unique()); diff --git a/source/paimon/app/panel/menu_panel.cpp b/source/paimon/app/panel/menu_panel.cpp index bb1a9c1..e70a041 100644 --- a/source/paimon/app/panel/menu_panel.cpp +++ b/source/paimon/app/panel/menu_panel.cpp @@ -48,7 +48,23 @@ void MenuPanel::showFileMenu() { if (result == NFD_OKAY) { auto& scene = Application::getInstance().getScene(); LOG_INFO("Loading model: {}", outPath); - scene.load(outPath); + scene.loadModel(outPath); + NFD_FreePathU8(outPath); + } else if (result == NFD_ERROR) { + 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()); diff --git a/source/paimon/core/ecs/scene.cpp b/source/paimon/core/ecs/scene.cpp index 39c08f9..be0a706 100644 --- a/source/paimon/core/ecs/scene.cpp +++ b/source/paimon/core/ecs/scene.cpp @@ -1,7 +1,12 @@ #include "paimon/core/ecs/scene.h" #include "paimon/core/ecs/components.h" +#include "paimon/core/ecs/entity.h" #include "paimon/core/io/gltf.h" +#include "paimon/core/io/ibl.h" +#include "paimon/config.h" +#include +#include namespace paimon { namespace ecs { @@ -22,9 +27,44 @@ Entity Scene::createEntity(const std::string &name) { } void Scene::destroyEntity(Entity entity) { - if (entity.isValid() && entity.getScene() == this) { - m_registry.destroy(entity.getHandle()); + if (!entity.isValid() || entity.getScene() != this) { + return; } + + // Destroy all descendants first to avoid leaving orphan entities. + std::vector childrenSnapshot; + if (entity.hasComponent()) { + childrenSnapshot = entity.getComponent().children; + } + + for (auto child : childrenSnapshot) { + if (child.isValid() && child.getScene() == this && child != entity) { + destroyEntity(child); + } + } + + // Remove this entity from its parent's children list. + if (entity.hasComponent()) { + auto parent = entity.getComponent().parent; + if (parent.isValid() && parent.getScene() == this && + parent.hasComponent()) { + auto &siblings = parent.getComponent().children; + siblings.erase(std::remove(siblings.begin(), siblings.end(), entity), + siblings.end()); + } + } + + if (m_mainCamera == entity) { + m_mainCamera = Entity{}; + } + if (m_directionalLight == entity) { + m_directionalLight = Entity{}; + } + if (m_environment == entity) { + m_environment = Entity{}; + } + + m_registry.destroy(entity.getHandle()); } entt::registry &Scene::getRegistry() { return m_registry; } @@ -37,10 +77,14 @@ bool Scene::valid(entt::entity entity) const { return m_registry.valid(entity); } -Entity Scene::load(const std::filesystem::path &filepath) { +void Scene::loadModel(const std::filesystem::path &filepath) { GltfLoader loader(filepath); loader.load(*this); - return loader.getRootEntity(); +} + +void Scene::loadEnvironment(const std::filesystem::path &filepath) { + IBLLoader loader(filepath); + loader.load(*this); } std::unique_ptr Scene::create() { @@ -70,11 +114,11 @@ std::unique_ptr Scene::create() { } { - // Initialize environment entity - auto environment = scene->createEntity("Environment"); - environment.addComponent(); + auto envEntity = scene->createEntity("Environment"); + envEntity.addComponent(); + scene->setEnvironment(envEntity); - scene->setEnvironment(environment); + scene->loadEnvironment(std::filesystem::path(PAIMON_TEXTURE_DIR) / "belfast_sunset_puresky_2k.hdr"); } return scene; diff --git a/source/paimon/core/ecs/scene.h b/source/paimon/core/ecs/scene.h index e2ecef9..a5dd9ee 100644 --- a/source/paimon/core/ecs/scene.h +++ b/source/paimon/core/ecs/scene.h @@ -67,7 +67,9 @@ class Scene { // Check if entity is valid bool valid(entt::entity entity) const; - Entity load(const std::filesystem::path &filepath); + void loadModel(const std::filesystem::path &filepath); + + void loadEnvironment(const std::filesystem::path &filepath); static std::unique_ptr create(); diff --git a/source/paimon/core/io/gltf.cpp b/source/paimon/core/io/gltf.cpp index 56c1d80..5103217 100644 --- a/source/paimon/core/io/gltf.cpp +++ b/source/paimon/core/io/gltf.cpp @@ -200,6 +200,8 @@ glm::mat4 parseMat4(const std::vector data) { GltfLoader::GltfLoader(const std::filesystem::path &filepath) { + m_filepath = filepath; + tinygltf::TinyGLTF loader; std::string errorMessage; std::string warningMessage; @@ -230,8 +232,14 @@ void GltfLoader::load(ecs::Scene &scene) { parseMaterials(); parseMeshes(); // Step 4: mesh primitives reference accessor Buffers + auto entityName = m_filepath.stem().string(); + auto rootEntity = scene.createEntity(entityName); + int sceneIndex = m_model.defaultScene >= 0 ? m_model.defaultScene : 0; - parseScene(m_model.scenes[sceneIndex], scene); + for (const auto nodeIndex : m_model.scenes[sceneIndex].nodes) { + const auto &node = m_model.nodes[nodeIndex]; + parseNode(node, rootEntity, scene); + } } // ============================================================================ @@ -468,12 +476,3 @@ void GltfLoader::parseNode(const tinygltf::Node &node, ecs::Entity parent, ecs:: parseNode(childNode, nodeEntity, scene); } } - -void GltfLoader::parseScene(const tinygltf::Scene &scene, ecs::Scene &ecs_scene) { - // Implementation for parsing a glTF scene into an ECS scene - m_rootEntity = ecs_scene.createEntity("RootNode"); - for (const auto nodeIndex : scene.nodes) { - const auto &node = m_model.nodes[nodeIndex]; - parseNode(node, m_rootEntity, ecs_scene); - } -} diff --git a/source/paimon/core/io/gltf.h b/source/paimon/core/io/gltf.h index b00c05f..7a4784d 100644 --- a/source/paimon/core/io/gltf.h +++ b/source/paimon/core/io/gltf.h @@ -31,10 +31,6 @@ class GltfLoader { void load(ecs::Scene &scene); - const ecs::Entity &getRootEntity() const { return m_rootEntity; } - - ecs::Entity getRootEntity() { return m_rootEntity; } - private: void parseBuffers(); void parseBufferViews(); @@ -44,12 +40,11 @@ class GltfLoader { void parseMeshes(); void parseNode(const tinygltf::Node &node, ecs::Entity parent, ecs::Scene &scene); - void parseScene(const tinygltf::Scene &scene, ecs::Scene &ecs_scene); private: - tinygltf::Model m_model; + std::filesystem::path m_filepath; - ecs::Entity m_rootEntity; + tinygltf::Model m_model; std::vector> m_samplers; std::vector> m_images; diff --git a/source/paimon/core/io/ibl.cpp b/source/paimon/core/io/ibl.cpp index ab5d61d..b189c4b 100644 --- a/source/paimon/core/io/ibl.cpp +++ b/source/paimon/core/io/ibl.cpp @@ -5,6 +5,7 @@ #include #include "paimon/core/ecs/components.h" +#include "paimon/core/ecs/entity.h" #include "paimon/core/log_system.h" #include "paimon/rendering/render_context.h" #include "paimon/utility/brdf_lut_pass.h" @@ -48,14 +49,9 @@ void IBLLoader::load(ecs::Scene &scene) { // 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"); - } + envEntity.removeComponent(); - auto &envComp = envEntity.getOrAddComponent(); + auto &envComp = envEntity.addComponent(); envComp.equirectangularMap = m_equirectangularTexture; envComp.irradianceMap = m_irradianceMapPass->getIrradianceMap(); envComp.prefilteredMap = m_prefilteredMapPass->getPrefilteredMap(); @@ -65,7 +61,7 @@ void IBLLoader::load(ecs::Scene &scene) { m_filepath.string()); #ifdef PAIMON_DEBUG - // save((std::filesystem::path(PAIMON_TEXTURE_DIR) / "ibl_output")); + save((std::filesystem::path(PAIMON_TEXTURE_DIR) / "ibl_output")); #endif } diff --git a/source/paimon/core/io/ibl.h b/source/paimon/core/io/ibl.h index b5c31b4..2a995d6 100644 --- a/source/paimon/core/io/ibl.h +++ b/source/paimon/core/io/ibl.h @@ -3,6 +3,7 @@ #include #include +#include "paimon/core/ecs/entity.h" #include "paimon/core/ecs/scene.h" #include "paimon/opengl/texture.h" @@ -31,7 +32,6 @@ class IBLLoader { IBLLoader &operator=(const IBLLoader &) = delete; void load(ecs::Scene &scene); - void save(const std::filesystem::path &directory); private: void loadHDRTexture(); @@ -41,6 +41,8 @@ class IBLLoader { void save2DTexture(const Texture &texture, const std::filesystem::path &filepath, int width, int height); + void save(const std::filesystem::path &directory); + private: std::filesystem::path m_filepath; diff --git a/source/paimon/core/macro.h b/source/paimon/core/macro.h index f19d554..77778e5 100644 --- a/source/paimon/core/macro.h +++ b/source/paimon/core/macro.h @@ -7,3 +7,15 @@ #if defined linux || defined __linux || defined __linux__ #define PAIMON_OS_UNIX #endif + +#ifdef PAIMON_OS_WINDOWS +#if defined (DEBUG) || defined (_DEBUG) +#define PAIMON_DEBUG +#endif +#endif + +#ifdef PAIMON_OS_UNIX +#if !defined (NDEBUG) +#define PAIMON_DEBUG +#endif +#endif diff --git a/source/paimon/rendering/render_pass/color_pass.cpp b/source/paimon/rendering/render_pass/color_pass.cpp index 1d5cdbb..4446254 100644 --- a/source/paimon/rendering/render_pass/color_pass.cpp +++ b/source/paimon/rendering/render_pass/color_pass.cpp @@ -72,6 +72,13 @@ ColorPass::ColorPass(RenderContext &renderContext) m_color_texture = std::make_unique(GL_TEXTURE_2D); m_depth_texture = std::make_unique(GL_TEXTURE_2D); + // 1x1 white fallback texture: ensures ao=1.0 when occlusion texture is absent, + // and correct defaults for other missing material textures. + m_default_white_texture = std::make_unique(GL_TEXTURE_2D); + m_default_white_texture->set_storage_2d(1, GL_RGBA8, 1, 1); + const uint8_t white[4] = {255, 255, 255, 255}; + m_default_white_texture->set_sub_image_2d(0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, white); + // Create uniform buffers and storage buffers m_transform_ubo.set_storage(sizeof(TransformUBO), nullptr, GL_DYNAMIC_STORAGE_BIT); @@ -278,24 +285,41 @@ void ColorPass::draw(RenderContext &ctx, const glm::ivec2 &resolution, materialData.roughnessFactor = pbr.roughnessFactor; m_material_ubo.set_sub_data(0, sizeof(MaterialUBO), &materialData); - // Bind textures with sampler - if (pbr.baseColorTexture && pbr.baseColorTexture->image) { - ctx.bindTexture(0, *pbr.baseColorTexture->image, *m_sampler); - } - if (pbr.metallicRoughnessTexture && - pbr.metallicRoughnessTexture->image) { - ctx.bindTexture(1, *pbr.metallicRoughnessTexture->image, - *m_sampler); - } - if (mat->normalTexture && mat->normalTexture->image) { - ctx.bindTexture(2, *mat->normalTexture->image, *m_sampler); + // Bind textures with sampler; fall back to white 1x1 so that + // missing textures don't leave stale bindings from previous draws. + // Critically: occlusion (slot 4) must default to white (ao=1.0), + // otherwise the entire IBL contribution is zeroed out. + auto& white = *m_default_white_texture; + ctx.bindTexture(0, (pbr.baseColorTexture && pbr.baseColorTexture->image) + ? *pbr.baseColorTexture->image : white, *m_sampler); + ctx.bindTexture(1, (pbr.metallicRoughnessTexture && pbr.metallicRoughnessTexture->image) + ? *pbr.metallicRoughnessTexture->image : white, *m_sampler); + ctx.bindTexture(2, (mat->normalTexture && mat->normalTexture->image) + ? *mat->normalTexture->image : white, *m_sampler); + ctx.bindTexture(3, (mat->emissiveTexture && mat->emissiveTexture->image) + ? *mat->emissiveTexture->image : white, *m_sampler); + ctx.bindTexture(4, (mat->occlusionTexture && mat->occlusionTexture->image) + ? *mat->occlusionTexture->image : white, *m_sampler); + } + + // Bind IBL textures (bindings 5/6/7 match shader layout) + auto envView = scene.view(); + for (auto [envEntity, env] : envView.each()) { + EnvironmentUBO envData; + envData.intensity = env.intensity; + envData.rotation = glm::mat4_cast(env.rotation); + m_environment_ubo.set_sub_data(0, sizeof(EnvironmentUBO), &envData); + + if (env.irradianceMap) { + ctx.bindTexture(5, *env.irradianceMap, *m_ibl_sampler); } - if (mat->emissiveTexture && mat->emissiveTexture->image) { - ctx.bindTexture(3, *mat->emissiveTexture->image, *m_sampler); + if (env.prefilteredMap) { + ctx.bindTexture(6, *env.prefilteredMap, *m_ibl_sampler); } - if (mat->occlusionTexture && mat->occlusionTexture->image) { - ctx.bindTexture(4, *mat->occlusionTexture->image, *m_sampler); + if (env.brdfLUT) { + ctx.bindTexture(7, *env.brdfLUT, *m_sampler); } + break; // Only one environment } // Bind IBL textures (bindings 5/6/7 match shader layout) diff --git a/source/paimon/rendering/render_pass/color_pass.h b/source/paimon/rendering/render_pass/color_pass.h index 0d233e9..96c2e6b 100644 --- a/source/paimon/rendering/render_pass/color_pass.h +++ b/source/paimon/rendering/render_pass/color_pass.h @@ -72,6 +72,7 @@ class ColorPass { std::unique_ptr m_color_texture; std::unique_ptr m_depth_texture; + std::unique_ptr m_default_white_texture; // fallback for missing material textures (ao=1) std::unique_ptr m_sampler; std::unique_ptr m_ibl_sampler; // Cubemap sampler for IBL textures