diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index 89e2c0e..3869ef0 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -6781,6 +6781,7 @@ public: } MetaCoreEditorStateSnapshot editorStateSnapshot; + editorStateSnapshot.SceneSnapshot.Environment = sceneDocument->Environment; editorStateSnapshot.SceneSnapshot.GameObjects = sceneDocument->GameObjects; editorStateSnapshot.SelectionSnapshot = sceneDocument->Selection; editorContext.RestoreStateSnapshot(editorStateSnapshot); @@ -6853,7 +6854,9 @@ public: MetaCoreSceneDocument sceneDocument; sceneDocument.Name = CurrentSceneName_.empty() ? MetaCoreSceneDisplayNameFromPath(normalizedScenePath) : CurrentSceneName_; - sceneDocument.GameObjects = editorContext.GetScene().CaptureSnapshot().GameObjects; + const MetaCoreSceneSnapshot sceneSnapshot = editorContext.GetScene().CaptureSnapshot(); + sceneDocument.Environment = sceneSnapshot.Environment; + sceneDocument.GameObjects = sceneSnapshot.GameObjects; sceneDocument.Selection.SelectedObjectIds = editorContext.GetSelectedObjectIds(); sceneDocument.Selection.ActiveObjectId = editorContext.GetActiveObjectId(); sceneDocument.Selection.SelectionAnchorId = editorContext.GetSelectionAnchorId(); diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index 49f4e2f..9bcaf86 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -2610,11 +2610,131 @@ private: class MetaCoreScenePanelProvider final : public MetaCoreIEditorPanelProvider { public: - std::string GetPanelId() const override { return "Scene"; } + std::string GetPanelId() const override { return "SceneSettings"; } std::string GetPanelTitle() const override { return "场景"; } bool IsOpenByDefault() const override { return true; } - void DrawPanel(MetaCoreEditorContext&) override { + void DrawPanel(MetaCoreEditorContext& editorContext) override { + MetaCoreScene& scene = editorContext.GetScene(); + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + + std::vector ktxPaths; + if (assetDatabaseService != nullptr && assetDatabaseService->HasProject()) { + for (const MetaCoreAssetRecord& asset : assetDatabaseService->GetAssetRegistry()) { + std::string extension = asset.RelativePath.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) { + return static_cast(std::tolower(value)); + }); + const std::filesystem::path normalizedPath = asset.RelativePath.lexically_normal(); + const auto firstPathComponent = normalizedPath.begin(); + if (extension == ".ktx" && + !normalizedPath.is_absolute() && + firstPathComponent != normalizedPath.end() && + *firstPathComponent == "Assets") { + ktxPaths.push_back(normalizedPath); + } + } + std::sort(ktxPaths.begin(), ktxPaths.end()); + ktxPaths.erase(std::unique(ktxPaths.begin(), ktxPaths.end()), ktxPaths.end()); + } + + const auto applyDiscreteChange = [&](const char* label, const std::function& mutator) { + (void)editorContext.ExecuteSnapshotCommand(label, [&]() { + MetaCoreSceneEnvironmentSettings settings = scene.GetEnvironmentSettings(); + mutator(settings); + return scene.SetEnvironmentSettings(settings); + }); + }; + + ImGui::BeginChild("SceneEnvironmentPanel", ImVec2(0.0F, 0.0F), ImGuiChildFlags_Borders); + ImGui::TextUnformatted("环境 / 天空盒"); + ImGui::Separator(); + + const MetaCoreSceneEnvironmentSettings& environment = scene.GetEnvironmentSettings(); + int sourceIndex = environment.Source == MetaCoreSceneEnvironmentSource::ProjectKtxPair ? 1 : 0; + const char* sourceLabels[] = {"内置环境", "项目 KTX 对"}; + if (ImGui::Combo("来源", &sourceIndex, sourceLabels, IM_ARRAYSIZE(sourceLabels))) { + applyDiscreteChange("修改场景环境来源", [sourceIndex](MetaCoreSceneEnvironmentSettings& settings) { + settings.Source = sourceIndex == 1 + ? MetaCoreSceneEnvironmentSource::ProjectKtxPair + : MetaCoreSceneEnvironmentSource::Builtin; + }); + } + + bool skyboxVisible = environment.SkyboxVisible; + if (ImGui::Checkbox("显示天空盒", &skyboxVisible)) { + applyDiscreteChange("切换天空盒显示", [skyboxVisible](MetaCoreSceneEnvironmentSettings& settings) { + settings.SkyboxVisible = skyboxVisible; + }); + } + + if (environment.Source == MetaCoreSceneEnvironmentSource::ProjectKtxPair) { + const auto drawKtxSelector = [&](const char* label, const std::filesystem::path& currentPath, bool ibl) { + const std::string preview = currentPath.empty() ? "未选择" : currentPath.generic_string(); + if (!ImGui::BeginCombo(label, preview.c_str())) { + return; + } + + if (ImGui::Selectable("清除", currentPath.empty())) { + applyDiscreteChange("修改天空盒资源", [ibl](MetaCoreSceneEnvironmentSettings& settings) { + if (ibl) settings.IblKtxPath.clear(); + else settings.SkyboxKtxPath.clear(); + }); + } + ImGui::Separator(); + for (const std::filesystem::path& path : ktxPaths) { + const std::string pathLabel = path.generic_string(); + const bool selected = path == currentPath; + if (ImGui::Selectable(pathLabel.c_str(), selected)) { + applyDiscreteChange("修改天空盒资源", [ibl, path](MetaCoreSceneEnvironmentSettings& settings) { + if (ibl) settings.IblKtxPath = path; + else settings.SkyboxKtxPath = path; + }); + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + }; + + drawKtxSelector("IBL KTX", environment.IblKtxPath, true); + drawKtxSelector("天空盒 KTX", environment.SkyboxKtxPath, false); + if (ktxPaths.empty()) { + ImGui::TextDisabled("Assets 下没有可用 KTX 资源。"); + } + } + + float rotationDegrees = environment.RotationDegrees; + if (ImGui::DragFloat("水平旋转", &rotationDegrees, 1.0F, 0.0F, 360.0F, "%.1f 度")) { + MetaCoreSceneEnvironmentSettings settings = environment; + settings.RotationDegrees = rotationDegrees; + (void)scene.SetEnvironmentSettings(settings); + } + MetaCoreTrackInspectorEdit(editorContext, "调整天空盒旋转", true); + + float indirectIntensity = environment.IndirectLightIntensity; + if (ImGui::DragFloat("环境光强度", &indirectIntensity, 100.0F, 0.0F, 100000.0F, "%.0f")) { + MetaCoreSceneEnvironmentSettings settings = environment; + settings.IndirectLightIntensity = indirectIntensity; + (void)scene.SetEnvironmentSettings(settings); + } + MetaCoreTrackInspectorEdit(editorContext, "调整环境光强度", true); + + float skyboxIntensity = environment.SkyboxIntensity; + if (ImGui::DragFloat("天空盒亮度", &skyboxIntensity, 100.0F, 0.0F, 100000.0F, "%.0f")) { + MetaCoreSceneEnvironmentSettings settings = environment; + settings.SkyboxIntensity = skyboxIntensity; + (void)scene.SetEnvironmentSettings(settings); + } + MetaCoreTrackInspectorEdit(editorContext, "调整天空盒亮度", true); + + if (ImGui::Button("恢复内置环境")) { + applyDiscreteChange("恢复内置环境", [](MetaCoreSceneEnvironmentSettings& settings) { + settings = MetaCoreSceneEnvironmentSettings{}; + }); + } + ImGui::EndChild(); } }; @@ -6324,6 +6444,9 @@ public: moduleRegistry.RegisterMenuProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); + // The central Scene / Game viewport is not a dockable provider, but it keeps + // its existing visibility state separate from the scene settings panel. + moduleRegistry.AccessPanelOpenState("Scene") = true; moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index e7d7fb5..bbd9c2d 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -1432,10 +1432,6 @@ void MetaCoreEditorApp::DrawEditorFrame() { MetaCoreDrawInfernuxStatusBar(*EditorContext_, statusPosition, statusSize); for (const auto& panelProvider : ModuleRegistry_.GetPanelProviders()) { - if (panelProvider->GetPanelId() == "Scene") { - continue; - } - bool& panelOpen = ModuleRegistry_.AccessPanelOpenState(panelProvider->GetPanelId()); if (!panelOpen) { continue; @@ -1754,6 +1750,7 @@ void MetaCoreEditorApp::EnsureDefaultDockLayout(unsigned int dockSpaceId, const ImGui::DockBuilderDockWindow("层级", leftNodeId); ImGui::DockBuilderDockWindow("检查器", rightNodeId); + ImGui::DockBuilderDockWindow("场景", rightNodeId); ImGui::DockBuilderDockWindow("项目", bottomNodeId); ImGui::DockBuilderDockWindow("控制台", bottomNodeId); ImGui::DockBuilderFinish(dockSpaceId); diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp index 52efc2f..d33399c 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp @@ -175,6 +175,18 @@ private: } [[nodiscard]] bool MetaCoreStateSnapshotEqual(const MetaCoreEditorStateSnapshot& lhs, const MetaCoreEditorStateSnapshot& rhs) { + const MetaCoreSceneEnvironmentSettings& lhsEnvironment = lhs.SceneSnapshot.Environment; + const MetaCoreSceneEnvironmentSettings& rhsEnvironment = rhs.SceneSnapshot.Environment; + if (lhsEnvironment.Source != rhsEnvironment.Source || + lhsEnvironment.IblKtxPath != rhsEnvironment.IblKtxPath || + lhsEnvironment.SkyboxKtxPath != rhsEnvironment.SkyboxKtxPath || + lhsEnvironment.SkyboxVisible != rhsEnvironment.SkyboxVisible || + !MetaCoreNearlyEqual(lhsEnvironment.RotationDegrees, rhsEnvironment.RotationDegrees) || + !MetaCoreNearlyEqual(lhsEnvironment.IndirectLightIntensity, rhsEnvironment.IndirectLightIntensity) || + !MetaCoreNearlyEqual(lhsEnvironment.SkyboxIntensity, rhsEnvironment.SkyboxIntensity)) { + return false; + } + if (lhs.SceneSnapshot.GameObjects.size() != rhs.SceneSnapshot.GameObjects.size()) { return false; } diff --git a/Source/MetaCoreRender/Private/Materials/grid.mat b/Source/MetaCoreRender/Private/Materials/grid.mat index ce83d1a..73d4017 100644 --- a/Source/MetaCoreRender/Private/Materials/grid.mat +++ b/Source/MetaCoreRender/Private/Materials/grid.mat @@ -6,6 +6,7 @@ material { depthWrite : false, depthCulling : true, doubleSided : true, + requires : [ color ], parameters : [ { type : float4, name : baseColorFactor } ], @@ -14,7 +15,7 @@ material { fragment { void material(inout MaterialInputs material) { prepareMaterial(material); - material.baseColor = materialParams.baseColorFactor; + material.baseColor = materialParams.baseColorFactor * getColor(); material.baseColor.rgb *= material.baseColor.a; } } diff --git a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp index 2965c3a..61e0cbd 100644 --- a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp @@ -55,6 +55,7 @@ #include #include #include +#include #include #include #include @@ -88,6 +89,10 @@ namespace MetaCore { namespace { +constexpr std::uint8_t kSceneContentLayerMask = 0x01U; +constexpr std::uint8_t kEnvironmentSkyboxLayerMask = 0x02U; +constexpr std::uint8_t kEditorGridLayerMask = 0x04U; + void* MetaCoreGetFilamentNativeWindowHandle(MetaCoreWindow& window) { #if defined(_WIN32) return window.GetNativeWindowHandle(); @@ -292,6 +297,7 @@ public: // 创建场景 Scene_ = Engine_->createScene(); + EnvironmentScene_ = Engine_->createScene(); LoadDefaultEnvironmentLighting(); // 创建默认灯光 @@ -315,6 +321,16 @@ public: View_->setCamera(Camera_); ConfigureSceneViewRenderState(); + EnvironmentView_ = Engine_->createView(); + EnvironmentView_->setScene(EnvironmentScene_); + EnvironmentView_->setCamera(Camera_); + ConfigureEnvironmentViewRenderState(); + + GridView_ = Engine_->createView(); + GridView_->setScene(Scene_); + GridView_->setCamera(Camera_); + ConfigureGridViewRenderState(); + if (offscreen) { // 创建 UI 视图 UIView_ = Engine_->createView(); @@ -352,15 +368,27 @@ public: // 4. 设置到 View View_->setRenderTarget(RenderTarget_); View_->setViewport({0, 0, static_cast(width), static_cast(height)}); + EnvironmentView_->setRenderTarget(RenderTarget_); + EnvironmentView_->setViewport({0, 0, static_cast(width), static_cast(height)}); + GridView_->setRenderTarget(RenderTarget_); + GridView_->setViewport({0, 0, static_cast(width), static_cast(height)}); ConfigureSceneViewRenderState(); + ConfigureEnvironmentViewRenderState(); + ConfigureGridViewRenderState(); } else { auto [width, height] = window.GetFramebufferSize(); View_->setViewport({0, 0, static_cast(width), static_cast(height)}); + EnvironmentView_->setViewport({0, 0, static_cast(width), static_cast(height)}); + GridView_->setViewport({0, 0, static_cast(width), static_cast(height)}); ConfigureSceneViewRenderState(); + ConfigureEnvironmentViewRenderState(); + ConfigureGridViewRenderState(); } // 确保开启后处理(为了HDR) View_->setPostProcessingEnabled(true); + EnvironmentView_->setPostProcessingEnabled(false); + GridView_->setPostProcessingEnabled(false); // 初始化 gltfio MaterialProvider_ = new MetaCoreGltfMaterialProvider( @@ -385,7 +413,6 @@ public: } void SetEditorGridVisible(bool visible) { - EditorGridVisibleRequested_ = visible; if (Engine_ == nullptr || Scene_ == nullptr) { return; } @@ -399,6 +426,11 @@ public: EntitiesInScene_.insert(EditorGridEntity_); EditorGridEntityInScene_ = true; } + if (!EditorGridAxisEntityInScene_) { + Scene_->addEntity(EditorGridAxisEntity_); + EntitiesInScene_.insert(EditorGridAxisEntity_); + EditorGridAxisEntityInScene_ = true; + } return; } @@ -407,10 +439,33 @@ public: EntitiesInScene_.erase(EditorGridEntity_); EditorGridEntityInScene_ = false; } + if (EditorGridAxisEntityInScene_) { + Scene_->remove(EditorGridAxisEntity_); + EntitiesInScene_.erase(EditorGridAxisEntity_); + EditorGridAxisEntityInScene_ = false; + } } [[nodiscard]] bool VerifyEditorGridVisibleForTesting() const { - return EditorGridEntityInScene_; + return EditorGridEntityInScene_ && EditorGridAxisEntityInScene_; + } + + [[nodiscard]] bool VerifyEnvironmentLoadedForTesting() const { + return EnvironmentLoaded_ && + EnvironmentIblTexture_ != nullptr && + EnvironmentSkyboxTexture_ != nullptr && + EnvironmentIndirectLight_ != nullptr && + EnvironmentSkybox_ != nullptr; + } + + [[nodiscard]] bool VerifyEnvironmentBackgroundVisibleForTesting() const { + return EnvironmentBackgroundVisible_ && + EnvironmentScene_ != nullptr && + EnvironmentScene_->getSkybox() == EnvironmentSkybox_; + } + + [[nodiscard]] float GetEnvironmentIndirectLightIntensityForTesting() const { + return EnvironmentIndirectLight_ != nullptr ? EnvironmentIndirectLight_->getIntensity() : 0.0F; } void Shutdown() { @@ -548,6 +603,14 @@ public: } // 销毁 View Engine_->destroy(View_); + if (EnvironmentView_) { + Engine_->destroy(EnvironmentView_); + EnvironmentView_ = nullptr; + } + if (GridView_) { + Engine_->destroy(GridView_); + GridView_ = nullptr; + } if (UIView_) { Engine_->destroy(UIView_); UIView_ = nullptr; @@ -565,6 +628,10 @@ public: } // 销毁其他资源 + if (EnvironmentScene_) { + Engine_->destroy(EnvironmentScene_); + EnvironmentScene_ = nullptr; + } Engine_->destroy(Scene_); Engine_->destroy(Renderer_); Engine_->destroy(SwapChain_); @@ -581,9 +648,10 @@ public: void SetProjectRootPath(const std::filesystem::path& projectRootPath) { if (ProjectRootPath_ != projectRootPath) { DestroyMaterialTextureCache(); + DestroyEnvironmentLighting(); } ProjectRootPath_ = projectRootPath; - LoadDefaultEnvironmentLighting(); + SynchronizeEnvironment(ActiveEnvironmentSettings_); } void DestroyMaterialTextureCache() { @@ -625,18 +693,27 @@ public: std::filesystem::path("build_filament") / "libs" / "ktxreader" / filename }; - const std::filesystem::path cwd = std::filesystem::current_path(); - for (const std::filesystem::path& relativePath : candidates) { - const std::filesystem::path absolutePath = (cwd / relativePath).lexically_normal(); - if (std::filesystem::exists(absolutePath)) { - return absolutePath; + std::vector searchRoots; + std::filesystem::path current = std::filesystem::current_path(); + for (int depth = 0; depth < 5 && !current.empty(); ++depth) { + searchRoots.push_back(current); + const std::filesystem::path parent = current.parent_path(); + if (parent == current) { + break; } + current = parent; + } + if (!ProjectRootPath_.empty()) { + searchRoots.push_back(ProjectRootPath_); + searchRoots.push_back(ProjectRootPath_.parent_path()); + } - const std::filesystem::path fromProjectRoot = (ProjectRootPath_.empty() - ? std::filesystem::path{} - : ProjectRootPath_ / ".." / relativePath).lexically_normal(); - if (!fromProjectRoot.empty() && std::filesystem::exists(fromProjectRoot)) { - return fromProjectRoot; + for (const std::filesystem::path& root : searchRoots) { + for (const std::filesystem::path& relativePath : candidates) { + const std::filesystem::path absolutePath = (root / relativePath).lexically_normal(); + if (std::filesystem::exists(absolutePath)) { + return absolutePath; + } } } return {}; @@ -669,50 +746,242 @@ public: return ktxreader::Ktx1Reader::createTexture(Engine_, bundle.release(), srgb); } - void LoadDefaultEnvironmentLighting() { - if (Engine_ == nullptr || Scene_ == nullptr || EnvironmentLoaded_) { - return; + [[nodiscard]] static bool IsSafeProjectRelativeKtxPath(const std::filesystem::path& path) { + if (path.empty() || path.is_absolute() || path.has_root_name() || path.has_root_directory()) { + return false; + } + for (const std::filesystem::path& component : path) { + if (component == "..") { + return false; + } } - const std::filesystem::path iblPath = ResolveDefaultEnvironmentPath("lightroom_14b_ibl.ktx"); - const std::filesystem::path skyboxPath = ResolveDefaultEnvironmentPath("lightroom_14b_skybox.ktx"); - if (iblPath.empty() || skyboxPath.empty()) { - MetaCoreFilamentDebugLog("Default IBL assets not found; falling back to solid clear color"); - return; + std::string extension = path.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) { + return static_cast(std::tolower(value)); + }); + return extension == ".ktx"; + } + + [[nodiscard]] bool ResolveEnvironmentPaths( + const MetaCoreSceneEnvironmentSettings& settings, + std::filesystem::path& outIblPath, + std::filesystem::path& outSkyboxPath + ) const { + outIblPath.clear(); + outSkyboxPath.clear(); + if (settings.Source == MetaCoreSceneEnvironmentSource::ProjectKtxPair) { + if (ProjectRootPath_.empty() || + !IsSafeProjectRelativeKtxPath(settings.IblKtxPath) || + !IsSafeProjectRelativeKtxPath(settings.SkyboxKtxPath)) { + return false; + } + + outIblPath = (ProjectRootPath_ / settings.IblKtxPath).lexically_normal(); + outSkyboxPath = (ProjectRootPath_ / settings.SkyboxKtxPath).lexically_normal(); + std::error_code errorCode; + return std::filesystem::is_regular_file(outIblPath, errorCode) && + std::filesystem::is_regular_file(outSkyboxPath, errorCode); } + outIblPath = ResolveDefaultEnvironmentPath("lightroom_14b_ibl.ktx"); + outSkyboxPath = ResolveDefaultEnvironmentPath("lightroom_14b_skybox.ktx"); + return !outIblPath.empty() && !outSkyboxPath.empty(); + } + + [[nodiscard]] static bool EnvironmentSettingsEqual( + const MetaCoreSceneEnvironmentSettings& lhs, + const MetaCoreSceneEnvironmentSettings& rhs + ) { + return lhs.Source == rhs.Source && + lhs.IblKtxPath == rhs.IblKtxPath && + lhs.SkyboxKtxPath == rhs.SkyboxKtxPath && + lhs.SkyboxVisible == rhs.SkyboxVisible && + std::abs(lhs.RotationDegrees - rhs.RotationDegrees) <= 0.0001F && + std::abs(lhs.IndirectLightIntensity - rhs.IndirectLightIntensity) <= 0.0001F && + std::abs(lhs.SkyboxIntensity - rhs.SkyboxIntensity) <= 0.0001F; + } + + [[nodiscard]] static float NormalizeEnvironmentRotation(float degrees) { + if (!std::isfinite(degrees)) { + return 0.0F; + } + float normalized = std::fmod(degrees, 360.0F); + if (normalized < 0.0F) { + normalized += 360.0F; + } + return normalized; + } + + void UpdateEnvironmentBackgroundVisibility() { + const bool shouldShow = + EnvironmentSkybox_ != nullptr && + EnvironmentScene_ != nullptr && + ActiveEnvironmentSettings_.SkyboxVisible && + CurrentSceneView_.ClearFlags == MetaCoreCameraClearFlags::Skybox; + if (EnvironmentBackgroundVisible_ != shouldShow) { + MetaCoreFilamentDebugLog( + std::string("Scene environment background ") + (shouldShow ? "enabled" : "disabled") + + " clearFlags=" + std::to_string(static_cast(CurrentSceneView_.ClearFlags)) + + " skyboxVisible=" + (ActiveEnvironmentSettings_.SkyboxVisible ? "true" : "false") + ); + } + if (EnvironmentScene_ != nullptr) { + if (shouldShow && EnvironmentScene_->getSkybox() != EnvironmentSkybox_) { + EnvironmentScene_->setSkybox(EnvironmentSkybox_); + } else if (!shouldShow && EnvironmentScene_->getSkybox() == EnvironmentSkybox_) { + EnvironmentScene_->setSkybox(nullptr); + } + } + EnvironmentBackgroundVisible_ = shouldShow; + } + + bool RebuildEnvironmentSkybox() { + if (Engine_ == nullptr || EnvironmentSkyboxTexture_ == nullptr) { + return false; + } + if (EnvironmentScene_ != nullptr && EnvironmentScene_->getSkybox() == EnvironmentSkybox_) { + EnvironmentScene_->setSkybox(nullptr); + } + if (EnvironmentSkybox_ != nullptr) { + Engine_->destroy(EnvironmentSkybox_); + EnvironmentSkybox_ = nullptr; + } + + EnvironmentSkybox_ = filament::Skybox::Builder() + .environment(EnvironmentSkyboxTexture_) + .intensity(std::max(0.0F, ActiveEnvironmentSettings_.SkyboxIntensity)) + .build(*Engine_); + if (EnvironmentSkybox_ == nullptr) { + return false; + } + EnvironmentSkybox_->setLayerMask(0xFFU, kEnvironmentSkyboxLayerMask); + UpdateEnvironmentBackgroundVisibility(); + return true; + } + + bool LoadEnvironmentResources( + const std::filesystem::path& iblPath, + const std::filesystem::path& skyboxPath + ) { filament::math::float3 sphericalHarmonics[9]{}; EnvironmentIblTexture_ = LoadKtx1Texture(iblPath, false, sphericalHarmonics); EnvironmentSkyboxTexture_ = LoadKtx1Texture(skyboxPath, false); if (EnvironmentIblTexture_ == nullptr || EnvironmentSkyboxTexture_ == nullptr) { - MetaCoreFilamentDebugLog("Default IBL KTX load failed"); - DestroyEnvironmentLighting(); - return; + return false; } EnvironmentIndirectLight_ = filament::IndirectLight::Builder() .reflections(EnvironmentIblTexture_) .irradiance(3, sphericalHarmonics) - .intensity(EnvironmentIntensity_) + .intensity(ActiveEnvironmentSettings_.IndirectLightIntensity) .build(*Engine_); - EnvironmentSkybox_ = filament::Skybox::Builder() - .environment(EnvironmentSkyboxTexture_) - .intensity(EnvironmentIntensity_) - .build(*Engine_); - - if (EnvironmentIndirectLight_ == nullptr || EnvironmentSkybox_ == nullptr) { - MetaCoreFilamentDebugLog("Default IBL object build failed"); - DestroyEnvironmentLighting(); - return; + if (EnvironmentIndirectLight_ == nullptr) { + return false; } Scene_->setIndirectLight(EnvironmentIndirectLight_); - Scene_->setSkybox(EnvironmentSkybox_); EnvironmentLoaded_ = true; - MetaCoreFilamentDebugLog( - "Default IBL loaded ibl=\"" + iblPath.generic_string() + - "\" skybox=\"" + skyboxPath.generic_string() + "\"" - ); + return RebuildEnvironmentSkybox(); + } + + void SynchronizeEnvironment(const MetaCoreSceneEnvironmentSettings& requestedSettings) { + if (Engine_ == nullptr || Scene_ == nullptr) { + return; + } + + std::filesystem::path iblPath; + std::filesystem::path skyboxPath; + const bool requestedPathsValid = ResolveEnvironmentPaths(requestedSettings, iblPath, skyboxPath); + if (!requestedPathsValid && requestedSettings.Source == MetaCoreSceneEnvironmentSource::ProjectKtxPair) { + const std::string failureSignature = + requestedSettings.IblKtxPath.generic_string() + "|" + + requestedSettings.SkyboxKtxPath.generic_string(); + if (LastEnvironmentValidationFailure_ != failureSignature) { + MetaCoreFilamentDebugLog( + "Scene environment KTX pair is unavailable or unsafe; using the built-in environment. ibl=\"" + + requestedSettings.IblKtxPath.generic_string() + "\" skybox=\"" + + requestedSettings.SkyboxKtxPath.generic_string() + "\"" + ); + LastEnvironmentValidationFailure_ = failureSignature; + } + MetaCoreSceneEnvironmentSettings fallbackSettings; + if (!ResolveEnvironmentPaths(fallbackSettings, iblPath, skyboxPath)) { + DestroyEnvironmentLighting(); + ActiveEnvironmentSettings_ = requestedSettings; + return; + } + } else if (!requestedPathsValid) { + DestroyEnvironmentLighting(); + ActiveEnvironmentSettings_ = requestedSettings; + return; + } else { + LastEnvironmentValidationFailure_.clear(); + } + + const bool resourcesChanged = + !EnvironmentLoaded_ || + ActiveEnvironmentSettings_.Source != requestedSettings.Source || + ActiveEnvironmentSettings_.IblKtxPath != requestedSettings.IblKtxPath || + ActiveEnvironmentSettings_.SkyboxKtxPath != requestedSettings.SkyboxKtxPath; + const bool skyboxIntensityChanged = + !EnvironmentSettingsEqual(ActiveEnvironmentSettings_, requestedSettings) && + std::abs(ActiveEnvironmentSettings_.SkyboxIntensity - requestedSettings.SkyboxIntensity) > 0.0001F; + + ActiveEnvironmentSettings_ = requestedSettings; + ActiveEnvironmentSettings_.RotationDegrees = NormalizeEnvironmentRotation(ActiveEnvironmentSettings_.RotationDegrees); + ActiveEnvironmentSettings_.IndirectLightIntensity = std::max(0.0F, ActiveEnvironmentSettings_.IndirectLightIntensity); + ActiveEnvironmentSettings_.SkyboxIntensity = std::max(0.0F, ActiveEnvironmentSettings_.SkyboxIntensity); + + if (resourcesChanged) { + DestroyEnvironmentLighting(); + if (!LoadEnvironmentResources(iblPath, skyboxPath)) { + DestroyEnvironmentLighting(); + if (requestedSettings.Source != MetaCoreSceneEnvironmentSource::ProjectKtxPair) { + MetaCoreFilamentDebugLog("Built-in scene environment KTX load failed; falling back to solid clear color"); + return; + } + + MetaCoreFilamentDebugLog("Scene environment KTX load failed; falling back to the built-in environment"); + MetaCoreSceneEnvironmentSettings fallbackSettings; + if (!ResolveEnvironmentPaths(fallbackSettings, iblPath, skyboxPath) || + !LoadEnvironmentResources(iblPath, skyboxPath)) { + MetaCoreFilamentDebugLog("Built-in scene environment fallback load failed; falling back to solid clear color"); + DestroyEnvironmentLighting(); + return; + } + } + + LoadedEnvironmentIblPath_ = iblPath; + LoadedEnvironmentSkyboxPath_ = skyboxPath; + MetaCoreFilamentDebugLog( + "Scene environment loaded ibl=\"" + iblPath.generic_string() + + "\" skybox=\"" + skyboxPath.generic_string() + "\"" + ); + } else { + if (EnvironmentIndirectLight_ != nullptr) { + EnvironmentIndirectLight_->setIntensity(ActiveEnvironmentSettings_.IndirectLightIntensity); + } + if (skyboxIntensityChanged && !RebuildEnvironmentSkybox()) { + MetaCoreFilamentDebugLog("Scene environment skybox intensity update failed"); + } + } + + if (EnvironmentIndirectLight_ != nullptr) { + const glm::mat3 rotation = glm::mat3(glm::rotate( + glm::mat4(1.0F), + glm::radians(ActiveEnvironmentSettings_.RotationDegrees), + glm::vec3(0.0F, 0.0F, 1.0F) + )); + EnvironmentIndirectLight_->setRotation( + *reinterpret_cast(glm::value_ptr(rotation)) + ); + } + UpdateEnvironmentBackgroundVisibility(); + } + + void LoadDefaultEnvironmentLighting() { + SynchronizeEnvironment(MetaCoreSceneEnvironmentSettings{}); } void DestroyEnvironmentLighting() { @@ -720,9 +989,9 @@ public: if (Scene_->getIndirectLight() == EnvironmentIndirectLight_) { Scene_->setIndirectLight(nullptr); } - if (Scene_->getSkybox() == EnvironmentSkybox_) { - Scene_->setSkybox(nullptr); - } + } + if (EnvironmentScene_ != nullptr && EnvironmentScene_->getSkybox() == EnvironmentSkybox_) { + EnvironmentScene_->setSkybox(nullptr); } if (Engine_ != nullptr) { @@ -745,6 +1014,9 @@ public: } } EnvironmentLoaded_ = false; + EnvironmentBackgroundVisible_ = false; + LoadedEnvironmentIblPath_.clear(); + LoadedEnvironmentSkyboxPath_.clear(); } [[nodiscard]] filament::Skybox* GetOrCreateSolidColorSkybox(const MetaCoreSceneView& sceneView) { @@ -984,6 +1256,7 @@ public: (void)compatibilityMeshOnly; LastSyncSnapshot_ = snapshot; + SynchronizeEnvironment(LastSyncSnapshot_.Environment); ObjectWorldMatrices_ = LastSyncSnapshot_.WorldMatrices; const bool materialDebugLogging = MetaCoreMaterialDebugLogEnabled(); @@ -1853,20 +2126,16 @@ public: void ApplySceneView(const MetaCoreSceneView& sceneView) { if (!Camera_ || !View_) return; CurrentSceneView_ = sceneView; + UpdateEditorGridTransform(sceneView); if (Scene_ != nullptr) { - filament::Skybox* skybox = nullptr; - if (sceneView.ClearFlags == MetaCoreCameraClearFlags::Skybox) { - skybox = EnvironmentSkybox_ != nullptr ? EnvironmentSkybox_ : GetOrCreateSolidColorSkybox(sceneView); - } else if (sceneView.ClearFlags == MetaCoreCameraClearFlags::SolidColor) { - skybox = GetOrCreateSolidColorSkybox(sceneView); - } - Scene_->setSkybox(skybox); + Scene_->setSkybox(nullptr); } + UpdateEnvironmentBackgroundVisibility(); // 1. 视图矩阵同步 const glm::mat4 viewMatrix = glm::lookAt(sceneView.CameraPosition, sceneView.CameraTarget, sceneView.CameraUp); - const glm::mat4 cameraModelMatrix = glm::inverse(viewMatrix); - Camera_->setModelMatrix(*reinterpret_cast(glm::value_ptr(cameraModelMatrix))); + CameraModelMatrix_ = glm::inverse(viewMatrix); + Camera_->setModelMatrix(*reinterpret_cast(glm::value_ptr(CameraModelMatrix_))); // 2. 投影矩阵:回归 Filament 原生设置 const auto& viewport = View_->getViewport(); @@ -1932,15 +2201,40 @@ public: } if (Renderer_->beginFrame(SwapChain_)) { - if (RenderTarget_) { - // 1. 渲染 3D 离屏视口 (输出到 RenderTarget) + const bool renderEnvironment = EnvironmentBackgroundVisible_ && EnvironmentView_ != nullptr; + const bool renderEditorGrid = GridView_ != nullptr && + (EditorGridEntityInScene_ || EditorGridAxisEntityInScene_); + const auto renderScenePass = [&](bool outputToRenderTarget) { filament::Renderer::ClearOptions options = BuildSceneClearOptions(); Renderer_->setClearOptions(options); - Renderer_->render(View_); + if (renderEnvironment) { + options.clear = false; + Renderer_->setClearOptions(options); + const glm::mat4 environmentCameraMatrix = glm::rotate( + glm::mat4(1.0F), + glm::radians(-ActiveEnvironmentSettings_.RotationDegrees), + glm::vec3(0.0F, 0.0F, 1.0F) + ) * CameraModelMatrix_; + Camera_->setModelMatrix(*reinterpret_cast(glm::value_ptr(environmentCameraMatrix))); + Renderer_->render(EnvironmentView_); + Camera_->setModelMatrix(*reinterpret_cast(glm::value_ptr(CameraModelMatrix_))); + } + if (renderEditorGrid) { + options.clear = false; + Renderer_->setClearOptions(options); + Renderer_->render(GridView_); + } + (void)outputToRenderTarget; + }; + if (RenderTarget_) { + // 1. 渲染 3D 离屏视口 (输出到 RenderTarget) + renderScenePass(true); + if (hasUiPass) { // 2. 准备并渲染 UI 视口 (输出到 SwapChain) + filament::Renderer::ClearOptions options; options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 灰色 options.clear = true; Renderer_->setClearOptions(options); @@ -1948,10 +2242,7 @@ public: } } else { // 3. 直接上屏渲染 - filament::Renderer::ClearOptions options = BuildSceneClearOptions(); - Renderer_->setClearOptions(options); - - Renderer_->render(View_); + renderScenePass(false); } Renderer_->endFrame(); @@ -1975,6 +2266,12 @@ public: " height=" + std::to_string(height) ); View_->setViewport({0, 0, static_cast(width), static_cast(height)}); + if (EnvironmentView_ != nullptr) { + EnvironmentView_->setViewport({0, 0, static_cast(width), static_cast(height)}); + } + if (GridView_ != nullptr) { + GridView_->setViewport({0, 0, static_cast(width), static_cast(height)}); + } return; } @@ -2018,7 +2315,17 @@ public: View_->setRenderTarget(RenderTarget_); View_->setViewport({0, 0, static_cast(width), static_cast(height)}); + if (EnvironmentView_ != nullptr) { + EnvironmentView_->setRenderTarget(RenderTarget_); + EnvironmentView_->setViewport({0, 0, static_cast(width), static_cast(height)}); + } + if (GridView_ != nullptr) { + GridView_->setRenderTarget(RenderTarget_); + GridView_->setViewport({0, 0, static_cast(width), static_cast(height)}); + } ConfigureSceneViewRenderState(); + ConfigureEnvironmentViewRenderState(); + ConfigureGridViewRenderState(); DebugRenderTargetState("Resize after recreate resources"); } @@ -2160,6 +2467,7 @@ public: private: struct MetaCoreEditorGridVertex { filament::math::float3 Position{}; + std::array Color{255, 255, 255, 255}; }; struct MetaCoreEditorGridPrimitiveRange { @@ -2167,6 +2475,58 @@ private: std::uint32_t Count = 0; }; + static constexpr int EditorGridExtent_ = 500; + static constexpr int EditorGridMajorStep_ = 10; + static constexpr int EditorGridAxisExtent_ = 100000; + + [[nodiscard]] float ResolveEditorGridSpacing(const MetaCoreSceneView& sceneView) const { + float viewScale = glm::distance(sceneView.CameraPosition, sceneView.CameraTarget); + if (sceneView.ProjectionMode == MetaCoreCameraProjectionMode::Orthographic) { + viewScale = std::max(viewScale, sceneView.OrthographicSize * 2.0F); + } + + const float desiredSpacing = std::max(1.0F, viewScale / 12.0F); + const float magnitude = std::pow(10.0F, std::floor(std::log10(desiredSpacing))); + const float normalizedSpacing = desiredSpacing / magnitude; + if (normalizedSpacing <= 1.0F) { + return magnitude; + } + if (normalizedSpacing <= 2.0F) { + return 2.0F * magnitude; + } + if (normalizedSpacing <= 5.0F) { + return 5.0F * magnitude; + } + return 10.0F * magnitude; + } + + void UpdateEditorGridTransform(const MetaCoreSceneView& sceneView) { + if (!EditorGridBuilt_ || Engine_ == nullptr || !EditorGridEntity_) { + return; + } + + const float spacing = ResolveEditorGridSpacing(sceneView); + const float majorSpacing = spacing * static_cast(EditorGridMajorStep_); + const glm::vec3 snappedCenter{ + std::round(sceneView.CameraTarget.x / majorSpacing) * majorSpacing, + std::round(sceneView.CameraTarget.y / majorSpacing) * majorSpacing, + 0.0F + }; + const glm::mat4 gridTransform = glm::scale( + glm::translate(glm::mat4(1.0F), snappedCenter), + glm::vec3(spacing, spacing, 1.0F) + ); + + auto& transformManager = Engine_->getTransformManager(); + const auto transformInstance = transformManager.getInstance(EditorGridEntity_); + if (transformInstance) { + transformManager.setTransform( + transformInstance, + *reinterpret_cast(glm::value_ptr(gridTransform)) + ); + } + } + [[nodiscard]] filament::Material* LoadEditorGridMaterial() { if (Engine_ == nullptr) { return nullptr; @@ -2255,8 +2615,6 @@ private: return false; } - constexpr int gridExtent = 100; - constexpr int majorStep = 10; enum class GridLineKind : std::size_t { Minor = 0, Major = 1, @@ -2273,40 +2631,72 @@ private: float x1, float y1 ) { + const bool shouldFade = kind == GridLineKind::Minor || kind == GridLineKind::Major; + const auto vertexAlpha = [&](float x, float y) { + if (!shouldFade) { + return std::uint8_t{255}; + } + + const float normalizedDistance = std::max(std::abs(x), std::abs(y)) / + static_cast(EditorGridExtent_); + const float fadeProgress = std::clamp((normalizedDistance - 0.58F) / 0.42F, 0.0F, 1.0F); + const float smoothFade = fadeProgress * fadeProgress * (3.0F - 2.0F * fadeProgress); + return static_cast(std::round((1.0F - smoothFade) * 255.0F)); + }; + auto& vertices = groupedVertices[static_cast(kind)]; - vertices.push_back(MetaCoreEditorGridVertex{filament::math::float3{x0, y0, 0.0F}}); - vertices.push_back(MetaCoreEditorGridVertex{filament::math::float3{x1, y1, 0.0F}}); + vertices.push_back(MetaCoreEditorGridVertex{ + filament::math::float3{x0, y0, 0.0F}, + {255, 255, 255, vertexAlpha(x0, y0)} + }); + vertices.push_back(MetaCoreEditorGridVertex{ + filament::math::float3{x1, y1, 0.0F}, + {255, 255, 255, vertexAlpha(x1, y1)} + }); }; - for (int coordinate = -gridExtent; coordinate <= gridExtent; ++coordinate) { - const bool isMajor = coordinate != 0 && (coordinate % majorStep) == 0; - const GridLineKind verticalKind = coordinate == 0 - ? GridLineKind::AxisY - : (isMajor ? GridLineKind::Major : GridLineKind::Minor); - appendLine( - verticalKind, - static_cast(coordinate), - static_cast(-gridExtent), - static_cast(coordinate), - static_cast(gridExtent) - ); + for (int coordinate = -EditorGridExtent_; coordinate <= EditorGridExtent_; ++coordinate) { + if (coordinate == 0) { + continue; + } - const GridLineKind horizontalKind = coordinate == 0 - ? GridLineKind::AxisX - : (isMajor ? GridLineKind::Major : GridLineKind::Minor); + const GridLineKind lineKind = (coordinate % EditorGridMajorStep_) == 0 + ? GridLineKind::Major + : GridLineKind::Minor; appendLine( - horizontalKind, - static_cast(-gridExtent), + lineKind, static_cast(coordinate), - static_cast(gridExtent), + static_cast(-EditorGridExtent_), + static_cast(coordinate), + static_cast(EditorGridExtent_) + ); + appendLine( + lineKind, + static_cast(-EditorGridExtent_), + static_cast(coordinate), + static_cast(EditorGridExtent_), static_cast(coordinate) ); } + appendLine( + GridLineKind::AxisX, + static_cast(-EditorGridAxisExtent_), + 0.0F, + static_cast(EditorGridAxisExtent_), + 0.0F + ); + appendLine( + GridLineKind::AxisY, + 0.0F, + static_cast(-EditorGridAxisExtent_), + 0.0F, + static_cast(EditorGridAxisExtent_) + ); std::vector vertices; std::vector indices; - vertices.reserve((gridExtent * 2 + 1) * 4); - indices.reserve((gridExtent * 2 + 1) * 4); + vertices.reserve((EditorGridExtent_ * 2 + 1) * 4 + 4); + indices.reserve((EditorGridExtent_ * 2 + 1) * 4 + 4); std::array(GridLineKind::Count)> ranges{}; for (std::size_t kindIndex = 0; kindIndex < groupedVertices.size(); ++kindIndex) { @@ -2335,6 +2725,14 @@ private: offsetof(MetaCoreEditorGridVertex, Position), sizeof(MetaCoreEditorGridVertex) ) + .attribute( + filament::VertexAttribute::COLOR, + 0, + filament::VertexBuffer::AttributeType::UBYTE4, + offsetof(MetaCoreEditorGridVertex, Color), + sizeof(MetaCoreEditorGridVertex) + ) + .normalized(filament::VertexAttribute::COLOR) .build(*Engine_); EditorGridIndexBuffer_ = filament::IndexBuffer::Builder() .indexCount(indices.size()) @@ -2400,15 +2798,16 @@ private: } EditorGridEntity_ = utils::EntityManager::get().create(); - auto builder = filament::RenderableManager::Builder(static_cast(GridLineKind::Count)); - builder - .boundingBox({{0.0F, 0.0F, 0.0F}, {static_cast(gridExtent), static_cast(gridExtent), 0.05F}}) + auto& transformManager = Engine_->getTransformManager(); + transformManager.create(EditorGridEntity_); + auto gridBuilder = filament::RenderableManager::Builder(2); + gridBuilder + .boundingBox({{0.0F, 0.0F, 0.0F}, {static_cast(EditorGridExtent_), static_cast(EditorGridExtent_), 0.05F}}) .culling(false) .castShadows(false) .receiveShadows(false); - - for (std::size_t kindIndex = 0; kindIndex < ranges.size(); ++kindIndex) { - builder + for (std::size_t kindIndex = 0; kindIndex < 2; ++kindIndex) { + gridBuilder .geometry( kindIndex, filament::RenderableManager::PrimitiveType::LINES, @@ -2421,10 +2820,48 @@ private: .blendOrder(kindIndex, static_cast(kindIndex)); } - if (static_cast(builder.build(*Engine_, EditorGridEntity_)) != 0) { + if (static_cast(gridBuilder.build(*Engine_, EditorGridEntity_)) != 0) { DestroyEditorGridRenderable(); return false; } + auto& renderableManager = Engine_->getRenderableManager(); + renderableManager.setLayerMask( + renderableManager.getInstance(EditorGridEntity_), + 0xFFU, + kEditorGridLayerMask + ); + + EditorGridAxisEntity_ = utils::EntityManager::get().create(); + transformManager.create(EditorGridAxisEntity_); + auto axisBuilder = filament::RenderableManager::Builder(2); + axisBuilder + .boundingBox({{0.0F, 0.0F, 0.0F}, {static_cast(EditorGridAxisExtent_), static_cast(EditorGridAxisExtent_), 0.05F}}) + .culling(false) + .castShadows(false) + .receiveShadows(false); + for (std::size_t kindIndex = 0; kindIndex < 2; ++kindIndex) { + const std::size_t sourceIndex = kindIndex + 2; + axisBuilder + .geometry( + kindIndex, + filament::RenderableManager::PrimitiveType::LINES, + EditorGridVertexBuffer_, + EditorGridIndexBuffer_, + ranges[sourceIndex].Offset, + ranges[sourceIndex].Count + ) + .material(kindIndex, EditorGridMaterialInstances_[sourceIndex]) + .blendOrder(kindIndex, static_cast(sourceIndex)); + } + if (static_cast(axisBuilder.build(*Engine_, EditorGridAxisEntity_)) != 0) { + DestroyEditorGridRenderable(); + return false; + } + renderableManager.setLayerMask( + renderableManager.getInstance(EditorGridAxisEntity_), + 0xFFU, + kEditorGridLayerMask + ); EditorGridBuilt_ = true; return true; @@ -2440,17 +2877,27 @@ private: EntitiesInScene_.erase(EditorGridEntity_); EditorGridEntityInScene_ = false; } - - if (EditorGridEntity_) { - auto& renderableManager = Engine_->getRenderableManager(); - if (renderableManager.getInstance(EditorGridEntity_)) { - renderableManager.destroy(EditorGridEntity_); - } - Engine_->destroy(EditorGridEntity_); - utils::EntityManager::get().destroy(EditorGridEntity_); - EditorGridEntity_ = {}; + if (EditorGridAxisEntityInScene_ && Scene_ != nullptr) { + Scene_->remove(EditorGridAxisEntity_); + EntitiesInScene_.erase(EditorGridAxisEntity_); + EditorGridAxisEntityInScene_ = false; } + const auto destroyGridEntity = [&](utils::Entity& entity) { + if (!entity) { + return; + } + auto& renderableManager = Engine_->getRenderableManager(); + if (renderableManager.getInstance(entity)) { + renderableManager.destroy(entity); + } + Engine_->destroy(entity); + utils::EntityManager::get().destroy(entity); + entity = {}; + }; + destroyGridEntity(EditorGridEntity_); + destroyGridEntity(EditorGridAxisEntity_); + for (filament::MaterialInstance*& materialInstance : EditorGridMaterialInstances_) { if (materialInstance != nullptr) { Engine_->destroy(materialInstance); @@ -2814,11 +3261,38 @@ private: } View_->setBlendMode(filament::View::BlendMode::OPAQUE); + View_->setVisibleLayers(0xFFU, kSceneContentLayerMask); for (uint8_t channel = 0; channel < 8; ++channel) { View_->setChannelDepthClearEnabled(channel, true); } } + void ConfigureEnvironmentViewRenderState() { + if (EnvironmentView_ == nullptr) { + return; + } + + // This view is rendered after the main scene, so its skybox composites + // only into pixels not covered by scene geometry. + EnvironmentView_->setBlendMode(filament::View::BlendMode::TRANSLUCENT); + EnvironmentView_->setVisibleLayers(0xFFU, kEnvironmentSkyboxLayerMask); + for (uint8_t channel = 0; channel < 8; ++channel) { + EnvironmentView_->setChannelDepthClearEnabled(channel, false); + } + } + + void ConfigureGridViewRenderState() { + if (GridView_ == nullptr) { + return; + } + + GridView_->setBlendMode(filament::View::BlendMode::TRANSLUCENT); + GridView_->setVisibleLayers(0xFFU, kEditorGridLayerMask); + for (uint8_t channel = 0; channel < 8; ++channel) { + GridView_->setChannelDepthClearEnabled(channel, false); + } + } + void DebugRenderTargetState(const char* event) const { std::ostringstream stream; stream @@ -2838,16 +3312,24 @@ private: filament::SwapChain* SwapChain_ = nullptr; filament::Renderer* Renderer_ = nullptr; filament::Scene* Scene_ = nullptr; + filament::Scene* EnvironmentScene_ = nullptr; filament::Camera* Camera_ = nullptr; filament::View* View_ = nullptr; + filament::View* EnvironmentView_ = nullptr; + filament::View* GridView_ = nullptr; filament::Texture* EnvironmentIblTexture_ = nullptr; filament::Texture* EnvironmentSkyboxTexture_ = nullptr; filament::IndirectLight* EnvironmentIndirectLight_ = nullptr; filament::Skybox* EnvironmentSkybox_ = nullptr; filament::Skybox* SolidColorSkybox_ = nullptr; filament::math::float4 SolidColorSkyboxColor_{-1.0f, -1.0f, -1.0f, -1.0f}; - float EnvironmentIntensity_ = 30000.0f; bool EnvironmentLoaded_ = false; + bool EnvironmentBackgroundVisible_ = false; + MetaCoreSceneEnvironmentSettings ActiveEnvironmentSettings_{}; + std::filesystem::path LoadedEnvironmentIblPath_{}; + std::filesystem::path LoadedEnvironmentSkyboxPath_{}; + std::string LastEnvironmentValidationFailure_{}; + glm::mat4 CameraModelMatrix_{1.0F}; filament::gltfio::AssetLoader* AssetLoader_ = nullptr; filament::gltfio::MaterialProvider* MaterialProvider_ = nullptr; @@ -2871,10 +3353,11 @@ private: std::unordered_map SceneLightEntities_; std::unordered_set EntitiesInScene_; - bool EditorGridVisibleRequested_ = false; bool EditorGridBuilt_ = false; bool EditorGridEntityInScene_ = false; + bool EditorGridAxisEntityInScene_ = false; utils::Entity EditorGridEntity_{}; + utils::Entity EditorGridAxisEntity_{}; filament::VertexBuffer* EditorGridVertexBuffer_ = nullptr; filament::IndexBuffer* EditorGridIndexBuffer_ = nullptr; filament::Material* EditorGridMaterial_ = nullptr; @@ -2985,4 +3468,16 @@ bool MetaCoreFilamentSceneBridge::VerifyEditorGridVisibleForTesting() const { return Impl_->VerifyEditorGridVisibleForTesting(); } +bool MetaCoreFilamentSceneBridge::VerifyEnvironmentLoadedForTesting() const { + return Impl_->VerifyEnvironmentLoadedForTesting(); +} + +bool MetaCoreFilamentSceneBridge::VerifyEnvironmentBackgroundVisibleForTesting() const { + return Impl_->VerifyEnvironmentBackgroundVisibleForTesting(); +} + +float MetaCoreFilamentSceneBridge::GetEnvironmentIndirectLightIntensityForTesting() const { + return Impl_->GetEnvironmentIndirectLightIntensityForTesting(); +} + } // namespace MetaCore diff --git a/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp b/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp index ba3277f..85513cf 100644 --- a/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp @@ -77,6 +77,7 @@ MetaCoreSceneRenderSyncSnapshot MetaCoreSceneRenderSync::BuildSnapshot(const Met MetaCoreSceneRenderSyncSnapshot snapshot; snapshot.FrameId = ++FrameId_; snapshot.SceneRevision = scene.GetRevision(); + snapshot.Environment = scene.GetEnvironmentSettings(); // 预处理:构建子树模型网格节点计数表,支撑在没有 Tag 时的 Fallback 回溯 std::unordered_map> subtreeModelCounts; diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h index 3126479..781eae7 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h @@ -45,6 +45,9 @@ public: [[nodiscard]] float GetDefaultLightIntensityForTesting() const; [[nodiscard]] bool VerifyEntityInSceneForTesting(MetaCoreId objectId) const; [[nodiscard]] bool VerifyEditorGridVisibleForTesting() const; + [[nodiscard]] bool VerifyEnvironmentLoadedForTesting() const; + [[nodiscard]] bool VerifyEnvironmentBackgroundVisibleForTesting() const; + [[nodiscard]] float GetEnvironmentIndirectLightIntensityForTesting() const; private: class MetaCoreFilamentSceneBridgeImpl; diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h index 1a60a34..7147d03 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h @@ -4,6 +4,7 @@ #include "MetaCoreFoundation/MetaCoreId.h" #include "MetaCoreRender/MetaCoreRenderTypes.h" #include "MetaCoreScene/MetaCoreComponents.h" +#include "MetaCoreScene/MetaCoreSceneDocument.h" #include #include @@ -76,6 +77,7 @@ struct MetaCoreRenderSyncLight { }; struct MetaCoreSceneRenderSyncSnapshot { + MetaCoreSceneEnvironmentSettings Environment{}; std::unordered_map WorldMatrices{}; std::unordered_map LocalMatrices{}; std::vector Renderables{}; diff --git a/Source/MetaCoreScene/Private/MetaCoreScene.cpp b/Source/MetaCoreScene/Private/MetaCoreScene.cpp index c68a129..29be98c 100644 --- a/Source/MetaCoreScene/Private/MetaCoreScene.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreScene.cpp @@ -23,6 +23,19 @@ std::string MetaCoreBuildCopyName(const std::string& name) { return name + " Copy"; } +[[nodiscard]] bool MetaCoreEnvironmentSettingsEqual( + const MetaCoreSceneEnvironmentSettings& lhs, + const MetaCoreSceneEnvironmentSettings& rhs +) { + return lhs.Source == rhs.Source && + lhs.IblKtxPath == rhs.IblKtxPath && + lhs.SkyboxKtxPath == rhs.SkyboxKtxPath && + lhs.SkyboxVisible == rhs.SkyboxVisible && + lhs.RotationDegrees == rhs.RotationDegrees && + lhs.IndirectLightIntensity == rhs.IndirectLightIntensity && + lhs.SkyboxIntensity == rhs.SkyboxIntensity; +} + } // namespace MetaCoreGameObject MetaCoreScene::CreateGameObject(const std::string& name, MetaCoreId parentId) { @@ -405,8 +418,18 @@ bool MetaCoreScene::ReparentGameObjects(const std::vector& objectIds return true; } +bool MetaCoreScene::SetEnvironmentSettings(const MetaCoreSceneEnvironmentSettings& settings) { + if (MetaCoreEnvironmentSettingsEqual(EnvironmentSettings_, settings)) { + return false; + } + EnvironmentSettings_ = settings; + IncrementRevision(); + return true; +} + MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const { MetaCoreSceneSnapshot snapshot; + snapshot.Environment = EnvironmentSettings_; auto objects = GetGameObjects(); snapshot.GameObjects.reserve(objects.size()); for (auto obj : objects) { @@ -439,6 +462,7 @@ MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const { void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) { Registry_.clear(); IdToEntity_.clear(); + EnvironmentSettings_ = snapshot.Environment; MetaCoreId maxId = 0; for (const MetaCoreGameObjectData& data : snapshot.GameObjects) { diff --git a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp index 0963156..13e3702 100644 --- a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp @@ -32,6 +32,33 @@ static MetaCoreAssetGuid StringToGuid(const std::string& str) { return str.empty() ? MetaCoreAssetGuid{} : MetaCoreAssetGuid::Parse(str).value_or(MetaCoreAssetGuid{}); } +static json SceneEnvironmentToJson(const MetaCoreSceneEnvironmentSettings& environment) { + json environmentJson = json::object(); + environmentJson["Source"] = static_cast(environment.Source); + environmentJson["IblKtxPath"] = environment.IblKtxPath.generic_string(); + environmentJson["SkyboxKtxPath"] = environment.SkyboxKtxPath.generic_string(); + environmentJson["SkyboxVisible"] = environment.SkyboxVisible; + environmentJson["RotationDegrees"] = environment.RotationDegrees; + environmentJson["IndirectLightIntensity"] = environment.IndirectLightIntensity; + environmentJson["SkyboxIntensity"] = environment.SkyboxIntensity; + return environmentJson; +} + +static MetaCoreSceneEnvironmentSettings JsonToSceneEnvironment(const json& environmentJson) { + MetaCoreSceneEnvironmentSettings environment; + if (!environmentJson.is_object()) { + return environment; + } + if (environmentJson.contains("Source")) environment.Source = static_cast(environmentJson["Source"].get()); + if (environmentJson.contains("IblKtxPath")) environment.IblKtxPath = environmentJson["IblKtxPath"].get(); + if (environmentJson.contains("SkyboxKtxPath")) environment.SkyboxKtxPath = environmentJson["SkyboxKtxPath"].get(); + if (environmentJson.contains("SkyboxVisible")) environment.SkyboxVisible = environmentJson["SkyboxVisible"].get(); + if (environmentJson.contains("RotationDegrees")) environment.RotationDegrees = environmentJson["RotationDegrees"].get(); + if (environmentJson.contains("IndirectLightIntensity")) environment.IndirectLightIntensity = environmentJson["IndirectLightIntensity"].get(); + if (environmentJson.contains("SkyboxIntensity")) environment.SkyboxIntensity = environmentJson["SkyboxIntensity"].get(); + return environment; +} + // 辅助方法:将单个 MetaCoreGameObjectData 转换为 json 格式 static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCoreTypeRegistry& registry) { json objJson = json::object(); @@ -340,6 +367,8 @@ bool MetaCoreSceneSerializer::SaveSceneToJson( for (const auto& field : sceneDesc->Fields) { if (field.Name == "Name") { sceneJson["SceneName"] = sceneDocument.Name; + } else if (field.Name == "Environment") { + sceneJson["Environment"] = SceneEnvironmentToJson(sceneDocument.Environment); } else if (field.Name == "Selection") { const MetaCoreStructDescriptor* selDesc = registry.FindStruct(); if (selDesc) { @@ -400,6 +429,8 @@ std::optional MetaCoreSceneSerializer::LoadSceneFromJson( for (const auto& field : sceneDesc->Fields) { if (field.Name == "Name" && sceneJson.contains("SceneName")) { sceneDocument.Name = sceneJson["SceneName"].get(); + } else if (field.Name == "Environment" && sceneJson.contains("Environment")) { + sceneDocument.Environment = JsonToSceneEnvironment(sceneJson["Environment"]); } else if (field.Name == "Selection" && sceneJson.contains("Selection")) { const auto& selectionJson = sceneJson["Selection"]; const MetaCoreStructDescriptor* selDesc = registry.FindStruct(); diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h index 04582c9..fefbfbf 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h @@ -14,6 +14,9 @@ MC_STRUCT() struct MetaCoreSceneSnapshot { MC_GENERATED_BODY() + MC_PROPERTY() + MetaCoreSceneEnvironmentSettings Environment{}; + MC_PROPERTY() std::vector GameObjects; }; @@ -40,6 +43,9 @@ public: [[nodiscard]] std::vector DuplicateGameObjects(const std::vector& objectIds); bool ReparentGameObjects(const std::vector& objectIds, MetaCoreId newParentId, bool keepWorldTransform); + [[nodiscard]] const MetaCoreSceneEnvironmentSettings& GetEnvironmentSettings() const { return EnvironmentSettings_; } + [[nodiscard]] bool SetEnvironmentSettings(const MetaCoreSceneEnvironmentSettings& settings); + [[nodiscard]] MetaCoreSceneSnapshot CaptureSnapshot() const; void RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot); @@ -54,6 +60,7 @@ private: entt::registry Registry_; std::unordered_map IdToEntity_; + MetaCoreSceneEnvironmentSettings EnvironmentSettings_{}; std::uint64_t Revision_ = 0; }; diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h index b1acae2..0299327 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h @@ -7,6 +7,7 @@ #include "MetaCoreScene/MetaCoreGameObject.h" #include +#include #include #include @@ -26,6 +27,38 @@ struct MetaCoreEditorSelectionSnapshot { MetaCoreId SelectionAnchorId = 0; }; +MC_ENUM() +enum class MetaCoreSceneEnvironmentSource { + Builtin = 0, + ProjectKtxPair +}; + +MC_STRUCT() +struct MetaCoreSceneEnvironmentSettings { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreSceneEnvironmentSource Source = MetaCoreSceneEnvironmentSource::Builtin; + + MC_PROPERTY() + std::filesystem::path IblKtxPath{}; + + MC_PROPERTY() + std::filesystem::path SkyboxKtxPath{}; + + MC_PROPERTY() + bool SkyboxVisible = true; + + MC_PROPERTY() + float RotationDegrees = 0.0F; + + MC_PROPERTY() + float IndirectLightIntensity = 30000.0F; + + MC_PROPERTY() + float SkyboxIntensity = 30000.0F; +}; + MC_STRUCT() struct MetaCoreSceneDocument { MC_GENERATED_BODY() @@ -33,6 +66,9 @@ struct MetaCoreSceneDocument { MC_PROPERTY() std::string Name{}; + MC_PROPERTY() + MetaCoreSceneEnvironmentSettings Environment{}; + MC_PROPERTY() std::vector GameObjects{}; diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 7aabd79..d543266 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -4537,6 +4537,133 @@ void MetaCoreTestHotReloadQueuesExternalMoveUntilTick() { std::filesystem::remove_all(tempProjectRoot); } +void MetaCoreTestSceneEnvironmentSettings() { + const std::filesystem::path tempRoot = std::filesystem::temp_directory_path() / "MetaCoreSceneEnvironmentSmoke"; + std::filesystem::remove_all(tempRoot); + std::filesystem::create_directories(tempRoot / "Assets" / "Environments"); + + const std::filesystem::path sourceEnvironmentDirectory = + MetaCoreTestRepositoryRoot() / "third_party" / "filament_installed" / "bin" / "assets" / "ibl" / "lightroom_14b"; + const std::filesystem::path sourceIbl = sourceEnvironmentDirectory / "lightroom_14b_ibl.ktx"; + const std::filesystem::path sourceSkybox = sourceEnvironmentDirectory / "lightroom_14b_skybox.ktx"; + MetaCoreExpect(std::filesystem::exists(sourceIbl) && std::filesystem::exists(sourceSkybox), "应提供默认 KTX 环境资源"); + const std::filesystem::path projectIbl = tempRoot / "Assets" / "Environments" / "studio_ibl.ktx"; + const std::filesystem::path projectSkybox = tempRoot / "Assets" / "Environments" / "studio_skybox.ktx"; + std::filesystem::copy_file(sourceIbl, projectIbl, std::filesystem::copy_options::overwrite_existing); + std::filesystem::copy_file(sourceSkybox, projectSkybox, std::filesystem::copy_options::overwrite_existing); + + MetaCore::MetaCoreSceneEnvironmentSettings environment; + environment.Source = MetaCore::MetaCoreSceneEnvironmentSource::ProjectKtxPair; + environment.IblKtxPath = std::filesystem::path("Assets") / "Environments" / "studio_ibl.ktx"; + environment.SkyboxKtxPath = std::filesystem::path("Assets") / "Environments" / "studio_skybox.ktx"; + environment.SkyboxVisible = true; + environment.RotationDegrees = 137.5F; + environment.IndirectLightIntensity = 25000.0F; + environment.SkyboxIntensity = 7500.0F; + + MetaCore::MetaCoreScene scene; + MetaCoreExpect(scene.SetEnvironmentSettings(environment), "应能设置场景环境"); + const MetaCore::MetaCoreSceneSnapshot environmentSnapshot = scene.CaptureSnapshot(); + (void)scene.SetEnvironmentSettings(MetaCore::MetaCoreSceneEnvironmentSettings{}); + scene.RestoreSnapshot(environmentSnapshot); + MetaCoreExpect(scene.GetEnvironmentSettings().Source == MetaCore::MetaCoreSceneEnvironmentSource::ProjectKtxPair, "场景快照应恢复环境来源"); + MetaCoreExpect(scene.GetEnvironmentSettings().IblKtxPath == environment.IblKtxPath, "场景快照应恢复 IBL 路径"); + MetaCoreExpect(std::abs(scene.GetEnvironmentSettings().SkyboxIntensity - environment.SkyboxIntensity) <= 0.0001F, "场景快照应恢复天空盒亮度"); + + MetaCore::MetaCoreWindow commandWindow; + MetaCore::MetaCoreRenderDevice commandRenderDevice; + MetaCore::MetaCoreEditorViewportRenderer commandViewportRenderer; + MetaCore::MetaCoreLogService commandLogService; + MetaCore::MetaCoreEditorModuleRegistry commandModuleRegistry; + MetaCore::MetaCoreEditorContext commandContext( + commandWindow, + commandRenderDevice, + commandViewportRenderer, + scene, + commandLogService, + commandModuleRegistry + ); + MetaCoreExpect(commandContext.ExecuteSnapshotCommand("修改天空盒显示", [&]() { + MetaCore::MetaCoreSceneEnvironmentSettings changed = scene.GetEnvironmentSettings(); + changed.SkyboxVisible = false; + return scene.SetEnvironmentSettings(changed); + }), "环境设置修改应进入撤销栈"); + MetaCoreExpect(!scene.GetEnvironmentSettings().SkyboxVisible, "执行环境设置命令后应更新天空盒显示"); + MetaCoreExpect(commandContext.UndoCommand(), "环境设置应支持撤销"); + MetaCoreExpect(scene.GetEnvironmentSettings().SkyboxVisible, "撤销后应恢复天空盒显示"); + MetaCoreExpect(commandContext.RedoCommand(), "环境设置应支持重做"); + MetaCoreExpect(!scene.GetEnvironmentSettings().SkyboxVisible, "重做后应再次关闭天空盒显示"); + (void)scene.SetEnvironmentSettings(environment); + + MetaCore::MetaCoreSceneRenderSync renderSync; + const MetaCore::MetaCoreSceneRenderSyncSnapshot renderSnapshot = renderSync.BuildSnapshot(scene); + MetaCoreExpect(renderSnapshot.Environment.IblKtxPath == environment.IblKtxPath, "渲染快照应包含环境路径"); + MetaCoreExpect(renderSnapshot.Renderables.empty(), "环境设置不应生成可渲染场景对象"); + + MetaCore::MetaCoreTypeRegistry registry; + MetaCore::MetaCoreRegisterFoundationGeneratedTypes(registry); + MetaCore::MetaCoreRegisterSceneGeneratedTypes(registry); + MetaCore::MetaCoreSceneDocument document; + document.Name = "EnvironmentScene"; + document.Environment = environment; + const std::filesystem::path jsonPath = tempRoot / "Environment.mcscene.json"; + MetaCoreExpect(MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(jsonPath, document, registry), "环境场景 JSON 应可保存"); + const auto loadedDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(jsonPath, registry); + MetaCoreExpect(loadedDocument.has_value(), "环境场景 JSON 应可读取"); + MetaCoreExpect(loadedDocument->Environment.SkyboxKtxPath == environment.SkyboxKtxPath, "环境场景 JSON 应保留天空盒路径"); + MetaCoreExpect(std::abs(loadedDocument->Environment.RotationDegrees - environment.RotationDegrees) <= 0.0001F, "环境场景 JSON 应保留旋转"); + + const std::filesystem::path packagePath = tempRoot / "Environment.mcscene"; + MetaCoreExpect(MetaCore::MetaCoreWriteScenePackage(packagePath, document), "环境场景包应可保存"); + const auto loadedPackage = MetaCore::MetaCoreReadScenePackage(packagePath); + MetaCoreExpect(loadedPackage.has_value(), "环境场景包应可读取"); + MetaCoreExpect(loadedPackage->Environment.Source == MetaCore::MetaCoreSceneEnvironmentSource::ProjectKtxPair, "环境场景包应保留来源"); + + const std::filesystem::path legacyJsonPath = tempRoot / "Legacy.mcscene.json"; + { + std::ofstream legacyOutput(legacyJsonPath, std::ios::trunc); + legacyOutput << "{\"SceneName\":\"Legacy\",\"GameObjects\":[]}"; + } + const auto legacyDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(legacyJsonPath, registry); + MetaCoreExpect(legacyDocument.has_value(), "旧场景缺少环境字段时仍应可读取"); + MetaCoreExpect(legacyDocument->Environment.Source == MetaCore::MetaCoreSceneEnvironmentSource::Builtin, "旧场景应使用内置环境默认值"); + + MetaCore::MetaCoreWindow window; + if (!window.Initialize(640, 480, "SceneEnvironmentSmoke")) { + std::cout << "[SKIP] Scene environment Filament smoke requires an available display." << std::endl; + std::filesystem::remove_all(tempRoot); + return; + } + MetaCore::MetaCoreFilamentSceneBridge bridge; + bridge.SetProjectRootPath(tempRoot); + MetaCoreExpect(bridge.Initialize(window), "环境测试桥接器应初始化成功"); + bridge.SyncScene(renderSnapshot, nullptr, false, false); + bridge.ApplySceneView(MetaCore::MetaCoreSceneView{}); + MetaCoreExpect(bridge.VerifyEnvironmentLoadedForTesting(), "自定义 KTX 对应加载为环境资源"); + MetaCoreExpect(bridge.VerifyEnvironmentBackgroundVisibleForTesting(), "天空盒应在 Skybox 清屏模式下显示"); + MetaCoreExpect( + std::abs(bridge.GetEnvironmentIndirectLightIntensityForTesting() - environment.IndirectLightIntensity) <= 0.0001F, + "环境光强度应写入 Filament" + ); + + MetaCore::MetaCoreSceneEnvironmentSettings hiddenEnvironment = environment; + hiddenEnvironment.SkyboxVisible = false; + (void)scene.SetEnvironmentSettings(hiddenEnvironment); + bridge.SyncScene(renderSync.BuildSnapshot(scene), nullptr, false, false); + MetaCoreExpect(!bridge.VerifyEnvironmentBackgroundVisibleForTesting(), "关闭天空盒后背景不应显示"); + MetaCoreExpect(bridge.VerifyEnvironmentLoadedForTesting(), "关闭天空盒不应销毁环境光"); + + MetaCore::MetaCoreSceneEnvironmentSettings invalidEnvironment = environment; + invalidEnvironment.IblKtxPath = std::filesystem::path("..") / "unsafe_ibl.ktx"; + (void)scene.SetEnvironmentSettings(invalidEnvironment); + bridge.SyncScene(renderSync.BuildSnapshot(scene), nullptr, false, false); + MetaCoreExpect(bridge.VerifyEnvironmentLoadedForTesting(), "无效自定义路径应回退到内置环境"); + + bridge.Shutdown(); + window.Shutdown(); + std::filesystem::remove_all(tempRoot); +} + } // namespace int main() { @@ -4618,6 +4745,8 @@ int main() { MetaCoreTestNativeScriptBehavioursRestorePrePlaySnapshotOnStop(); std::cout << "[RUN] MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot..." << std::endl; MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot(); + std::cout << "[RUN] MetaCoreTestSceneEnvironmentSettings..." << std::endl; + MetaCoreTestSceneEnvironmentSettings(); std::cout << "[RUN] MetaCoreTestCameraComponentJsonRoundTripAndLegacyDefaults..." << std::endl; MetaCoreTestCameraComponentJsonRoundTripAndLegacyDefaults(); std::cout << "[RUN] MetaCoreTestCameraSelectionAndProjectionUtilities..." << std::endl;