场景加载优化
This commit is contained in:
parent
ddeba5bbbc
commit
31a408d813
@ -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 <filesystem>
|
||||
@ -30,6 +31,7 @@
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@ -450,10 +452,50 @@ int main(int argc, char* argv[]) {
|
||||
startupSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(projectPath);
|
||||
}
|
||||
MetaCore::MetaCoreScene scene;
|
||||
std::unordered_map<MetaCore::MetaCoreAssetGuid, std::filesystem::path, MetaCore::MetaCoreAssetGuidHasher> 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<MetaCore::MetaCoreSceneDocument> {
|
||||
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::size_t>(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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 <sstream>
|
||||
#include <system_error>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#if defined(__linux__)
|
||||
#include <sys/utsname.h>
|
||||
#endif
|
||||
@ -562,6 +564,20 @@ std::optional<MetaCoreRuntimeDeliveryDocument> MetaCoreReadRuntimeDeliveryDocume
|
||||
if (module.is_string()) document.ScriptModules.emplace_back(module.get<std::string>());
|
||||
}
|
||||
}
|
||||
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<std::string>())) 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<MetaCoreRuntimeDeliveryDocument> 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<MetaCoreSceneDocument> startupSceneDocument;
|
||||
std::vector<MetaCoreRuntimeDeliveryDocument::SceneEntry> 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<MetaCoreAssetGuid, std::filesystem::path, MetaCoreAssetGuidHasher> 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<MetaCoreAssetGuid, MetaCorePrefabDocument, MetaCoreAssetGuidHasher> 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<MetaCorePrefabDocument> {
|
||||
const auto iterator = prefabDocuments.find(guid);
|
||||
return iterator == prefabDocuments.end() ? std::nullopt : std::optional(iterator->second);
|
||||
}
|
||||
);
|
||||
|
||||
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> visitedScenes;
|
||||
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> activeScenes;
|
||||
std::string sceneDependencyError;
|
||||
std::function<bool(
|
||||
const MetaCoreSceneDocument&,
|
||||
const std::filesystem::path&,
|
||||
const std::filesystem::path&
|
||||
)> 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<MetaCoreAssetGuid> 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);
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "MetaCoreFoundation/MetaCoreAssetGuid.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
@ -62,6 +64,13 @@ struct MetaCoreRuntimeDeliveryDocument {
|
||||
std::filesystem::path UiManifest = "Content/Runtime/Ui.mcruntime";
|
||||
std::filesystem::path ReleaseManifest = "Manifest/MetaCore.release.json";
|
||||
std::vector<std::filesystem::path> ScriptModules{};
|
||||
struct SceneEntry {
|
||||
MetaCoreAssetGuid SceneGuid{};
|
||||
std::filesystem::path PackagePath{};
|
||||
std::uint64_t SourceHash = 0;
|
||||
std::vector<MetaCoreAssetGuid> Dependencies{};
|
||||
};
|
||||
std::vector<SceneEntry> ScenePackages{};
|
||||
};
|
||||
|
||||
struct MetaCoreReleaseFileEntry {
|
||||
|
||||
@ -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 <nlohmann/json.hpp>
|
||||
|
||||
#include <imgui.h>
|
||||
@ -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<MetaCoreSceneDocument> {
|
||||
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<MetaCoreLoadedSceneInfo> GetOpenScenes(const MetaCoreEditorContext& editorContext) const override {
|
||||
const MetaCoreSceneWorld* world = editorContext.GetSceneWorld();
|
||||
return world == nullptr ? std::vector<MetaCoreLoadedSceneInfo>{} : world->GetLoadedScenes();
|
||||
}
|
||||
|
||||
void UpdateDirtyState(const MetaCoreSceneSnapshot& snapshot) override {
|
||||
if (ReflectionRegistry_ == nullptr) {
|
||||
SetDirtyState(true);
|
||||
@ -10111,6 +10204,7 @@ private:
|
||||
std::string CurrentSceneName_{};
|
||||
std::vector<std::byte> SavedSnapshotBytes_{};
|
||||
bool Dirty_ = false;
|
||||
std::unordered_map<MetaCoreAssetGuid, std::filesystem::path, MetaCoreAssetGuidHasher> 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<std::filesystem::path> MetaCoreBuiltinPrefabService::CreatePrefabF
|
||||
}
|
||||
|
||||
MetaCorePrefabDocument prefabDocument;
|
||||
prefabDocument.PrefabGuid = prefabGuid;
|
||||
prefabDocument.Name = rootObject ? rootObject.GetName() : normalizedPrefabPath.stem().string();
|
||||
|
||||
std::vector<MetaCoreGameObjectData> prefabDataObjects;
|
||||
@ -11844,6 +11954,7 @@ std::optional<std::filesystem::path> 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<MetaCoreTransformComponent>();
|
||||
@ -11902,6 +12013,10 @@ std::optional<MetaCoreId> 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<MetaCoreId, MetaCoreId> idMap;
|
||||
std::vector<MetaCoreGameObject> clonedObjects;
|
||||
@ -11925,6 +12040,17 @@ std::optional<MetaCoreId> MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen
|
||||
}
|
||||
|
||||
clonedObject.GetComponent<MetaCoreTransformComponent>() = sourceObject.Transform;
|
||||
if (targetSceneGuid.IsValid()) {
|
||||
const MetaCoreAssetGuid nodeGuid = sourceObject.StableObjectGuid.IsValid()
|
||||
? sourceObject.StableObjectGuid
|
||||
: MetaCoreBuildDeterministicGuid(prefabAssetGuid, sourceObject.Id);
|
||||
clonedObject.AddComponent<MetaCoreSceneObjectIdentityComponent>(
|
||||
MetaCoreSceneObjectIdentityComponent{
|
||||
targetSceneGuid,
|
||||
MetaCoreBuildDeterministicGuid(instanceGuid, nodeGuid)
|
||||
}
|
||||
);
|
||||
}
|
||||
if (sourceObject.Camera.has_value()) clonedObject.AddComponent<MetaCoreCameraComponent>(*sourceObject.Camera);
|
||||
if (sourceObject.Light.has_value()) clonedObject.AddComponent<MetaCoreLightComponent>(*sourceObject.Light);
|
||||
if (sourceObject.Rotator.has_value()) clonedObject.AddComponent<MetaCoreRotatorComponent>(*sourceObject.Rotator);
|
||||
@ -11942,11 +12068,14 @@ std::optional<MetaCoreId> MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen
|
||||
if (sourceObject.DecalProjector.has_value()) clonedObject.AddComponent<MetaCoreDecalProjectorComponent>(*sourceObject.DecalProjector);
|
||||
if (sourceObject.Terrain.has_value()) clonedObject.AddComponent<MetaCoreTerrainComponent>(*sourceObject.Terrain);
|
||||
|
||||
clonedObject.AddComponent<MetaCorePrefabInstanceMetadata>(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<MetaCorePrefabInstanceMetadata>(metadata);
|
||||
}
|
||||
|
||||
for (MetaCoreGameObject& clonedObject : clonedObjects) {
|
||||
@ -11981,7 +12110,23 @@ std::optional<MetaCoreId> 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<MetaCoreId> MetaCoreBuiltinSceneEditingService::CreateGameObject(
|
||||
}
|
||||
|
||||
MetaCoreGameObject object = editorContext.GetScene().CreateGameObject(objectName, parentId);
|
||||
if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && world->GetActiveSceneGuid().IsValid()) {
|
||||
object.AddComponent<MetaCoreSceneObjectIdentityComponent>(MetaCoreSceneObjectIdentityComponent{
|
||||
world->GetActiveSceneGuid(), MetaCoreAssetGuid::Generate()
|
||||
});
|
||||
}
|
||||
if (withMeshRenderer) {
|
||||
auto& meshRenderer = object.AddComponent<MetaCoreMeshRendererComponent>(MetaCoreMeshRendererComponent{});
|
||||
if (objectName == "Plane") {
|
||||
@ -12251,6 +12401,11 @@ std::optional<MetaCoreId> MetaCoreBuiltinSceneEditingService::CreateCamera(
|
||||
}
|
||||
|
||||
MetaCoreGameObject cameraObject = editorContext.GetScene().CreateGameObject("Camera", parentId);
|
||||
if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && world->GetActiveSceneGuid().IsValid()) {
|
||||
cameraObject.AddComponent<MetaCoreSceneObjectIdentityComponent>(MetaCoreSceneObjectIdentityComponent{
|
||||
world->GetActiveSceneGuid(), MetaCoreAssetGuid::Generate()
|
||||
});
|
||||
}
|
||||
MetaCoreCameraComponent& camera = cameraObject.AddComponent<MetaCoreCameraComponent>(MetaCoreCameraComponent{});
|
||||
camera.IsPrimary = !hasEnabledPrimaryCamera;
|
||||
|
||||
@ -12388,6 +12543,11 @@ std::optional<MetaCoreId> 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>(MetaCoreSceneObjectIdentityComponent{
|
||||
world->GetActiveSceneGuid(), MetaCoreAssetGuid::Generate()
|
||||
});
|
||||
}
|
||||
instantiatedRootId = modelRootObject.GetId();
|
||||
|
||||
std::vector<MetaCoreId> nodeToIdMap(importedDocument->Nodes.size(), 0);
|
||||
@ -12479,6 +12639,11 @@ std::optional<MetaCoreId> 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>(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<MetaCoreSceneObjectIdentityComponent>()) {
|
||||
targetSceneGuid = parent.GetComponent<MetaCoreSceneObjectIdentityComponent>().SceneGuid;
|
||||
}
|
||||
}
|
||||
for (MetaCoreId selectedId : selectedIds) {
|
||||
const auto selected = editorContext.GetScene().FindGameObject(selectedId);
|
||||
if (!selected || !selected.HasComponent<MetaCoreSceneObjectIdentityComponent>()) {
|
||||
continue;
|
||||
}
|
||||
const auto selectedScene = selected.GetComponent<MetaCoreSceneObjectIdentityComponent>().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);
|
||||
|
||||
@ -2829,23 +2829,48 @@ static std::array<char, 256> 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<MetaCoreLoadedSceneInfo>{};
|
||||
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<void*>(static_cast<intptr_t>(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<MetaCoreSceneObjectIdentityComponent>()) continue;
|
||||
if (object.GetComponent<MetaCoreSceneObjectIdentityComponent>().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" &&
|
||||
|
||||
@ -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<glm::vec3> streamingSources;
|
||||
for (const auto object : Scene_.GetGameObjects()) {
|
||||
if (object.HasComponent<MetaCoreCameraComponent>() &&
|
||||
object.GetComponent<MetaCoreCameraComponent>().Enabled &&
|
||||
object.GetComponent<MetaCoreCameraComponent>().IsPrimary) {
|
||||
streamingSources.push_back(object.GetComponent<MetaCoreTransformComponent>().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)
|
||||
|
||||
@ -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<MetaCoreTransformComponent>().Position;
|
||||
const float scaleRadius = std::max({gameObject.GetComponent<MetaCoreTransformComponent>().Scale.x, gameObject.GetComponent<MetaCoreTransformComponent>().Scale.y, gameObject.GetComponent<MetaCoreTransformComponent>().Scale.z});
|
||||
const auto& scale = gameObject.GetComponent<MetaCoreTransformComponent>().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;
|
||||
}
|
||||
|
||||
|
||||
@ -31,6 +31,7 @@ private:
|
||||
float FieldOfViewDegrees_ = 60.0F;
|
||||
float MoveSpeed_ = 6.0F;
|
||||
float MoveSpeedBoost_ = 4.0F;
|
||||
float FramingRadius_ = 10.0F;
|
||||
};
|
||||
|
||||
} // namespace MetaCore
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
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<MetaCoreId>& objectIds,
|
||||
glm::vec3& boundsMin,
|
||||
glm::vec3& boundsMax
|
||||
) {
|
||||
const auto assetEditingService =
|
||||
editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetEditingService>();
|
||||
std::unordered_map<MetaCoreAssetGuid, std::optional<MetaCoreModelAssetDocument>, MetaCoreAssetGuidHasher>
|
||||
modelAssetCache;
|
||||
bool initialized = false;
|
||||
for (const auto& object : editorContext.GetScene().GetGameObjects()) {
|
||||
if (!objectIds.empty() && !objectIds.contains(object.GetId())) continue;
|
||||
if (!object.HasComponent<MetaCoreMeshRendererComponent>()) continue;
|
||||
const auto& renderer = object.GetComponent<MetaCoreMeshRendererComponent>();
|
||||
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<MetaCoreId> 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<MetaCoreTransformComponent>().Scale.x),
|
||||
std::abs(selectedObject.GetComponent<MetaCoreTransformComponent>().Scale.y),
|
||||
std::abs(selectedObject.GetComponent<MetaCoreTransformComponent>().Scale.z),
|
||||
0.5F
|
||||
});
|
||||
const glm::vec3 objectMin = selectedObject.GetComponent<MetaCoreTransformComponent>().Position - glm::vec3(extent);
|
||||
const glm::vec3 objectMax = selectedObject.GetComponent<MetaCoreTransformComponent>().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<MetaCoreMeshRendererComponent>() && !gameObject.GetComponent<MetaCoreMeshRendererComponent>().Visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float extent = std::max({
|
||||
std::abs(gameObject.GetComponent<MetaCoreTransformComponent>().Scale.x),
|
||||
std::abs(gameObject.GetComponent<MetaCoreTransformComponent>().Scale.y),
|
||||
std::abs(gameObject.GetComponent<MetaCoreTransformComponent>().Scale.z),
|
||||
0.5F
|
||||
});
|
||||
const glm::vec3 objectMin = gameObject.GetComponent<MetaCoreTransformComponent>().Position - glm::vec3(extent);
|
||||
const glm::vec3 objectMax = gameObject.GetComponent<MetaCoreTransformComponent>().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);
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
|
||||
#include "MetaCoreRender/MetaCoreRenderDevice.h"
|
||||
#include "MetaCoreScene/MetaCoreScene.h"
|
||||
#include "MetaCoreScene/MetaCoreSceneWorld.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
@ -38,6 +39,7 @@ private:
|
||||
MetaCoreRenderDevice RenderDevice_{};
|
||||
MetaCoreEditorViewportRenderer ViewportRenderer_{};
|
||||
MetaCoreScene Scene_{};
|
||||
MetaCoreSceneWorld SceneWorld_{Scene_};
|
||||
MetaCoreLogService LogService_{};
|
||||
MetaCoreEditorModuleRegistry ModuleRegistry_{};
|
||||
MetaCoreSceneInteractionService SceneInteractionService_{};
|
||||
|
||||
@ -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<MetaCoreEditorCameraController> CameraController_;
|
||||
|
||||
@ -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 <cstddef>
|
||||
@ -310,8 +311,13 @@ public:
|
||||
[[nodiscard]] virtual std::vector<std::filesystem::path> 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<MetaCoreLoadedSceneInfo> GetOpenScenes(const MetaCoreEditorContext& editorContext) const = 0;
|
||||
virtual void UpdateDirtyState(const MetaCoreSceneSnapshot& snapshot) = 0;
|
||||
};
|
||||
|
||||
|
||||
@ -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<MetaCoreBvhItem> 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<MetaCoreId> 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<float> softwareDepth(static_cast<std::size_t>(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::uint32_t>(std::clamp(bounds.x,0.0F,1.0F)*hzbWidth)),maxX=std::min(hzbWidth-1U,static_cast<std::uint32_t>(std::clamp(bounds.z,0.0F,1.0F)*hzbWidth));const std::uint32_t minY=std::min(hzbHeight-1U,static_cast<std::uint32_t>(std::clamp(bounds.y,0.0F,1.0F)*hzbHeight)),maxY=std::min(hzbHeight-1U,static_cast<std::uint32_t>(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<std::size_t>(y)*hzbWidth+x]=std::min(softwareDepth[static_cast<std::size_t>(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::uint32_t>(std::clamp(bounds.x,0.0F,1.0F)*hzbWidth)),maxX=std::min(hzbWidth-1U,static_cast<std::uint32_t>(std::clamp(bounds.z,0.0F,1.0F)*hzbWidth));const std::uint32_t minY=std::min(hzbHeight-1U,static_cast<std::uint32_t>(std::clamp(bounds.y,0.0F,1.0F)*hzbHeight)),maxY=std::min(hzbHeight-1U,static_cast<std::uint32_t>(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<std::size_t>(y)*hzbWidth+x]=std::min(softwareDepth[static_cast<std::size_t>(y)*hzbWidth+x],farthestDepth);}
|
||||
(void)PreviousHzb_.Build(hzbWidth,hzbHeight,softwareDepth);LastFrameVisibleObjects_=std::move(currentVisible);PreviousVisibilityCameraPosition_=CurrentSceneView_.CameraPosition;PreviousVisibilityCameraDirection_=cameraDirection;HasPreviousVisibilityCamera_=true;
|
||||
std::vector<MetaCoreInstanceCandidate> instanceCandidates;
|
||||
instanceCandidates.reserve(snapshot.Renderables.size());
|
||||
|
||||
@ -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;
|
||||
|
||||
388
Source/MetaCoreScene/Private/MetaCorePrefab.cpp
Normal file
388
Source/MetaCoreScene/Private/MetaCorePrefab.cpp
Normal file
@ -0,0 +1,388 @@
|
||||
#include "MetaCoreScene/MetaCorePrefab.h"
|
||||
|
||||
#include "MetaCoreScene/MetaCoreSceneWorld.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <unordered_set>
|
||||
|
||||
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<float>(), parsed[1].get<float>(), parsed[2].get<float>()};
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool ReadScalar(std::string_view value, T& output) {
|
||||
try {
|
||||
output = json::parse(value).get<T>();
|
||||
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<MetaCoreId> 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<MetaCoreResolvedPrefab> MetaCorePrefabResolver::Resolve(
|
||||
const MetaCoreAssetGuid& prefabGuid,
|
||||
std::string* error
|
||||
) const {
|
||||
std::vector<MetaCoreAssetGuid> stack;
|
||||
return ResolveInternal(prefabGuid, stack, error);
|
||||
}
|
||||
|
||||
std::optional<MetaCoreResolvedPrefab> MetaCorePrefabResolver::ResolveInternal(
|
||||
const MetaCoreAssetGuid& prefabGuid,
|
||||
std::vector<MetaCoreAssetGuid>& 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<MetaCoreId, MetaCoreId> 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<std::vector<MetaCoreGameObjectData>> MetaCorePrefabResolver::Instantiate(
|
||||
const MetaCoreAssetGuid& prefabGuid,
|
||||
const MetaCoreAssetGuid& instanceGuid,
|
||||
const std::vector<MetaCorePrefabOverrideDocument>& 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<MetaCoreId, MetaCoreId> 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<std::string> MetaCorePrefabResolver::ValidateDependencies(const MetaCoreAssetGuid& prefabGuid) const {
|
||||
std::string error;
|
||||
const auto result = Resolve(prefabGuid, &error);
|
||||
std::vector<std::string> 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
|
||||
@ -465,6 +465,11 @@ MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const {
|
||||
MetaCoreGameObjectData data;
|
||||
data.Id = obj.GetId();
|
||||
data.ParentId = obj.GetParentId();
|
||||
if (obj.HasComponent<MetaCoreSceneObjectIdentityComponent>()) {
|
||||
const auto& identity = obj.GetComponent<MetaCoreSceneObjectIdentityComponent>();
|
||||
data.SceneGuid = identity.SceneGuid;
|
||||
data.StableObjectGuid = identity.StableObjectGuid;
|
||||
}
|
||||
data.Name = obj.GetName();
|
||||
data.Transform = obj.GetComponent<MetaCoreTransformComponent>();
|
||||
|
||||
@ -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<MetaCoreTransformComponent>() = data.Transform;
|
||||
if (data.SceneGuid.IsValid() || data.StableObjectGuid.IsValid()) {
|
||||
obj.AddComponent<MetaCoreSceneObjectIdentityComponent>(
|
||||
MetaCoreSceneObjectIdentityComponent{data.SceneGuid, data.StableObjectGuid}
|
||||
);
|
||||
}
|
||||
|
||||
if (data.Camera.has_value())
|
||||
obj.AddComponent<MetaCoreCameraComponent>(data.Camera.value());
|
||||
|
||||
@ -15,7 +15,8 @@ namespace {
|
||||
[[nodiscard]] MetaCorePackageDocument MetaCoreBuildTypedScenePackage(
|
||||
const MetaCoreAssetGuid& assetGuid,
|
||||
const std::string& objectName,
|
||||
std::vector<std::byte> payload
|
||||
std::vector<std::byte> payload,
|
||||
const MetaCoreSceneDocument& scene
|
||||
) {
|
||||
MetaCorePackageDocument document;
|
||||
document.Header.PackageType = MetaCorePackageType::Scene;
|
||||
@ -29,6 +30,17 @@ namespace {
|
||||
0,
|
||||
static_cast<std::uint64_t>(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<MetaCoreSceneDocument> MetaCoreReadScenePackage(const std::filesys
|
||||
if (!package.has_value() || package->Header.PackageType != MetaCorePackageType::Scene) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return MetaCoreReadTypedPayload<MetaCoreSceneDocument>(*package, registry, "MetaCoreSceneDocument");
|
||||
auto scene = MetaCoreReadTypedPayload<MetaCoreSceneDocument>(*package, registry, "MetaCoreSceneDocument");
|
||||
if (!scene || scene->SchemaVersion > GMetaCoreSceneSchemaVersion) return std::nullopt;
|
||||
return scene;
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> MetaCoreResolveStartupScenePackagePath(const std::filesystem::path& projectFilePath) {
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
|
||||
#include "MetaCoreFoundation/MetaCoreReflection.h"
|
||||
#include "MetaCoreScene/MetaCoreSceneWorld.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
|
||||
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<int>(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<MetaCoreObjectReferenceStrength>(value.value("Strength", 0));
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
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<int>(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<MetaCoreId>();
|
||||
} else if (objField.Name == "ParentId" && objJson.contains("ParentId")) {
|
||||
obj.ParentId = objJson["ParentId"].get<MetaCoreId>();
|
||||
} else if (objField.Name == "SceneGuid" && objJson.contains("SceneGuid")) {
|
||||
obj.SceneGuid = StringToGuid(objJson["SceneGuid"].get<std::string>());
|
||||
} else if (objField.Name == "StableObjectGuid" && objJson.contains("StableObjectGuid")) {
|
||||
obj.StableObjectGuid = StringToGuid(objJson["StableObjectGuid"].get<std::string>());
|
||||
} else if (objField.Name == "Name" && objJson.contains("Name")) {
|
||||
obj.Name = objJson["Name"].get<std::string>();
|
||||
} 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<MetaCorePhysicsConstraintType>(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<std::string>());
|
||||
else if (prefabField.Name == "PrefabObjectId" && prefabJson.contains("PrefabObjectId")) prefab.PrefabObjectId = prefabJson["PrefabObjectId"].get<MetaCoreId>();
|
||||
else if (prefabField.Name == "PrefabInstanceRootId" && prefabJson.contains("PrefabInstanceRootId")) prefab.PrefabInstanceRootId = prefabJson["PrefabInstanceRootId"].get<MetaCoreId>();
|
||||
else if (prefabField.Name == "InstanceGuid" && prefabJson.contains("InstanceGuid")) prefab.InstanceGuid = StringToGuid(prefabJson["InstanceGuid"].get<std::string>());
|
||||
else if (prefabField.Name == "PrefabNodeGuid" && prefabJson.contains("PrefabNodeGuid")) prefab.PrefabNodeGuid = StringToGuid(prefabJson["PrefabNodeGuid"].get<std::string>());
|
||||
}
|
||||
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<int>(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<MetaCorePrefabOverrideOperation>(value.value("Operation", 0));
|
||||
result.ValueJson = value.value("ValueJson", std::string{});
|
||||
return result;
|
||||
}
|
||||
|
||||
static json SchemaVersionsToJson(const std::vector<MetaCoreSchemaVersionEntry>& versions) {
|
||||
json result = json::array();
|
||||
for (const auto& version : versions) {
|
||||
result.push_back({{"TypeId", version.TypeId}, {"Version", version.Version}});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::vector<MetaCoreSchemaVersionEntry> JsonToSchemaVersions(const json& values) {
|
||||
std::vector<MetaCoreSchemaVersionEntry> 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<std::uint8_t>(first >> (index * 8U));
|
||||
result.Bytes[index + 8] = static_cast<std::uint8_t>(second >> (index * 8U));
|
||||
}
|
||||
result.Bytes[6] = static_cast<std::uint8_t>((result.Bytes[6] & 0x0FU) | 0x50U);
|
||||
result.Bytes[8] = static_cast<std::uint8_t>((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<MetaCoreId, MetaCoreAssetGuid> 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<MetaCoreSceneDocument>();
|
||||
@ -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<MetaCoreEditorSelectionSnapshot>();
|
||||
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<int>(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<MetaCoreSceneDocument> 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<std::string>());
|
||||
|
||||
// 验证 MetaCoreSceneDocument 反射注册
|
||||
const MetaCoreStructDescriptor* sceneDesc = registry.FindStruct<MetaCoreSceneDocument>();
|
||||
@ -703,9 +886,35 @@ std::optional<MetaCoreSceneDocument> 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<MetaCoreSubSceneLoadPolicy>(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<MetaCorePrefabDocument> 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<std::string>();
|
||||
}
|
||||
@ -760,6 +993,17 @@ std::optional<MetaCorePrefabDocument> 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;
|
||||
|
||||
565
Source/MetaCoreScene/Private/MetaCoreSceneWorld.cpp
Normal file
565
Source/MetaCoreScene/Private/MetaCoreSceneWorld.cpp
Normal file
@ -0,0 +1,565 @@
|
||||
#include "MetaCoreScene/MetaCoreSceneWorld.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <thread>
|
||||
|
||||
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<MetaCoreId, MetaCoreId>& 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<std::optional<MetaCoreSceneDocument>> 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<std::uint8_t>((first >> (index * 8U)) & 0xFFU);
|
||||
result.Bytes[index + 8] = static_cast<std::uint8_t>((second >> (index * 8U)) & 0xFFU);
|
||||
}
|
||||
result.Bytes[6] = static_cast<std::uint8_t>((result.Bytes[6] & 0x0FU) | 0x50U);
|
||||
result.Bytes[8] = static_cast<std::uint8_t>((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<std::uint8_t>((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<MetaCoreScene>()), 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<PendingLoad>();
|
||||
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<MetaCoreSceneDocument> 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<MetaCoreAssetGuid> 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<MetaCoreId, MetaCoreId> 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<MetaCorePhysicsConstraintComponent>()) {
|
||||
auto& constraint = object.GetComponent<MetaCorePhysicsConstraintComponent>();
|
||||
if (constraint.TargetObjectReference.IsValid()) {
|
||||
const auto target = ResolveObject(constraint.TargetObjectReference);
|
||||
constraint.TargetObjectId = target ? target.GetId() : 0;
|
||||
}
|
||||
}
|
||||
if (object.HasComponent<MetaCoreTimelineDirectorComponent>()) {
|
||||
for (auto& binding : object.GetComponent<MetaCoreTimelineDirectorComponent>().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<MetaCoreLoadedSceneInfo> MetaCoreSceneWorld::GetLoadedScenes() const {
|
||||
std::vector<MetaCoreLoadedSceneInfo> 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<MetaCoreSceneLoadStatus> 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<MetaCoreSceneObjectIdentityComponent>()) return {};
|
||||
const auto& identity = object.GetComponent<MetaCoreSceneObjectIdentityComponent>();
|
||||
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<MetaCoreSceneObjectIdentityComponent>())
|
||||
object.AddComponent<MetaCoreSceneObjectIdentityComponent>();
|
||||
if (!sourceSceneGuid.IsValid())
|
||||
sourceSceneGuid = object.GetComponent<MetaCoreSceneObjectIdentityComponent>().SceneGuid;
|
||||
object.GetComponent<MetaCoreSceneObjectIdentityComponent>().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<MetaCoreSceneDocument> 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<MetaCoreId, MetaCoreId> runtimeToSerialized = loaded->second.RuntimeToSerializedId;
|
||||
std::unordered_set<MetaCoreId> 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<MetaCoreAssetGuid, MetaCoreId, MetaCoreAssetGuidHasher> serializedIds;
|
||||
for (const auto& object : entry.Document.GameObjects)
|
||||
serializedIds[object.StableObjectGuid] = object.Id;
|
||||
for (const auto object : Scene_->GetGameObjects()) {
|
||||
if (!object.HasComponent<MetaCoreSceneObjectIdentityComponent>()) continue;
|
||||
const auto& identity = object.GetComponent<MetaCoreSceneObjectIdentityComponent>();
|
||||
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<std::string> MetaCoreSceneWorld::ValidateDependencies(const MetaCoreSceneDocument& root) const {
|
||||
std::vector<std::string> issues;
|
||||
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> visited;
|
||||
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> active;
|
||||
std::function<void(const MetaCoreSceneDocument&)> 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<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> localDependencies;
|
||||
std::vector<MetaCoreAssetGuid> 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<glm::vec3>& sourcePositions) {
|
||||
std::vector<MetaCoreSubSceneDocument> 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<MetaCoreSceneObjectIdentityComponent>()) continue;
|
||||
const auto& identity = object.GetComponent<MetaCoreSceneObjectIdentityComponent>();
|
||||
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<MetaCoreSceneObjectIdentityComponent>())
|
||||
sourceSceneGuid = object.GetComponent<MetaCoreSceneObjectIdentityComponent>().SceneGuid;
|
||||
if (object.HasComponent<MetaCorePhysicsConstraintComponent>()) {
|
||||
const auto& reference = object.GetComponent<MetaCorePhysicsConstraintComponent>().TargetObjectReference;
|
||||
if (reference.IsValid() && reference.Strength == MetaCoreObjectReferenceStrength::Hard &&
|
||||
reference.SceneGuid != sourceSceneGuid)
|
||||
HardResidentScenes_.insert(reference.SceneGuid);
|
||||
}
|
||||
if (object.HasComponent<MetaCoreTimelineDirectorComponent>()) {
|
||||
for (const auto& binding : object.GetComponent<MetaCoreTimelineDirectorComponent>().Bindings) {
|
||||
if (binding.ObjectReference.IsValid() &&
|
||||
binding.ObjectReference.Strength == MetaCoreObjectReferenceStrength::Hard &&
|
||||
binding.ObjectReference.SceneGuid != sourceSceneGuid)
|
||||
HardResidentScenes_.insert(binding.ObjectReference.SceneGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace MetaCore
|
||||
@ -4,6 +4,7 @@
|
||||
#include "MetaCoreFoundation/MetaCoreId.h"
|
||||
#include "MetaCoreFoundation/MetaCoreReflection.h"
|
||||
#include "MetaCoreRendering/MetaCoreRendering.h"
|
||||
#include "MetaCoreScene/MetaCoreObjectReference.h"
|
||||
|
||||
#include <glm/vec3.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
@ -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};
|
||||
|
||||
@ -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{};
|
||||
|
||||
@ -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
|
||||
64
Source/MetaCoreScene/Public/MetaCoreScene/MetaCorePrefab.h
Normal file
64
Source/MetaCoreScene/Public/MetaCoreScene/MetaCorePrefab.h
Normal file
@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "MetaCoreScene/MetaCoreSceneDocument.h"
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace MetaCore {
|
||||
|
||||
struct MetaCoreResolvedPrefab {
|
||||
MetaCorePrefabDocument Document{};
|
||||
std::vector<MetaCorePrefabOverrideDocument> OrphanOverrides{};
|
||||
std::vector<MetaCoreAssetGuid> DependencyChain{};
|
||||
};
|
||||
|
||||
class MetaCorePrefabOverrideRegistry {
|
||||
public:
|
||||
using PropertyHandler = std::function<bool(MetaCoreGameObjectData&, std::string_view, std::string_view)>;
|
||||
|
||||
MetaCorePrefabOverrideRegistry();
|
||||
void RegisterPropertyHandler(std::string componentTypeId, PropertyHandler handler);
|
||||
[[nodiscard]] bool Apply(
|
||||
MetaCorePrefabDocument& document,
|
||||
const MetaCorePrefabOverrideDocument& overrideValue
|
||||
) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, PropertyHandler> PropertyHandlers_{};
|
||||
};
|
||||
|
||||
class MetaCorePrefabResolver {
|
||||
public:
|
||||
using PrefabLoader = std::function<std::optional<MetaCorePrefabDocument>(const MetaCoreAssetGuid&)>;
|
||||
|
||||
explicit MetaCorePrefabResolver(
|
||||
PrefabLoader loader,
|
||||
MetaCorePrefabOverrideRegistry registry = MetaCorePrefabOverrideRegistry{}
|
||||
);
|
||||
|
||||
[[nodiscard]] std::optional<MetaCoreResolvedPrefab> Resolve(
|
||||
const MetaCoreAssetGuid& prefabGuid,
|
||||
std::string* error = nullptr
|
||||
) const;
|
||||
[[nodiscard]] std::optional<std::vector<MetaCoreGameObjectData>> Instantiate(
|
||||
const MetaCoreAssetGuid& prefabGuid,
|
||||
const MetaCoreAssetGuid& instanceGuid,
|
||||
const std::vector<MetaCorePrefabOverrideDocument>& instanceOverrides = {},
|
||||
std::string* error = nullptr
|
||||
) const;
|
||||
[[nodiscard]] std::vector<std::string> ValidateDependencies(const MetaCoreAssetGuid& prefabGuid) const;
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::optional<MetaCoreResolvedPrefab> ResolveInternal(
|
||||
const MetaCoreAssetGuid& prefabGuid,
|
||||
std::vector<MetaCoreAssetGuid>& stack,
|
||||
std::string* error
|
||||
) const;
|
||||
|
||||
PrefabLoader Loader_{};
|
||||
MetaCorePrefabOverrideRegistry OverrideRegistry_{};
|
||||
};
|
||||
|
||||
} // namespace MetaCore
|
||||
@ -5,6 +5,7 @@
|
||||
#include "MetaCoreFoundation/MetaCoreAssetGuid.h"
|
||||
#include "MetaCoreFoundation/MetaCoreAssetTypes.h"
|
||||
#include "MetaCoreScene/MetaCoreGameObject.h"
|
||||
#include "MetaCoreScene/MetaCoreObjectReference.h"
|
||||
#include <glm/vec3.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
@ -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<MetaCorePrefabOverrideDocument> Overrides{};
|
||||
};
|
||||
|
||||
MC_STRUCT()
|
||||
struct MetaCorePrefabInstanceDocument {
|
||||
MC_GENERATED_BODY()
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreAssetGuid InstanceGuid{};
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreAssetGuid PrefabAssetGuid{};
|
||||
|
||||
MC_PROPERTY()
|
||||
MetaCoreAssetGuid ParentObjectGuid{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCorePrefabOverrideDocument> 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<MetaCoreGameObjectData> GameObjects{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreSubSceneDocument> SubScenes{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCorePrefabInstanceDocument> PrefabInstances{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreSchemaVersionEntry> 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<MetaCoreGameObjectData> GameObjects{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreNestedPrefabDocument> NestedPrefabs{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCorePrefabOverrideDocument> Overrides{};
|
||||
|
||||
MC_PROPERTY()
|
||||
std::vector<MetaCoreSchemaVersionEntry> ComponentVersions{};
|
||||
};
|
||||
|
||||
MC_ENUM()
|
||||
|
||||
175
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneWorld.h
Normal file
175
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneWorld.h
Normal file
@ -0,0 +1,175 @@
|
||||
#pragma once
|
||||
|
||||
#include "MetaCoreScene/MetaCoreScene.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
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<MetaCoreId> 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<MetaCoreId> RuntimeObjectIds{};
|
||||
};
|
||||
|
||||
struct MetaCoreSceneWorldSnapshotEntry {
|
||||
MetaCoreSceneDocument Document{};
|
||||
MetaCoreSceneLoadMode LoadMode = MetaCoreSceneLoadMode::Additive;
|
||||
bool Dirty = false;
|
||||
};
|
||||
|
||||
struct MetaCoreSceneWorldSnapshot {
|
||||
MetaCoreSceneSnapshot CompositeScene{};
|
||||
MetaCoreAssetGuid ActiveSceneGuid{};
|
||||
std::vector<MetaCoreSceneWorldSnapshotEntry> Scenes{};
|
||||
};
|
||||
|
||||
class MetaCoreSceneWorld {
|
||||
public:
|
||||
using SceneLoader = std::function<std::optional<MetaCoreSceneDocument>(const MetaCoreAssetGuid&)>;
|
||||
using LifecycleListener = std::function<void(const MetaCoreSceneLifecycleEvent&)>;
|
||||
|
||||
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<glm::vec3>& 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<MetaCoreLoadedSceneInfo> GetLoadedScenes() const;
|
||||
[[nodiscard]] std::optional<MetaCoreSceneLoadStatus> 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<MetaCoreSceneDocument> CaptureSceneDocument(const MetaCoreAssetGuid& sceneGuid) const;
|
||||
[[nodiscard]] MetaCoreSceneWorldSnapshot CaptureWorldSnapshot() const;
|
||||
void RestoreWorldSnapshot(const MetaCoreSceneWorldSnapshot& snapshot);
|
||||
[[nodiscard]] std::vector<std::string> 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<MetaCoreId> RuntimeObjectIds{};
|
||||
std::unordered_map<MetaCoreId, MetaCoreId> 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<MetaCoreScene> OwnedScene_{};
|
||||
MetaCoreScene* Scene_ = nullptr;
|
||||
MetaCoreAssetGuid ActiveSceneGuid_{};
|
||||
std::unordered_map<MetaCoreAssetGuid, LoadedScene, MetaCoreAssetGuidHasher> LoadedScenes_{};
|
||||
std::unordered_map<ObjectKey, MetaCoreId, ObjectKeyHasher> ObjectIndex_{};
|
||||
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> HardResidentScenes_{};
|
||||
std::vector<std::unique_ptr<PendingLoad>> PendingLoads_{};
|
||||
std::unordered_map<std::uint64_t, MetaCoreSceneLoadStatus> LoadStatuses_{};
|
||||
std::unordered_map<std::uint64_t, LifecycleListener> 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
|
||||
@ -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. 编辑器生产力与插件边界
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
81
tests/MetaCoreSceneBenchmark.cpp
Normal file
81
tests/MetaCoreSceneBenchmark.cpp
Normal file
@ -0,0 +1,81 @@
|
||||
#include "MetaCoreScene/MetaCorePrefab.h"
|
||||
#include "MetaCoreScene/MetaCoreSceneWorld.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
double MillisecondsSince(std::chrono::steady_clock::time_point begin) {
|
||||
return std::chrono::duration<double, std::milli>(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<double> 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<std::size_t>(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<MetaCorePrefabDocument> {
|
||||
return guid == prefab.PrefabGuid ? std::optional(prefab) : std::nullopt;
|
||||
});
|
||||
std::vector<double> 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<std::size_t>(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;
|
||||
}
|
||||
306
tests/MetaCoreSceneTests.cpp
Normal file
306
tests/MetaCoreSceneTests.cpp
Normal file
@ -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 <chrono>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
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<MetaCoreAssetGuid, MetaCoreSceneDocument, MetaCoreAssetGuidHasher> documents;
|
||||
documents.emplace(second.SceneGuid, second);
|
||||
MetaCoreSceneWorld world([&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreSceneDocument> {
|
||||
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<MetaCoreSceneDocument> {
|
||||
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<MetaCoreAssetGuid, MetaCoreSceneDocument, MetaCoreAssetGuidHasher> documents{
|
||||
{root.SceneGuid, root}, {child.SceneGuid, child}
|
||||
};
|
||||
MetaCoreSceneWorld world([&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreSceneDocument> {
|
||||
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<MetaCoreAssetGuid, MetaCorePrefabDocument, MetaCoreAssetGuidHasher> prefabs{
|
||||
{base.PrefabGuid, base}, {nested.PrefabGuid, nested}, {variant.PrefabGuid, variant}
|
||||
};
|
||||
MetaCorePrefabResolver resolver([&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCorePrefabDocument> {
|
||||
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<MetaCorePrefabOverrideDocument> 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;
|
||||
}
|
||||
@ -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 <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user