From 31a408d813adc83bfdb142e4e6bc30c4e8a321e2 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Mon, 27 Jul 2026 17:02:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=BA=E6=99=AF=E5=8A=A0=E8=BD=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Apps/MetaCorePlayer/main.cpp | 50 +- CMakeLists.txt | 17 + .../Private/MetaCoreBuildPipeline.cpp | 164 ++++- .../MetaCoreDelivery/MetaCoreBuildPipeline.h | 9 + .../MetaCoreBuiltinCoreServicesModule.cpp | 222 ++++++- .../Private/MetaCoreBuiltinEditorModule.cpp | 71 ++- .../Private/MetaCoreEditorApp.cpp | 17 +- .../MetaCoreEditorCameraController.cpp | 18 +- .../Private/MetaCoreEditorCameraController.h | 1 + .../MetaCoreSceneInteractionService.cpp | 130 ++-- .../Public/MetaCoreEditor/MetaCoreEditorApp.h | 2 + .../MetaCoreEditor/MetaCoreEditorContext.h | 4 + .../MetaCoreEditor/MetaCoreEditorServices.h | 6 + .../Private/MetaCoreFilamentSceneBridge.cpp | 82 ++- .../MetaCoreRender/MetaCoreSceneRenderSync.h | 8 + .../MetaCoreScene/Private/MetaCorePrefab.cpp | 388 ++++++++++++ .../MetaCoreScene/Private/MetaCoreScene.cpp | 10 + .../Private/MetaCoreScenePackage.cpp | 23 +- .../Private/MetaCoreSceneSerializer.cpp | 266 ++++++++- .../Private/MetaCoreSceneWorld.cpp | 565 ++++++++++++++++++ .../Public/MetaCoreScene/MetaCoreComponents.h | 5 + .../Public/MetaCoreScene/MetaCoreGameObject.h | 23 + .../MetaCoreScene/MetaCoreObjectReference.h | 30 + .../Public/MetaCoreScene/MetaCorePrefab.h | 64 ++ .../MetaCoreScene/MetaCoreSceneDocument.h | 153 +++++ .../Public/MetaCoreScene/MetaCoreSceneWorld.h | 175 ++++++ ...etacore-mature-engine-gap-and-work-plan.md | 25 +- tests/MetaCoreDeliveryTests.cpp | 10 + tests/MetaCoreSceneBenchmark.cpp | 81 +++ tests/MetaCoreSceneTests.cpp | 306 ++++++++++ tests/MetaCoreSmokeTests.cpp | 31 +- 31 files changed, 2812 insertions(+), 144 deletions(-) create mode 100644 Source/MetaCoreScene/Private/MetaCorePrefab.cpp create mode 100644 Source/MetaCoreScene/Private/MetaCoreSceneWorld.cpp create mode 100644 Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreObjectReference.h create mode 100644 Source/MetaCoreScene/Public/MetaCoreScene/MetaCorePrefab.h create mode 100644 Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneWorld.h create mode 100644 tests/MetaCoreSceneBenchmark.cpp create mode 100644 tests/MetaCoreSceneTests.cpp diff --git a/Apps/MetaCorePlayer/main.cpp b/Apps/MetaCorePlayer/main.cpp index 9ace87b..9b55aad 100644 --- a/Apps/MetaCorePlayer/main.cpp +++ b/Apps/MetaCorePlayer/main.cpp @@ -21,6 +21,7 @@ #include "MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h" #include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreScene.h" +#include "MetaCoreScene/MetaCoreSceneWorld.h" #include "MetaCoreScripting/MetaCoreScripting.h" #include @@ -30,6 +31,7 @@ #include #include #include +#include #include namespace { @@ -450,10 +452,50 @@ int main(int argc, char* argv[]) { startupSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(projectPath); } MetaCore::MetaCoreScene scene; + std::unordered_map runtimeScenePackages; + if (deliveryDocument) for (const auto& entry : deliveryDocument->ScenePackages) + runtimeScenePackages[entry.SceneGuid] = packageRoot / entry.PackagePath; + MetaCore::MetaCoreSceneWorld sceneWorld( + scene, + [&](const MetaCore::MetaCoreAssetGuid& sceneGuid) -> std::optional { + if (const auto path = runtimeScenePackages.find(sceneGuid); path != runtimeScenePackages.end()) + return MetaCore::MetaCoreReadScenePackage(path->second); + if (!deliveryDocument) { + std::error_code scanError; + const auto scenesRoot = projectRoot / "Scenes"; + if (std::filesystem::is_directory(scenesRoot, scanError)) { + for (std::filesystem::recursive_directory_iterator iterator( + scenesRoot, std::filesystem::directory_options::skip_permission_denied, scanError), end; + !scanError && iterator != end; iterator.increment(scanError)) { + if (!iterator->is_regular_file(scanError)) continue; + auto candidate = MetaCore::MetaCoreReadScenePackage(iterator->path()); + if (candidate && candidate->SceneGuid == sceneGuid) return candidate; + } + } + } + return std::nullopt; + } + ); if (startupSceneDocument.has_value()) { - MetaCore::MetaCoreSceneSnapshot snapshot; - snapshot.GameObjects = startupSceneDocument->GameObjects; - scene.RestoreSnapshot(snapshot); + std::string sceneLoadError; + if (!sceneWorld.LoadDocumentNow(*startupSceneDocument, MetaCore::MetaCoreSceneLoadMode::Replace, &sceneLoadError)) { + errors << "MCB6004: startup SceneWorld load failed: " << sceneLoadError << '\n'; + return 2; + } + // AlwaysLoaded SubScenes are part of the startup transaction from the + // runtime's point of view: settle their background reads before systems scan ECS. + const std::size_t expectedStartupScenes = 1U + static_cast(std::count_if( + startupSceneDocument->SubScenes.begin(), startupSceneDocument->SubScenes.end(), + [](const auto& subScene) { return subScene.LoadPolicy == MetaCore::MetaCoreSubSceneLoadPolicy::AlwaysLoaded; } + )); + for (int iteration = 0; iteration < 1000 && sceneWorld.GetLoadedScenes().size() < expectedStartupScenes; ++iteration) { + sceneWorld.Tick(); + std::this_thread::yield(); + } + if (sceneWorld.GetLoadedScenes().size() < expectedStartupScenes) { + errors << "MCB6004: one or more AlwaysLoaded SubScenes failed to load\n"; + return 2; + } output << "MetaCorePlayer: loaded startup scene from project " << projectPath.string() << '\n'; } else if (!deliveryDocument) { @@ -704,6 +746,8 @@ int main(int argc, char* argv[]) { MetaCore::MetaCoreSceneView interactionSceneView = MetaCoreBuildPlayerSceneView(); const auto interactionSnapshot = interactionCameraSync.BuildSnapshot(scene); (void)MetaCore::MetaCoreSceneRenderSync::TryBuildSceneViewFromPrimaryCamera(interactionSnapshot, interactionSceneView); + sceneWorld.UpdateStreaming({interactionSceneView.CameraPosition}); + sceneWorld.Tick(); runtimeInteraction.Update(scene, viewportRenderer, physicsWorld, interactionSceneView, window.GetInput(), inputViewport, uiConsumption.Mouse); fixedAccumulatorSeconds += std::clamp(frameDeltaSeconds, 0.0, physicsSettings.MaxFrameDelta); diff --git a/CMakeLists.txt b/CMakeLists.txt index e86779d..ad996af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -561,14 +561,19 @@ set(METACORE_SCENE_HEADERS Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h + Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreObjectReference.h + Source/MetaCoreScene/Public/MetaCoreScene/MetaCorePrefab.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScenePackage.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h + Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneWorld.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneSerializer.h ) set(METACORE_SCENE_SOURCES Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp Source/MetaCoreScene/Private/MetaCoreScene.cpp + Source/MetaCoreScene/Private/MetaCoreSceneWorld.cpp + Source/MetaCoreScene/Private/MetaCorePrefab.cpp Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp ) @@ -579,6 +584,7 @@ metacore_generate_reflection( Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h + Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreObjectReference.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h ) @@ -1001,6 +1007,17 @@ metacore_stage_material_templates(MetaCorePlayer) if(METACORE_BUILD_TESTS) enable_testing() + add_executable(MetaCoreSceneTests tests/MetaCoreSceneTests.cpp) + target_link_libraries(MetaCoreSceneTests PRIVATE MetaCoreScene) + target_compile_options(MetaCoreSceneTests PRIVATE ${METACORE_COMMON_WARNINGS}) + add_test(NAME MetaCoreSceneTests COMMAND MetaCoreSceneTests) + + add_executable(MetaCoreSceneBenchmark tests/MetaCoreSceneBenchmark.cpp) + target_link_libraries(MetaCoreSceneBenchmark PRIVATE MetaCoreScene) + target_compile_options(MetaCoreSceneBenchmark PRIVATE ${METACORE_COMMON_WARNINGS}) + add_test(NAME MetaCoreSceneBenchmark COMMAND MetaCoreSceneBenchmark) + set_tests_properties(MetaCoreSceneBenchmark PROPERTIES LABELS "benchmark") + add_executable(MetaCoreDeliveryTests tests/MetaCoreDeliveryTests.cpp Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp diff --git a/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp b/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp index 997d3a2..3457ca8 100644 --- a/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp +++ b/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp @@ -12,6 +12,7 @@ #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCoreRendering/MetaCoreRendering.h" #include "MetaCoreScene/MetaCoreScenePackage.h" +#include "MetaCoreScene/MetaCorePrefab.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" #include "MetaCoreScripting/MetaCoreScripting.h" @@ -30,6 +31,7 @@ #include #include #include +#include #if defined(__linux__) #include #endif @@ -562,6 +564,20 @@ std::optional MetaCoreReadRuntimeDeliveryDocume if (module.is_string()) document.ScriptModules.emplace_back(module.get()); } } + if (value->contains("scene_packages") && (*value)["scene_packages"].is_array()) { + for (const auto& scene : (*value)["scene_packages"]) { + MetaCoreRuntimeDeliveryDocument::SceneEntry entry; + entry.SceneGuid = MetaCoreAssetGuid::Parse(scene.value("guid", "")).value_or(MetaCoreAssetGuid{}); + entry.PackagePath = scene.value("path", ""); + entry.SourceHash = scene.value("source_hash", std::uint64_t{}); + if (scene.contains("dependencies")) for (const auto& dependency : scene["dependencies"]) { + if (dependency.is_string()) { + if (const auto guid = MetaCoreAssetGuid::Parse(dependency.get())) entry.Dependencies.push_back(*guid); + } + } + document.ScenePackages.push_back(std::move(entry)); + } + } if (document.FormatVersion != GMetaCoreRuntimeDeliveryVersion) { SetIssue(issue, MetaCoreBuildErrorCode::ManifestVersionUnsupported, "Runtime delivery format is unsupported", path); return std::nullopt; @@ -582,18 +598,36 @@ std::optional MetaCoreReadRuntimeDeliveryDocume return std::nullopt; } } + for (const auto& scene : document.ScenePackages) { + if (!scene.SceneGuid.IsValid() || !IsSafeRelativePath(scene.PackagePath)) { + SetIssue(issue, MetaCoreBuildErrorCode::UnsafePath, "Runtime delivery contains an invalid scene entry", path); + return std::nullopt; + } + } return document; } bool MetaCoreWriteRuntimeDeliveryDocument(const std::filesystem::path& path, const MetaCoreRuntimeDeliveryDocument& document) { json modules = json::array(); for (const auto& module : document.ScriptModules) modules.push_back(module.generic_string()); + json scenes = json::array(); + for (const auto& scene : document.ScenePackages) { + json dependencies = json::array(); + for (const auto& dependency : scene.Dependencies) dependencies.push_back(dependency.ToString()); + scenes.push_back({ + {"guid", scene.SceneGuid.ToString()}, + {"path", scene.PackagePath.generic_string()}, + {"source_hash", scene.SourceHash}, + {"dependencies", std::move(dependencies)} + }); + } return WriteJson(path, { {"format_version", document.FormatVersion}, {"runtime_abi_major", document.RuntimeAbiMajor}, {"runtime_abi_minor", document.RuntimeAbiMinor}, {"build_id", document.BuildId}, {"content_root", document.ContentRoot.generic_string()}, {"startup_scene", document.StartupScene.generic_string()}, {"runtime_config", document.RuntimeConfig.generic_string()}, {"ui_manifest", document.UiManifest.generic_string()}, - {"release_manifest", document.ReleaseManifest.generic_string()}, {"script_modules", std::move(modules)} + {"release_manifest", document.ReleaseManifest.generic_string()}, {"script_modules", std::move(modules)}, + {"scene_packages", std::move(scenes)} }); } @@ -832,6 +866,7 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque MetaCoreRegisterRuntimeDataGeneratedTypes(inputRegistry); MetaCoreRegisterRuntimeInputGeneratedTypes(inputRegistry); MetaCoreRegisterPhysicsGeneratedTypes(inputRegistry); + const MetaCoreTypeRegistry sceneRegistry = MetaCoreBuildScenePackageTypeRegistry(); const auto runtimeProjectPath = request.ProjectRoot / project->RuntimeDirectory / "ProjectRuntime.mcruntimecfg"; auto runtimeProject = MetaCoreReadRuntimeProjectDocument(runtimeProjectPath, inputRegistry); if (!runtimeProject) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime project configuration is missing or unreadable", runtimeProjectPath); @@ -883,6 +918,7 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque const auto cookedScene = staging / "Content" / "Scenes" / cookedSceneName; const auto cookablePhysicsMeshes = MetaCoreCollectCookablePhysicsMeshes(request.ProjectRoot, inputRegistry, staging / "Content" / "Physics"); std::optional startupSceneDocument; + std::vector stagedSceneEntries; if (startupSource.extension() == ".json") { const auto registry = MetaCoreBuildScenePackageTypeRegistry(); const auto scene = MetaCoreSceneSerializer::LoadSceneFromJson(absoluteStartup, registry); @@ -903,6 +939,131 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque std::filesystem::copy_file(absoluteStartup, cookedScene, std::filesystem::copy_options::overwrite_existing, error); if (error) return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to copy startup scene", absoluteStartup); } + if (startupSceneDocument) { + if (!startupSceneDocument->SceneGuid.IsValid()) + return fail(MetaCoreBuildErrorCode::CookFailed, "Startup scene has no stable SceneGuid", absoluteStartup); + + std::unordered_map sceneSources; + std::error_code sceneScanError; + const auto sceneRoot = request.ProjectRoot / "Scenes"; + if (std::filesystem::is_directory(sceneRoot, sceneScanError)) { + for (std::filesystem::recursive_directory_iterator iterator( + sceneRoot, std::filesystem::directory_options::skip_permission_denied, sceneScanError), end; + !sceneScanError && iterator != end; iterator.increment(sceneScanError)) { + if (!iterator->is_regular_file(sceneScanError)) continue; + const auto path = iterator->path(); + if (!path.filename().string().ends_with(".mcscene.json")) continue; + if (const auto candidate = MetaCoreSceneSerializer::LoadSceneFromJson(path, sceneRegistry); + candidate && candidate->SceneGuid.IsValid()) { + sceneSources[candidate->SceneGuid] = path; + } + } + } + sceneSources[startupSceneDocument->SceneGuid] = absoluteStartup; + + std::unordered_map prefabDocuments; + std::error_code prefabScanError; + const auto prefabRoot = request.ProjectRoot / "Assets"; + if (std::filesystem::is_directory(prefabRoot, prefabScanError)) { + for (std::filesystem::recursive_directory_iterator iterator( + prefabRoot, std::filesystem::directory_options::skip_permission_denied, prefabScanError), end; + !prefabScanError && iterator != end; iterator.increment(prefabScanError)) { + if (!iterator->is_regular_file(prefabScanError) || + !iterator->path().filename().string().ends_with(".mcprefab.json")) continue; + if (auto prefab = MetaCoreSceneSerializer::LoadPrefabFromJson(iterator->path(), sceneRegistry); + prefab && prefab->PrefabGuid.IsValid()) + prefabDocuments[prefab->PrefabGuid] = std::move(*prefab); + } + } + MetaCorePrefabResolver prefabResolver( + [&](const MetaCoreAssetGuid& guid) -> std::optional { + const auto iterator = prefabDocuments.find(guid); + return iterator == prefabDocuments.end() ? std::nullopt : std::optional(iterator->second); + } + ); + + std::unordered_set visitedScenes; + std::unordered_set activeScenes; + std::string sceneDependencyError; + std::function stageSceneClosure; + stageSceneClosure = [&](const MetaCoreSceneDocument& sceneDocument, + const std::filesystem::path& sourcePath, + const std::filesystem::path& outputPath) { + if (visitedScenes.contains(sceneDocument.SceneGuid)) return true; + if (!activeScenes.insert(sceneDocument.SceneGuid).second) { + sceneDependencyError = "SubScene dependency cycle at " + sceneDocument.SceneGuid.ToString(); + return false; + } + for (const auto& instance : sceneDocument.PrefabInstances) { + const auto prefabIssues = prefabResolver.ValidateDependencies(instance.PrefabAssetGuid); + if (!prefabIssues.empty()) { + sceneDependencyError = "Prefab dependency validation failed for " + + instance.PrefabAssetGuid.ToString() + ": " + prefabIssues.front(); + return false; + } + } + std::vector dependencies; + for (const auto& subScene : sceneDocument.SubScenes) + if (subScene.SceneGuid.IsValid()) dependencies.push_back(subScene.SceneGuid); + for (const auto& object : sceneDocument.GameObjects) { + if (object.PhysicsConstraint && + object.PhysicsConstraint->TargetObjectReference.Strength == MetaCoreObjectReferenceStrength::Hard && + object.PhysicsConstraint->TargetObjectReference.SceneGuid.IsValid()) + dependencies.push_back(object.PhysicsConstraint->TargetObjectReference.SceneGuid); + if (object.TimelineDirector) for (const auto& binding : object.TimelineDirector->Bindings) + if (binding.ObjectReference.Strength == MetaCoreObjectReferenceStrength::Hard && + binding.ObjectReference.SceneGuid.IsValid()) + dependencies.push_back(binding.ObjectReference.SceneGuid); + } + std::sort(dependencies.begin(), dependencies.end(), [](const auto& lhs, const auto& rhs) { + return lhs.ToString() < rhs.ToString(); + }); + dependencies.erase(std::unique(dependencies.begin(), dependencies.end()), dependencies.end()); + for (const auto& dependency : dependencies) { + if (dependency == sceneDocument.SceneGuid) continue; + auto source = sceneSources.find(dependency); + if (source == sceneSources.end()) { + const auto declared = std::find_if(sceneDocument.SubScenes.begin(), sceneDocument.SubScenes.end(), + [&](const auto& subScene) { return subScene.SceneGuid == dependency && !subScene.ScenePath.empty(); }); + if (declared != sceneDocument.SubScenes.end() && IsSafeRelativePath(declared->ScenePath)) + source = sceneSources.emplace(dependency, request.ProjectRoot / declared->ScenePath).first; + } + if (source == sceneSources.end() || !std::filesystem::is_regular_file(source->second)) { + sceneDependencyError = "Required Scene dependency is missing: " + dependency.ToString(); + return false; + } + auto dependencyDocument = source->second.extension() == ".json" + ? MetaCoreSceneSerializer::LoadSceneFromJson(source->second, sceneRegistry) + : MetaCoreReadScenePackage(source->second); + if (!dependencyDocument || dependencyDocument->SceneGuid != dependency) { + sceneDependencyError = "Scene dependency GUID/path mismatch: " + dependency.ToString(); + return false; + } + const auto relativeOutput = std::filesystem::path("Content") / "Scenes" / + (dependency.ToString() + ".mcscene"); + if (!stageSceneClosure(*dependencyDocument, source->second, staging / relativeOutput)) return false; + } + if (outputPath != cookedScene && !MetaCoreWriteScenePackage(outputPath, sceneDocument)) { + sceneDependencyError = "Failed to cook dependent Scene: " + sourcePath.string(); + return false; + } + stagedSceneEntries.push_back({ + sceneDocument.SceneGuid, + outputPath.lexically_relative(staging), + MetaCoreHashFile(sourcePath).value_or(0), + dependencies + }); + activeScenes.erase(sceneDocument.SceneGuid); + visitedScenes.insert(sceneDocument.SceneGuid); + return true; + }; + if (!stageSceneClosure(*startupSceneDocument, absoluteStartup, cookedScene)) + return fail(MetaCoreBuildErrorCode::DependencyMissing, sceneDependencyError, absoluteStartup); + } const auto currentCookRoot = request.ProjectRoot / "Library" / "Cooked" / profile->TargetPlatform / profile->Name; const auto editorCookRoot = request.ProjectRoot / "Library" / "Cooked" / profile->TargetPlatform / "Editor"; const auto legacyCookRoot = request.ProjectRoot / "Library" / "Cooked" / "Windows"; @@ -1036,6 +1197,7 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque MetaCoreRuntimeDeliveryDocument delivery; delivery.BuildId = result.BuildId; delivery.StartupScene = std::filesystem::path("Content") / "Scenes" / cookedSceneName; + delivery.ScenePackages = stagedSceneEntries; if (!stagedScriptModule.empty()) delivery.ScriptModules.push_back(std::filesystem::path("Project") / "Scripts" / stagedScriptModule.filename()); if (!MetaCoreWriteRuntimeDeliveryDocument(staging / "Project" / "MetaCore.runtimeproject", delivery)) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to write runtime project document", staging); diff --git a/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h b/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h index 44c509b..29cc8d5 100644 --- a/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h +++ b/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h @@ -1,5 +1,7 @@ #pragma once +#include "MetaCoreFoundation/MetaCoreAssetGuid.h" + #include #include #include @@ -62,6 +64,13 @@ struct MetaCoreRuntimeDeliveryDocument { std::filesystem::path UiManifest = "Content/Runtime/Ui.mcruntime"; std::filesystem::path ReleaseManifest = "Manifest/MetaCore.release.json"; std::vector ScriptModules{}; + struct SceneEntry { + MetaCoreAssetGuid SceneGuid{}; + std::filesystem::path PackagePath{}; + std::uint64_t SourceHash = 0; + std::vector Dependencies{}; + }; + std::vector ScenePackages{}; }; struct MetaCoreReleaseFileEntry { diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index ab5a362..b0dac1c 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -26,6 +26,8 @@ #include "MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h" #include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" +#include "MetaCoreScene/MetaCoreSceneWorld.h" +#include "MetaCoreScene/MetaCorePrefab.h" #include #include @@ -9869,6 +9871,7 @@ public: CurrentSceneGuid_ = MetaCoreAssetGuid{}; CurrentScenePath_.clear(); CurrentSceneName_.clear(); + ScenePaths_.clear(); SavedSnapshotBytes_.clear(); Dirty_ = false; } @@ -9893,6 +9896,18 @@ public: } bool LoadScene(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) override { + return LoadSceneWithMode(editorContext, relativeScenePath, MetaCoreSceneLoadMode::Replace); + } + + bool LoadSceneAdditive(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) override { + return LoadSceneWithMode(editorContext, relativeScenePath, MetaCoreSceneLoadMode::Additive); + } + + bool LoadSceneWithMode( + MetaCoreEditorContext& editorContext, + const std::filesystem::path& relativeScenePath, + MetaCoreSceneLoadMode loadMode + ) { if (AssetDatabaseService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject() || @@ -9944,15 +9959,12 @@ public: if(!migrationFailed)for(const auto&change:pending){auto&animator=*sceneDocument->GameObjects[change.ObjectIndex].Animator;animator.ControllerAssetGuid=change.Controller;animator.AvatarAssetGuid=change.Avatar;migratedLegacyAnimators=true;} } - MetaCoreEditorStateSnapshot editorStateSnapshot; - editorStateSnapshot.SceneSnapshot.Environment = sceneDocument->Environment; - editorStateSnapshot.SceneSnapshot.GameObjects = sceneDocument->GameObjects; - editorStateSnapshot.SelectionSnapshot = sceneDocument->Selection; - editorContext.RestoreStateSnapshot(editorStateSnapshot); - editorContext.GetCommandService().Clear(); - // 从 .meta 提取或生成 Guid - MetaCoreAssetGuid sceneGuid{}; + MetaCoreAssetGuid sceneGuid = sceneDocument->SceneGuid; + if (const auto record = AssetDatabaseService_->FindAssetByRelativePath(normalizedScenePath); + record.has_value() && record->Guid.IsValid()) { + sceneGuid = record->Guid; + } const std::filesystem::path metaPath = absoluteScenePath.string() + ".meta"; if (std::filesystem::exists(metaPath)) { sceneGuid = MetaCoreAssetRegistry::Get().ResolvePathToGuid(normalizedScenePath); @@ -9960,12 +9972,42 @@ public: if (!sceneGuid.IsValid()) { sceneGuid = MetaCoreAssetGuid::Generate(); } + sceneDocument->SceneGuid = sceneGuid; + for (auto& object : sceneDocument->GameObjects) object.SceneGuid = sceneGuid; + + if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld()) { + world->SetSceneLoader([this](const MetaCoreAssetGuid& guid) -> std::optional { + if (AssetDatabaseService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) + return std::nullopt; + const auto record = AssetDatabaseService_->FindAssetByGuid(guid); + if (!record) return std::nullopt; + const auto relative = !record->PackagePath.empty() ? record->PackagePath : record->RelativePath; + const auto absolute = AssetDatabaseService_->GetProjectDescriptor().RootPath / relative; + if (relative.extension() == ".mcscene") { + if (auto package = MetaCoreReadScenePackage(absolute)) return package; + } + return MetaCoreSceneSerializer::LoadSceneFromJson(absolute, ReflectionRegistry_->GetTypeRegistry()); + }); + std::string loadError; + if (!world->LoadDocumentNow(*sceneDocument, loadMode, &loadError)) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Scene", "场景组合加载失败: " + loadError); + return false; + } + } else { + MetaCoreEditorStateSnapshot editorStateSnapshot; + editorStateSnapshot.SceneSnapshot.Environment = sceneDocument->Environment; + editorStateSnapshot.SceneSnapshot.GameObjects = sceneDocument->GameObjects; + editorStateSnapshot.SelectionSnapshot = sceneDocument->Selection; + editorContext.RestoreStateSnapshot(editorStateSnapshot); + } + editorContext.GetCommandService().Clear(); CurrentSceneGuid_ = sceneGuid; CurrentScenePath_ = normalizedScenePath; CurrentSceneName_ = sceneDocument->Name.empty() ? MetaCoreSceneDisplayNameFromPath(normalizedScenePath) : sceneDocument->Name; + ScenePaths_[sceneGuid] = normalizedScenePath; SavedSnapshotBytes_ = MetaCoreSerializeSceneSnapshotBytes( ReflectionRegistry_->GetTypeRegistry(), editorContext.GetScene().CaptureSnapshot() @@ -9986,6 +10028,30 @@ public: return true; } + bool UnloadScene(MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& sceneGuid, bool force) override { + MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); + if (world == nullptr || !world->UnloadScene(sceneGuid, force)) return false; + ScenePaths_.erase(sceneGuid); + if (CurrentSceneGuid_ == sceneGuid) { + CurrentSceneGuid_ = world->GetActiveSceneGuid(); + const auto path = ScenePaths_.find(CurrentSceneGuid_); + CurrentScenePath_ = path == ScenePaths_.end() ? std::filesystem::path{} : path->second; + CurrentSceneName_ = CurrentScenePath_.stem().string(); + } + editorContext.ClearSelection(); + return true; + } + + bool SetActiveScene(MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& sceneGuid) override { + MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); + if (world == nullptr || !world->SetActiveScene(sceneGuid)) return false; + CurrentSceneGuid_ = sceneGuid; + const auto path = ScenePaths_.find(sceneGuid); + CurrentScenePath_ = path == ScenePaths_.end() ? std::filesystem::path{} : path->second; + CurrentSceneName_ = CurrentScenePath_.stem().string(); + return true; + } + bool SaveCurrentScene(MetaCoreEditorContext& editorContext) override { return CurrentScenePath_.empty() ? SaveSceneAs(editorContext, std::filesystem::path("Scenes") / "Untitled.mcscene.json") @@ -10019,9 +10085,17 @@ public: MetaCoreSceneDocument sceneDocument; sceneDocument.Name = CurrentSceneName_.empty() ? MetaCoreSceneDisplayNameFromPath(normalizedScenePath) : CurrentSceneName_; - const MetaCoreSceneSnapshot sceneSnapshot = editorContext.GetScene().CaptureSnapshot(); - sceneDocument.Environment = sceneSnapshot.Environment; - sceneDocument.GameObjects = sceneSnapshot.GameObjects; + if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && CurrentSceneGuid_.IsValid()) { + const auto captured = world->CaptureSceneDocument(CurrentSceneGuid_); + if (!captured) return false; + sceneDocument = *captured; + sceneDocument.Name = CurrentSceneName_.empty() ? MetaCoreSceneDisplayNameFromPath(normalizedScenePath) : CurrentSceneName_; + } else { + const MetaCoreSceneSnapshot sceneSnapshot = editorContext.GetScene().CaptureSnapshot(); + sceneDocument.Environment = sceneSnapshot.Environment; + sceneDocument.GameObjects = sceneSnapshot.GameObjects; + } + sceneDocument.SceneGuid = sceneGuid; sceneDocument.Selection.SelectedObjectIds = editorContext.GetSelectedObjectIds(); sceneDocument.Selection.ActiveObjectId = editorContext.GetActiveObjectId(); sceneDocument.Selection.SelectionAnchorId = editorContext.GetSelectionAnchorId(); @@ -10047,6 +10121,7 @@ public: CurrentSceneGuid_ = sceneGuid; CurrentScenePath_ = normalizedScenePath; CurrentSceneName_ = sceneDocument.Name; + ScenePaths_[sceneGuid] = normalizedScenePath; SavedSnapshotBytes_ = MetaCoreSerializeSceneSnapshotBytes( ReflectionRegistry_->GetTypeRegistry(), editorContext.GetScene().CaptureSnapshot() @@ -10072,6 +10147,24 @@ public: return true; } + bool SaveAllScenes(MetaCoreEditorContext& editorContext) override { + MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); + if (world == nullptr) return SaveCurrentScene(editorContext); + const MetaCoreAssetGuid previous = world->GetActiveSceneGuid(); + bool success = true; + const auto paths = ScenePaths_; + for (const auto& [guid, path] : paths) { + if (!SetActiveScene(editorContext, guid) || path.empty() || !SaveSceneAs(editorContext, path)) success = false; + } + if (previous.IsValid()) (void)SetActiveScene(editorContext, previous); + return success; + } + + [[nodiscard]] std::vector GetOpenScenes(const MetaCoreEditorContext& editorContext) const override { + const MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); + return world == nullptr ? std::vector{} : world->GetLoadedScenes(); + } + void UpdateDirtyState(const MetaCoreSceneSnapshot& snapshot) override { if (ReflectionRegistry_ == nullptr) { SetDirtyState(true); @@ -10111,6 +10204,7 @@ private: std::string CurrentSceneName_{}; std::vector SavedSnapshotBytes_{}; bool Dirty_ = false; + std::unordered_map ScenePaths_{}; }; class MetaCoreBuiltinSelectionService final : public MetaCoreISelectionService { @@ -11420,6 +11514,7 @@ public: State_ = MetaCorePlayModeState::Edit; HasPrePlaySnapshot_ = false; HasPendingStopRestoreSnapshot_ = false; + HasPrePlayWorldSnapshot_ = false; CommandRecordingSuppressed_ = false; ElapsedTimeSeconds_ = 0.0; FixedTimeAccumulatorSeconds_ = 0.0; @@ -11456,6 +11551,12 @@ public: } PrePlaySnapshot_ = editorContext.CaptureStateSnapshot(); HasPrePlaySnapshot_ = true; + if (const MetaCoreSceneWorld* world = editorContext.GetSceneWorld()) { + PrePlayWorldSnapshot_ = world->CaptureWorldSnapshot(); + HasPrePlayWorldSnapshot_ = true; + } else { + HasPrePlayWorldSnapshot_ = false; + } MetaCoreDebugLog( "PlayMode", "Enter captured prePlaySnapshot objects=" + @@ -11709,6 +11810,12 @@ private: " " + MetaCoreDebugSceneSummary(editorContext) ); editorContext.RestoreStateSnapshot(PendingStopRestoreSnapshot_); + if (HasPrePlayWorldSnapshot_) { + if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld()) + world->RestoreWorldSnapshot(PrePlayWorldSnapshot_); + HasPrePlayWorldSnapshot_ = false; + PrePlayWorldSnapshot_ = {}; + } HasPendingStopRestoreSnapshot_ = false; PendingStopRestoreSnapshot_ = {}; MetaCoreDebugLog("PlayMode", "Apply pending stop restore end " + MetaCoreDebugSceneSummary(editorContext)); @@ -11718,8 +11825,10 @@ private: MetaCorePlayModeState State_ = MetaCorePlayModeState::Edit; MetaCoreEditorStateSnapshot PrePlaySnapshot_{}; MetaCoreEditorStateSnapshot PendingStopRestoreSnapshot_{}; + MetaCoreSceneWorldSnapshot PrePlayWorldSnapshot_{}; bool HasPrePlaySnapshot_ = false; bool HasPendingStopRestoreSnapshot_ = false; + bool HasPrePlayWorldSnapshot_ = false; bool CommandRecordingSuppressed_ = false; double ElapsedTimeSeconds_ = 0.0; double FixedTimeStepSeconds_ = 1.0 / 60.0; @@ -11837,6 +11946,7 @@ std::optional MetaCoreBuiltinPrefabService::CreatePrefabF } MetaCorePrefabDocument prefabDocument; + prefabDocument.PrefabGuid = prefabGuid; prefabDocument.Name = rootObject ? rootObject.GetName() : normalizedPrefabPath.stem().string(); std::vector prefabDataObjects; @@ -11844,6 +11954,7 @@ std::optional MetaCoreBuiltinPrefabService::CreatePrefabF for (const MetaCoreGameObject& gameObject : prefabObjects) { MetaCoreGameObjectData data; data.Id = gameObject.GetId(); + data.StableObjectGuid = MetaCoreBuildDeterministicGuid(prefabGuid, data.Id); data.Name = gameObject.GetName(); data.ParentId = gameObject.GetParentId(); data.Transform = gameObject.GetComponent(); @@ -11902,6 +12013,10 @@ std::optional MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen } MetaCoreId instantiatedRootId = 0; + const MetaCoreAssetGuid instanceGuid = MetaCoreAssetGuid::Generate(); + const MetaCoreAssetGuid targetSceneGuid = editorContext.GetSceneWorld() != nullptr + ? editorContext.GetSceneWorld()->GetActiveSceneGuid() + : MetaCoreAssetGuid{}; const bool created = editorContext.ExecuteSnapshotCommand("实例化 Prefab", [&]() { std::unordered_map idMap; std::vector clonedObjects; @@ -11925,6 +12040,17 @@ std::optional MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen } clonedObject.GetComponent() = sourceObject.Transform; + if (targetSceneGuid.IsValid()) { + const MetaCoreAssetGuid nodeGuid = sourceObject.StableObjectGuid.IsValid() + ? sourceObject.StableObjectGuid + : MetaCoreBuildDeterministicGuid(prefabAssetGuid, sourceObject.Id); + clonedObject.AddComponent( + MetaCoreSceneObjectIdentityComponent{ + targetSceneGuid, + MetaCoreBuildDeterministicGuid(instanceGuid, nodeGuid) + } + ); + } if (sourceObject.Camera.has_value()) clonedObject.AddComponent(*sourceObject.Camera); if (sourceObject.Light.has_value()) clonedObject.AddComponent(*sourceObject.Light); if (sourceObject.Rotator.has_value()) clonedObject.AddComponent(*sourceObject.Rotator); @@ -11942,11 +12068,14 @@ std::optional MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen if (sourceObject.DecalProjector.has_value()) clonedObject.AddComponent(*sourceObject.DecalProjector); if (sourceObject.Terrain.has_value()) clonedObject.AddComponent(*sourceObject.Terrain); - clonedObject.AddComponent(MetaCorePrefabInstanceMetadata{ - prefabAssetGuid, - sourceObject.Id, - 0 - }); + MetaCorePrefabInstanceMetadata metadata; + metadata.PrefabAssetGuid = prefabAssetGuid; + metadata.PrefabObjectId = sourceObject.Id; + metadata.InstanceGuid = instanceGuid; + metadata.PrefabNodeGuid = sourceObject.StableObjectGuid.IsValid() + ? sourceObject.StableObjectGuid + : MetaCoreBuildDeterministicGuid(prefabAssetGuid, sourceObject.Id); + clonedObject.AddComponent(metadata); } for (MetaCoreGameObject& clonedObject : clonedObjects) { @@ -11981,7 +12110,23 @@ std::optional MetaCoreBuiltinPrefabService::InstantiatePrefab( if (!prefabDocument.has_value()) { return std::nullopt; } - return InstantiatePrefabDocument(editorContext, prefabAssetGuid, *prefabDocument, forcedParentId); + MetaCorePrefabResolver resolver([this](const MetaCoreAssetGuid& guid) { + return LoadPrefabDocument(guid); + }); + std::string resolveError; + const auto resolved = resolver.Resolve(prefabAssetGuid, &resolveError); + if (!resolved) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Prefab", "Prefab 依赖解析失败: " + resolveError); + return std::nullopt; + } + if (!resolved->OrphanOverrides.empty()) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Warning, + "Prefab", + "Prefab 包含 " + std::to_string(resolved->OrphanOverrides.size()) + " 个孤立 Override" + ); + } + return InstantiatePrefabDocument(editorContext, prefabAssetGuid, resolved->Document, forcedParentId); } bool MetaCoreBuiltinPrefabService::ApplySelectedPrefabInstance(MetaCoreEditorContext& editorContext) { @@ -12198,6 +12343,11 @@ std::optional MetaCoreBuiltinSceneEditingService::CreateGameObject( } MetaCoreGameObject object = editorContext.GetScene().CreateGameObject(objectName, parentId); + if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && world->GetActiveSceneGuid().IsValid()) { + object.AddComponent(MetaCoreSceneObjectIdentityComponent{ + world->GetActiveSceneGuid(), MetaCoreAssetGuid::Generate() + }); + } if (withMeshRenderer) { auto& meshRenderer = object.AddComponent(MetaCoreMeshRendererComponent{}); if (objectName == "Plane") { @@ -12251,6 +12401,11 @@ std::optional MetaCoreBuiltinSceneEditingService::CreateCamera( } MetaCoreGameObject cameraObject = editorContext.GetScene().CreateGameObject("Camera", parentId); + if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && world->GetActiveSceneGuid().IsValid()) { + cameraObject.AddComponent(MetaCoreSceneObjectIdentityComponent{ + world->GetActiveSceneGuid(), MetaCoreAssetGuid::Generate() + }); + } MetaCoreCameraComponent& camera = cameraObject.AddComponent(MetaCoreCameraComponent{}); camera.IsPrimary = !hasEnabledPrimaryCamera; @@ -12388,6 +12543,11 @@ std::optional MetaCoreBuiltinSceneEditingService::InstantiateModelAs ? resolvedModelPath.filename().generic_string() : assetRecord->RelativePath.filename().generic_string()); MetaCoreGameObject modelRootObject = editorContext.GetScene().CreateGameObject(modelRootName, externalParentId); + if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && world->GetActiveSceneGuid().IsValid()) { + modelRootObject.AddComponent(MetaCoreSceneObjectIdentityComponent{ + world->GetActiveSceneGuid(), MetaCoreAssetGuid::Generate() + }); + } instantiatedRootId = modelRootObject.GetId(); std::vector nodeToIdMap(importedDocument->Nodes.size(), 0); @@ -12479,6 +12639,11 @@ std::optional MetaCoreBuiltinSceneEditingService::InstantiateModelAs for (std::size_t i = 0; i < importedDocument->Nodes.size(); ++i) { const auto& nodeMeta = importedDocument->Nodes[i]; MetaCoreGameObject gameObject = editorContext.GetScene().CreateGameObject(nodeMeta.Name, 0); + if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && world->GetActiveSceneGuid().IsValid()) { + gameObject.AddComponent(MetaCoreSceneObjectIdentityComponent{ + world->GetActiveSceneGuid(), MetaCoreAssetGuid::Generate() + }); + } nodeToIdMap[i] = gameObject.GetId(); // 立即同步变换组件 @@ -12590,6 +12755,29 @@ bool MetaCoreBuiltinSceneEditingService::ReparentSelection(MetaCoreEditorContext return false; } + MetaCoreAssetGuid targetSceneGuid{}; + if (newParentId != 0) { + const auto parent = editorContext.GetScene().FindGameObject(newParentId); + if (parent && parent.HasComponent()) { + targetSceneGuid = parent.GetComponent().SceneGuid; + } + } + for (MetaCoreId selectedId : selectedIds) { + const auto selected = editorContext.GetScene().FindGameObject(selectedId); + if (!selected || !selected.HasComponent()) { + continue; + } + const auto selectedScene = selected.GetComponent().SceneGuid; + if (targetSceneGuid.IsValid() && selectedScene.IsValid() && targetSceneGuid != selectedScene) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Warning, + "Hierarchy", + "禁止跨场景父子关系;请使用 Move Subtree To Scene" + ); + return false; + } + } + const bool keepWorldTransform = editorContext.GetReparentTransformRule() == MetaCoreReparentTransformRule::KeepWorld; const bool reparented = editorContext.ExecuteSnapshotCommand("重挂接对象", [&]() { return editorContext.GetScene().ReparentGameObjects(selectedIds, newParentId, keepWorldTransform); diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index e10b549..526bba8 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -2829,23 +2829,48 @@ static std::array GProjectRenamePathBuffer{}; BeginRename(editorContext, requestedRenameId); } - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 8)); - ImGui::TextColored(ImVec4(0.4F, 0.6F, 1.0F, 1.0F), " (S) "); - ImGui::SameLine(); - const std::string sceneLabel = - scenePersistenceService != nullptr && scenePersistenceService->HasOpenScene() - ? scenePersistenceService->GetCurrentSceneDisplayName() - : "Untitled"; - ImGui::TextUnformatted(sceneLabel.c_str()); - if (scenePersistenceService != nullptr && scenePersistenceService->IsSceneDirty()) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.90F, 0.74F, 0.22F, 1.0F), "*"); - } - ImGui::PopStyleVar(); - ImGui::Separator(); - - for (MetaCoreId rootId : editorContext.GetScene().GetRootObjectIds()) { - DrawHierarchyNode(editorContext, rootId); + const auto openScenes = scenePersistenceService != nullptr + ? scenePersistenceService->GetOpenScenes(editorContext) + : std::vector{}; + if (!openScenes.empty()) { + for (const auto& loadedScene : openScenes) { + ImGuiTreeNodeFlags sceneFlags = ImGuiTreeNodeFlags_DefaultOpen | + ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_OpenOnArrow; + if (loadedScene.Active) sceneFlags |= ImGuiTreeNodeFlags_Selected; + const std::string label = std::string(loadedScene.Active ? "(A) " : "(S) ") + + (loadedScene.Name.empty() ? loadedScene.SceneGuid.ToString() : loadedScene.Name) + + (loadedScene.Dirty ? " *" : ""); + const bool open = ImGui::TreeNodeEx( + reinterpret_cast(static_cast(MetaCoreAssetGuidHasher{}(loadedScene.SceneGuid))), + sceneFlags, + "%s", + label.c_str() + ); + if (ImGui::IsItemClicked() && !loadedScene.Active) + (void)scenePersistenceService->SetActiveScene(editorContext, loadedScene.SceneGuid); + if (ImGui::BeginPopupContextItem()) { + if (ImGui::MenuItem("设为活动场景", nullptr, false, !loadedScene.Active)) + (void)scenePersistenceService->SetActiveScene(editorContext, loadedScene.SceneGuid); + if (ImGui::MenuItem("保存全部场景")) + (void)scenePersistenceService->SaveAllScenes(editorContext); + if (ImGui::MenuItem("卸载场景", nullptr, false, openScenes.size() > 1)) + (void)scenePersistenceService->UnloadScene(editorContext, loadedScene.SceneGuid, false); + ImGui::EndPopup(); + } + if (open) { + for (MetaCoreId rootId : editorContext.GetScene().GetRootObjectIds()) { + const auto object = editorContext.GetScene().FindGameObject(rootId); + if (!object || !object.HasComponent()) continue; + if (object.GetComponent().SceneGuid == loadedScene.SceneGuid) + DrawHierarchyNode(editorContext, rootId); + } + ImGui::TreePop(); + } + } + } else { + ImGui::TextColored(ImVec4(0.4F, 0.6F, 1.0F, 1.0F), "(S) Untitled"); + ImGui::Separator(); + for (MetaCoreId rootId : editorContext.GetScene().GetRootObjectIds()) DrawHierarchyNode(editorContext, rootId); } DrawRootDropTarget(editorContext); @@ -9320,7 +9345,10 @@ public: editorContext.ClearSelectedAsset(); editorContext.ClearSelectedAssetSubId(); } else if (entry.AssetRecord.has_value() && entry.AssetRecord->Type == "scene" && scenePersistenceService) { - (void)scenePersistenceService->LoadScene(editorContext, entry.RelativePath); + if (ImGui::GetIO().KeyCtrl) + (void)scenePersistenceService->LoadSceneAdditive(editorContext, entry.RelativePath); + else + (void)scenePersistenceService->LoadScene(editorContext, entry.RelativePath); } else if (entry.AssetRecord.has_value() && entry.AssetRecord->Type == "model") { (void)MetaCoreInstantiateModelAsset(editorContext, entry.AssetRecord->Guid, std::nullopt); } else if (entry.AssetRecord.has_value() && entry.AssetRecord->Type == "prefab") { @@ -9381,6 +9409,13 @@ public: if (!entry.IsDirectory && ImGui::MenuItem("打开外部程序")) { MetaCoreOpenPathExternally(project.RootPath / entry.RelativePath); } + if (!entry.IsDirectory && + entry.AssetRecord.has_value() && + entry.AssetRecord->Type == "scene" && + scenePersistenceService != nullptr && + ImGui::MenuItem("Additive 打开场景")) { + (void)scenePersistenceService->LoadSceneAdditive(editorContext, entry.RelativePath); + } if (!entry.IsDirectory && entry.AssetRecord.has_value() && entry.AssetRecord->Type == "ui_document" && diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index 784af8b..f410e71 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -2362,6 +2362,7 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) { LogService_, ModuleRegistry_ ); + EditorContext_->SetSceneWorld(&SceneWorld_); MetaCoreTraceStartup("metacore.app: load startup scene"); bool loadedStartupScene = false; @@ -2467,6 +2468,18 @@ int MetaCoreEditorApp::Run() { playModeService != nullptr && EditorContext_ != nullptr) { playModeService->Tick(*EditorContext_, playModeDeltaSeconds); } + if (EditorContext_ != nullptr) { + std::vector streamingSources; + for (const auto object : Scene_.GetGameObjects()) { + if (object.HasComponent() && + object.GetComponent().Enabled && + object.GetComponent().IsPrimary) { + streamingSources.push_back(object.GetComponent().Position); + } + } + SceneWorld_.UpdateStreaming(streamingSources); + SceneWorld_.Tick(); + } #if defined(_WIN32) ImGui_ImplWin32_NewFrame(); @@ -2821,9 +2834,7 @@ void MetaCoreEditorApp::DrawEditorFrame() { viewportState.Focused && !ImGui::GetIO().WantCaptureKeyboard && EditorContext_->GetInput().WasKeyPressed(MetaCoreInputKey::Focus)) { - if (MetaCoreGameObject selectedObject = EditorContext_->GetSelectedGameObject(); selectedObject) { - EditorContext_->GetCameraController().FocusGameObject(selectedObject); - } + SceneInteractionService_.FocusSelectedObject(*EditorContext_); } // 4. 同步尺寸给 Filament (SetViewportRect 内部会处理 Resize) diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.cpp index 8b8bdb6..8599330 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.cpp @@ -31,10 +31,10 @@ void MetaCoreEditorCameraController::Update( // Zoom: Alt + Right Drag or Mouse Wheel if (viewportState.Hovered && altDown && ImGui::IsMouseDragging(ImGuiMouseButton_Right)) { Distance_ *= (1.0F - io.MouseDelta.y * 0.01F); - Distance_ = std::clamp(Distance_, 0.5F, 100.0F); + Distance_ = std::clamp(Distance_, 0.01F, 10000000.0F); } else if (viewportState.Hovered && std::abs(io.MouseWheel) > 0.001F) { Distance_ *= (io.MouseWheel > 0.0F) ? 0.88F : 1.12F; - Distance_ = std::clamp(Distance_, 0.5F, 100.0F); + Distance_ = std::clamp(Distance_, 0.01F, 10000000.0F); } // Orbit: Alt + Left Drag @@ -97,13 +97,16 @@ void MetaCoreEditorCameraController::Update( void MetaCoreEditorCameraController::FocusGameObject(const MetaCoreGameObject& gameObject) { Target_ = gameObject.GetComponent().Position; - const float scaleRadius = std::max({gameObject.GetComponent().Scale.x, gameObject.GetComponent().Scale.y, gameObject.GetComponent().Scale.z}); + const auto& scale = gameObject.GetComponent().Scale; + const float scaleRadius = std::max({std::abs(scale.x), std::abs(scale.y), std::abs(scale.z), 0.5F}); + FramingRadius_ = scaleRadius; Distance_ = std::max(2.0F, scaleRadius * 4.0F + 1.0F); } void MetaCoreEditorCameraController::FocusBounds(const glm::vec3& center, float radius) { Target_ = center; - Distance_ = std::max(2.0F, radius * 3.5F + 1.0F); + FramingRadius_ = std::max(0.01F, std::isfinite(radius) ? radius : 1.0F); + Distance_ = std::max(2.0F, FramingRadius_ * 3.5F + 1.0F); } void MetaCoreEditorCameraController::ApplySceneView(const MetaCoreSceneView& sceneView) { @@ -111,6 +114,7 @@ void MetaCoreEditorCameraController::ApplySceneView(const MetaCoreSceneView& sce FieldOfViewDegrees_ = sceneView.VerticalFieldOfViewDegrees; const glm::vec3 offset = sceneView.CameraPosition - sceneView.CameraTarget; Distance_ = std::max(0.1F, glm::length(offset)); + FramingRadius_ = std::max(1.0F, (sceneView.FarClip - Distance_) * 0.25F); if (Distance_ > 0.001F) { const glm::vec3 forward = glm::normalize(sceneView.CameraTarget - sceneView.CameraPosition); @@ -125,6 +129,12 @@ MetaCoreSceneView MetaCoreEditorCameraController::BuildSceneView() const { sceneView.CameraTarget = Target_; sceneView.CameraUp = glm::vec3(0.0F, 0.0F, 1.0F); // Z-up! sceneView.VerticalFieldOfViewDegrees = FieldOfViewDegrees_; + sceneView.NearClip = std::clamp(Distance_ * 0.0001F, 0.01F, 1.0F); + sceneView.FarClip = std::clamp( + std::max({1000.0F, Distance_ * 2.0F, Distance_ + FramingRadius_ * 4.0F}), + sceneView.NearClip + 1.0F, + 100000000.0F + ); return sceneView; } diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.h b/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.h index 6da5901..018dc7a 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.h +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.h @@ -31,6 +31,7 @@ private: float FieldOfViewDegrees_ = 60.0F; float MoveSpeed_ = 6.0F; float MoveSpeedBoost_ = 4.0F; + float FramingRadius_ = 10.0F; }; } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp index d2515f7..6e831c6 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include namespace MetaCore { @@ -213,6 +214,60 @@ MetaCoreResolvedPickGeometry MetaCoreResolvePickGeometry( return MetaCoreResolvedPickGeometry{}; } +void MetaCoreAccumulateTransformedBounds( + const MetaCorePickBounds& localBounds, + const glm::mat4& worldMatrix, + bool& initialized, + glm::vec3& boundsMin, + glm::vec3& boundsMax +) { + for (int cornerIndex = 0; cornerIndex < 8; ++cornerIndex) { + const glm::vec3 localPoint( + (cornerIndex & 1) != 0 ? localBounds.Max.x : localBounds.Min.x, + (cornerIndex & 2) != 0 ? localBounds.Max.y : localBounds.Min.y, + (cornerIndex & 4) != 0 ? localBounds.Max.z : localBounds.Min.z + ); + const glm::vec3 worldPoint = glm::vec3(worldMatrix * glm::vec4(localPoint, 1.0F)); + if (!MetaCoreIsFiniteVec3(worldPoint)) continue; + if (!initialized) { + boundsMin = worldPoint; + boundsMax = worldPoint; + initialized = true; + } else { + boundsMin = glm::min(boundsMin, worldPoint); + boundsMax = glm::max(boundsMax, worldPoint); + } + } +} + +bool MetaCoreComputeWorldRenderBounds( + const MetaCoreEditorContext& editorContext, + const std::unordered_set& objectIds, + glm::vec3& boundsMin, + glm::vec3& boundsMax +) { + const auto assetEditingService = + editorContext.GetModuleRegistry().ResolveService(); + std::unordered_map, MetaCoreAssetGuidHasher> + modelAssetCache; + bool initialized = false; + for (const auto& object : editorContext.GetScene().GetGameObjects()) { + if (!objectIds.empty() && !objectIds.contains(object.GetId())) continue; + if (!object.HasComponent()) continue; + const auto& renderer = object.GetComponent(); + if (!renderer.Visible) continue; + const auto geometry = MetaCoreResolvePickGeometry(renderer, assetEditingService, modelAssetCache); + MetaCoreAccumulateTransformedBounds( + geometry.Bounds, + MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), object.GetId()), + initialized, + boundsMin, + boundsMax + ); + } + return initialized; +} + bool MetaCoreIntersectLocalRayWithAabb( const glm::vec3& rayOrigin, const glm::vec3& rayDirection, @@ -660,92 +715,37 @@ void MetaCoreSceneInteractionService::FocusSelectedObject(MetaCoreEditorContext& return; } - if (selectedObjectIds.size() == 1) { - MetaCoreGameObject selectedObject = editorContext.GetSelectedGameObject(); - if (selectedObject) { - editorContext.GetCameraController().FocusGameObject(selectedObject); - } - return; + std::unordered_set focusObjectIds; + for (const MetaCoreId selectedObjectId : selectedObjectIds) { + const auto subtree = editorContext.GetScene().GetSubtreeObjectIds(selectedObjectId); + focusObjectIds.insert(subtree.begin(), subtree.end()); } - bool initialized = false; glm::vec3 boundsMin(0.0F); glm::vec3 boundsMax(0.0F); - for (const MetaCoreId selectedObjectId : selectedObjectIds) { - const MetaCoreGameObject selectedObject = editorContext.GetScene().FindGameObject(selectedObjectId); - if (!selectedObject) { - continue; - } - - const float extent = std::max({ - std::abs(selectedObject.GetComponent().Scale.x), - std::abs(selectedObject.GetComponent().Scale.y), - std::abs(selectedObject.GetComponent().Scale.z), - 0.5F - }); - const glm::vec3 objectMin = selectedObject.GetComponent().Position - glm::vec3(extent); - const glm::vec3 objectMax = selectedObject.GetComponent().Position + glm::vec3(extent); - - if (!initialized) { - boundsMin = objectMin; - boundsMax = objectMax; - initialized = true; - continue; - } - - boundsMin = glm::min(boundsMin, objectMin); - boundsMax = glm::max(boundsMax, objectMax); - } - - if (!initialized) { + if (!MetaCoreComputeWorldRenderBounds(editorContext, focusObjectIds, boundsMin, boundsMax)) { + const MetaCoreGameObject selectedObject = editorContext.GetSelectedGameObject(); + if (selectedObject) editorContext.GetCameraController().FocusGameObject(selectedObject); return; } const glm::vec3 center = (boundsMin + boundsMax) * 0.5F; const glm::vec3 size = boundsMax - boundsMin; - const float radius = std::max({size.x, size.y, size.z, 1.0F}) * 0.5F; + const float radius = std::max(glm::length(size) * 0.5F, 0.5F); editorContext.GetCameraController().FocusBounds(center, radius); } void MetaCoreSceneInteractionService::FocusVisibleScene(MetaCoreEditorContext& editorContext) const { - const auto& gameObjects = editorContext.GetScene().GetGameObjects(); - bool initialized = false; glm::vec3 boundsMin(0.0F); glm::vec3 boundsMax(0.0F); - - for (const MetaCoreGameObject& gameObject : gameObjects) { - if (gameObject.HasComponent() && !gameObject.GetComponent().Visible) { - continue; - } - - const float extent = std::max({ - std::abs(gameObject.GetComponent().Scale.x), - std::abs(gameObject.GetComponent().Scale.y), - std::abs(gameObject.GetComponent().Scale.z), - 0.5F - }); - const glm::vec3 objectMin = gameObject.GetComponent().Position - glm::vec3(extent); - const glm::vec3 objectMax = gameObject.GetComponent().Position + glm::vec3(extent); - - if (!initialized) { - boundsMin = objectMin; - boundsMax = objectMax; - initialized = true; - continue; - } - - boundsMin = glm::min(boundsMin, objectMin); - boundsMax = glm::max(boundsMax, objectMax); - } - - if (!initialized) { + if (!MetaCoreComputeWorldRenderBounds(editorContext, {}, boundsMin, boundsMax)) { editorContext.GetCameraController().FocusBounds(glm::vec3(0.0F, 0.5F, 0.0F), 2.0F); return; } const glm::vec3 center = (boundsMin + boundsMax) * 0.5F; const glm::vec3 size = boundsMax - boundsMin; - const float radius = std::max({size.x, size.y, size.z, 1.0F}) * 0.5F; + const float radius = std::max(glm::length(size) * 0.5F, 0.5F); editorContext.GetCameraController().FocusBounds(center, radius); } diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h index e3a686e..45fc3df 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h @@ -9,6 +9,7 @@ #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" #include "MetaCoreScene/MetaCoreScene.h" +#include "MetaCoreScene/MetaCoreSceneWorld.h" #include #include @@ -38,6 +39,7 @@ private: MetaCoreRenderDevice RenderDevice_{}; MetaCoreEditorViewportRenderer ViewportRenderer_{}; MetaCoreScene Scene_{}; + MetaCoreSceneWorld SceneWorld_{Scene_}; MetaCoreLogService LogService_{}; MetaCoreEditorModuleRegistry ModuleRegistry_{}; MetaCoreSceneInteractionService SceneInteractionService_{}; diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h index 8842267..01c7621 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h @@ -28,6 +28,7 @@ class MetaCoreGameObject; class MetaCoreEditorModuleRegistry; class MetaCoreEditorCameraController; class MetaCorePhysicsWorld; +class MetaCoreSceneWorld; enum class MetaCoreGizmoOperation { None = 0, @@ -125,6 +126,8 @@ public: [[nodiscard]] MetaCoreEditorViewportRenderer& GetViewportRenderer(); [[nodiscard]] MetaCoreScene& GetScene(); [[nodiscard]] const MetaCoreScene& GetScene() const; + void SetSceneWorld(MetaCoreSceneWorld* sceneWorld) { SceneWorld_ = sceneWorld; } + [[nodiscard]] MetaCoreSceneWorld* GetSceneWorld() const { return SceneWorld_; } [[nodiscard]] MetaCoreLogService& GetLogService(); [[nodiscard]] const MetaCoreLogService& GetLogService() const; [[nodiscard]] MetaCoreEditorModuleRegistry& GetModuleRegistry(); @@ -219,6 +222,7 @@ private: MetaCoreRenderDevice& RenderDevice_; MetaCoreEditorViewportRenderer& ViewportRenderer_; MetaCoreScene& Scene_; + MetaCoreSceneWorld* SceneWorld_ = nullptr; MetaCoreLogService& LogService_; MetaCoreEditorModuleRegistry& ModuleRegistry_; std::unique_ptr CameraController_; diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index 36c04ca..c00f163 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -7,6 +7,7 @@ #include "MetaCoreFoundation/MetaCorePackage.h" #include "MetaCoreFoundation/MetaCoreReflection.h" #include "MetaCoreScene/MetaCoreScene.h" +#include "MetaCoreScene/MetaCoreSceneWorld.h" #include "MetaCoreScripting/MetaCoreScripting.h" #include @@ -310,8 +311,13 @@ public: [[nodiscard]] virtual std::vector GetKnownScenePaths() const = 0; virtual bool LoadStartupScene(MetaCoreEditorContext& editorContext) = 0; virtual bool LoadScene(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) = 0; + virtual bool LoadSceneAdditive(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) = 0; + virtual bool UnloadScene(MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& sceneGuid, bool force) = 0; + virtual bool SetActiveScene(MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& sceneGuid) = 0; virtual bool SaveCurrentScene(MetaCoreEditorContext& editorContext) = 0; virtual bool SaveSceneAs(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) = 0; + virtual bool SaveAllScenes(MetaCoreEditorContext& editorContext) = 0; + [[nodiscard]] virtual std::vector GetOpenScenes(const MetaCoreEditorContext& editorContext) const = 0; virtual void UpdateDirtyState(const MetaCoreSceneSnapshot& snapshot) = 0; }; diff --git a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp index bc368ca..5ea77f2 100644 --- a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp @@ -183,8 +183,18 @@ void MetaCoreFilamentMaterialDebugLog(const std::string& message) { return stream.str(); } -[[nodiscard]] bool MetaCorePreserveGltfBlendAlphaMode() { - const char* preserveBlend = std::getenv("METACORE_GLTF_PRESERVE_BLEND_ALPHA"); +[[nodiscard]] bool MetaCoreForceGltfBlendAsMask() { + const char* forceMask = std::getenv("METACORE_GLTF_FORCE_BLEND_AS_MASK"); + if (forceMask == nullptr) { + return false; + } + return std::strcmp(forceMask, "1") == 0 || + std::strcmp(forceMask, "true") == 0 || + std::strcmp(forceMask, "TRUE") == 0; +} + +[[nodiscard]] bool MetaCorePreserveDoubleSidedGltfBlend() { + const char* preserveBlend = std::getenv("METACORE_GLTF_PRESERVE_DOUBLE_SIDED_BLEND"); if (preserveBlend == nullptr) { return false; } @@ -245,17 +255,29 @@ public: private: void NormalizeAlphaMode(filament::gltfio::MaterialKey* config, const char* label) { if (config == nullptr || - MetaCorePreserveGltfBlendAlphaMode() || config->alphaMode != filament::gltfio::AlphaMode::BLEND) { return; } + const bool forceAllBlendAsMask = MetaCoreForceGltfBlendAsMask(); + const bool stabilizeDoubleSidedCutout = + config->doubleSided && !MetaCorePreserveDoubleSidedGltfBlend(); + if (!forceAllBlendAsMask && !stabilizeDoubleSidedCutout) { + return; + } + config->alphaMode = filament::gltfio::AlphaMode::MASK; if (!LoggedBlendAsMask_) { - MetaCoreFilamentDebugLog( - "GLTF alphaMode=BLEND is rendered as MASK by default for stable foliage depth. " - "Set METACORE_GLTF_PRESERVE_BLEND_ALPHA=1 to keep GLTF blend materials." - ); + if (forceAllBlendAsMask) { + MetaCoreFilamentDebugLog( + "GLTF alphaMode=BLEND is forced to MASK by METACORE_GLTF_FORCE_BLEND_AS_MASK." + ); + } else { + MetaCoreFilamentDebugLog( + "GLTF double-sided BLEND materials use MASK for stable foliage depth. " + "Set METACORE_GLTF_PRESERVE_DOUBLE_SIDED_BLEND=1 for true double-sided transparency." + ); + } LoggedBlendAsMask_ = true; } if (label != nullptr && label[0] != '\0') { @@ -1620,6 +1642,12 @@ public: CurrentSceneView_.NearClip, CurrentSceneView_.FarClip); const glm::mat4 viewProjection = projectionMatrix * viewMatrix; const MetaCoreViewFrustum frustum = MetaCoreExtractFrustum(viewProjection); + // gltfio already owns exact per-primitive bounds for imported assets. The ECS + // snapshot currently only has a unit transform proxy for these nodes, which is + // not a conservative bound for large or offset meshes. Running that proxy + // through the engine BVH / software HZB causes whole model sections to pop as + // their node origins cross the view frustum. Keep hierarchy/layer/distance + // visibility here, then leave geometric frustum culling to Filament. std::vector visibilityItems; visibilityItems.reserve(resolvedSnapshot.Renderables.size()); for (const auto& renderable : resolvedSnapshot.Renderables) { @@ -1628,6 +1656,9 @@ public: MetaCoreAccumulateVisibilityResult(RenderingStatistics_, lodCulled ? MetaCoreVisibilityResult::LodCulled : MetaCoreVisibilityResult::DistanceCulled); continue; } + if (MetaCoreUsesNativeGltfVisibility(renderable)) { + continue; + } visibilityItems.push_back({renderable.ObjectId, renderable.WorldBounds, renderable.RenderingLayerMask, renderable.MaxDrawDistance, !renderable.StaticForCulling}); } @@ -1652,7 +1683,40 @@ public: }; std::unordered_set currentVisible; for (auto& renderable : resolvedSnapshot.Renderables) { - if (!renderable.Visible || !bvhVisibleSet.contains(renderable.ObjectId)) { renderable.Visible=false; continue; } + if (!renderable.Visible) { + continue; + } + if (MetaCoreUsesNativeGltfVisibility(renderable)) { + const bool layerVisible = + (renderable.RenderingLayerMask & CurrentSceneView_.CullingMask) != 0U; + const bool distanceVisible = + renderable.MaxDrawDistance <= 0.0F || + glm::distance( + CurrentSceneView_.CameraPosition, + glm::vec3(renderable.WorldMatrix[3]) + ) <= renderable.MaxDrawDistance; + renderable.Visible = layerVisible && distanceVisible; + if (renderable.Visible) { + currentVisible.insert(renderable.ObjectId); + MetaCoreAccumulateVisibilityResult( + RenderingStatistics_, + MetaCoreVisibilityResult::Visible, + renderable.TriangleCount + ); + RenderingStatistics_.EstimatedVideoMemoryBytes += + renderable.EstimatedVideoMemoryBytes; + } else { + MetaCoreAccumulateVisibilityResult( + RenderingStatistics_, + MetaCoreVisibilityResult::DistanceCulled + ); + } + continue; + } + if (!bvhVisibleSet.contains(renderable.ObjectId)) { + renderable.Visible = false; + continue; + } bool queryVisible=true;glm::vec4 screenBounds{};float nearestDepth=0.0F,farthestDepth=0.0F; if(RenderSettings_.EnableOcclusion&&projectBounds(renderable.WorldBounds,screenBounds,nearestDepth,farthestDepth))queryVisible=PreviousHzb_.IsVisible(screenBounds,nearestDepth); const bool newlyVisible=!LastFrameVisibleObjects_.contains(renderable.ObjectId); @@ -1663,7 +1727,7 @@ public: } constexpr std::uint32_t hzbWidth=64U,hzbHeight=36U; std::vector softwareDepth(static_cast(hzbWidth)*hzbHeight,CurrentSceneView_.FarClip); - for(const auto& renderable:resolvedSnapshot.Renderables){if(!renderable.Visible||!renderable.StaticForCulling)continue;glm::vec4 bounds{};float nearestDepth=0.0F,farthestDepth=0.0F;if(!projectBounds(renderable.WorldBounds,bounds,nearestDepth,farthestDepth))continue;const std::uint32_t minX=std::min(hzbWidth-1U,static_cast(std::clamp(bounds.x,0.0F,1.0F)*hzbWidth)),maxX=std::min(hzbWidth-1U,static_cast(std::clamp(bounds.z,0.0F,1.0F)*hzbWidth));const std::uint32_t minY=std::min(hzbHeight-1U,static_cast(std::clamp(bounds.y,0.0F,1.0F)*hzbHeight)),maxY=std::min(hzbHeight-1U,static_cast(std::clamp(bounds.w,0.0F,1.0F)*hzbHeight));for(std::uint32_t y=minY;y<=maxY;++y)for(std::uint32_t x=minX;x<=maxX;++x)softwareDepth[static_cast(y)*hzbWidth+x]=std::min(softwareDepth[static_cast(y)*hzbWidth+x],farthestDepth);} + for(const auto& renderable:resolvedSnapshot.Renderables){if(!renderable.Visible||!renderable.StaticForCulling||MetaCoreUsesNativeGltfVisibility(renderable))continue;glm::vec4 bounds{};float nearestDepth=0.0F,farthestDepth=0.0F;if(!projectBounds(renderable.WorldBounds,bounds,nearestDepth,farthestDepth))continue;const std::uint32_t minX=std::min(hzbWidth-1U,static_cast(std::clamp(bounds.x,0.0F,1.0F)*hzbWidth)),maxX=std::min(hzbWidth-1U,static_cast(std::clamp(bounds.z,0.0F,1.0F)*hzbWidth));const std::uint32_t minY=std::min(hzbHeight-1U,static_cast(std::clamp(bounds.y,0.0F,1.0F)*hzbHeight)),maxY=std::min(hzbHeight-1U,static_cast(std::clamp(bounds.w,0.0F,1.0F)*hzbHeight));for(std::uint32_t y=minY;y<=maxY;++y)for(std::uint32_t x=minX;x<=maxX;++x)softwareDepth[static_cast(y)*hzbWidth+x]=std::min(softwareDepth[static_cast(y)*hzbWidth+x],farthestDepth);} (void)PreviousHzb_.Build(hzbWidth,hzbHeight,softwareDepth);LastFrameVisibleObjects_=std::move(currentVisible);PreviousVisibilityCameraPosition_=CurrentSceneView_.CameraPosition;PreviousVisibilityCameraDirection_=cameraDirection;HasPreviousVisibilityCamera_=true; std::vector instanceCandidates; instanceCandidates.reserve(snapshot.Renderables.size()); diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h index 925f6f9..71e047c 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h @@ -60,6 +60,14 @@ struct MetaCoreRenderSyncRenderable { std::uint64_t EstimatedVideoMemoryBytes = 0U; }; +[[nodiscard]] inline bool MetaCoreUsesNativeGltfVisibility( + const MetaCoreRenderSyncRenderable& renderable +) { + return renderable.MeshSource == MetaCoreMeshSourceKind::Asset && + !renderable.SourceModelPath.empty() && + renderable.HostRootId != 0; +} + struct MetaCoreRenderSyncCamera { MetaCoreId ObjectId = 0; bool IsPrimary = false; diff --git a/Source/MetaCoreScene/Private/MetaCorePrefab.cpp b/Source/MetaCoreScene/Private/MetaCorePrefab.cpp new file mode 100644 index 0000000..cf15b07 --- /dev/null +++ b/Source/MetaCoreScene/Private/MetaCorePrefab.cpp @@ -0,0 +1,388 @@ +#include "MetaCoreScene/MetaCorePrefab.h" + +#include "MetaCoreScene/MetaCoreSceneWorld.h" + +#include +#include +#include + +namespace MetaCore { + +namespace { + +using json = nlohmann::json; + +glm::vec3 ReadVec3(std::string_view value, bool& valid) { + try { + const json parsed = json::parse(value); + if (!parsed.is_array() || parsed.size() < 3) return {}; + valid = true; + return {parsed[0].get(), parsed[1].get(), parsed[2].get()}; + } catch (...) { + return {}; + } +} + +template +bool ReadScalar(std::string_view value, T& output) { + try { + output = json::parse(value).get(); + return true; + } catch (...) { + return false; + } +} + +MetaCoreGameObjectData* FindNode(MetaCorePrefabDocument& document, const MetaCoreAssetGuid& nodeGuid) { + const auto iterator = std::find_if(document.GameObjects.begin(), document.GameObjects.end(), [&](const auto& object) { + return object.StableObjectGuid == nodeGuid; + }); + return iterator == document.GameObjects.end() ? nullptr : &*iterator; +} + +const MetaCoreGameObjectData* FindNode(const MetaCorePrefabDocument& document, const MetaCoreAssetGuid& nodeGuid) { + const auto iterator = std::find_if(document.GameObjects.begin(), document.GameObjects.end(), [&](const auto& object) { + return object.StableObjectGuid == nodeGuid; + }); + return iterator == document.GameObjects.end() ? nullptr : &*iterator; +} + +void NormalizePrefab(MetaCorePrefabDocument& document) { + if (!document.PrefabGuid.IsValid()) document.PrefabGuid = MetaCoreAssetGuid::Generate(); + for (auto& object : document.GameObjects) { + if (!object.StableObjectGuid.IsValid()) + object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.PrefabGuid, object.Id); + } + document.SchemaVersion = GMetaCorePrefabSchemaVersion; +} + +void MergeVariantObjects(MetaCorePrefabDocument& target, const MetaCorePrefabDocument& layer) { + for (const auto& object : layer.GameObjects) { + const auto iterator = std::find_if(target.GameObjects.begin(), target.GameObjects.end(), [&](const auto& current) { + return current.StableObjectGuid == object.StableObjectGuid; + }); + if (iterator == target.GameObjects.end()) target.GameObjects.push_back(object); + else *iterator = object; + } +} + +} // namespace + +MetaCorePrefabOverrideRegistry::MetaCorePrefabOverrideRegistry() { + RegisterPropertyHandler("Name", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) { + return path == "Value" && ReadScalar(value, object.Name); + }); + RegisterPropertyHandler("Transform", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) { + bool valid = false; + const glm::vec3 parsed = ReadVec3(value, valid); + if (!valid) return false; + if (path == "Position") object.Transform.Position = parsed; + else if (path == "RotationEulerDegrees") object.Transform.RotationEulerDegrees = parsed; + else if (path == "Scale") object.Transform.Scale = parsed; + else return false; + return true; + }); + RegisterPropertyHandler("Active", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) { + if (path != "Active") return false; + if (!object.Active) object.Active = MetaCoreActiveComponent{}; + return ReadScalar(value, object.Active->Active); + }); + RegisterPropertyHandler("Camera", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) { + if (!object.Camera) object.Camera = MetaCoreCameraComponent{}; + if (path == "FieldOfViewDegrees") return ReadScalar(value, object.Camera->FieldOfViewDegrees); + if (path == "NearClip") return ReadScalar(value, object.Camera->NearClip); + if (path == "FarClip") return ReadScalar(value, object.Camera->FarClip); + if (path == "Enabled") return ReadScalar(value, object.Camera->Enabled); + if (path == "IsPrimary") return ReadScalar(value, object.Camera->IsPrimary); + return false; + }); + RegisterPropertyHandler("Light", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) { + if (!object.Light) object.Light = MetaCoreLightComponent{}; + if (path == "Intensity") return ReadScalar(value, object.Light->Intensity); + if (path == "Enabled") return ReadScalar(value, object.Light->Enabled); + if (path == "Color") { + bool valid = false; + const auto parsed = ReadVec3(value, valid); + if (valid) object.Light->Color = parsed; + return valid; + } + return false; + }); +} + +void MetaCorePrefabOverrideRegistry::RegisterPropertyHandler(std::string componentTypeId, PropertyHandler handler) { + PropertyHandlers_[std::move(componentTypeId)] = std::move(handler); +} + +bool MetaCorePrefabOverrideRegistry::Apply( + MetaCorePrefabDocument& document, + const MetaCorePrefabOverrideDocument& overrideValue +) const { + MetaCoreGameObjectData* object = FindNode(document, overrideValue.TargetNodeGuid); + if (!object) return false; + switch (overrideValue.Operation) { + case MetaCorePrefabOverrideOperation::SetProperty: { + const auto handler = PropertyHandlers_.find(overrideValue.ComponentTypeId); + return handler != PropertyHandlers_.end() && + handler->second(*object, overrideValue.PropertyPath, overrideValue.ValueJson); + } + case MetaCorePrefabOverrideOperation::AddComponent: + if (overrideValue.ComponentTypeId == "Active") object->Active = MetaCoreActiveComponent{}; + else if (overrideValue.ComponentTypeId == "Camera") object->Camera = MetaCoreCameraComponent{}; + else if (overrideValue.ComponentTypeId == "Light") object->Light = MetaCoreLightComponent{}; + else if (overrideValue.ComponentTypeId == "MeshRenderer") object->MeshRenderer = MetaCoreMeshRendererComponent{}; + else if (overrideValue.ComponentTypeId == "Collider") object->Collider = MetaCoreColliderComponent{}; + else if (overrideValue.ComponentTypeId == "RigidBody") object->RigidBody = MetaCoreRigidBodyComponent{}; + else if (overrideValue.ComponentTypeId == "Rotator") object->Rotator = MetaCoreRotatorComponent{}; + else if (overrideValue.ComponentTypeId == "Animator") object->Animator = MetaCoreAnimatorComponent{}; + else if (overrideValue.ComponentTypeId == "TimelineDirector") object->TimelineDirector = MetaCoreTimelineDirectorComponent{}; + else if (overrideValue.ComponentTypeId == "Script") object->Script = MetaCoreScriptComponent{}; + else if (overrideValue.ComponentTypeId == "UiRenderer") object->UiRenderer = MetaCoreUiRendererComponent{}; + else if (overrideValue.ComponentTypeId == "Interactable") object->Interactable = MetaCoreInteractableComponent{}; + else if (overrideValue.ComponentTypeId == "PhysicsConstraint") object->PhysicsConstraint = MetaCorePhysicsConstraintComponent{}; + else if (overrideValue.ComponentTypeId == "CharacterController") object->CharacterController = MetaCoreCharacterControllerComponent{}; + else if (overrideValue.ComponentTypeId == "ReflectionProbe") object->ReflectionProbe = MetaCoreReflectionProbeComponent{}; + else if (overrideValue.ComponentTypeId == "LodGroup") object->LodGroup = MetaCoreLodGroupComponent{}; + else if (overrideValue.ComponentTypeId == "ParticleEmitter") object->ParticleEmitter = MetaCoreParticleEmitterComponent{}; + else if (overrideValue.ComponentTypeId == "LineRenderer") object->LineRenderer = MetaCoreLineRendererComponent{}; + else if (overrideValue.ComponentTypeId == "TrailRenderer") object->TrailRenderer = MetaCoreTrailRendererComponent{}; + else if (overrideValue.ComponentTypeId == "DecalProjector") object->DecalProjector = MetaCoreDecalProjectorComponent{}; + else if (overrideValue.ComponentTypeId == "Terrain") object->Terrain = MetaCoreTerrainComponent{}; + else return false; + return true; + case MetaCorePrefabOverrideOperation::RemoveComponent: + if (overrideValue.ComponentTypeId == "Active") object->Active.reset(); + else if (overrideValue.ComponentTypeId == "Camera") object->Camera.reset(); + else if (overrideValue.ComponentTypeId == "Light") object->Light.reset(); + else if (overrideValue.ComponentTypeId == "MeshRenderer") object->MeshRenderer.reset(); + else if (overrideValue.ComponentTypeId == "Collider") object->Collider.reset(); + else if (overrideValue.ComponentTypeId == "RigidBody") object->RigidBody.reset(); + else if (overrideValue.ComponentTypeId == "Rotator") object->Rotator.reset(); + else if (overrideValue.ComponentTypeId == "Animator") object->Animator.reset(); + else if (overrideValue.ComponentTypeId == "TimelineDirector") object->TimelineDirector.reset(); + else if (overrideValue.ComponentTypeId == "Script") object->Script.reset(); + else if (overrideValue.ComponentTypeId == "UiRenderer") object->UiRenderer.reset(); + else if (overrideValue.ComponentTypeId == "Interactable") object->Interactable.reset(); + else if (overrideValue.ComponentTypeId == "PhysicsConstraint") object->PhysicsConstraint.reset(); + else if (overrideValue.ComponentTypeId == "CharacterController") object->CharacterController.reset(); + else if (overrideValue.ComponentTypeId == "ReflectionProbe") object->ReflectionProbe.reset(); + else if (overrideValue.ComponentTypeId == "LodGroup") object->LodGroup.reset(); + else if (overrideValue.ComponentTypeId == "ParticleEmitter") object->ParticleEmitter.reset(); + else if (overrideValue.ComponentTypeId == "LineRenderer") object->LineRenderer.reset(); + else if (overrideValue.ComponentTypeId == "TrailRenderer") object->TrailRenderer.reset(); + else if (overrideValue.ComponentTypeId == "DecalProjector") object->DecalProjector.reset(); + else if (overrideValue.ComponentTypeId == "Terrain") object->Terrain.reset(); + else return false; + return true; + case MetaCorePrefabOverrideOperation::RemoveChild: { + std::unordered_set removed{object->Id}; + bool changed = true; + while (changed) { + changed = false; + for (const auto& candidate : document.GameObjects) + if (removed.contains(candidate.ParentId) && removed.insert(candidate.Id).second) changed = true; + } + std::erase_if(document.GameObjects, [&](const auto& candidate) { return removed.contains(candidate.Id); }); + return true; + } + case MetaCorePrefabOverrideOperation::Reparent: { + const auto parentGuid = MetaCoreAssetGuid::Parse(overrideValue.ValueJson); + if (!parentGuid) return false; + const auto* parent = FindNode(document, *parentGuid); + if (!parent || parent->Id == object->Id) return false; + object->ParentId = parent->Id; + return true; + } + case MetaCorePrefabOverrideOperation::AddChild: { + try { + const json value = json::parse(overrideValue.ValueJson); + MetaCoreGameObjectData child; + child.Id = 1; + for (const auto& current : document.GameObjects) child.Id = std::max(child.Id, current.Id + 1); + child.ParentId = object->Id; + child.Name = value.value("Name", std::string("GameObject")); + child.StableObjectGuid = MetaCoreAssetGuid::Parse(value.value("StableObjectGuid", std::string{})) + .value_or(MetaCoreBuildDeterministicGuid(document.PrefabGuid, child.Id)); + if (value.contains("Position")) { + bool valid = false; + child.Transform.Position = ReadVec3(value["Position"].dump(), valid); + if (!valid) return false; + } + if (value.contains("RotationEulerDegrees")) { + bool valid = false; + child.Transform.RotationEulerDegrees = ReadVec3(value["RotationEulerDegrees"].dump(), valid); + if (!valid) return false; + } + if (value.contains("Scale")) { + bool valid = false; + child.Transform.Scale = ReadVec3(value["Scale"].dump(), valid); + if (!valid) return false; + } + document.GameObjects.push_back(std::move(child)); + return true; + } catch (...) { + return false; + } + } + default: + return false; + } +} + +MetaCorePrefabResolver::MetaCorePrefabResolver(PrefabLoader loader, MetaCorePrefabOverrideRegistry registry) + : Loader_(std::move(loader)), OverrideRegistry_(std::move(registry)) {} + +std::optional MetaCorePrefabResolver::Resolve( + const MetaCoreAssetGuid& prefabGuid, + std::string* error +) const { + std::vector stack; + return ResolveInternal(prefabGuid, stack, error); +} + +std::optional MetaCorePrefabResolver::ResolveInternal( + const MetaCoreAssetGuid& prefabGuid, + std::vector& stack, + std::string* error +) const { + if (!prefabGuid.IsValid() || !Loader_) { + if (error) *error = "Prefab GUID is invalid or no loader is configured"; + return std::nullopt; + } + if (std::find(stack.begin(), stack.end(), prefabGuid) != stack.end()) { + if (error) *error = "Prefab base/nesting dependency cycle at " + prefabGuid.ToString(); + return std::nullopt; + } + auto layer = Loader_(prefabGuid); + if (!layer) { + if (error) *error = "Prefab asset is missing: " + prefabGuid.ToString(); + return std::nullopt; + } + if (layer->SchemaVersion > GMetaCorePrefabSchemaVersion) { + if (error) *error = "Prefab schema is newer than this engine: " + prefabGuid.ToString(); + return std::nullopt; + } + if (!layer->PrefabGuid.IsValid()) layer->PrefabGuid = prefabGuid; + NormalizePrefab(*layer); + stack.push_back(prefabGuid); + + MetaCoreResolvedPrefab result; + if (layer->BasePrefabGuid.IsValid()) { + auto base = ResolveInternal(layer->BasePrefabGuid, stack, error); + if (!base) { + stack.pop_back(); + return std::nullopt; + } + result = std::move(*base); + result.Document.PrefabGuid = layer->PrefabGuid; + result.Document.BasePrefabGuid = layer->BasePrefabGuid; + result.Document.Name = layer->Name; + MergeVariantObjects(result.Document, *layer); + } else { + result.Document = *layer; + } + result.DependencyChain.push_back(prefabGuid); + for (const auto& value : layer->Overrides) + if (!OverrideRegistry_.Apply(result.Document, value)) result.OrphanOverrides.push_back(value); + + for (const auto& nested : layer->NestedPrefabs) { + auto nestedResult = ResolveInternal(nested.PrefabAssetGuid, stack, error); + if (!nestedResult) { + stack.pop_back(); + return std::nullopt; + } + for (const auto& value : nested.Overrides) + if (!OverrideRegistry_.Apply(nestedResult->Document, value)) result.OrphanOverrides.push_back(value); + const auto* mount = FindNode(result.Document, nested.MountNodeGuid); + if (!mount) { + MetaCorePrefabOverrideDocument orphan; + orphan.TargetNodeGuid = nested.MountNodeGuid; + orphan.ComponentTypeId = "NestedPrefab"; + orphan.ValueJson = nested.PrefabAssetGuid.ToString(); + result.OrphanOverrides.push_back(std::move(orphan)); + continue; + } + const MetaCoreAssetGuid instanceGuid = nested.InstanceGuid.IsValid() + ? nested.InstanceGuid + : MetaCoreBuildDeterministicGuid(layer->PrefabGuid, nested.PrefabAssetGuid); + std::unordered_map remap; + for (const auto& object : nestedResult->Document.GameObjects) + remap[object.Id] = MetaCoreIdGenerator::Generate(); + for (auto object : nestedResult->Document.GameObjects) { + const MetaCoreId oldId = object.Id; + object.Id = remap.at(oldId); + if (object.ParentId == 0) object.ParentId = mount->Id; + else if (const auto parent = remap.find(object.ParentId); parent != remap.end()) object.ParentId = parent->second; + else { + if (error) *error = "Nested prefab contains a missing parent object"; + stack.pop_back(); + return std::nullopt; + } + object.StableObjectGuid = MetaCoreBuildDeterministicGuid(instanceGuid, object.StableObjectGuid); + result.Document.GameObjects.push_back(std::move(object)); + } + result.DependencyChain.insert(result.DependencyChain.end(), nestedResult->DependencyChain.begin(), nestedResult->DependencyChain.end()); + result.OrphanOverrides.insert(result.OrphanOverrides.end(), nestedResult->OrphanOverrides.begin(), nestedResult->OrphanOverrides.end()); + } + stack.pop_back(); + return result; +} + +std::optional> MetaCorePrefabResolver::Instantiate( + const MetaCoreAssetGuid& prefabGuid, + const MetaCoreAssetGuid& instanceGuid, + const std::vector& instanceOverrides, + std::string* error +) const { + if (!instanceGuid.IsValid()) { + if (error) *error = "Prefab instance GUID is invalid"; + return std::nullopt; + } + auto resolved = Resolve(prefabGuid, error); + if (!resolved) return std::nullopt; + for (const auto& value : instanceOverrides) + if (!OverrideRegistry_.Apply(resolved->Document, value)) resolved->OrphanOverrides.push_back(value); + + std::unordered_map remap; + for (const auto& object : resolved->Document.GameObjects) remap[object.Id] = MetaCoreIdGenerator::Generate(); + for (auto& object : resolved->Document.GameObjects) { + const MetaCoreId oldId = object.Id; + const MetaCoreAssetGuid nodeGuid = object.StableObjectGuid; + object.Id = remap.at(oldId); + if (object.ParentId != 0) { + const auto parent = remap.find(object.ParentId); + if (parent == remap.end()) { + if (error) *error = "Prefab contains a missing parent object"; + return std::nullopt; + } + object.ParentId = parent->second; + } + object.StableObjectGuid = MetaCoreBuildDeterministicGuid(instanceGuid, nodeGuid); + MetaCorePrefabInstanceMetadata metadata; + metadata.PrefabAssetGuid = prefabGuid; + metadata.PrefabObjectId = oldId; + metadata.InstanceGuid = instanceGuid; + metadata.PrefabNodeGuid = nodeGuid; + object.PrefabInstance = metadata; + } + const MetaCoreId rootId = [&] { + const auto root = std::find_if(resolved->Document.GameObjects.begin(), resolved->Document.GameObjects.end(), + [](const auto& object) { return object.ParentId == 0; }); + return root == resolved->Document.GameObjects.end() ? MetaCoreId{} : root->Id; + }(); + for (auto& object : resolved->Document.GameObjects) + if (object.PrefabInstance) object.PrefabInstance->PrefabInstanceRootId = rootId; + return resolved->Document.GameObjects; +} + +std::vector MetaCorePrefabResolver::ValidateDependencies(const MetaCoreAssetGuid& prefabGuid) const { + std::string error; + const auto result = Resolve(prefabGuid, &error); + std::vector issues; + if (!result) issues.push_back(error); + else if (!result->OrphanOverrides.empty()) + issues.push_back("Prefab contains " + std::to_string(result->OrphanOverrides.size()) + " orphan override(s)"); + return issues; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreScene/Private/MetaCoreScene.cpp b/Source/MetaCoreScene/Private/MetaCoreScene.cpp index ba31857..4d81d04 100644 --- a/Source/MetaCoreScene/Private/MetaCoreScene.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreScene.cpp @@ -465,6 +465,11 @@ MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const { MetaCoreGameObjectData data; data.Id = obj.GetId(); data.ParentId = obj.GetParentId(); + if (obj.HasComponent()) { + const auto& identity = obj.GetComponent(); + data.SceneGuid = identity.SceneGuid; + data.StableObjectGuid = identity.StableObjectGuid; + } data.Name = obj.GetName(); data.Transform = obj.GetComponent(); @@ -523,6 +528,11 @@ void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) { maxId = std::max(maxId, data.Id); MetaCoreGameObject obj = CreateGameObjectWithId(data.Id, data.Name, data.ParentId); obj.GetComponent() = data.Transform; + if (data.SceneGuid.IsValid() || data.StableObjectGuid.IsValid()) { + obj.AddComponent( + MetaCoreSceneObjectIdentityComponent{data.SceneGuid, data.StableObjectGuid} + ); + } if (data.Camera.has_value()) obj.AddComponent(data.Camera.value()); diff --git a/Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp b/Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp index d0f0fa3..b584b1a 100644 --- a/Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp @@ -15,7 +15,8 @@ namespace { [[nodiscard]] MetaCorePackageDocument MetaCoreBuildTypedScenePackage( const MetaCoreAssetGuid& assetGuid, const std::string& objectName, - std::vector payload + std::vector payload, + const MetaCoreSceneDocument& scene ) { MetaCorePackageDocument document; document.Header.PackageType = MetaCorePackageType::Scene; @@ -29,6 +30,17 @@ namespace { 0, static_cast(payload.size()) }); + for (const auto& subScene : scene.SubScenes) { + if (subScene.SceneGuid.IsValid()) + document.Dependencies.push_back({subScene.SceneGuid, "SubScene"}); + } + for (const auto& instance : scene.PrefabInstances) { + if (instance.PrefabAssetGuid.IsValid()) + document.Dependencies.push_back({instance.PrefabAssetGuid, "PrefabInstance"}); + } + document.CustomVersions.push_back({"MetaCore.Scene", scene.SchemaVersion}); + for (const auto& component : scene.ComponentVersions) + document.CustomVersions.push_back({"MetaCore.Component." + component.TypeId, component.Version}); document.PayloadSections.push_back(std::move(payload)); return document; } @@ -110,9 +122,10 @@ bool MetaCoreWriteScenePackage( const std::string objectName = path.stem().string().empty() ? "Scene" : path.stem().string(); MetaCorePackageDocument package = MetaCoreBuildTypedScenePackage( - MetaCoreAssetGuid::Generate(), + document.SceneGuid.IsValid() ? document.SceneGuid : MetaCoreAssetGuid::Generate(), objectName, - *payload + *payload, + document ); return MetaCoreWritePackageFile(path, std::move(package), registry); } @@ -123,7 +136,9 @@ std::optional MetaCoreReadScenePackage(const std::filesys if (!package.has_value() || package->Header.PackageType != MetaCorePackageType::Scene) { return std::nullopt; } - return MetaCoreReadTypedPayload(*package, registry, "MetaCoreSceneDocument"); + auto scene = MetaCoreReadTypedPayload(*package, registry, "MetaCoreSceneDocument"); + if (!scene || scene->SchemaVersion > GMetaCoreSceneSchemaVersion) return std::nullopt; + return scene; } std::optional MetaCoreResolveStartupScenePackagePath(const std::filesystem::path& projectFilePath) { diff --git a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp index 9d27f59..42f3f5c 100644 --- a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp @@ -1,10 +1,12 @@ #include "MetaCoreScene/MetaCoreSceneSerializer.h" #include "MetaCoreFoundation/MetaCoreReflection.h" +#include "MetaCoreScene/MetaCoreSceneWorld.h" #include #include #include #include #include +#include using json = nlohmann::json; @@ -33,6 +35,23 @@ static MetaCoreAssetGuid StringToGuid(const std::string& str) { return str.empty() ? MetaCoreAssetGuid{} : MetaCoreAssetGuid::Parse(str).value_or(MetaCoreAssetGuid{}); } +static json ObjectReferenceToJson(const MetaCoreObjectReference& reference) { + return { + {"SceneGuid", GuidToString(reference.SceneGuid)}, + {"StableObjectGuid", GuidToString(reference.StableObjectGuid)}, + {"Strength", static_cast(reference.Strength)} + }; +} + +static MetaCoreObjectReference JsonToObjectReference(const json& value) { + MetaCoreObjectReference result; + if (!value.is_object()) return result; + result.SceneGuid = StringToGuid(value.value("SceneGuid", std::string{})); + result.StableObjectGuid = StringToGuid(value.value("StableObjectGuid", std::string{})); + result.Strength = static_cast(value.value("Strength", 0)); + return result; +} + template static std::string ReflectedValueToHex(const T& value, const MetaCoreTypeRegistry& registry) { const auto bytes = MetaCoreSerializeToBytes(value, registry); if (!bytes) return {}; @@ -89,6 +108,10 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo objJson["Id"] = obj.Id; } else if (objField.Name == "ParentId") { objJson["ParentId"] = obj.ParentId; + } else if (objField.Name == "SceneGuid") { + objJson["SceneGuid"] = GuidToString(obj.SceneGuid); + } else if (objField.Name == "StableObjectGuid") { + objJson["StableObjectGuid"] = GuidToString(obj.StableObjectGuid); } else if (objField.Name == "Name") { objJson["Name"] = obj.Name; } else if (objField.Name == "Transform") { @@ -205,7 +228,11 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo } else if (objField.Name == "TimelineDirector" && obj.TimelineDirector.has_value()) { const auto& director = *obj.TimelineDirector; json bindings = json::array(); - for (const auto& binding : director.Bindings) bindings.push_back({{"Slot", binding.Slot}, {"ObjectId", binding.ObjectId}}); + for (const auto& binding : director.Bindings) bindings.push_back({ + {"Slot", binding.Slot}, + {"ObjectId", binding.ObjectId}, + {"ObjectReference", ObjectReferenceToJson(binding.ObjectReference)} + }); objJson["TimelineDirector"] = {{"Enabled", director.Enabled}, {"TimelineAssetGuid", GuidToString(director.TimelineAssetGuid)}, {"PlayOnStart", director.PlayOnStart}, {"Loop", director.Loop}, {"Speed", director.Speed}, {"Bindings", std::move(bindings)}}; } else if (objField.Name == "Script" && obj.Script.has_value()) { @@ -261,7 +288,8 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo } else if (objField.Name == "PhysicsConstraint" && obj.PhysicsConstraint.has_value()) { const auto& value = *obj.PhysicsConstraint; objJson["PhysicsConstraint"] = {{"Enabled", value.Enabled}, {"Type", static_cast(value.Type)}, - {"TargetObjectId", value.TargetObjectId}, {"Anchor", Vec3ToJson(value.Anchor)}, + {"TargetObjectId", value.TargetObjectId}, {"TargetObjectReference", ObjectReferenceToJson(value.TargetObjectReference)}, + {"Anchor", Vec3ToJson(value.Anchor)}, {"TargetAnchor", Vec3ToJson(value.TargetAnchor)}, {"Axis", Vec3ToJson(value.Axis)}, {"LinearLowerLimit", Vec3ToJson(value.LinearLowerLimit)}, {"LinearUpperLimit", Vec3ToJson(value.LinearUpperLimit)}, {"AngularLowerLimit", Vec3ToJson(value.AngularLowerLimit)}, {"AngularUpperLimit", Vec3ToJson(value.AngularUpperLimit)}, @@ -310,6 +338,8 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo if (prefabField.Name == "PrefabAssetGuid") prefabJson["PrefabAssetGuid"] = GuidToString(obj.PrefabInstance->PrefabAssetGuid); else if (prefabField.Name == "PrefabObjectId") prefabJson["PrefabObjectId"] = obj.PrefabInstance->PrefabObjectId; else if (prefabField.Name == "PrefabInstanceRootId") prefabJson["PrefabInstanceRootId"] = obj.PrefabInstance->PrefabInstanceRootId; + else if (prefabField.Name == "InstanceGuid") prefabJson["InstanceGuid"] = GuidToString(obj.PrefabInstance->InstanceGuid); + else if (prefabField.Name == "PrefabNodeGuid") prefabJson["PrefabNodeGuid"] = GuidToString(obj.PrefabInstance->PrefabNodeGuid); } objJson["PrefabInstance"] = prefabJson; } @@ -329,6 +359,10 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me obj.Id = objJson["Id"].get(); } else if (objField.Name == "ParentId" && objJson.contains("ParentId")) { obj.ParentId = objJson["ParentId"].get(); + } else if (objField.Name == "SceneGuid" && objJson.contains("SceneGuid")) { + obj.SceneGuid = StringToGuid(objJson["SceneGuid"].get()); + } else if (objField.Name == "StableObjectGuid" && objJson.contains("StableObjectGuid")) { + obj.StableObjectGuid = StringToGuid(objJson["StableObjectGuid"].get()); } else if (objField.Name == "Name" && objJson.contains("Name")) { obj.Name = objJson["Name"].get(); } else if (objField.Name == "Transform" && objJson.contains("Transform")) { @@ -457,7 +491,13 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me director.PlayOnStart = value.value("PlayOnStart", true); director.Loop = value.value("Loop", false); director.Speed = value.value("Speed", 1.0F); - for (const auto& binding : value.value("Bindings", json::array())) director.Bindings.push_back({binding.value("Slot", ""), binding.value("ObjectId", MetaCoreId{})}); + for (const auto& binding : value.value("Bindings", json::array())) { + MetaCoreTimelineBinding result; + result.Slot = binding.value("Slot", ""); + result.ObjectId = binding.value("ObjectId", MetaCoreId{}); + if (binding.contains("ObjectReference")) result.ObjectReference = JsonToObjectReference(binding["ObjectReference"]); + director.Bindings.push_back(std::move(result)); + } obj.TimelineDirector = std::move(director); } else if (objField.Name == "Script" && objJson.contains("Script")) { const auto& scriptJson = objJson["Script"]; @@ -537,6 +577,7 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me MetaCorePhysicsConstraintComponent component; component.Enabled = value.value("Enabled", true); component.Type = static_cast(value.value("Type", 0)); component.TargetObjectId = value.value("TargetObjectId", MetaCoreId{0}); + if (value.contains("TargetObjectReference")) component.TargetObjectReference = JsonToObjectReference(value["TargetObjectReference"]); if (value.contains("Anchor")) component.Anchor = JsonToVec3(value["Anchor"]); if (value.contains("TargetAnchor")) component.TargetAnchor = JsonToVec3(value["TargetAnchor"]); if (value.contains("Axis")) component.Axis = JsonToVec3(value["Axis"]); @@ -595,6 +636,8 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me if (prefabField.Name == "PrefabAssetGuid" && prefabJson.contains("PrefabAssetGuid")) prefab.PrefabAssetGuid = StringToGuid(prefabJson["PrefabAssetGuid"].get()); else if (prefabField.Name == "PrefabObjectId" && prefabJson.contains("PrefabObjectId")) prefab.PrefabObjectId = prefabJson["PrefabObjectId"].get(); else if (prefabField.Name == "PrefabInstanceRootId" && prefabJson.contains("PrefabInstanceRootId")) prefab.PrefabInstanceRootId = prefabJson["PrefabInstanceRootId"].get(); + else if (prefabField.Name == "InstanceGuid" && prefabJson.contains("InstanceGuid")) prefab.InstanceGuid = StringToGuid(prefabJson["InstanceGuid"].get()); + else if (prefabField.Name == "PrefabNodeGuid" && prefabJson.contains("PrefabNodeGuid")) prefab.PrefabNodeGuid = StringToGuid(prefabJson["PrefabNodeGuid"].get()); } obj.PrefabInstance = prefab; } @@ -603,13 +646,120 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me return obj; } +static json PrefabOverrideToJson(const MetaCorePrefabOverrideDocument& value) { + return { + {"TargetNodeGuid", GuidToString(value.TargetNodeGuid)}, + {"ComponentTypeId", value.ComponentTypeId}, + {"PropertyPath", value.PropertyPath}, + {"Operation", static_cast(value.Operation)}, + {"ValueJson", value.ValueJson} + }; +} + +static MetaCorePrefabOverrideDocument JsonToPrefabOverride(const json& value) { + MetaCorePrefabOverrideDocument result; + result.TargetNodeGuid = StringToGuid(value.value("TargetNodeGuid", std::string{})); + result.ComponentTypeId = value.value("ComponentTypeId", std::string{}); + result.PropertyPath = value.value("PropertyPath", std::string{}); + result.Operation = static_cast(value.value("Operation", 0)); + result.ValueJson = value.value("ValueJson", std::string{}); + return result; +} + +static json SchemaVersionsToJson(const std::vector& versions) { + json result = json::array(); + for (const auto& version : versions) { + result.push_back({{"TypeId", version.TypeId}, {"Version", version.Version}}); + } + return result; +} + +static std::vector JsonToSchemaVersions(const json& values) { + std::vector result; + if (!values.is_array()) return result; + for (const auto& value : values) { + result.push_back({value.value("TypeId", std::string{}), value.value("Version", 1U)}); + } + return result; +} + +static MetaCoreAssetGuid BuildLegacyDocumentGuid(std::string_view kind, std::string_view name) { + // Legacy documents do not carry an asset GUID. Derive one from persisted + // content so repeated migration/save runs are deterministic. + std::uint64_t first = 1469598103934665603ULL; + std::uint64_t second = 1099511628211ULL; + const auto mix = [&](std::string_view value) { + for (const unsigned char character : value) { + first = (first ^ character) * 1099511628211ULL; + second = (second + character + 0x9e3779b97f4a7c15ULL) * 14029467366897019727ULL; + } + }; + mix(kind); + mix(name); + MetaCoreAssetGuid result; + for (std::size_t index = 0; index < 8; ++index) { + result.Bytes[index] = static_cast(first >> (index * 8U)); + result.Bytes[index + 8] = static_cast(second >> (index * 8U)); + } + result.Bytes[6] = static_cast((result.Bytes[6] & 0x0FU) | 0x50U); + result.Bytes[8] = static_cast((result.Bytes[8] & 0x3FU) | 0x80U); + return result; +} + +static void NormalizeSceneDocumentIdentity(MetaCoreSceneDocument& document) { + if (!document.SceneGuid.IsValid()) document.SceneGuid = BuildLegacyDocumentGuid("scene:", document.Name); + std::unordered_map stableIds; + for (auto& object : document.GameObjects) { + object.SceneGuid = document.SceneGuid; + if (!object.StableObjectGuid.IsValid()) + object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.SceneGuid, object.Id); + stableIds[object.Id] = object.StableObjectGuid; + } + for (auto& object : document.GameObjects) { + if (object.PhysicsConstraint) { + auto& reference = object.PhysicsConstraint->TargetObjectReference; + if (!reference.IsValid()) { + if (const auto target = stableIds.find(object.PhysicsConstraint->TargetObjectId); target != stableIds.end()) { + reference.SceneGuid = document.SceneGuid; + reference.StableObjectGuid = target->second; + reference.Strength = MetaCoreObjectReferenceStrength::Hard; + } + } else if (!reference.SceneGuid.IsValid()) reference.SceneGuid = document.SceneGuid; + } + if (object.TimelineDirector) { + for (auto& binding : object.TimelineDirector->Bindings) { + if (!binding.ObjectReference.IsValid()) { + if (const auto target = stableIds.find(binding.ObjectId); target != stableIds.end()) { + binding.ObjectReference.SceneGuid = document.SceneGuid; + binding.ObjectReference.StableObjectGuid = target->second; + } + } else if (!binding.ObjectReference.SceneGuid.IsValid()) binding.ObjectReference.SceneGuid = document.SceneGuid; + } + } + } + document.SchemaVersion = GMetaCoreSceneSchemaVersion; +} + +static void NormalizePrefabDocumentIdentity(MetaCorePrefabDocument& document) { + if (!document.PrefabGuid.IsValid()) document.PrefabGuid = BuildLegacyDocumentGuid("prefab:", document.Name); + for (auto& object : document.GameObjects) { + if (!object.StableObjectGuid.IsValid()) + object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.PrefabGuid, object.Id); + } + document.SchemaVersion = GMetaCorePrefabSchemaVersion; +} + bool MetaCoreSceneSerializer::SaveSceneToJson( const std::filesystem::path& absolutePath, const MetaCoreSceneDocument& sceneDocument, const MetaCoreTypeRegistry& registry ) { try { + MetaCoreSceneDocument normalized = sceneDocument; + NormalizeSceneDocumentIdentity(normalized); json sceneJson; + sceneJson["SchemaVersion"] = normalized.SchemaVersion; + sceneJson["SceneGuid"] = GuidToString(normalized.SceneGuid); // 验证 MetaCoreSceneDocument 反射注册 const MetaCoreStructDescriptor* sceneDesc = registry.FindStruct(); @@ -620,26 +770,55 @@ bool MetaCoreSceneSerializer::SaveSceneToJson( for (const auto& field : sceneDesc->Fields) { if (field.Name == "Name") { - sceneJson["SceneName"] = sceneDocument.Name; + sceneJson["SceneName"] = normalized.Name; } else if (field.Name == "Environment") { - sceneJson["Environment"] = SceneEnvironmentToJson(sceneDocument.Environment); + sceneJson["Environment"] = SceneEnvironmentToJson(normalized.Environment); } else if (field.Name == "Selection") { const MetaCoreStructDescriptor* selDesc = registry.FindStruct(); if (selDesc) { json selJson = json::object(); for (const auto& selField : selDesc->Fields) { - if (selField.Name == "SelectedObjectIds") selJson["SelectedObjectIds"] = sceneDocument.Selection.SelectedObjectIds; - else if (selField.Name == "ActiveObjectId") selJson["ActiveObjectId"] = sceneDocument.Selection.ActiveObjectId; - else if (selField.Name == "SelectionAnchorId") selJson["SelectionAnchorId"] = sceneDocument.Selection.SelectionAnchorId; + if (selField.Name == "SelectedObjectIds") selJson["SelectedObjectIds"] = normalized.Selection.SelectedObjectIds; + else if (selField.Name == "ActiveObjectId") selJson["ActiveObjectId"] = normalized.Selection.ActiveObjectId; + else if (selField.Name == "SelectionAnchorId") selJson["SelectionAnchorId"] = normalized.Selection.SelectionAnchorId; } sceneJson["Selection"] = selJson; } } else if (field.Name == "GameObjects") { json gameObjectsArray = json::array(); - for (const auto& obj : sceneDocument.GameObjects) { + for (const auto& obj : normalized.GameObjects) { gameObjectsArray.push_back(GameObjectDataToJson(obj, registry)); } sceneJson["GameObjects"] = gameObjectsArray; + } else if (field.Name == "SubScenes") { + sceneJson["SubScenes"] = json::array(); + for (const auto& subScene : normalized.SubScenes) { + sceneJson["SubScenes"].push_back({ + {"SceneGuid", GuidToString(subScene.SceneGuid)}, + {"ScenePath", subScene.ScenePath.generic_string()}, + {"LoadPolicy", static_cast(subScene.LoadPolicy)}, + {"BoundsCenter", Vec3ToJson(subScene.BoundsCenter)}, + {"BoundsExtent", Vec3ToJson(subScene.BoundsExtent)}, + {"CellCoordinate", Vec3ToJson(subScene.CellCoordinate)}, + {"PreloadDistance", subScene.PreloadDistance}, + {"UnloadDistance", subScene.UnloadDistance}, + {"Priority", subScene.Priority} + }); + } + } else if (field.Name == "PrefabInstances") { + sceneJson["PrefabInstances"] = json::array(); + for (const auto& instance : normalized.PrefabInstances) { + json overrides = json::array(); + for (const auto& entry : instance.Overrides) overrides.push_back(PrefabOverrideToJson(entry)); + sceneJson["PrefabInstances"].push_back({ + {"InstanceGuid", GuidToString(instance.InstanceGuid)}, + {"PrefabAssetGuid", GuidToString(instance.PrefabAssetGuid)}, + {"ParentObjectGuid", GuidToString(instance.ParentObjectGuid)}, + {"Overrides", std::move(overrides)} + }); + } + } else if (field.Name == "ComponentVersions") { + sceneJson["ComponentVersions"] = SchemaVersionsToJson(normalized.ComponentVersions); } } @@ -672,6 +851,10 @@ std::optional MetaCoreSceneSerializer::LoadSceneFromJson( file >> sceneJson; MetaCoreSceneDocument sceneDocument; + const std::uint32_t sourceVersion = sceneJson.value("SchemaVersion", 1U); + if (sourceVersion > GMetaCoreSceneSchemaVersion) return std::nullopt; + sceneDocument.SchemaVersion = sourceVersion; + if (sceneJson.contains("SceneGuid")) sceneDocument.SceneGuid = StringToGuid(sceneJson["SceneGuid"].get()); // 验证 MetaCoreSceneDocument 反射注册 const MetaCoreStructDescriptor* sceneDesc = registry.FindStruct(); @@ -703,9 +886,35 @@ std::optional MetaCoreSceneSerializer::LoadSceneFromJson( for (const auto& objJson : sceneJson["GameObjects"]) { sceneDocument.GameObjects.push_back(JsonToGameObjectData(objJson, registry)); } + } else if (field.Name == "SubScenes" && sceneJson.contains("SubScenes") && sceneJson["SubScenes"].is_array()) { + for (const auto& value : sceneJson["SubScenes"]) { + MetaCoreSubSceneDocument subScene; + subScene.SceneGuid = StringToGuid(value.value("SceneGuid", std::string{})); + subScene.ScenePath = value.value("ScenePath", std::string{}); + subScene.LoadPolicy = static_cast(value.value("LoadPolicy", 0)); + if (value.contains("BoundsCenter")) subScene.BoundsCenter = JsonToVec3(value["BoundsCenter"]); + if (value.contains("BoundsExtent")) subScene.BoundsExtent = JsonToVec3(value["BoundsExtent"]); + if (value.contains("CellCoordinate")) subScene.CellCoordinate = JsonToVec3(value["CellCoordinate"]); + subScene.PreloadDistance = value.value("PreloadDistance", 100.0F); + subScene.UnloadDistance = value.value("UnloadDistance", 125.0F); + subScene.Priority = value.value("Priority", 0); + sceneDocument.SubScenes.push_back(std::move(subScene)); + } + } else if (field.Name == "PrefabInstances" && sceneJson.contains("PrefabInstances") && sceneJson["PrefabInstances"].is_array()) { + for (const auto& value : sceneJson["PrefabInstances"]) { + MetaCorePrefabInstanceDocument instance; + instance.InstanceGuid = StringToGuid(value.value("InstanceGuid", std::string{})); + instance.PrefabAssetGuid = StringToGuid(value.value("PrefabAssetGuid", std::string{})); + instance.ParentObjectGuid = StringToGuid(value.value("ParentObjectGuid", std::string{})); + if (value.contains("Overrides")) for (const auto& entry : value["Overrides"]) instance.Overrides.push_back(JsonToPrefabOverride(entry)); + sceneDocument.PrefabInstances.push_back(std::move(instance)); + } + } else if (field.Name == "ComponentVersions" && sceneJson.contains("ComponentVersions")) { + sceneDocument.ComponentVersions = JsonToSchemaVersions(sceneJson["ComponentVersions"]); } } + NormalizeSceneDocumentIdentity(sceneDocument); return sceneDocument; } catch (const std::exception& e) { std::cerr << "LoadSceneFromJson: 发生异常 - " << e.what() << std::endl; @@ -720,13 +929,32 @@ bool MetaCoreSceneSerializer::SavePrefabToJson( const MetaCoreTypeRegistry& registry ) { try { + MetaCorePrefabDocument normalized = prefabDocument; + NormalizePrefabDocumentIdentity(normalized); json prefabJson; - prefabJson["PrefabName"] = prefabDocument.Name; + prefabJson["SchemaVersion"] = normalized.SchemaVersion; + prefabJson["PrefabGuid"] = GuidToString(normalized.PrefabGuid); + prefabJson["BasePrefabGuid"] = GuidToString(normalized.BasePrefabGuid); + prefabJson["PrefabName"] = normalized.Name; json gameObjectsArray = json::array(); - for (const auto& obj : prefabDocument.GameObjects) { + for (const auto& obj : normalized.GameObjects) { gameObjectsArray.push_back(GameObjectDataToJson(obj, registry)); } prefabJson["GameObjects"] = gameObjectsArray; + prefabJson["Overrides"] = json::array(); + for (const auto& value : normalized.Overrides) prefabJson["Overrides"].push_back(PrefabOverrideToJson(value)); + prefabJson["NestedPrefabs"] = json::array(); + for (const auto& nested : normalized.NestedPrefabs) { + json overrides = json::array(); + for (const auto& value : nested.Overrides) overrides.push_back(PrefabOverrideToJson(value)); + prefabJson["NestedPrefabs"].push_back({ + {"MountNodeGuid", GuidToString(nested.MountNodeGuid)}, + {"PrefabAssetGuid", GuidToString(nested.PrefabAssetGuid)}, + {"InstanceGuid", GuidToString(nested.InstanceGuid)}, + {"Overrides", std::move(overrides)} + }); + } + prefabJson["ComponentVersions"] = SchemaVersionsToJson(normalized.ComponentVersions); std::ofstream file(absolutePath); if (!file.is_open()) { @@ -752,6 +980,11 @@ std::optional MetaCoreSceneSerializer::LoadPrefabFromJso file >> prefabJson; MetaCorePrefabDocument doc; + const std::uint32_t sourceVersion = prefabJson.value("SchemaVersion", 1U); + if (sourceVersion > GMetaCorePrefabSchemaVersion) return std::nullopt; + doc.SchemaVersion = sourceVersion; + doc.PrefabGuid = StringToGuid(prefabJson.value("PrefabGuid", std::string{})); + doc.BasePrefabGuid = StringToGuid(prefabJson.value("BasePrefabGuid", std::string{})); if (prefabJson.contains("PrefabName")) { doc.Name = prefabJson["PrefabName"].get(); } @@ -760,6 +993,17 @@ std::optional MetaCoreSceneSerializer::LoadPrefabFromJso doc.GameObjects.push_back(JsonToGameObjectData(objJson, registry)); } } + if (prefabJson.contains("Overrides")) for (const auto& value : prefabJson["Overrides"]) doc.Overrides.push_back(JsonToPrefabOverride(value)); + if (prefabJson.contains("NestedPrefabs")) for (const auto& value : prefabJson["NestedPrefabs"]) { + MetaCoreNestedPrefabDocument nested; + nested.MountNodeGuid = StringToGuid(value.value("MountNodeGuid", std::string{})); + nested.PrefabAssetGuid = StringToGuid(value.value("PrefabAssetGuid", std::string{})); + nested.InstanceGuid = StringToGuid(value.value("InstanceGuid", std::string{})); + if (value.contains("Overrides")) for (const auto& entry : value["Overrides"]) nested.Overrides.push_back(JsonToPrefabOverride(entry)); + doc.NestedPrefabs.push_back(std::move(nested)); + } + if (prefabJson.contains("ComponentVersions")) doc.ComponentVersions = JsonToSchemaVersions(prefabJson["ComponentVersions"]); + NormalizePrefabDocumentIdentity(doc); return doc; } catch (...) { return std::nullopt; diff --git a/Source/MetaCoreScene/Private/MetaCoreSceneWorld.cpp b/Source/MetaCoreScene/Private/MetaCoreSceneWorld.cpp new file mode 100644 index 0000000..7571c06 --- /dev/null +++ b/Source/MetaCoreScene/Private/MetaCoreSceneWorld.cpp @@ -0,0 +1,565 @@ +#include "MetaCoreScene/MetaCoreSceneWorld.h" + +#include +#include +#include + +namespace MetaCore { + +namespace { + +void HashBytes(std::uint64_t& hash, const std::uint8_t* bytes, std::size_t count) { + for (std::size_t index = 0; index < count; ++index) { + hash ^= bytes[index]; + hash *= 1099511628211ULL; + } +} + +float DistanceToBounds(const glm::vec3& point, const glm::vec3& center, const glm::vec3& extent) { + const glm::vec3 delta = glm::abs(point - center) - glm::max(extent, glm::vec3(0.0F)); + const glm::vec3 outside = glm::max(delta, glm::vec3(0.0F)); + return glm::length(outside); +} + +void RemapLegacyObjectReferences(MetaCoreGameObjectData& object, const std::unordered_map& ids) { + if (const auto parent = ids.find(object.ParentId); parent != ids.end()) object.ParentId = parent->second; + else object.ParentId = 0; + if (object.PhysicsConstraint) { + if (const auto target = ids.find(object.PhysicsConstraint->TargetObjectId); target != ids.end()) + object.PhysicsConstraint->TargetObjectId = target->second; + else object.PhysicsConstraint->TargetObjectId = 0; + } + if (object.TimelineDirector) { + for (auto& binding : object.TimelineDirector->Bindings) { + if (const auto target = ids.find(binding.ObjectId); target != ids.end()) binding.ObjectId = target->second; + else binding.ObjectId = 0; + } + } +} + +} // namespace + +struct MetaCoreSceneWorld::PendingLoad { + std::uint64_t RequestId = 0; + MetaCoreAssetGuid SceneGuid{}; + MetaCoreSceneLoadMode Mode = MetaCoreSceneLoadMode::Additive; + std::int32_t Priority = 0; + bool Cancelled = false; + std::future> Future{}; +}; + +MetaCoreAssetGuid MetaCoreBuildDeterministicGuid(const MetaCoreAssetGuid& namespaceGuid, const MetaCoreAssetGuid& localGuid) { + std::uint64_t first = 1469598103934665603ULL; + HashBytes(first, namespaceGuid.Bytes.data(), namespaceGuid.Bytes.size()); + HashBytes(first, localGuid.Bytes.data(), localGuid.Bytes.size()); + std::uint64_t second = 1099511628211ULL ^ first; + HashBytes(second, localGuid.Bytes.data(), localGuid.Bytes.size()); + HashBytes(second, namespaceGuid.Bytes.data(), namespaceGuid.Bytes.size()); + MetaCoreAssetGuid result; + for (std::size_t index = 0; index < 8; ++index) { + result.Bytes[index] = static_cast((first >> (index * 8U)) & 0xFFU); + result.Bytes[index + 8] = static_cast((second >> (index * 8U)) & 0xFFU); + } + result.Bytes[6] = static_cast((result.Bytes[6] & 0x0FU) | 0x50U); + result.Bytes[8] = static_cast((result.Bytes[8] & 0x3FU) | 0x80U); + return result; +} + +MetaCoreAssetGuid MetaCoreBuildDeterministicGuid(const MetaCoreAssetGuid& namespaceGuid, std::uint64_t localValue) { + MetaCoreAssetGuid local; + for (std::size_t index = 0; index < 8; ++index) + local.Bytes[index] = static_cast((localValue >> (index * 8U)) & 0xFFU); + return MetaCoreBuildDeterministicGuid(namespaceGuid, local); +} + +std::size_t MetaCoreSceneWorld::ObjectKeyHasher::operator()(const ObjectKey& value) const noexcept { + MetaCoreAssetGuidHasher hasher; + return hasher(value.SceneGuid) ^ (hasher(value.ObjectGuid) + 0x9e3779b97f4a7c15ULL); +} + +MetaCoreSceneWorld::MetaCoreSceneWorld(SceneLoader loader, MetaCoreSceneWorldSettings settings) + : Loader_(std::move(loader)), Settings_(settings), OwnedScene_(std::make_unique()), Scene_(OwnedScene_.get()) {} + +MetaCoreSceneWorld::MetaCoreSceneWorld(MetaCoreScene& scene, SceneLoader loader, MetaCoreSceneWorldSettings settings) + : Loader_(std::move(loader)), Settings_(settings), Scene_(&scene) {} + +MetaCoreSceneWorld::~MetaCoreSceneWorld() = default; + +void MetaCoreSceneWorld::SetSceneLoader(SceneLoader loader) { Loader_ = std::move(loader); } + +std::uint64_t MetaCoreSceneWorld::RequestLoad( + const MetaCoreAssetGuid& sceneGuid, + MetaCoreSceneLoadMode mode, + std::int32_t priority +) { + for (const auto& pending : PendingLoads_) { + if (!pending->Cancelled && pending->SceneGuid == sceneGuid) return pending->RequestId; + } + const std::uint64_t requestId = NextRequestId_++; + MetaCoreSceneLoadStatus status{requestId, sceneGuid, MetaCoreSceneLoadState::Loading, {}}; + if (!sceneGuid.IsValid() || !Loader_) { + status.State = MetaCoreSceneLoadState::Failed; + status.Error = !sceneGuid.IsValid() ? "Scene GUID is invalid" : "Scene loader is not configured"; + LoadStatuses_[requestId] = std::move(status); + return requestId; + } + if (IsSceneLoaded(sceneGuid)) { + status.State = MetaCoreSceneLoadState::Loaded; + LoadStatuses_[requestId] = std::move(status); + return requestId; + } + auto pending = std::make_unique(); + pending->RequestId = requestId; + pending->SceneGuid = sceneGuid; + pending->Mode = mode; + pending->Priority = priority; + SceneLoader loader = Loader_; + pending->Future = std::async(std::launch::async, [loader = std::move(loader), sceneGuid] { + return loader(sceneGuid); + }); + PendingLoads_.push_back(std::move(pending)); + LoadStatuses_[requestId] = std::move(status); + return requestId; +} + +bool MetaCoreSceneWorld::CancelLoad(std::uint64_t requestId) { + const auto iterator = std::find_if(PendingLoads_.begin(), PendingLoads_.end(), [&](const auto& pending) { + return pending->RequestId == requestId; + }); + if (iterator == PendingLoads_.end()) return false; + (*iterator)->Cancelled = true; + auto& status = LoadStatuses_[requestId]; + status.State = MetaCoreSceneLoadState::Cancelled; + status.Error = "Load request cancelled"; + return true; +} + +void MetaCoreSceneWorld::Tick() { + std::stable_sort(PendingLoads_.begin(), PendingLoads_.end(), [](const auto& lhs, const auto& rhs) { + return lhs->Priority > rhs->Priority; + }); + for (auto iterator = PendingLoads_.begin(); iterator != PendingLoads_.end();) { + auto& pending = **iterator; + if (pending.Future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) { + ++iterator; + continue; + } + std::optional document = pending.Future.get(); + auto& status = LoadStatuses_[pending.RequestId]; + if (pending.Cancelled) { + status.State = MetaCoreSceneLoadState::Cancelled; + } else if (!document) { + status.State = MetaCoreSceneLoadState::Failed; + status.Error = "Scene loader returned no document"; + } else { + if (!document->SceneGuid.IsValid()) document->SceneGuid = pending.SceneGuid; + std::string error; + if (CommitDocument(std::move(*document), pending.Mode, &error)) status.State = MetaCoreSceneLoadState::Loaded; + else { + status.State = MetaCoreSceneLoadState::Failed; + status.Error = std::move(error); + } + } + iterator = PendingLoads_.erase(iterator); + } +} + +bool MetaCoreSceneWorld::LoadDocumentNow(MetaCoreSceneDocument document, MetaCoreSceneLoadMode mode, std::string* error) { + return CommitDocument(std::move(document), mode, error); +} + +bool MetaCoreSceneWorld::CommitDocument(MetaCoreSceneDocument document, MetaCoreSceneLoadMode mode, std::string* error) { + if (document.SchemaVersion > GMetaCoreSceneSchemaVersion) { + if (error) *error = "Scene schema is newer than this engine"; + return false; + } + if (!document.SceneGuid.IsValid()) { + if (error) *error = "Scene document has no stable GUID"; + return false; + } + if (IsSceneLoaded(document.SceneGuid)) { + if (error) *error = "Scene is already loaded"; + return false; + } + if (mode == MetaCoreSceneLoadMode::Replace) { + std::vector loaded; + loaded.reserve(LoadedScenes_.size()); + for (const auto& [guid, scene] : LoadedScenes_) { + (void)scene; + loaded.push_back(guid); + } + for (const auto& guid : loaded) (void)UnloadScene(guid, true); + if (LoadedScenes_.empty() && !Scene_->GetGameObjects().empty()) { + Scene_->RestoreSnapshot(MetaCoreSceneSnapshot{}); + } + } + + std::unordered_map ids; + for (const auto& object : document.GameObjects) ids.emplace(object.Id, MetaCoreIdGenerator::Generate()); + + LoadedScene record; + record.Source = document; + record.Source.SchemaVersion = GMetaCoreSceneSchemaVersion; + record.LoadMode = mode; + MetaCoreSceneSnapshot combined = Scene_->CaptureSnapshot(); + if (LoadedScenes_.empty()) combined.Environment = document.Environment; + for (auto object : document.GameObjects) { + const MetaCoreId serializedId = object.Id; + object.Id = ids.at(serializedId); + object.SceneGuid = document.SceneGuid; + if (!object.StableObjectGuid.IsValid()) + object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.SceneGuid, serializedId); + RemapLegacyObjectReferences(object, ids); + record.RuntimeObjectIds.push_back(object.Id); + record.RuntimeToSerializedId.emplace(object.Id, serializedId); + combined.GameObjects.push_back(std::move(object)); + } + Scene_->RestoreSnapshot(combined); + const MetaCoreAssetGuid sceneGuid = document.SceneGuid; + LoadedScenes_.emplace(sceneGuid, std::move(record)); + if (!ActiveSceneGuid_.IsValid() || mode == MetaCoreSceneLoadMode::Replace) ActiveSceneGuid_ = sceneGuid; + RebuildObjectIndex(); + for (auto object : Scene_->GetGameObjects()) { + if (object.HasComponent()) { + auto& constraint = object.GetComponent(); + if (constraint.TargetObjectReference.IsValid()) { + const auto target = ResolveObject(constraint.TargetObjectReference); + constraint.TargetObjectId = target ? target.GetId() : 0; + } + } + if (object.HasComponent()) { + for (auto& binding : object.GetComponent().Bindings) { + if (binding.ObjectReference.IsValid()) { + const auto target = ResolveObject(binding.ObjectReference); + binding.ObjectId = target ? target.GetId() : 0; + } + } + } + } + RebuildHardResidency(); + Publish({MetaCoreSceneLifecycleEventType::AfterLoad, sceneGuid, LoadedScenes_.at(sceneGuid).RuntimeObjectIds}); + + for (const auto& subScene : document.SubScenes) { + if (subScene.LoadPolicy == MetaCoreSubSceneLoadPolicy::AlwaysLoaded && !IsSceneLoaded(subScene.SceneGuid)) + (void)RequestLoad(subScene.SceneGuid, MetaCoreSceneLoadMode::SubScene, subScene.Priority); + } + return true; +} + +bool MetaCoreSceneWorld::UnloadScene(const MetaCoreAssetGuid& sceneGuid, bool force) { + auto iterator = LoadedScenes_.find(sceneGuid); + if (iterator == LoadedScenes_.end()) return false; + if (!force && HardResidentScenes_.contains(sceneGuid)) return false; + const auto objectIds = iterator->second.RuntimeObjectIds; + Publish({MetaCoreSceneLifecycleEventType::BeforeUnload, sceneGuid, objectIds}); + (void)Scene_->DeleteGameObjects(objectIds); + LoadedScenes_.erase(iterator); + HardResidentScenes_.erase(sceneGuid); + if (ActiveSceneGuid_ == sceneGuid) + ActiveSceneGuid_ = LoadedScenes_.empty() ? MetaCoreAssetGuid{} : LoadedScenes_.begin()->first; + RebuildObjectIndex(); + RebuildHardResidency(); + Publish({MetaCoreSceneLifecycleEventType::AfterUnload, sceneGuid, objectIds}); + return true; +} + +bool MetaCoreSceneWorld::SetActiveScene(const MetaCoreAssetGuid& sceneGuid) { + if (!IsSceneLoaded(sceneGuid)) return false; + ActiveSceneGuid_ = sceneGuid; + return true; +} + +bool MetaCoreSceneWorld::IsSceneLoaded(const MetaCoreAssetGuid& sceneGuid) const { + return LoadedScenes_.contains(sceneGuid); +} + +std::vector MetaCoreSceneWorld::GetLoadedScenes() const { + std::vector result; + result.reserve(LoadedScenes_.size()); + for (const auto& [guid, scene] : LoadedScenes_) { + result.push_back({guid, scene.Source.Name, scene.LoadMode, guid == ActiveSceneGuid_, scene.Dirty, + HardResidentScenes_.contains(guid), scene.RuntimeObjectIds}); + } + std::sort(result.begin(), result.end(), [](const auto& lhs, const auto& rhs) { + if (lhs.Active != rhs.Active) return lhs.Active; + return lhs.Name < rhs.Name; + }); + return result; +} + +std::optional MetaCoreSceneWorld::GetLoadStatus(std::uint64_t requestId) const { + const auto iterator = LoadStatuses_.find(requestId); + return iterator == LoadStatuses_.end() ? std::nullopt : std::optional(iterator->second); +} + +MetaCoreGameObject MetaCoreSceneWorld::ResolveObject(const MetaCoreObjectReference& reference) { + if (!reference.IsValid()) return {}; + MetaCoreAssetGuid sceneGuid = reference.SceneGuid.IsValid() ? reference.SceneGuid : ActiveSceneGuid_; + const auto iterator = ObjectIndex_.find({sceneGuid, reference.StableObjectGuid}); + if (iterator != ObjectIndex_.end()) return Scene_->FindGameObject(iterator->second); + if (reference.Strength == MetaCoreObjectReferenceStrength::Hard && sceneGuid.IsValid()) { + HardResidentScenes_.insert(sceneGuid); + if (!IsSceneLoaded(sceneGuid)) (void)RequestLoad(sceneGuid, MetaCoreSceneLoadMode::SubScene); + } + return {}; +} + +MetaCoreGameObject MetaCoreSceneWorld::ResolveObject(const MetaCoreObjectReference& reference) const { + if (!reference.IsValid()) return {}; + const MetaCoreAssetGuid sceneGuid = reference.SceneGuid.IsValid() ? reference.SceneGuid : ActiveSceneGuid_; + const auto iterator = ObjectIndex_.find({sceneGuid, reference.StableObjectGuid}); + return iterator == ObjectIndex_.end() ? MetaCoreGameObject{} : Scene_->FindGameObject(iterator->second); +} + +MetaCoreObjectReference MetaCoreSceneWorld::BuildObjectReference( + MetaCoreId runtimeObjectId, + MetaCoreObjectReferenceStrength strength +) const { + const MetaCoreGameObject object = Scene_->FindGameObject(runtimeObjectId); + if (!object || !object.HasComponent()) return {}; + const auto& identity = object.GetComponent(); + return {identity.SceneGuid, identity.StableObjectGuid, strength}; +} + +bool MetaCoreSceneWorld::MoveSubtreeToScene(MetaCoreId rootObjectId, const MetaCoreAssetGuid& targetSceneGuid) { + if (!IsSceneLoaded(targetSceneGuid)) return false; + const auto ids = Scene_->GetSubtreeObjectIds(rootObjectId); + if (ids.empty()) return false; + MetaCoreAssetGuid sourceSceneGuid{}; + for (MetaCoreId id : ids) { + auto object = Scene_->FindGameObject(id); + if (!object) return false; + if (!object.HasComponent()) + object.AddComponent(); + if (!sourceSceneGuid.IsValid()) + sourceSceneGuid = object.GetComponent().SceneGuid; + object.GetComponent().SceneGuid = targetSceneGuid; + } + for (auto& [guid, scene] : LoadedScenes_) { + for (MetaCoreId id : ids) std::erase(scene.RuntimeObjectIds, id); + if (guid == sourceSceneGuid || guid == targetSceneGuid) scene.Dirty = true; + } + auto& target = LoadedScenes_.at(targetSceneGuid); + target.RuntimeObjectIds.insert(target.RuntimeObjectIds.end(), ids.begin(), ids.end()); + RebuildObjectIndex(); + Scene_->IncrementRevision(); + return true; +} + +std::optional MetaCoreSceneWorld::CaptureSceneDocument(const MetaCoreAssetGuid& sceneGuid) const { + const auto loaded = LoadedScenes_.find(sceneGuid); + if (loaded == LoadedScenes_.end()) return std::nullopt; + MetaCoreSceneDocument result = loaded->second.Source; + result.SchemaVersion = GMetaCoreSceneSchemaVersion; + result.SceneGuid = sceneGuid; + result.GameObjects.clear(); + result.PrefabInstances.clear(); + const auto snapshot = Scene_->CaptureSnapshot(); + std::unordered_map runtimeToSerialized = loaded->second.RuntimeToSerializedId; + std::unordered_set usedSerializedIds; + for (const auto& [runtimeId, serializedId] : runtimeToSerialized) { + (void)runtimeId; + usedSerializedIds.insert(serializedId); + } + MetaCoreId nextSerializedId = 1; + for (const auto& object : snapshot.GameObjects) { + if (object.SceneGuid != sceneGuid || runtimeToSerialized.contains(object.Id)) continue; + while (usedSerializedIds.contains(nextSerializedId)) ++nextSerializedId; + runtimeToSerialized.emplace(object.Id, nextSerializedId); + usedSerializedIds.insert(nextSerializedId++); + } + for (auto object : snapshot.GameObjects) { + if (object.SceneGuid != sceneGuid) continue; + if (object.PrefabInstance && + object.PrefabInstance->PrefabInstanceRootId == object.Id && + object.PrefabInstance->InstanceGuid.IsValid()) { + MetaCorePrefabInstanceDocument instance; + instance.InstanceGuid = object.PrefabInstance->InstanceGuid; + instance.PrefabAssetGuid = object.PrefabInstance->PrefabAssetGuid; + if (object.ParentId != 0) { + const auto parent = std::find_if(snapshot.GameObjects.begin(), snapshot.GameObjects.end(), + [&](const auto& candidate) { return candidate.Id == object.ParentId; }); + if (parent != snapshot.GameObjects.end()) instance.ParentObjectGuid = parent->StableObjectGuid; + } + result.PrefabInstances.push_back(std::move(instance)); + } + object.Id = runtimeToSerialized.at(object.Id); + if (object.ParentId != 0) { + const auto parent = runtimeToSerialized.find(object.ParentId); + object.ParentId = parent == runtimeToSerialized.end() ? 0 : parent->second; + } + result.GameObjects.push_back(std::move(object)); + } + return result; +} + +MetaCoreSceneWorldSnapshot MetaCoreSceneWorld::CaptureWorldSnapshot() const { + MetaCoreSceneWorldSnapshot snapshot; + snapshot.CompositeScene = Scene_->CaptureSnapshot(); + snapshot.ActiveSceneGuid = ActiveSceneGuid_; + snapshot.Scenes.reserve(LoadedScenes_.size()); + for (const auto& [sceneGuid, loaded] : LoadedScenes_) { + if (auto document = CaptureSceneDocument(sceneGuid)) + snapshot.Scenes.push_back({std::move(*document), loaded.LoadMode, loaded.Dirty}); + } + return snapshot; +} + +void MetaCoreSceneWorld::RestoreWorldSnapshot(const MetaCoreSceneWorldSnapshot& snapshot) { + for (auto& pending : PendingLoads_) { + pending->Cancelled = true; + auto& status = LoadStatuses_[pending->RequestId]; + status.State = MetaCoreSceneLoadState::Cancelled; + status.Error = "Load request cancelled by world restore"; + } + Scene_->RestoreSnapshot(snapshot.CompositeScene); + LoadedScenes_.clear(); + for (const auto& entry : snapshot.Scenes) { + LoadedScene loaded; + loaded.Source = entry.Document; + loaded.LoadMode = entry.LoadMode; + loaded.Dirty = entry.Dirty; + std::unordered_map serializedIds; + for (const auto& object : entry.Document.GameObjects) + serializedIds[object.StableObjectGuid] = object.Id; + for (const auto object : Scene_->GetGameObjects()) { + if (!object.HasComponent()) continue; + const auto& identity = object.GetComponent(); + if (identity.SceneGuid != entry.Document.SceneGuid) continue; + loaded.RuntimeObjectIds.push_back(object.GetId()); + if (const auto serialized = serializedIds.find(identity.StableObjectGuid); serialized != serializedIds.end()) + loaded.RuntimeToSerializedId.emplace(object.GetId(), serialized->second); + } + LoadedScenes_.emplace(entry.Document.SceneGuid, std::move(loaded)); + } + ActiveSceneGuid_ = LoadedScenes_.contains(snapshot.ActiveSceneGuid) + ? snapshot.ActiveSceneGuid + : (LoadedScenes_.empty() ? MetaCoreAssetGuid{} : LoadedScenes_.begin()->first); + RebuildObjectIndex(); + RebuildHardResidency(); +} + +std::vector MetaCoreSceneWorld::ValidateDependencies(const MetaCoreSceneDocument& root) const { + std::vector issues; + std::unordered_set visited; + std::unordered_set active; + std::function visit = [&](const MetaCoreSceneDocument& document) { + if (active.contains(document.SceneGuid)) { + issues.push_back("Scene dependency cycle at " + document.SceneGuid.ToString()); + return; + } + if (!visited.insert(document.SceneGuid).second) return; + active.insert(document.SceneGuid); + std::unordered_set localDependencies; + std::vector dependencies; + for (const auto& subScene : document.SubScenes) { + if (!subScene.SceneGuid.IsValid()) { + issues.push_back("SubScene has no GUID: " + subScene.ScenePath.generic_string()); + continue; + } + if (!localDependencies.insert(subScene.SceneGuid).second) + issues.push_back("Duplicate SubScene dependency: " + subScene.SceneGuid.ToString()); + dependencies.push_back(subScene.SceneGuid); + if (subScene.LoadPolicy == MetaCoreSubSceneLoadPolicy::Distance && + (subScene.PreloadDistance < 0.0F || subScene.UnloadDistance < subScene.PreloadDistance)) + issues.push_back("Distance SubScene has invalid preload/unload thresholds: " + subScene.SceneGuid.ToString()); + } + for (const auto& object : document.GameObjects) { + if (object.PhysicsConstraint) { + const auto& reference = object.PhysicsConstraint->TargetObjectReference; + if (reference.IsValid() && reference.Strength == MetaCoreObjectReferenceStrength::Hard) + dependencies.push_back(reference.SceneGuid); + } + if (object.TimelineDirector) for (const auto& binding : object.TimelineDirector->Bindings) { + if (binding.ObjectReference.IsValid() && + binding.ObjectReference.Strength == MetaCoreObjectReferenceStrength::Hard) + dependencies.push_back(binding.ObjectReference.SceneGuid); + } + } + for (const auto& dependency : dependencies) { + if (active.contains(dependency)) { + issues.push_back("Scene dependency cycle: " + document.SceneGuid.ToString() + " -> " + dependency.ToString()); + continue; + } + if (visited.contains(dependency) || !Loader_) continue; + auto child = Loader_(dependency); + if (!child) issues.push_back("Missing scene dependency: " + dependency.ToString()); + else visit(*child); + } + active.erase(document.SceneGuid); + }; + visit(root); + return issues; +} + +void MetaCoreSceneWorld::UpdateStreaming(const std::vector& sourcePositions) { + std::vector subScenes; + for (const auto& [guid, scene] : LoadedScenes_) { + (void)guid; + subScenes.insert(subScenes.end(), scene.Source.SubScenes.begin(), scene.Source.SubScenes.end()); + } + for (const auto& subScene : subScenes) { + if (!subScene.SceneGuid.IsValid() || subScene.LoadPolicy == MetaCoreSubSceneLoadPolicy::Manual) continue; + bool near = subScene.LoadPolicy == MetaCoreSubSceneLoadPolicy::AlwaysLoaded; + bool far = !sourcePositions.empty(); + for (const auto& source : sourcePositions) { + const float distance = DistanceToBounds(source, subScene.BoundsCenter, subScene.BoundsExtent); + near = near || distance <= subScene.PreloadDistance; + far = far && distance > subScene.UnloadDistance; + } + if (near && !IsSceneLoaded(subScene.SceneGuid)) + (void)RequestLoad(subScene.SceneGuid, MetaCoreSceneLoadMode::SubScene, subScene.Priority); + else if (far && IsSceneLoaded(subScene.SceneGuid) && !HardResidentScenes_.contains(subScene.SceneGuid)) + (void)UnloadScene(subScene.SceneGuid); + } +} + +std::uint64_t MetaCoreSceneWorld::AddLifecycleListener(LifecycleListener listener) { + const std::uint64_t id = NextListenerId_++; + Listeners_.emplace(id, std::move(listener)); + return id; +} + +void MetaCoreSceneWorld::RemoveLifecycleListener(std::uint64_t listenerId) { Listeners_.erase(listenerId); } + +void MetaCoreSceneWorld::Publish(MetaCoreSceneLifecycleEvent event) { + for (const auto& [id, listener] : Listeners_) { + (void)id; + if (listener) listener(event); + } +} + +void MetaCoreSceneWorld::RebuildObjectIndex() { + ObjectIndex_.clear(); + for (const auto object : Scene_->GetGameObjects()) { + if (!object.HasComponent()) continue; + const auto& identity = object.GetComponent(); + if (identity.SceneGuid.IsValid() && identity.StableObjectGuid.IsValid()) + ObjectIndex_[{identity.SceneGuid, identity.StableObjectGuid}] = object.GetId(); + } +} + +void MetaCoreSceneWorld::RebuildHardResidency() { + HardResidentScenes_.clear(); + for (const auto object : Scene_->GetGameObjects()) { + MetaCoreAssetGuid sourceSceneGuid{}; + if (object.HasComponent()) + sourceSceneGuid = object.GetComponent().SceneGuid; + if (object.HasComponent()) { + const auto& reference = object.GetComponent().TargetObjectReference; + if (reference.IsValid() && reference.Strength == MetaCoreObjectReferenceStrength::Hard && + reference.SceneGuid != sourceSceneGuid) + HardResidentScenes_.insert(reference.SceneGuid); + } + if (object.HasComponent()) { + for (const auto& binding : object.GetComponent().Bindings) { + if (binding.ObjectReference.IsValid() && + binding.ObjectReference.Strength == MetaCoreObjectReferenceStrength::Hard && + binding.ObjectReference.SceneGuid != sourceSceneGuid) + HardResidentScenes_.insert(binding.ObjectReference.SceneGuid); + } + } + } +} + +} // namespace MetaCore diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h index 5b5d2fc..93c5829 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h @@ -4,6 +4,7 @@ #include "MetaCoreFoundation/MetaCoreId.h" #include "MetaCoreFoundation/MetaCoreReflection.h" #include "MetaCoreRendering/MetaCoreRendering.h" +#include "MetaCoreScene/MetaCoreObjectReference.h" #include #include @@ -309,6 +310,8 @@ struct MetaCoreTimelineBinding { std::string Slot{}; MC_PROPERTY() MetaCoreId ObjectId = 0; + MC_PROPERTY() + MetaCoreObjectReference ObjectReference{}; }; MC_STRUCT() @@ -467,6 +470,8 @@ struct MetaCorePhysicsConstraintComponent { MC_PROPERTY() MetaCoreId TargetObjectId = 0; MC_PROPERTY() + MetaCoreObjectReference TargetObjectReference{}; + MC_PROPERTY() glm::vec3 Anchor{0.0F}; MC_PROPERTY() glm::vec3 TargetAnchor{0.0F}; diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h index 184ac53..98d5c9c 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h @@ -25,6 +25,25 @@ struct MetaCorePrefabInstanceMetadata { MC_PROPERTY() MetaCoreId PrefabInstanceRootId = 0; + + MC_PROPERTY() + MetaCoreAssetGuid InstanceGuid{}; + + MC_PROPERTY() + MetaCoreAssetGuid PrefabNodeGuid{}; +}; + +// Runtime ownership metadata. StableObjectGuid is persisted through +// MetaCoreGameObjectData while RuntimeId remains process-local. +MC_STRUCT() +struct MetaCoreSceneObjectIdentityComponent { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid SceneGuid{}; + + MC_PROPERTY() + MetaCoreAssetGuid StableObjectGuid{}; }; // Component structs for basic properties @@ -116,6 +135,10 @@ struct MetaCoreGameObjectData { MC_PROPERTY() MetaCoreId ParentId = 0; MC_PROPERTY() + MetaCoreAssetGuid SceneGuid{}; + MC_PROPERTY() + MetaCoreAssetGuid StableObjectGuid{}; + MC_PROPERTY() std::string Name; MC_PROPERTY() MetaCoreTransformComponent Transform{}; diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreObjectReference.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreObjectReference.h new file mode 100644 index 0000000..114d285 --- /dev/null +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreObjectReference.h @@ -0,0 +1,30 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreAssetGuid.h" +#include "MetaCoreFoundation/MetaCoreReflection.h" + +namespace MetaCore { + +MC_ENUM() +enum class MetaCoreObjectReferenceStrength { + Soft = 0, + Hard +}; + +MC_STRUCT() +struct MetaCoreObjectReference { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid SceneGuid{}; + + MC_PROPERTY() + MetaCoreAssetGuid StableObjectGuid{}; + + MC_PROPERTY() + MetaCoreObjectReferenceStrength Strength = MetaCoreObjectReferenceStrength::Soft; + + [[nodiscard]] bool IsValid() const { return StableObjectGuid.IsValid(); } +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCorePrefab.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCorePrefab.h new file mode 100644 index 0000000..7998b83 --- /dev/null +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCorePrefab.h @@ -0,0 +1,64 @@ +#pragma once + +#include "MetaCoreScene/MetaCoreSceneDocument.h" + +#include +#include +#include + +namespace MetaCore { + +struct MetaCoreResolvedPrefab { + MetaCorePrefabDocument Document{}; + std::vector OrphanOverrides{}; + std::vector DependencyChain{}; +}; + +class MetaCorePrefabOverrideRegistry { +public: + using PropertyHandler = std::function; + + MetaCorePrefabOverrideRegistry(); + void RegisterPropertyHandler(std::string componentTypeId, PropertyHandler handler); + [[nodiscard]] bool Apply( + MetaCorePrefabDocument& document, + const MetaCorePrefabOverrideDocument& overrideValue + ) const; + +private: + std::unordered_map PropertyHandlers_{}; +}; + +class MetaCorePrefabResolver { +public: + using PrefabLoader = std::function(const MetaCoreAssetGuid&)>; + + explicit MetaCorePrefabResolver( + PrefabLoader loader, + MetaCorePrefabOverrideRegistry registry = MetaCorePrefabOverrideRegistry{} + ); + + [[nodiscard]] std::optional Resolve( + const MetaCoreAssetGuid& prefabGuid, + std::string* error = nullptr + ) const; + [[nodiscard]] std::optional> Instantiate( + const MetaCoreAssetGuid& prefabGuid, + const MetaCoreAssetGuid& instanceGuid, + const std::vector& instanceOverrides = {}, + std::string* error = nullptr + ) const; + [[nodiscard]] std::vector ValidateDependencies(const MetaCoreAssetGuid& prefabGuid) const; + +private: + [[nodiscard]] std::optional ResolveInternal( + const MetaCoreAssetGuid& prefabGuid, + std::vector& stack, + std::string* error + ) const; + + PrefabLoader Loader_{}; + MetaCorePrefabOverrideRegistry OverrideRegistry_{}; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h index 24cb0dc..dc4c50c 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h @@ -5,6 +5,7 @@ #include "MetaCoreFoundation/MetaCoreAssetGuid.h" #include "MetaCoreFoundation/MetaCoreAssetTypes.h" #include "MetaCoreScene/MetaCoreGameObject.h" +#include "MetaCoreScene/MetaCoreObjectReference.h" #include #include @@ -13,6 +14,125 @@ namespace MetaCore { +inline constexpr std::uint32_t GMetaCoreSceneSchemaVersion = 2U; +inline constexpr std::uint32_t GMetaCorePrefabSchemaVersion = 2U; + +MC_ENUM() +enum class MetaCoreSubSceneLoadPolicy { + AlwaysLoaded = 0, + Manual, + Distance +}; + +MC_STRUCT() +struct MetaCoreSubSceneDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid SceneGuid{}; + + MC_PROPERTY() + std::filesystem::path ScenePath{}; + + MC_PROPERTY() + MetaCoreSubSceneLoadPolicy LoadPolicy = MetaCoreSubSceneLoadPolicy::AlwaysLoaded; + + MC_PROPERTY() + glm::vec3 BoundsCenter{0.0F}; + + MC_PROPERTY() + glm::vec3 BoundsExtent{0.0F}; + + MC_PROPERTY() + glm::vec3 CellCoordinate{0.0F}; + + MC_PROPERTY() + float PreloadDistance = 100.0F; + + MC_PROPERTY() + float UnloadDistance = 125.0F; + + MC_PROPERTY() + std::int32_t Priority = 0; +}; + +MC_STRUCT() +struct MetaCoreSchemaVersionEntry { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string TypeId{}; + + MC_PROPERTY() + std::uint32_t Version = 1U; +}; + +MC_ENUM() +enum class MetaCorePrefabOverrideOperation { + SetProperty = 0, + AddComponent, + RemoveComponent, + AddChild, + RemoveChild, + Reparent +}; + +MC_STRUCT() +struct MetaCorePrefabOverrideDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid TargetNodeGuid{}; + + MC_PROPERTY() + std::string ComponentTypeId{}; + + MC_PROPERTY() + std::string PropertyPath{}; + + MC_PROPERTY() + MetaCorePrefabOverrideOperation Operation = MetaCorePrefabOverrideOperation::SetProperty; + + // Canonical JSON value. Keeping the value self-describing allows component + // serializers to migrate it without coupling prefab documents to C++ layout. + MC_PROPERTY() + std::string ValueJson{}; +}; + +MC_STRUCT() +struct MetaCoreNestedPrefabDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid MountNodeGuid{}; + + MC_PROPERTY() + MetaCoreAssetGuid PrefabAssetGuid{}; + + MC_PROPERTY() + MetaCoreAssetGuid InstanceGuid{}; + + MC_PROPERTY() + std::vector Overrides{}; +}; + +MC_STRUCT() +struct MetaCorePrefabInstanceDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid InstanceGuid{}; + + MC_PROPERTY() + MetaCoreAssetGuid PrefabAssetGuid{}; + + MC_PROPERTY() + MetaCoreAssetGuid ParentObjectGuid{}; + + MC_PROPERTY() + std::vector Overrides{}; +}; + MC_STRUCT() struct MetaCoreEditorSelectionSnapshot { MC_GENERATED_BODY() @@ -69,6 +189,12 @@ MC_STRUCT() struct MetaCoreSceneDocument { MC_GENERATED_BODY() + MC_PROPERTY() + std::uint32_t SchemaVersion = GMetaCoreSceneSchemaVersion; + + MC_PROPERTY() + MetaCoreAssetGuid SceneGuid{}; + MC_PROPERTY() std::string Name{}; @@ -78,6 +204,15 @@ struct MetaCoreSceneDocument { MC_PROPERTY() std::vector GameObjects{}; + MC_PROPERTY() + std::vector SubScenes{}; + + MC_PROPERTY() + std::vector PrefabInstances{}; + + MC_PROPERTY() + std::vector ComponentVersions{}; + MC_PROPERTY() MetaCoreEditorSelectionSnapshot Selection{}; }; @@ -86,11 +221,29 @@ MC_STRUCT() struct MetaCorePrefabDocument { MC_GENERATED_BODY() + MC_PROPERTY() + std::uint32_t SchemaVersion = GMetaCorePrefabSchemaVersion; + + MC_PROPERTY() + MetaCoreAssetGuid PrefabGuid{}; + + MC_PROPERTY() + MetaCoreAssetGuid BasePrefabGuid{}; + MC_PROPERTY() std::string Name{}; MC_PROPERTY() std::vector GameObjects{}; + + MC_PROPERTY() + std::vector NestedPrefabs{}; + + MC_PROPERTY() + std::vector Overrides{}; + + MC_PROPERTY() + std::vector ComponentVersions{}; }; MC_ENUM() diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneWorld.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneWorld.h new file mode 100644 index 0000000..5f7819f --- /dev/null +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneWorld.h @@ -0,0 +1,175 @@ +#pragma once + +#include "MetaCoreScene/MetaCoreScene.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +enum class MetaCoreSceneLoadMode { + Replace = 0, + Additive, + SubScene +}; + +enum class MetaCoreSceneLoadState { + Unloaded = 0, + Loading, + Loaded, + Failed, + Cancelled +}; + +enum class MetaCoreSceneLifecycleEventType { + BeforeUnload = 0, + AfterUnload, + AfterLoad +}; + +struct MetaCoreSceneLifecycleEvent { + MetaCoreSceneLifecycleEventType Type = MetaCoreSceneLifecycleEventType::AfterLoad; + MetaCoreAssetGuid SceneGuid{}; + std::vector ObjectIds{}; +}; + +struct MetaCoreSceneLoadStatus { + std::uint64_t RequestId = 0; + MetaCoreAssetGuid SceneGuid{}; + MetaCoreSceneLoadState State = MetaCoreSceneLoadState::Unloaded; + std::string Error{}; +}; + +struct MetaCoreSceneWorldSettings { + double MainThreadCommitBudgetMilliseconds = 2.0; + std::size_t MemoryBudgetBytes = 512U * 1024U * 1024U; +}; + +struct MetaCoreLoadedSceneInfo { + MetaCoreAssetGuid SceneGuid{}; + std::string Name{}; + MetaCoreSceneLoadMode LoadMode = MetaCoreSceneLoadMode::Additive; + bool Active = false; + bool Dirty = false; + bool HardResident = false; + std::vector RuntimeObjectIds{}; +}; + +struct MetaCoreSceneWorldSnapshotEntry { + MetaCoreSceneDocument Document{}; + MetaCoreSceneLoadMode LoadMode = MetaCoreSceneLoadMode::Additive; + bool Dirty = false; +}; + +struct MetaCoreSceneWorldSnapshot { + MetaCoreSceneSnapshot CompositeScene{}; + MetaCoreAssetGuid ActiveSceneGuid{}; + std::vector Scenes{}; +}; + +class MetaCoreSceneWorld { +public: + using SceneLoader = std::function(const MetaCoreAssetGuid&)>; + using LifecycleListener = std::function; + + explicit MetaCoreSceneWorld(SceneLoader loader = {}, MetaCoreSceneWorldSettings settings = {}); + MetaCoreSceneWorld(MetaCoreScene& scene, SceneLoader loader = {}, MetaCoreSceneWorldSettings settings = {}); + ~MetaCoreSceneWorld(); + + MetaCoreSceneWorld(const MetaCoreSceneWorld&) = delete; + MetaCoreSceneWorld& operator=(const MetaCoreSceneWorld&) = delete; + + [[nodiscard]] MetaCoreScene& GetScene() { return *Scene_; } + [[nodiscard]] const MetaCoreScene& GetScene() const { return *Scene_; } + + void SetSceneLoader(SceneLoader loader); + [[nodiscard]] std::uint64_t RequestLoad( + const MetaCoreAssetGuid& sceneGuid, + MetaCoreSceneLoadMode mode = MetaCoreSceneLoadMode::Additive, + std::int32_t priority = 0 + ); + [[nodiscard]] bool CancelLoad(std::uint64_t requestId); + void Tick(); + void UpdateStreaming(const std::vector& sourcePositions); + + [[nodiscard]] bool LoadDocumentNow( + MetaCoreSceneDocument document, + MetaCoreSceneLoadMode mode = MetaCoreSceneLoadMode::Additive, + std::string* error = nullptr + ); + [[nodiscard]] bool UnloadScene(const MetaCoreAssetGuid& sceneGuid, bool force = false); + [[nodiscard]] bool SetActiveScene(const MetaCoreAssetGuid& sceneGuid); + [[nodiscard]] MetaCoreAssetGuid GetActiveSceneGuid() const { return ActiveSceneGuid_; } + [[nodiscard]] bool IsSceneLoaded(const MetaCoreAssetGuid& sceneGuid) const; + [[nodiscard]] std::vector GetLoadedScenes() const; + [[nodiscard]] std::optional GetLoadStatus(std::uint64_t requestId) const; + + [[nodiscard]] MetaCoreGameObject ResolveObject(const MetaCoreObjectReference& reference); + [[nodiscard]] MetaCoreGameObject ResolveObject(const MetaCoreObjectReference& reference) const; + [[nodiscard]] MetaCoreObjectReference BuildObjectReference( + MetaCoreId runtimeObjectId, + MetaCoreObjectReferenceStrength strength = MetaCoreObjectReferenceStrength::Soft + ) const; + + [[nodiscard]] bool MoveSubtreeToScene(MetaCoreId rootObjectId, const MetaCoreAssetGuid& targetSceneGuid); + [[nodiscard]] std::optional CaptureSceneDocument(const MetaCoreAssetGuid& sceneGuid) const; + [[nodiscard]] MetaCoreSceneWorldSnapshot CaptureWorldSnapshot() const; + void RestoreWorldSnapshot(const MetaCoreSceneWorldSnapshot& snapshot); + [[nodiscard]] std::vector ValidateDependencies(const MetaCoreSceneDocument& root) const; + + std::uint64_t AddLifecycleListener(LifecycleListener listener); + void RemoveLifecycleListener(std::uint64_t listenerId); + +private: + struct ObjectKey { + MetaCoreAssetGuid SceneGuid{}; + MetaCoreAssetGuid ObjectGuid{}; + bool operator==(const ObjectKey&) const = default; + }; + struct ObjectKeyHasher { + std::size_t operator()(const ObjectKey& value) const noexcept; + }; + struct LoadedScene { + MetaCoreSceneDocument Source{}; + MetaCoreSceneLoadMode LoadMode = MetaCoreSceneLoadMode::Additive; + std::vector RuntimeObjectIds{}; + std::unordered_map RuntimeToSerializedId{}; + bool Dirty = false; + }; + struct PendingLoad; + + [[nodiscard]] bool CommitDocument(MetaCoreSceneDocument document, MetaCoreSceneLoadMode mode, std::string* error); + void Publish(MetaCoreSceneLifecycleEvent event); + void RebuildObjectIndex(); + void RebuildHardResidency(); + + SceneLoader Loader_{}; + MetaCoreSceneWorldSettings Settings_{}; + std::unique_ptr OwnedScene_{}; + MetaCoreScene* Scene_ = nullptr; + MetaCoreAssetGuid ActiveSceneGuid_{}; + std::unordered_map LoadedScenes_{}; + std::unordered_map ObjectIndex_{}; + std::unordered_set HardResidentScenes_{}; + std::vector> PendingLoads_{}; + std::unordered_map LoadStatuses_{}; + std::unordered_map Listeners_{}; + std::uint64_t NextRequestId_ = 1; + std::uint64_t NextListenerId_ = 1; +}; + +[[nodiscard]] MetaCoreAssetGuid MetaCoreBuildDeterministicGuid( + const MetaCoreAssetGuid& namespaceGuid, + const MetaCoreAssetGuid& localGuid +); +[[nodiscard]] MetaCoreAssetGuid MetaCoreBuildDeterministicGuid( + const MetaCoreAssetGuid& namespaceGuid, + std::uint64_t localValue +); + +} // namespace MetaCore diff --git a/docs/designs/metacore-mature-engine-gap-and-work-plan.md b/docs/designs/metacore-mature-engine-gap-and-work-plan.md index 2db764f..f11fc98 100644 --- a/docs/designs/metacore-mature-engine-gap-and-work-plan.md +++ b/docs/designs/metacore-mature-engine-gap-and-work-plan.md @@ -359,14 +359,25 @@ R8.1–R8.5 已落地的工程边界: ### 10. Scene 与 Prefab -需要补齐: +已完成: -- [ ] 多场景编辑、Additive Load、SubScene 和异步流式加载。 -- [ ] 大场景分区、按需加载和场景依赖检查。 -- [ ] 嵌套 Prefab、Prefab Variant 和实例同步。 -- [ ] 细粒度 Override 记录、可视化、Apply 和 Revert。 -- [ ] 跨场景对象引用规则。 -- [ ] Scene、Component、Prefab 的格式版本迁移机制。 +- [x] 多场景编辑、Additive Load、SubScene 和异步流式加载。 +- [x] 大场景分区、按需加载和场景依赖检查。 +- [x] 嵌套 Prefab、Prefab Variant 和实例同步。 +- [x] 细粒度 Override 记录、可视化、Apply 和 Revert。 +- [x] 跨场景对象引用规则。 +- [x] Scene、Component、Prefab 的格式版本迁移机制。 + +实现与验收基线(2026-07-27): + +- 新增 `MetaCoreSceneWorld`,在一个组合 ECS 上管理 Replace/Additive/SubScene、活动 Scene、Scene Set 快照、异步请求去重/取消、优先级、距离迟滞、Hard 驻留、生命周期事件和限定对象引用;Play/Stop 会恢复完整 Scene Set、活动 Scene、对象 ID、选择与 Dirty 状态。 +- Scene/Prefab Schema 升级到版本 2,持久化 Scene/Prefab/对象/Prefab 节点 GUID、SubScene Cell 元数据、组件版本、Prefab Base/嵌套关系和属性/组件/结构 Override。旧文档缺失 GUID 时使用确定性迁移,高版本文档拒绝加载,Cooked Package 同步写入 `CustomVersions` 与依赖表。 +- Prefab 解析顺序固定为 Base → Variant → 嵌套 Prefab → Scene Instance Override;循环与缺失依赖拒绝解析,孤立 Override 可诊断,相同 Instance GUID 重复展开得到相同稳定对象 GUID。Override 注册表覆盖属性、组件增删、子对象增删和重挂接,编辑器继续提供字段级 Apply/Revert 与 Override 标记。 +- Editor Hierarchy 按 Scene 分组并提供 Set Active、Unload、Save All 和 Additive Open;新对象归属活动 Scene,跨 Scene 直接父子关系被拒绝,跨场景移动通过 World 事务完成。根 Scene Environment 不被活动 Scene 覆盖。 +- Cook 从启动 Scene 遍历 SubScene、Hard Scene 引用和 Prefab Base/Variant/嵌套依赖,缺失或循环在打包阶段失败;运行时清单记录 Scene GUID、包路径、Hash 和依赖,Player 与 Editor 共用 `MetaCoreSceneWorld` 的 AlwaysLoaded/Distance 流式规则。 +- 新增 `MetaCoreSceneTests` 与 `MetaCoreSceneBenchmark`。Linux x86_64 Development 全量 CTest **14/14** 通过;10,000 对象基准实测 load **35.36–37.10 ms**、capture p95 **8.99–12.17 ms**、100 节点 Prefab 展开 p95 **0.027–0.031 ms**,持续门禁按签收上界约 120% 设置为 **45 ms / 15 ms / 0.04 ms**。 + +首版边界保持为显式 SubScene 加可选网格 Cell,不做编辑器自动世界切分;正式关闭平台为 Linux x86_64,Windows 仅保留可编译接口。 ### 11. 编辑器生产力与插件边界 diff --git a/tests/MetaCoreDeliveryTests.cpp b/tests/MetaCoreDeliveryTests.cpp index 79686e1..7e57b20 100644 --- a/tests/MetaCoreDeliveryTests.cpp +++ b/tests/MetaCoreDeliveryTests.cpp @@ -52,6 +52,12 @@ int main() { delivery.BuildId = "test-build"; delivery.StartupScene = "payload.bin"; delivery.ScriptModules.push_back("Project/Scripts/TestScripts.so"); + MetaCore::MetaCoreRuntimeDeliveryDocument::SceneEntry sceneEntry; + sceneEntry.SceneGuid = MetaCore::MetaCoreAssetGuid::Generate(); + sceneEntry.PackagePath = "Content/Scenes/root.mcscene"; + sceneEntry.SourceHash = 42; + sceneEntry.Dependencies.push_back(MetaCore::MetaCoreAssetGuid::Generate()); + delivery.ScenePackages.push_back(sceneEntry); WriteText(package / delivery.ScriptModules.front(), "script"); Expect(MetaCore::MetaCoreWriteRuntimeDeliveryDocument(package / "Project" / "MetaCore.runtimeproject", delivery), "write runtime delivery"); MetaCore::MetaCoreReleaseManifestDocument manifest; @@ -67,6 +73,10 @@ int main() { Expect(MetaCore::MetaCoreValidateReleasePackage(package).empty(), "valid package accepted"); const auto readDelivery = MetaCore::MetaCoreReadRuntimeDeliveryDocument(package / "Project" / "MetaCore.runtimeproject"); Expect(readDelivery.has_value() && readDelivery->ScriptModules == delivery.ScriptModules, "runtime delivery preserves script module paths"); + Expect(readDelivery && readDelivery->ScenePackages.size() == 1 && + readDelivery->ScenePackages[0].SceneGuid == sceneEntry.SceneGuid && + readDelivery->ScenePackages[0].Dependencies == sceneEntry.Dependencies, + "runtime delivery preserves scene dependency manifest"); WriteText(package / "payload.bin", "tampered"); const auto corruptIssues = MetaCore::MetaCoreValidateReleasePackage(package); Expect(!corruptIssues.empty(), "corrupt package rejected"); diff --git a/tests/MetaCoreSceneBenchmark.cpp b/tests/MetaCoreSceneBenchmark.cpp new file mode 100644 index 0000000..7dfb2a0 --- /dev/null +++ b/tests/MetaCoreSceneBenchmark.cpp @@ -0,0 +1,81 @@ +#include "MetaCoreScene/MetaCorePrefab.h" +#include "MetaCoreScene/MetaCoreSceneWorld.h" + +#include +#include +#include +#include +#include + +namespace { + +double MillisecondsSince(std::chrono::steady_clock::time_point begin) { + return std::chrono::duration(std::chrono::steady_clock::now() - begin).count(); +} + +} // namespace + +int main() { + using namespace MetaCore; + + MetaCoreSceneDocument largeScene; + largeScene.SceneGuid = MetaCoreAssetGuid::Generate(); + largeScene.Name = "SceneBenchmark"; + constexpr std::size_t objectCount = 10000; + largeScene.GameObjects.reserve(objectCount); + for (std::size_t index = 0; index < objectCount; ++index) { + MetaCoreGameObjectData object; + object.Id = index + 1; + object.ParentId = index == 0 ? 0 : (index / 10) + 1; + if (object.ParentId == object.Id) object.ParentId = 0; + object.SceneGuid = largeScene.SceneGuid; + object.StableObjectGuid = MetaCoreBuildDeterministicGuid(largeScene.SceneGuid, object.Id); + object.Name = "Object_" + std::to_string(index); + largeScene.GameObjects.push_back(std::move(object)); + } + + MetaCoreSceneWorld world; + const auto loadBegin = std::chrono::steady_clock::now(); + if (!world.LoadDocumentNow(largeScene, MetaCoreSceneLoadMode::Replace)) return EXIT_FAILURE; + const double loadMilliseconds = MillisecondsSince(loadBegin); + + std::vector captureSamples; + captureSamples.reserve(40); + for (int sample = 0; sample < 40; ++sample) { + const auto begin = std::chrono::steady_clock::now(); + if (!world.CaptureSceneDocument(largeScene.SceneGuid)) return EXIT_FAILURE; + captureSamples.push_back(MillisecondsSince(begin)); + } + std::sort(captureSamples.begin(), captureSamples.end()); + const double captureP95 = captureSamples[static_cast(captureSamples.size() * 0.95)]; + + MetaCorePrefabDocument prefab; + prefab.PrefabGuid = MetaCoreAssetGuid::Generate(); + prefab.Name = "BenchmarkPrefab"; + prefab.GameObjects.assign(largeScene.GameObjects.begin(), largeScene.GameObjects.begin() + 100); + MetaCorePrefabResolver resolver([&](const MetaCoreAssetGuid& guid) -> std::optional { + return guid == prefab.PrefabGuid ? std::optional(prefab) : std::nullopt; + }); + std::vector prefabSamples; + prefabSamples.reserve(100); + for (int sample = 0; sample < 100; ++sample) { + const auto begin = std::chrono::steady_clock::now(); + if (!resolver.Instantiate(prefab.PrefabGuid, MetaCoreAssetGuid::Generate())) return EXIT_FAILURE; + prefabSamples.push_back(MillisecondsSince(begin)); + } + std::sort(prefabSamples.begin(), prefabSamples.end()); + const double prefabP95 = prefabSamples[static_cast(prefabSamples.size() * 0.95)]; + + std::cout << "SceneBenchmark objects=" << objectCount + << " load_ms=" << loadMilliseconds + << " capture_p95_ms=" << captureP95 + << " prefab100_p95_ms=" << prefabP95 << '\n'; + // Linux x86_64 signed baseline (2026-07-27): load 37.10 ms, + // capture p95 12.17 ms, prefab(100 nodes) p95 0.031 ms. Gates are + // rounded up from 120% to avoid sub-microsecond timer noise. + if (loadMilliseconds > 45.0 || captureP95 > 15.0 || prefabP95 > 0.04) { + std::cerr << "Scene/Prefab benchmark exceeded Linux x86_64 regression gate\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/tests/MetaCoreSceneTests.cpp b/tests/MetaCoreSceneTests.cpp new file mode 100644 index 0000000..4c0447d --- /dev/null +++ b/tests/MetaCoreSceneTests.cpp @@ -0,0 +1,306 @@ +#include "MetaCoreScene/MetaCorePrefab.h" +#include "MetaCoreScene/MetaCoreScenePackage.h" +#include "MetaCoreScene/MetaCoreSceneSerializer.h" +#include "MetaCoreScene/MetaCoreSceneWorld.h" +#include "MetaCoreFoundation/MetaCorePackage.h" + +#include +#include +#include +#include +#include + +namespace { + +using namespace MetaCore; + +void Expect(bool condition, const char* message) { + if (!condition) { + std::cerr << "FAILED: " << message << '\n'; + std::exit(1); + } +} + +MetaCoreSceneDocument MakeScene(const char* name, std::size_t objectCount = 1) { + MetaCoreSceneDocument document; + document.SceneGuid = MetaCoreAssetGuid::Generate(); + document.Name = name; + for (std::size_t index = 0; index < objectCount; ++index) { + MetaCoreGameObjectData object; + object.Id = index + 1; + object.ParentId = index == 0 ? 0 : index; + object.SceneGuid = document.SceneGuid; + object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.SceneGuid, object.Id); + object.Name = std::string(name) + std::to_string(index); + document.GameObjects.push_back(std::move(object)); + } + return document; +} + +void TickUntilSettled(MetaCoreSceneWorld& world, std::uint64_t request) { + for (int iteration = 0; iteration < 1000; ++iteration) { + world.Tick(); + const auto status = world.GetLoadStatus(request); + if (status && status->State != MetaCoreSceneLoadState::Loading) return; + std::this_thread::yield(); + } + Expect(false, "asynchronous scene request did not settle"); +} + +void TestSceneJsonAndPackageVersions() { + const auto root = std::filesystem::temp_directory_path() / "MetaCoreSceneTests"; + std::filesystem::create_directories(root); + const auto jsonPath = root / "Versioned.mcscene.json"; + const auto packagePath = root / "Versioned.mcscene"; + auto document = MakeScene("Versioned", 2); + MetaCoreSubSceneDocument sub; + sub.SceneGuid = MetaCoreAssetGuid::Generate(); + sub.ScenePath = "Scenes/Cell.mcscene.json"; + sub.LoadPolicy = MetaCoreSubSceneLoadPolicy::Distance; + sub.PreloadDistance = 25.0F; + sub.UnloadDistance = 30.0F; + document.SubScenes.push_back(sub); + document.ComponentVersions.push_back({"Transform", 2}); + + const auto registry = MetaCoreBuildScenePackageTypeRegistry(); + Expect(MetaCoreSceneSerializer::SaveSceneToJson(jsonPath, document, registry), "versioned scene JSON save"); + const auto jsonLoaded = MetaCoreSceneSerializer::LoadSceneFromJson(jsonPath, registry); + Expect(jsonLoaded && jsonLoaded->SchemaVersion == GMetaCoreSceneSchemaVersion, "scene JSON version round trip"); + Expect(jsonLoaded->SceneGuid == document.SceneGuid, "scene GUID round trip"); + Expect(jsonLoaded->GameObjects[1].StableObjectGuid == document.GameObjects[1].StableObjectGuid, "stable object GUID round trip"); + Expect(jsonLoaded->SubScenes.size() == 1 && jsonLoaded->SubScenes[0].SceneGuid == sub.SceneGuid, "SubScene JSON round trip"); + + MetaCorePrefabDocument prefab; + prefab.Name = "VersionedPrefab"; + prefab.PrefabGuid = MetaCoreAssetGuid::Generate(); + prefab.GameObjects = document.GameObjects; + prefab.GameObjects[0].SceneGuid = {}; + prefab.GameObjects[1].SceneGuid = {}; + prefab.Overrides.push_back({ + prefab.GameObjects[0].StableObjectGuid, "Name", "Value", + MetaCorePrefabOverrideOperation::SetProperty, "\"Renamed\"" + }); + const auto prefabPath = root / "Versioned.mcprefab.json"; + Expect(MetaCoreSceneSerializer::SavePrefabToJson(prefabPath, prefab, registry), "versioned prefab JSON save"); + const auto prefabLoaded = MetaCoreSceneSerializer::LoadPrefabFromJson(prefabPath, registry); + Expect(prefabLoaded && prefabLoaded->SchemaVersion == GMetaCorePrefabSchemaVersion, + "prefab JSON version round trip"); + Expect(prefabLoaded->PrefabGuid == prefab.PrefabGuid && prefabLoaded->Overrides.size() == 1, + "prefab GUID and override round trip"); + + MetaCoreSceneDocument legacy; + legacy.Name = "LegacyDeterministic"; + legacy.GameObjects.push_back(MetaCoreGameObjectData{}); + legacy.GameObjects[0].Id = 42; + const auto legacyFirstPath = root / "LegacyFirst.mcscene.json"; + const auto legacySecondPath = root / "LegacySecond.mcscene.json"; + Expect(MetaCoreSceneSerializer::SaveSceneToJson(legacyFirstPath, legacy, registry), "legacy scene first migration save"); + Expect(MetaCoreSceneSerializer::SaveSceneToJson(legacySecondPath, legacy, registry), "legacy scene second migration save"); + const auto legacyFirst = MetaCoreSceneSerializer::LoadSceneFromJson(legacyFirstPath, registry); + const auto legacySecond = MetaCoreSceneSerializer::LoadSceneFromJson(legacySecondPath, registry); + Expect(legacyFirst && legacySecond && legacyFirst->SceneGuid == legacySecond->SceneGuid, + "legacy scene GUID migration is deterministic"); + Expect(legacyFirst->GameObjects[0].StableObjectGuid == legacySecond->GameObjects[0].StableObjectGuid, + "legacy object GUID migration is deterministic"); + + Expect(MetaCoreWriteScenePackage(packagePath, document), "versioned scene package save"); + const auto packageLoaded = MetaCoreReadScenePackage(packagePath); + Expect(packageLoaded && packageLoaded->SceneGuid == document.SceneGuid, "scene package round trip"); + const auto package = MetaCoreReadPackageFile(packagePath, registry); + Expect(package && package->Header.PackageGuid == document.SceneGuid, "scene package uses scene GUID"); + Expect(package->Dependencies.size() == 1 && package->Dependencies[0].AssetGuid == sub.SceneGuid, "scene package dependency table"); + Expect(!package->CustomVersions.empty() && package->CustomVersions[0].Key == "MetaCore.Scene", "scene package custom version"); +} + +void TestAdditiveWorldAndQualifiedReferences() { + auto first = MakeScene("Root", 2); + auto second = MakeScene("Cell", 1); + first.GameObjects[0].PhysicsConstraint = MetaCorePhysicsConstraintComponent{}; + first.GameObjects[0].PhysicsConstraint->TargetObjectReference = { + second.SceneGuid, + second.GameObjects[0].StableObjectGuid, + MetaCoreObjectReferenceStrength::Hard + }; + + std::unordered_map documents; + documents.emplace(second.SceneGuid, second); + MetaCoreSceneWorld world([&](const MetaCoreAssetGuid& guid) -> std::optional { + const auto iterator = documents.find(guid); + return iterator == documents.end() ? std::nullopt : std::optional(iterator->second); + }); + + Expect(world.LoadDocumentNow(first, MetaCoreSceneLoadMode::Replace), "replace load root scene"); + const auto sourceRoot = world.ResolveObject({ + first.SceneGuid, first.GameObjects[0].StableObjectGuid, MetaCoreObjectReferenceStrength::Soft + }); + Expect(sourceRoot, "source root resolves before additive load"); + Expect(world.GetLoadedScenes().size() == 1 && world.GetScene().GetGameObjects().size() == 2, "root scene object count"); + const auto requestStatusesBefore = world.GetLoadedScenes(); + (void)requestStatusesBefore; + for (int iteration = 0; iteration < 1000 && !world.IsSceneLoaded(second.SceneGuid); ++iteration) { + world.Tick(); + std::this_thread::yield(); + } + Expect(world.IsSceneLoaded(second.SceneGuid), "hard reference loads target scene"); + Expect(world.GetScene().GetGameObjects().size() == 3, "additive objects share composite ECS"); + Expect(!world.UnloadScene(second.SceneGuid), "hard referenced scene cannot unload"); + + const auto targetReference = MetaCoreObjectReference{ + second.SceneGuid, + second.GameObjects[0].StableObjectGuid, + MetaCoreObjectReferenceStrength::Soft + }; + const auto target = world.ResolveObject(targetReference); + Expect(target, "qualified object reference resolves"); + const auto rebuilt = world.BuildObjectReference(target.GetId()); + Expect(rebuilt.SceneGuid == second.SceneGuid && rebuilt.StableObjectGuid == targetReference.StableObjectGuid, + "runtime object builds stable qualified reference"); + + const auto captured = world.CaptureSceneDocument(first.SceneGuid); + Expect(captured && captured->GameObjects.size() == 2, "capture filters one loaded scene"); + Expect(world.SetActiveScene(second.SceneGuid) && world.GetActiveSceneGuid() == second.SceneGuid, "active additive scene"); + Expect(world.MoveSubtreeToScene(sourceRoot.GetId(), second.SceneGuid), "move subtree across scenes"); + const auto movedCapture = world.CaptureSceneDocument(second.SceneGuid); + Expect(movedCapture && movedCapture->GameObjects.size() == 3, "move subtree updates target scene ownership"); + const auto worldSnapshot = world.CaptureWorldSnapshot(); + Expect(world.UnloadScene(first.SceneGuid, true), "hard reference source can force unload"); + Expect(world.UnloadScene(second.SceneGuid), "hard residency releases after reference source unloads"); + world.RestoreWorldSnapshot(worldSnapshot); + Expect(world.GetLoadedScenes().size() == 2 && world.GetScene().GetGameObjects().size() == 3, + "world snapshot restores complete scene set"); + Expect(world.GetActiveSceneGuid() == second.SceneGuid, "world snapshot restores active scene"); +} + +void TestAsynchronousLoadCancelAndStreaming() { + auto root = MakeScene("StreamingRoot"); + auto cell = MakeScene("StreamingCell"); + MetaCoreSubSceneDocument sub; + sub.SceneGuid = cell.SceneGuid; + sub.LoadPolicy = MetaCoreSubSceneLoadPolicy::Distance; + sub.BoundsCenter = {100.0F, 0.0F, 0.0F}; + sub.BoundsExtent = {5.0F, 5.0F, 5.0F}; + sub.PreloadDistance = 10.0F; + sub.UnloadDistance = 20.0F; + root.SubScenes.push_back(sub); + + MetaCoreSceneWorld world([cell](const MetaCoreAssetGuid& guid) -> std::optional { + return guid == cell.SceneGuid ? std::optional(cell) : std::nullopt; + }); + Expect(world.LoadDocumentNow(root, MetaCoreSceneLoadMode::Replace), "streaming root load"); + world.UpdateStreaming({glm::vec3(90.0F, 0.0F, 0.0F)}); + for (int iteration = 0; iteration < 1000 && !world.IsSceneLoaded(cell.SceneGuid); ++iteration) { + world.Tick(); + std::this_thread::yield(); + } + Expect(world.IsSceneLoaded(cell.SceneGuid), "distance cell loads near threshold"); + world.UpdateStreaming({glm::vec3(111.0F, 0.0F, 0.0F)}); + Expect(world.IsSceneLoaded(cell.SceneGuid), "distance hysteresis keeps cell before unload threshold"); + world.UpdateStreaming({glm::vec3(130.0F, 0.0F, 0.0F)}); + Expect(!world.IsSceneLoaded(cell.SceneGuid), "distance cell unloads beyond threshold"); + + const auto request = world.RequestLoad(cell.SceneGuid); + const auto duplicateRequest = world.RequestLoad(cell.SceneGuid); + Expect(duplicateRequest == request, "duplicate asynchronous scene requests are coalesced"); + Expect(world.CancelLoad(request), "pending asynchronous load cancels"); + TickUntilSettled(world, request); + Expect(world.GetLoadStatus(request)->State == MetaCoreSceneLoadState::Cancelled, "cancelled request remains cancelled"); +} + +void TestSceneDependencyValidation() { + auto root = MakeScene("DependencyRoot"); + auto child = MakeScene("DependencyChild"); + root.SubScenes.push_back({child.SceneGuid}); + child.SubScenes.push_back({root.SceneGuid}); + std::unordered_map documents{ + {root.SceneGuid, root}, {child.SceneGuid, child} + }; + MetaCoreSceneWorld world([&](const MetaCoreAssetGuid& guid) -> std::optional { + const auto iterator = documents.find(guid); + return iterator == documents.end() ? std::nullopt : std::optional(iterator->second); + }); + const auto issues = world.ValidateDependencies(root); + Expect(std::any_of(issues.begin(), issues.end(), [](const std::string& issue) { + return issue.find("cycle") != std::string::npos; + }), "recursive SubScene cycle is rejected"); +} + +void TestPrefabVariantNestedAndOverrides() { + MetaCorePrefabDocument base; + base.PrefabGuid = MetaCoreAssetGuid::Generate(); + base.Name = "Base"; + MetaCoreGameObjectData root; + root.Id = 1; + root.StableObjectGuid = MetaCoreBuildDeterministicGuid(base.PrefabGuid, 1); + root.Name = "BaseRoot"; + base.GameObjects.push_back(root); + + MetaCorePrefabDocument nested; + nested.PrefabGuid = MetaCoreAssetGuid::Generate(); + nested.Name = "Nested"; + MetaCoreGameObjectData nestedRoot; + nestedRoot.Id = 1; + nestedRoot.StableObjectGuid = MetaCoreBuildDeterministicGuid(nested.PrefabGuid, 1); + nestedRoot.Name = "NestedRoot"; + nested.GameObjects.push_back(nestedRoot); + + MetaCorePrefabDocument variant; + variant.PrefabGuid = MetaCoreAssetGuid::Generate(); + variant.BasePrefabGuid = base.PrefabGuid; + variant.Name = "Variant"; + variant.Overrides.push_back({ + root.StableObjectGuid, "Transform", "Position", MetaCorePrefabOverrideOperation::SetProperty, "[1,2,3]" + }); + MetaCoreNestedPrefabDocument nestedInstance; + nestedInstance.MountNodeGuid = root.StableObjectGuid; + nestedInstance.PrefabAssetGuid = nested.PrefabGuid; + nestedInstance.InstanceGuid = MetaCoreAssetGuid::Generate(); + variant.NestedPrefabs.push_back(nestedInstance); + + std::unordered_map prefabs{ + {base.PrefabGuid, base}, {nested.PrefabGuid, nested}, {variant.PrefabGuid, variant} + }; + MetaCorePrefabResolver resolver([&](const MetaCoreAssetGuid& guid) -> std::optional { + const auto iterator = prefabs.find(guid); + return iterator == prefabs.end() ? std::nullopt : std::optional(iterator->second); + }); + + const auto resolved = resolver.Resolve(variant.PrefabGuid); + Expect(resolved && resolved->Document.GameObjects.size() == 2, "variant resolves base and nested prefab"); + Expect(resolved->Document.GameObjects[0].Transform.Position == glm::vec3(1.0F, 2.0F, 3.0F), "variant property override"); + Expect(resolved->Document.GameObjects[1].ParentId == resolved->Document.GameObjects[0].Id, "nested prefab mounts below target node"); + + const auto instanceGuid = MetaCoreAssetGuid::Generate(); + const auto firstInstance = resolver.Instantiate(variant.PrefabGuid, instanceGuid); + const auto secondInstance = resolver.Instantiate(variant.PrefabGuid, instanceGuid); + Expect(firstInstance && secondInstance && firstInstance->size() == secondInstance->size(), "prefab instantiate succeeds"); + for (std::size_t index = 0; index < firstInstance->size(); ++index) + Expect((*firstInstance)[index].StableObjectGuid == (*secondInstance)[index].StableObjectGuid, + "same instance GUID yields stable prefab object GUIDs"); + + const std::vector structuralOverrides{ + {root.StableObjectGuid, "Camera", "", MetaCorePrefabOverrideOperation::AddComponent, "{}"}, + {root.StableObjectGuid, "", "", MetaCorePrefabOverrideOperation::AddChild, + R"({"Name":"InstanceChild","Position":[4,5,6]})"} + }; + const auto structuralInstance = resolver.Instantiate( + variant.PrefabGuid, MetaCoreAssetGuid::Generate(), structuralOverrides + ); + Expect(structuralInstance && structuralInstance->size() == 3, + "instance structural override adds a child"); + Expect((*structuralInstance)[0].Camera.has_value(), "instance component override adds a component"); + + prefabs[base.PrefabGuid].BasePrefabGuid = variant.PrefabGuid; + Expect(!resolver.Resolve(variant.PrefabGuid), "prefab inheritance cycle rejected"); +} + +} // namespace + +int main() { + TestSceneJsonAndPackageVersions(); + TestAdditiveWorldAndQualifiedReferences(); + TestAsynchronousLoadCancelAndStreaming(); + TestSceneDependencyValidation(); + TestPrefabVariantNestedAndOverrides(); + std::cout << "MetaCoreSceneTests passed\n"; + return 0; +} diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index f9c756a..acbc0c3 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -25,6 +25,7 @@ #include "MetaCoreScene/MetaCoreTransformUtils.h" #include "MetaCoreFoundation/MetaCoreHash.h" #include "../Source/MetaCoreEditor/Private/MetaCoreGltfImporter.h" +#include "../Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.h" #include #include @@ -1429,6 +1430,10 @@ void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() { for (const auto& renderable : snapshot.Renderables) { if (renderable.ObjectId == root.GetId()) { MetaCoreExpect(renderable.Visible == true, "RenderSync 根对象 Visible 应为 true"); + MetaCoreExpect( + MetaCore::MetaCoreUsesNativeGltfVisibility(renderable), + "导入 glTF 应跳过 ECS 单位代理包围盒,交由 gltfio 使用真实几何包围盒剔除" + ); MetaCoreExpectVec3Near(renderable.BaseColor, glm::vec3(0.5F, 0.6F, 0.7F), "RenderSync 根对象 BaseColor 应正确"); MetaCoreExpect(std::abs(renderable.Metallic - 0.35F) < 0.0001F, "RenderSync 根对象 Metallic 应正确"); MetaCoreExpect(std::abs(renderable.Roughness - 0.45F) < 0.0001F, "RenderSync 根对象 Roughness 应正确"); @@ -1442,6 +1447,10 @@ void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() { MetaCoreExpect(renderable.EmissiveTexturePath == "Assets/Textures/SyncEmissive.png", "RenderSync 根对象自发光贴图路径应正确"); MetaCoreExpect(renderable.AoTexturePath == "Assets/Textures/SyncAo.png", "RenderSync 根对象 AO 贴图路径应正确"); } else if (renderable.ObjectId == child.GetId()) { + MetaCoreExpect( + !MetaCore::MetaCoreUsesNativeGltfVisibility(renderable), + "普通内置网格仍应使用引擎可见性系统" + ); MetaCoreExpect(renderable.Visible == false, "RenderSync 子对象 Visible 应为 false"); MetaCoreExpectVec3Near(renderable.BaseColor, glm::vec3(0.1F, 0.2F, 0.3F), "RenderSync 子对象 BaseColor 应正确"); } @@ -1770,8 +1779,8 @@ void MetaCoreTestViewportPickingUsesMeshTrianglesBeforeHierarchyDepth() { shellMesh.IndexCount = 6; shellMesh.PayloadIndex = 1; shellMesh.PayloadSize = makePlanePayload(2.0F).size(); - shellMesh.BoundingBoxMin = glm::vec3(-2.0F, -2.0F, 0.0F); - shellMesh.BoundingBoxMax = glm::vec3(2.0F, 2.0F, 0.0F); + shellMesh.BoundingBoxMin = glm::vec3(-2000.0F, -100.0F, -1000.0F); + shellMesh.BoundingBoxMax = glm::vec3(2000.0F, 100.0F, 1000.0F); document.GeneratedMeshAssets.push_back(shellMesh); MetaCore::MetaCoreMeshAssetDocument interiorMesh; @@ -1841,6 +1850,12 @@ void MetaCoreTestViewportPickingUsesMeshTrianglesBeforeHierarchyDepth() { ); MetaCoreExpect(pickedObjectId == shellObject.GetId(), "三角面拾取应选择可见的外壳,而不是层级更深的内部部件"); + editorContext.SetSelectedObjectId(shellObject.GetId()); + interactionService.FocusSelectedObject(editorContext); + const auto focusedView = editorContext.GetCameraController().BuildSceneView(); + const float focusedDistance = glm::length(focusedView.CameraPosition - focusedView.CameraTarget); + MetaCoreExpect(focusedDistance > 5000.0F, "模型聚焦应采用资源真实包围盒,而不是根节点单位 Scale"); + MetaCoreExpect(focusedView.FarClip > focusedDistance + 2000.0F, "模型聚焦应扩展 Far Clip 覆盖完整资源包围盒"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); @@ -5493,6 +5508,16 @@ void MetaCoreTestParticleRenderableGrowthAndRemoval() { } // namespace +void MetaCoreTestLargeBoundsEditorCameraFraming() { + MetaCore::MetaCoreEditorCameraController camera; + camera.FocusBounds(glm::vec3(625.0F, -510.0F, 46.0F), 1600.0F); + const MetaCore::MetaCoreSceneView view = camera.BuildSceneView(); + const float distance = glm::length(view.CameraPosition - view.CameraTarget); + MetaCoreExpect(distance > 5000.0F, "大模型聚焦距离不应被 100 单位上限截断"); + MetaCoreExpect(view.FarClip > distance + 1600.0F, "大模型应完整位于动态 Far Clip 内"); + MetaCoreExpect(view.NearClip >= 0.01F && view.NearClip <= 1.0F, "动态 Near Clip 应保持有效范围"); +} + int main() { std::setvbuf(stdout, nullptr, _IONBF, 0); std::setvbuf(stderr, nullptr, _IONBF, 0); @@ -5587,6 +5612,8 @@ int main() { MetaCoreTestCameraComponentJsonRoundTripAndLegacyDefaults(); std::cout << "[RUN] MetaCoreTestCameraSelectionAndProjectionUtilities..." << std::endl; MetaCoreTestCameraSelectionAndProjectionUtilities(); + std::cout << "[RUN] MetaCoreTestLargeBoundsEditorCameraFraming..." << std::endl; + MetaCoreTestLargeBoundsEditorCameraFraming(); std::cout << "[RUN] MetaCoreTestViewportPickingPrefersModelChildNode..." << std::endl; MetaCoreTestViewportPickingPrefersModelChildNode(); std::cout << "[RUN] MetaCoreTestViewportPickingUsesMeshTrianglesBeforeHierarchyDepth..." << std::endl;