From db78bed4997a3e9f83e0efd7a6c45ec2664685ef Mon Sep 17 00:00:00 2001 From: ayuan9957 <107920784+ayuan9957@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:28:58 +0800 Subject: [PATCH] feat(build): implement cooked player packaging flow --- Apps/MetaCoreLauncher/main.cpp | 12 +- Apps/MetaCorePlayer/main.cpp | 158 ++- CMakeLists.txt | 65 +- CMakePresets.json | 18 +- .../MetaCoreBuiltinCoreServicesModule.cpp | 875 ++++++++++++++-- .../Private/MetaCoreBuiltinEditorModule.cpp | 969 +++++++++++++++++- .../Private/MetaCoreEditorApp.cpp | 87 +- .../Private/MetaCoreGltfImporter.cpp | 400 +++----- .../Public/MetaCoreEditor/MetaCoreEditorApp.h | 2 + .../MetaCoreEditor/MetaCoreEditorServices.h | 27 + .../Private/MetaCoreAssetRegistry.cpp | 22 +- .../MetaCoreEditorViewportRenderer.cpp | 2 +- .../Private/MetaCoreFilamentSceneBridge.cpp | 334 +++++- .../Private/MetaCoreImGuiHelper.cpp | 63 +- .../Private/MetaCorePlayerRenderer.cpp | 82 ++ .../Private/MetaCoreRenderDevice.cpp | 8 +- .../Private/MetaCoreSceneRenderSync.cpp | 8 +- .../MetaCoreFilamentSceneBridge.h | 3 +- .../MetaCoreRender/MetaCoreImGuiHelper.h | 1 + .../MetaCoreRender/MetaCorePlayerRenderer.h | 31 + .../MetaCoreRender/MetaCoreRenderDevice.h | 3 +- .../MetaCoreRender/MetaCoreRenderTypes.h | 10 + .../MetaCoreRender/MetaCoreSceneRenderSync.h | 6 + .../Private/MetaCoreRuntimeDataProject.cpp | 8 + .../MetaCoreRuntimeDataProject.h | 6 + tests/MetaCoreSmokeTests.cpp | 247 +++++ tools/MetaCoreBuildPackageTool/main.cpp | 70 ++ vcpkg.json | 24 +- 28 files changed, 3054 insertions(+), 487 deletions(-) create mode 100644 Source/MetaCoreRender/Private/MetaCorePlayerRenderer.cpp create mode 100644 Source/MetaCoreRender/Public/MetaCoreRender/MetaCorePlayerRenderer.h diff --git a/Apps/MetaCoreLauncher/main.cpp b/Apps/MetaCoreLauncher/main.cpp index 445022c..353755c 100644 --- a/Apps/MetaCoreLauncher/main.cpp +++ b/Apps/MetaCoreLauncher/main.cpp @@ -267,7 +267,9 @@ public: openButton->setObjectName("smallButton"); openButton->setFixedWidth(64); connect(openButton, &QPushButton::clicked, this, [this]() { - QDesktopServices::openUrl(QUrl::fromLocalFile(ToQString(Project_.RootPath))); + if (OnOpen) { + OnOpen(); + } }); rootLayout->addWidget(openButton); } @@ -297,6 +299,7 @@ private: public: std::function OnSelected{}; + std::function OnOpen{}; private: LauncherProject Project_{}; @@ -748,6 +751,10 @@ private: SelectedProjectIndex_ = index; RefreshProjectList(); }; + card->OnOpen = [this, index]() { + SelectedProjectIndex_ = index; + LaunchSelectedProject(); + }; ProjectListLayout_->addWidget(card); } ProjectListLayout_->addStretch(1); @@ -779,7 +786,7 @@ private: SelectedProjectIndex_ = 0; SaveProjects(); RefreshProjectList(); - ShowStatus("Created " + dialog.ProjectName()); + LaunchSelectedProject(); } void RemoveSelectedProject() { @@ -844,6 +851,7 @@ private: QProcess process; process.setProgram(ToQString(editorPath)); + process.setArguments(QStringList{ "--project", ToQString(project.RootPath) }); process.setWorkingDirectory(ToQString(ExecutableDirectory_)); QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); environment.insert("METACORE_PROJECT_PATH", ToQString(project.RootPath)); diff --git a/Apps/MetaCorePlayer/main.cpp b/Apps/MetaCorePlayer/main.cpp index e5925ae..3661525 100644 --- a/Apps/MetaCorePlayer/main.cpp +++ b/Apps/MetaCorePlayer/main.cpp @@ -2,7 +2,7 @@ #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreFoundation/MetaCorePackage.h" #include "MetaCoreFoundation/MetaCoreProject.h" -#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" +#include "MetaCoreRender/MetaCorePlayerRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" #include "MetaCoreRender/MetaCoreRuntimeUiRenderer.h" #include "MetaCoreRender/MetaCoreRenderTypes.h" @@ -15,7 +15,10 @@ #include "MetaCoreScene/MetaCoreScene.h" #include "MetaCoreScene/MetaCoreUiRmlCompiler.h" +#include + #include +#include #include #include #include @@ -132,6 +135,68 @@ struct MetaCoreRuntimeDataSourceInstance { return iterator == manifest.Entries.end() ? nullptr : &*iterator; } +[[nodiscard]] const MetaCore::MetaCoreCookManifestEntry* MetaCoreFindCookedManifestEntry( + const MetaCore::MetaCoreCookManifestDocument& manifest, + const MetaCore::MetaCoreAssetGuid& assetGuid +) { + if (!assetGuid.IsValid()) { + return nullptr; + } + const auto iterator = std::find_if( + manifest.Entries.begin(), + manifest.Entries.end(), + [&](const MetaCore::MetaCoreCookManifestEntry& entry) { + return entry.AssetGuid == assetGuid; + } + ); + return iterator == manifest.Entries.end() ? nullptr : &*iterator; +} + +[[nodiscard]] bool MetaCoreIsCookedRuntimeGltfModel(const std::filesystem::path& relativeSourcePath) { + std::string extension = relativeSourcePath.extension().generic_string(); + std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return extension == ".glb" || extension == ".gltf"; +} + +void MetaCoreRewriteSceneGltfModelPathsToCooked( + MetaCore::MetaCoreSceneDocument& sceneDocument, + const MetaCore::MetaCoreCookManifestDocument& manifest +) { + for (MetaCore::MetaCoreGameObjectData& gameObject : sceneDocument.GameObjects) { + if (!gameObject.MeshRenderer.has_value()) { + continue; + } + if (gameObject.MeshRenderer->SourceModelPath.empty() && + !gameObject.MeshRenderer->SourceModelAssetGuid.IsValid()) { + continue; + } + + const std::filesystem::path sourceModelPath(gameObject.MeshRenderer->SourceModelPath); + const MetaCore::MetaCoreCookManifestEntry* entry = nullptr; + if (!sourceModelPath.empty() && MetaCoreIsCookedRuntimeGltfModel(sourceModelPath)) { + entry = MetaCoreFindCookedManifestEntry(manifest, sourceModelPath); + } + if (entry == nullptr && gameObject.MeshRenderer->SourceModelAssetGuid.IsValid()) { + entry = MetaCoreFindCookedManifestEntry(manifest, gameObject.MeshRenderer->SourceModelAssetGuid); + } + if (entry == nullptr || entry->CookedPath.empty()) { + continue; + } + if (!MetaCoreIsCookedRuntimeGltfModel(entry->SourcePackagePath)) { + continue; + } + + const std::string cookedModelPath = entry->CookedPath.generic_string(); + gameObject.MeshRenderer->SourceModelPath = cookedModelPath; + gameObject.MeshRenderer->SourceModelAssetGuid = {}; + if (gameObject.ModelRootTag.has_value()) { + gameObject.ModelRootTag->SourceModelPath = cookedModelPath; + } + } +} + template [[nodiscard]] std::optional MetaCoreReadTypedPackagePayload( const MetaCore::MetaCorePackageDocument& package, @@ -579,6 +644,57 @@ void MetaCoreLogRuntimeBindingIssues( } } +struct MetaCorePlayerLaunchConfig { + std::string Platform{"Windows"}; + std::string GraphicsBackend{"OpenGL"}; + std::string Architecture{"x64"}; + std::string BuildProfile{"Development"}; +}; + +[[nodiscard]] MetaCorePlayerLaunchConfig MetaCoreReadPlayerLaunchConfig( + const std::filesystem::path& configPath +) { + MetaCorePlayerLaunchConfig config; + std::ifstream input(configPath, std::ios::binary); + if (!input.is_open()) { + return config; + } + + nlohmann::json document; + try { + input >> document; + } catch (const nlohmann::json::exception&) { + return config; + } + + const auto targetIterator = document.find("target"); + if (targetIterator != document.end() && targetIterator->is_object()) { + config.Platform = targetIterator->value("platform", config.Platform); + config.GraphicsBackend = targetIterator->value("graphics_backend", config.GraphicsBackend); + config.Architecture = targetIterator->value("architecture", config.Architecture); + } + const auto runtimeIterator = document.find("runtime"); + if (runtimeIterator != document.end() && runtimeIterator->is_object()) { + config.BuildProfile = runtimeIterator->value("build_profile", config.BuildProfile); + } + return config; +} + +[[nodiscard]] MetaCore::MetaCoreRenderBackend MetaCoreSelectRenderBackend( + const MetaCorePlayerLaunchConfig& launchConfig +) { + if (launchConfig.GraphicsBackend == "Vulkan") { + return MetaCore::MetaCoreRenderBackend::Vulkan; + } + if (launchConfig.GraphicsBackend == "WebGPU") { + return MetaCore::MetaCoreRenderBackend::WebGPU; + } + if (launchConfig.GraphicsBackend == "WebGL") { + std::cout << "MetaCorePlayer: WebGL is not available in native Player; using OpenGL backend\n"; + } + return MetaCore::MetaCoreRenderBackend::OpenGL; +} + } // namespace int main(int argc, char* argv[]) { @@ -597,6 +713,13 @@ int main(int argc, char* argv[]) { if (argc > 0) { std::filesystem::current_path(std::filesystem::path(argv[0]).parent_path()); } + const MetaCorePlayerLaunchConfig launchConfig = + MetaCoreReadPlayerLaunchConfig(std::filesystem::current_path() / "MetaCorePlayer.config.json"); + std::cout << "MetaCorePlayer: target platform=" << launchConfig.Platform + << " graphics=" << launchConfig.GraphicsBackend + << " arch=" << launchConfig.Architecture + << " config_profile=" << launchConfig.BuildProfile + << '\n'; MetaCore::MetaCoreWindow window; if (!window.Initialize(1280, 720, "MetaCore Player")) { @@ -605,14 +728,16 @@ int main(int argc, char* argv[]) { } MetaCore::MetaCoreRenderDevice renderDevice; - if (!renderDevice.Initialize(window)) { + MetaCore::MetaCoreRenderDeviceConfig renderConfig{}; + renderConfig.Backend = MetaCoreSelectRenderBackend(launchConfig); + if (!renderDevice.Initialize(window, renderConfig)) { std::cerr << "MetaCorePlayer: render device initialize failed\n"; return 1; } - MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; - if (!viewportRenderer.Initialize(renderDevice, window, false)) { - std::cerr << "MetaCorePlayer: viewport renderer initialize failed\n"; + MetaCore::MetaCorePlayerRenderer playerRenderer; + if (!playerRenderer.Initialize(renderDevice, window)) { + std::cerr << "MetaCorePlayer: renderer initialize failed\n"; return 1; } @@ -634,7 +759,7 @@ int main(int argc, char* argv[]) { projectRoot = *discovered; } const std::filesystem::path projectPath = MetaCore::MetaCoreGetProjectFilePath(projectRoot); - viewportRenderer.SetProjectRootPath(projectRoot); + playerRenderer.SetProjectRootPath(projectRoot); MetaCore::MetaCoreTypeRegistry typeRegistry; MetaCore::MetaCoreRegisterFoundationGeneratedTypes(typeRegistry); MetaCore::MetaCoreRegisterSceneGeneratedTypes(typeRegistry); @@ -719,6 +844,9 @@ int main(int argc, char* argv[]) { } MetaCore::MetaCoreScene scene; if (startupSceneDocument.has_value()) { + if (cookedManifest.has_value()) { + MetaCoreRewriteSceneGltfModelPathsToCooked(*startupSceneDocument, *cookedManifest); + } MetaCore::MetaCoreSceneSnapshot snapshot; snapshot.GameObjects = startupSceneDocument->GameObjects; scene.RestoreSnapshot(snapshot); @@ -802,8 +930,8 @@ int main(int argc, char* argv[]) { const auto diagnosticsPath = (projectRoot / runtimeProjectDocument.DiagnosticsPath).lexically_normal(); const auto loadedSourcesDocument = MetaCore::MetaCoreReadRuntimeDataSourcesDocument(sourcesPath, typeRegistry); const auto loadedBindingsDocument = MetaCore::MetaCoreReadRuntimeBindingsDocument(bindingsPath, typeRegistry); - auto sourcesDocument = loadedSourcesDocument.value_or(MetaCoreBuildDefaultRuntimeDataSourcesDocument()); - const auto bindingsDocument = loadedBindingsDocument.value_or(MetaCoreBuildDefaultRuntimeBindingsDocument()); + auto sourcesDocument = loadedSourcesDocument.value_or(MetaCore::MetaCoreRuntimeDataSourcesDocument{}); + const auto bindingsDocument = loadedBindingsDocument.value_or(MetaCore::MetaCoreRuntimeBindingsDocument{}); if (loadedSourcesDocument.has_value() && loadedBindingsDocument.has_value()) { std::cout << "MetaCorePlayer: loaded runtime config from binary .mcruntime documents\n"; @@ -812,9 +940,9 @@ int main(int argc, char* argv[]) { const bool bindingsExists = std::filesystem::exists(bindingsPath); if ((sourcesExists && !loadedSourcesDocument.has_value()) || (bindingsExists && !loadedBindingsDocument.has_value())) { - std::cerr << "MetaCorePlayer: runtime config exists but is unreadable or corrupted, using built-in fallback config\n"; + std::cerr << "MetaCorePlayer: runtime config exists but is unreadable or corrupted; continuing without runtime data bindings\n"; } else { - std::cout << "MetaCorePlayer: runtime config missing, using built-in fallback config\n"; + std::cout << "MetaCorePlayer: runtime config missing; continuing without runtime data bindings\n"; } } if (const auto replayPathError = MetaCoreResolveRuntimeDataReplayFilePaths(sourcesDocument, projectRoot); @@ -900,7 +1028,7 @@ int main(int argc, char* argv[]) { while (!window.ShouldClose()) { window.BeginFrame(); const auto [windowWidth, windowHeight] = window.GetWindowSize(); - viewportRenderer.SetViewportRect(MetaCore::MetaCoreViewportRect{ + playerRenderer.SetViewportRect(MetaCore::MetaCoreViewportRect{ 0.0F, 0.0F, static_cast(windowWidth), @@ -957,10 +1085,10 @@ int main(int argc, char* argv[]) { } } MetaCoreLogRuntimeBindingIssues(diagnostics, reportedBindingIssues); - viewportRenderer.RenderSceneToViewport(scene, MetaCoreBuildPlayerSceneView(), true); + playerRenderer.RenderScene(scene, MetaCoreBuildPlayerSceneView()); runtimeUiRenderer.Render(); - viewportRenderer.SetRuntimeUiOverlayFrame(runtimeUiRenderer.GetLastFrame(), windowWidth, windowHeight); - viewportRenderer.RenderAll(); + playerRenderer.SetRuntimeUiOverlayFrame(runtimeUiRenderer.GetLastFrame(), windowWidth, windowHeight); + playerRenderer.RenderAll(); renderDevice.RenderFrame(); renderDevice.PresentFrame(); window.EndFrame(); @@ -968,7 +1096,7 @@ int main(int argc, char* argv[]) { runtimeLifecycleExecutor.ExitScene(scene, runtimeLifecycleErrorSink); runtimeUiRenderer.Shutdown(); - viewportRenderer.Shutdown(); + playerRenderer.Shutdown(); renderDevice.Shutdown(); window.Shutdown(); return 0; diff --git a/CMakeLists.txt b/CMakeLists.txt index 197e16f..a66e38c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ if(MSVC) endif() option(METACORE_BUILD_TESTS "Build MetaCore tests" ON) +option(METACORE_BUILD_LAUNCHER "Build MetaCore Qt launcher" ON) list(PREPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") @@ -48,7 +49,9 @@ include(MetaCoreFilament) find_package(glm CONFIG REQUIRED) find_package(imgui CONFIG REQUIRED) find_package(RmlUi CONFIG REQUIRED) -find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets) +if(METACORE_BUILD_LAUNCHER) + find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets) +endif() set(METACORE_COMMON_WARNINGS) if(MSVC) @@ -65,7 +68,7 @@ add_custom_command( OUTPUT "${METACORE_UI_BLIT_MATERIAL_OUTPUT}" COMMAND "${METACORE_FILAMENT_MATC}" --platform desktop - --api opengl + --api all --output "${METACORE_UI_BLIT_MATERIAL_OUTPUT}" "${METACORE_UI_BLIT_MATERIAL_SOURCE}" DEPENDS @@ -273,6 +276,7 @@ set(METACORE_RENDER_HEADERS Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h + Source/MetaCoreRender/Public/MetaCoreRender/MetaCorePlayerRenderer.h Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRenderDevice.h Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRuntimeUiRenderer.h Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h @@ -283,6 +287,7 @@ set(METACORE_RENDER_SOURCES Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp + Source/MetaCoreRender/Private/MetaCorePlayerRenderer.cpp Source/MetaCoreRender/Private/MetaCoreRenderDevice.cpp Source/MetaCoreRender/Private/MetaCoreRuntimeUiRenderer.cpp Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp @@ -437,6 +442,8 @@ target_link_libraries(MetaCoreEditor imgui::imgui ) +metacore_use_filament(MetaCoreEditor) + target_compile_options(MetaCoreEditor PRIVATE ${METACORE_COMMON_WARNINGS}) target_compile_definitions(MetaCoreEditor PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) @@ -455,32 +462,34 @@ target_link_libraries(MetaCoreEditorApp ) metacore_stage_ui_blit_material(MetaCoreEditorApp) -add_executable(MetaCoreLauncher - Apps/MetaCoreLauncher/main.cpp -) +if(METACORE_BUILD_LAUNCHER) + add_executable(MetaCoreLauncher + Apps/MetaCoreLauncher/main.cpp + ) -target_link_libraries(MetaCoreLauncher - PRIVATE - MetaCoreFoundation - Qt6::Widgets -) -target_compile_options(MetaCoreLauncher PRIVATE ${METACORE_COMMON_WARNINGS}) -target_compile_definitions(MetaCoreLauncher PRIVATE NOMINMAX) -if(WIN32) - if(EXISTS "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Core.dll") - add_custom_command(TARGET MetaCoreLauncher POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Core.dll" - "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Gui.dll" - "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Widgets.dll" - "$" - COMMAND ${CMAKE_COMMAND} -E make_directory - "$/platforms" - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${METACORE_QTBASE_PACKAGE_DIR}/Qt6/plugins/platforms/qwindows.dll" - "$/platforms" - VERBATIM - ) + target_link_libraries(MetaCoreLauncher + PRIVATE + MetaCoreFoundation + Qt6::Widgets + ) + target_compile_options(MetaCoreLauncher PRIVATE ${METACORE_COMMON_WARNINGS}) + target_compile_definitions(MetaCoreLauncher PRIVATE NOMINMAX) + if(WIN32) + if(EXISTS "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Core.dll") + add_custom_command(TARGET MetaCoreLauncher POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Core.dll" + "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Gui.dll" + "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Widgets.dll" + "$" + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/platforms" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${METACORE_QTBASE_PACKAGE_DIR}/Qt6/plugins/platforms/qwindows.dll" + "$/platforms" + VERBATIM + ) + endif() endif() endif() @@ -516,7 +525,9 @@ if(METACORE_BUILD_TESTS) target_link_libraries(MetaCoreSmokeTests PRIVATE MetaCoreEditor + MetaCoreRender MetaCoreRuntimeData + RmlUi::RmlUi ) target_compile_options(MetaCoreSmokeTests PRIVATE ${METACORE_COMMON_WARNINGS}) add_dependencies(MetaCoreSmokeTests MetaCoreRuntimeConfigTool) diff --git a/CMakePresets.json b/CMakePresets.json index 84b924c..9e59bc8 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -14,7 +14,8 @@ "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", "cacheVariables": { "CMAKE_CXX_STANDARD": "20", - "METACORE_BUILD_TESTS": "ON" + "METACORE_BUILD_TESTS": "ON", + "METACORE_BUILD_LAUNCHER": "OFF" } }, { @@ -32,6 +33,16 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } + }, + { + "name": "vs2022-launcher-release", + "inherits": "base", + "displayName": "VS2022 Launcher Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "METACORE_BUILD_LAUNCHER": "ON", + "VCPKG_MANIFEST_FEATURES": "launcher" + } } ], "buildPresets": [ @@ -44,6 +55,11 @@ "name": "build-release", "configurePreset": "vs2022-release", "configuration": "Release" + }, + { + "name": "build-launcher-release", + "configurePreset": "vs2022-launcher-release", + "configuration": "Release" } ], "testPresets": [ diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index 03f9d11..5100985 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +64,31 @@ namespace MetaCore { namespace { +[[nodiscard]] bool MetaCorePathExistsNoThrow(const std::filesystem::path& path) { + std::error_code error; + return std::filesystem::exists(path, error); +} + +[[nodiscard]] bool MetaCoreDirectoryExistsNoThrow(const std::filesystem::path& path) { + std::error_code error; + return std::filesystem::is_directory(path, error); +} + +[[nodiscard]] bool MetaCoreRegularFileExistsNoThrow(const std::filesystem::path& path) { + std::error_code error; + return std::filesystem::is_regular_file(path, error); +} + +[[nodiscard]] bool MetaCoreEntryIsRegularFileNoThrow(const std::filesystem::directory_entry& entry) { + std::error_code error; + return entry.is_regular_file(error); +} + +[[nodiscard]] bool MetaCoreEntryIsDirectoryNoThrow(const std::filesystem::directory_entry& entry) { + std::error_code error; + return entry.is_directory(error); +} + bool MetaCoreWriteMetaJson( const std::filesystem::path& absoluteMetaPath, const MetaCoreAssetMetadataDocument& metadata @@ -589,23 +616,65 @@ void MetaCoreApplyModelImportSettings(MetaCoreModelAssetDocument& document) { return guid; } +struct MetaCoreGeneratedAssetChoicesCache { + std::uint64_t Fingerprint = 0; + std::vector Choices{}; + bool Valid = false; +}; + +[[nodiscard]] std::uint64_t MetaCoreBuildGeneratedAssetChoicesFingerprint( + const MetaCoreIAssetDatabaseService& assetDatabaseService +) { + std::uint64_t fingerprint = MetaCoreHashString(assetDatabaseService.GetProjectDescriptor().RootPath.generic_string()); + for (const MetaCoreAssetRecord& record : assetDatabaseService.GetAssetRegistry()) { + if (record.Type != "model") { + continue; + } + + fingerprint = MetaCoreHashCombine(fingerprint, MetaCoreHashString(record.Guid.ToString())); + fingerprint = MetaCoreHashCombine(fingerprint, MetaCoreHashString(record.RelativePath.generic_string())); + fingerprint = MetaCoreHashCombine(fingerprint, MetaCoreHashString(record.PackagePath.generic_string())); + fingerprint = MetaCoreHashCombine(fingerprint, MetaCoreHashString(record.ImporterId)); + fingerprint = MetaCoreHashCombine(fingerprint, record.SourceHash); + + const std::filesystem::path relativePackagePath = + !record.PackagePath.empty() ? record.PackagePath : record.RelativePath; + const std::filesystem::path absolutePackagePath = + assetDatabaseService.GetProjectDescriptor().RootPath / relativePackagePath; + std::error_code errorCode; + const auto packageSize = std::filesystem::file_size(absolutePackagePath, errorCode); + fingerprint = MetaCoreHashCombine(fingerprint, errorCode ? 0 : packageSize); + errorCode.clear(); + const auto packageWriteTime = std::filesystem::last_write_time(absolutePackagePath, errorCode); + fingerprint = MetaCoreHashCombine( + fingerprint, + errorCode ? 0 : static_cast(packageWriteTime.time_since_epoch().count()) + ); + } + return fingerprint; +} + template [[nodiscard]] std::vector MetaCoreCollectGeneratedAssetChoices( MetaCoreEditorContext& editorContext, TLabelBuilder&& labelBuilder ) { + static MetaCoreGeneratedAssetChoicesCache cache; std::vector choices; - // 基础版暂时返回空,避免每帧读取所有模型包导致卡顿 - return choices; - const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); const auto packageService = editorContext.GetModuleRegistry().ResolveService(); const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + cache = MetaCoreGeneratedAssetChoicesCache{}; return choices; } + const std::uint64_t fingerprint = MetaCoreBuildGeneratedAssetChoicesFingerprint(*assetDatabaseService); + if (cache.Valid && cache.Fingerprint == fingerprint) { + return cache.Choices; + } + for (const MetaCoreAssetRecord& record : assetDatabaseService->GetAssetRegistry()) { if (record.Type != "model") { continue; @@ -643,6 +712,9 @@ template std::sort(choices.begin(), choices.end(), [](const MetaCoreGeneratedAssetChoice& lhs, const MetaCoreGeneratedAssetChoice& rhs) { return lhs.Label < rhs.Label; }); + cache.Fingerprint = fingerprint; + cache.Choices = choices; + cache.Valid = true; return choices; } @@ -1899,10 +1971,23 @@ void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorCon ImGui::Separator(); if (ImGui::MenuItem("打开材质资源 (Open Material Resource)")) { const auto assetEditingService = editorContext.GetModuleRegistry().ResolveService(); - if (const auto resolvedMaterial = assetEditingService != nullptr - ? assetEditingService->ResolveGeneratedAsset(materialGuid) + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + if (const auto materialRecord = assetDatabaseService != nullptr + ? assetDatabaseService->FindAssetByGuid(materialGuid) : std::nullopt; - resolvedMaterial.has_value() && resolvedMaterial->GeneratedKind == "material") { + materialRecord.has_value() && materialRecord->Type == "material") { + editorContext.SelectAsset(MetaCoreSelectedAssetState{ + materialRecord->Guid, + materialRecord->RelativePath, + materialRecord->Type, + materialRecord->StorageKind + }); + editorContext.SetSelectedAssetSubId({}); + editorContext.ClearSelection(); + } else if (const auto resolvedMaterial = assetEditingService != nullptr + ? assetEditingService->ResolveGeneratedAsset(materialGuid) + : std::nullopt; + resolvedMaterial.has_value() && resolvedMaterial->GeneratedKind == "material") { editorContext.SelectAsset(MetaCoreSelectedAssetState{ resolvedMaterial->SourceAsset.Guid, resolvedMaterial->SourceAsset.RelativePath, @@ -2118,9 +2203,6 @@ void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorCon std::string materialName = "Default-Material"; const auto assetEditingService = editorContext.GetModuleRegistry().ResolveService(); - const auto resolvedMaterial = assetEditingService != nullptr - ? assetEditingService->ResolveGeneratedAsset(materialGuid) - : std::nullopt; const auto materialDocument = assetEditingService != nullptr ? assetEditingService->LoadMaterialAsset(materialGuid) : std::nullopt; @@ -2151,9 +2233,7 @@ void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorCon drawList->AddCircleFilled(highlightCenter, 0.8F, IM_COL32(255, 255, 255, 255)); if (isPanelOpen) { - if (!resolvedMaterial.has_value() || - resolvedMaterial->GeneratedKind != "material" || - !materialDocument.has_value()) { + if (!materialDocument.has_value()) { ImGui::TextDisabled("未找到对应材质资源"); } else { MetaCoreMaterialAssetDocument materialAsset = *materialDocument; @@ -3672,12 +3752,20 @@ public: [[nodiscard]] std::vector GetDirectoriesUnder(const std::filesystem::path& relativeDirectory) const override { std::vector directories; const auto absoluteDirectory = ResolveDirectory(relativeDirectory); - if (!absoluteDirectory.has_value() || !std::filesystem::exists(*absoluteDirectory)) { + if (!absoluteDirectory.has_value() || !MetaCoreDirectoryExistsNoThrow(*absoluteDirectory)) { return directories; } - for (const auto& entry : std::filesystem::directory_iterator(*absoluteDirectory)) { - if (entry.is_directory()) { + std::error_code iteratorError; + for (std::filesystem::directory_iterator iterator(*absoluteDirectory, std::filesystem::directory_options::skip_permission_denied, iteratorError), end; + iterator != end; + iterator.increment(iteratorError)) { + if (iteratorError) { + iteratorError.clear(); + continue; + } + const auto& entry = *iterator; + if (MetaCoreEntryIsDirectoryNoThrow(entry)) { directories.push_back(entry.path().lexically_relative(Project_.RootPath)); } } @@ -3725,6 +3813,12 @@ public: private: [[nodiscard]] std::optional ResolveDirectory(const std::filesystem::path& relativeDirectory) const; [[nodiscard]] std::optional RefreshAndFindCreatedAsset(const std::filesystem::path& relativePath); + [[nodiscard]] std::optional RegisterCreatedJsonAsset( + const std::filesystem::path& relativePath, + const MetaCoreAssetGuid& assetGuid, + std::string_view assetType, + std::string_view importerId + ); [[nodiscard]] bool WriteJsonAssetMetadata( const std::filesystem::path& absolutePath, const std::filesystem::path& relativePath, @@ -4092,12 +4186,20 @@ void MetaCoreBuiltinAssetDatabaseService::SaveProjectDescriptor() const { void MetaCoreBuiltinAssetDatabaseService::RefreshScenePathsFromDisk() { Project_.ScenePaths.clear(); - if (!std::filesystem::exists(Project_.ScenesPath)) { + if (!MetaCoreDirectoryExistsNoThrow(Project_.ScenesPath)) { return; } - for (const auto& entry : std::filesystem::recursive_directory_iterator(Project_.ScenesPath)) { - if (entry.is_regular_file() && MetaCoreIsScenePath(entry.path())) { + std::error_code iteratorError; + for (std::filesystem::recursive_directory_iterator iterator(Project_.ScenesPath, std::filesystem::directory_options::skip_permission_denied, iteratorError), end; + iterator != end; + iterator.increment(iteratorError)) { + if (iteratorError) { + iteratorError.clear(); + continue; + } + const auto& entry = *iterator; + if (MetaCoreEntryIsRegularFileNoThrow(entry) && MetaCoreIsScenePath(entry.path())) { Project_.ScenePaths.push_back(entry.path().lexically_relative(Project_.RootPath)); } } @@ -4116,9 +4218,17 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { const auto& registry = ReflectionRegistry_->GetTypeRegistry(); - if (std::filesystem::exists(Project_.ScenesPath)) { - for (const auto& entry : std::filesystem::recursive_directory_iterator(Project_.ScenesPath)) { - if (!entry.is_regular_file() || !MetaCoreIsScenePath(entry.path())) { + if (MetaCoreDirectoryExistsNoThrow(Project_.ScenesPath)) { + std::error_code iteratorError; + for (std::filesystem::recursive_directory_iterator iterator(Project_.ScenesPath, std::filesystem::directory_options::skip_permission_denied, iteratorError), end; + iterator != end; + iterator.increment(iteratorError)) { + if (iteratorError) { + iteratorError.clear(); + continue; + } + const auto& entry = *iterator; + if (!MetaCoreEntryIsRegularFileNoThrow(entry) || !MetaCoreIsScenePath(entry.path())) { continue; } @@ -4180,9 +4290,17 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { } } - if (std::filesystem::exists(Project_.AssetsPath)) { - for (const auto& entry : std::filesystem::recursive_directory_iterator(Project_.AssetsPath)) { - if (!entry.is_regular_file()) { + if (MetaCoreDirectoryExistsNoThrow(Project_.AssetsPath)) { + std::error_code iteratorError; + for (std::filesystem::recursive_directory_iterator iterator(Project_.AssetsPath, std::filesystem::directory_options::skip_permission_denied, iteratorError), end; + iterator != end; + iterator.increment(iteratorError)) { + if (iteratorError) { + iteratorError.clear(); + continue; + } + const auto& entry = *iterator; + if (!MetaCoreEntryIsRegularFileNoThrow(entry)) { continue; } @@ -4204,6 +4322,9 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { const auto modelDocument = MetaCoreReadModelAssetDocument(*package, registry); if (modelDocument.has_value() && !modelDocument->SourcePath.empty()) { packageSourcePath = modelDocument->SourcePath; + if (!MetaCorePathExistsNoThrow(Project_.RootPath / packageSourcePath)) { + continue; + } } } @@ -4274,10 +4395,18 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { } } - if (std::filesystem::exists(Project_.UiPath) && + if (MetaCoreDirectoryExistsNoThrow(Project_.UiPath) && !MetaCorePathIsUnderOrEqual(Project_.UiPath, Project_.AssetsPath)) { - for (const auto& entry : std::filesystem::recursive_directory_iterator(Project_.UiPath)) { - if (!entry.is_regular_file() || + std::error_code iteratorError; + for (std::filesystem::recursive_directory_iterator iterator(Project_.UiPath, std::filesystem::directory_options::skip_permission_denied, iteratorError), end; + iterator != end; + iterator.increment(iteratorError)) { + if (iteratorError) { + iteratorError.clear(); + continue; + } + const auto& entry = *iterator; + if (!MetaCoreEntryIsRegularFileNoThrow(entry) || MetaCoreIsMetaPath(entry.path()) || MetaCoreIsLegacyMetaPath(entry.path()) || MetaCoreIsPackagePath(entry.path())) { @@ -4416,6 +4545,60 @@ std::optional MetaCoreBuiltinAssetDatabaseService::RefreshA return FindAssetByRelativePath(relativePath.lexically_normal()); } +std::optional MetaCoreBuiltinAssetDatabaseService::RegisterCreatedJsonAsset( + const std::filesystem::path& relativePath, + const MetaCoreAssetGuid& assetGuid, + std::string_view assetType, + std::string_view importerId +) { + if (!HasProject() || !assetGuid.IsValid()) { + return std::nullopt; + } + + const std::filesystem::path normalizedPath = relativePath.lexically_normal(); + const std::filesystem::path absolutePath = Project_.RootPath / normalizedPath; + const std::filesystem::path metaPath = MetaCoreBuildMetaPath(absolutePath); + MetaCoreAssetRecord record{ + assetGuid, + normalizedPath, + std::string(assetType), + MetaCoreAssetStorageKind::SourceFile, + normalizedPath, + normalizedPath, + std::filesystem::exists(metaPath) ? metaPath.lexically_relative(Project_.RootPath) : std::filesystem::path{}, + std::string(importerId), + MetaCoreHashFile(absolutePath).value_or(0) + }; + + AssetRecords_.erase( + std::remove_if(AssetRecords_.begin(), AssetRecords_.end(), [&](const MetaCoreAssetRecord& existing) { + return existing.RelativePath == normalizedPath || existing.Guid == assetGuid; + }), + AssetRecords_.end() + ); + AssetRecords_.push_back(record); + std::sort(AssetRecords_.begin(), AssetRecords_.end(), [](const auto& lhs, const auto& rhs) { + return MetaCorePathToPortableString(lhs.RelativePath) < MetaCorePathToPortableString(rhs.RelativePath); + }); + + MetaCoreAssetRegistry::Get().RegisterAsset(assetGuid, normalizedPath); + + if (ModuleRegistry_ != nullptr) { + if (const auto cookService = ModuleRegistry_->ResolveService(); cookService != nullptr) { + (void)cookService->CookAsset(assetGuid); + } + ModuleRegistry_->AccessEventBus().Publish(MetaCoreEditorEvent{ + MetaCoreEditorEventType::AssetDatabaseChanged, + "Asset created", + Project_.RootPath, + 0, + true + }); + } + + return record; +} + bool MetaCoreBuiltinAssetDatabaseService::WriteJsonAssetMetadata( const std::filesystem::path& absolutePath, const std::filesystem::path& relativePath, @@ -4550,7 +4733,7 @@ std::optional MetaCoreBuiltinAssetDatabaseService::CreateMa if (!WriteJsonAssetMetadata(absolutePath, normalizedPath, document.AssetGuid, "material", "MaterialImporter")) { return std::nullopt; } - return RefreshAndFindCreatedAsset(normalizedPath); + return RegisterCreatedJsonAsset(normalizedPath, document.AssetGuid, "material", "MaterialImporter"); } std::optional MetaCoreBuiltinAssetDatabaseService::CreateUiDocumentAsset( @@ -4687,6 +4870,133 @@ void MetaCoreApplyRuntimeProjectDefaults( return std::filesystem::current_path() / "MetaCorePlayer.exe"; } +[[nodiscard]] std::string_view MetaCoreBuildPlatformToString(MetaCoreBuildPlatform platform) { + switch (platform) { + case MetaCoreBuildPlatform::Windows: + return "Windows"; + case MetaCoreBuildPlatform::Linux: + return "Linux"; + case MetaCoreBuildPlatform::Android: + return "Android"; + case MetaCoreBuildPlatform::Web: + return "Web"; + } + return "Windows"; +} + +[[nodiscard]] std::string_view MetaCoreGraphicsBackendToString(MetaCoreGraphicsBackend backend) { + switch (backend) { + case MetaCoreGraphicsBackend::OpenGL: + return "OpenGL"; + case MetaCoreGraphicsBackend::Vulkan: + return "Vulkan"; + case MetaCoreGraphicsBackend::WebGL: + return "WebGL"; + case MetaCoreGraphicsBackend::WebGPU: + return "WebGPU"; + } + return "OpenGL"; +} + +[[nodiscard]] std::string_view MetaCoreBuildArchitectureToString(MetaCoreBuildArchitecture architecture) { + switch (architecture) { + case MetaCoreBuildArchitecture::X64: + return "x64"; + case MetaCoreBuildArchitecture::Arm64: + return "arm64"; + case MetaCoreBuildArchitecture::Wasm32: + return "wasm32"; + } + return "x64"; +} + +[[nodiscard]] std::string MetaCoreToLowerAscii(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) { + return static_cast(std::tolower(character)); + }); + return value; +} + +[[nodiscard]] std::optional MetaCoreValidatePlayerExecutableForTarget( + const std::filesystem::path& playerExecutable, + const MetaCoreBuildTarget& target +) { + if (!MetaCoreRegularFileExistsNoThrow(playerExecutable)) { + return "MetaCorePlayer executable was not found: " + playerExecutable.generic_string(); + } + if (target.Platform == MetaCoreBuildPlatform::Windows && + MetaCoreToLowerAscii(playerExecutable.extension().generic_string()) != ".exe") { + return "Windows Player package requires a .exe player executable: " + playerExecutable.generic_string(); + } + return std::nullopt; +} + +[[nodiscard]] std::optional MetaCoreValidateBuildTarget(const MetaCoreBuildTarget& target) { + const bool isWebPlatform = target.Platform == MetaCoreBuildPlatform::Web; + const bool isWebGraphics = target.GraphicsBackend == MetaCoreGraphicsBackend::WebGL || + target.GraphicsBackend == MetaCoreGraphicsBackend::WebGPU; + const bool isNativeGraphics = target.GraphicsBackend == MetaCoreGraphicsBackend::OpenGL || + target.GraphicsBackend == MetaCoreGraphicsBackend::Vulkan; + + if (isWebPlatform) { + if (target.Architecture != MetaCoreBuildArchitecture::Wasm32) { + return "Web build target requires wasm32 architecture"; + } + if (!isWebGraphics) { + return "Web build target requires WebGL or WebGPU graphics backend"; + } + return "Web build packaging is not implemented yet"; + } + + if (target.Architecture == MetaCoreBuildArchitecture::Wasm32) { + return "wasm32 architecture is only valid for Web build targets"; + } + if (isWebGraphics) { + return "WebGL and WebGPU graphics backends are only valid for Web build targets"; + } + if (!isNativeGraphics) { + return "Native build target requires OpenGL or Vulkan graphics backend"; + } + if (target.Platform != MetaCoreBuildPlatform::Windows) { + return "Build packaging currently supports Windows Player packages only"; + } + return std::nullopt; +} + +[[nodiscard]] bool MetaCoreWritePlayerConfigForPackage( + const std::filesystem::path& outputPath, + const MetaCoreBuildTarget& target, + const MetaCoreRuntimeProjectDocument& runtimeProject, + std::string& error +) { + const nlohmann::json config = { + {"version", 1}, + {"target", { + {"platform", MetaCoreBuildPlatformToString(target.Platform)}, + {"graphics_backend", MetaCoreGraphicsBackendToString(target.GraphicsBackend)}, + {"architecture", MetaCoreBuildArchitectureToString(target.Architecture)} + }}, + {"runtime", { + {"build_profile", runtimeProject.BuildProfileName}, + {"startup_scene", runtimeProject.StartupScenePath.generic_string()}, + {"startup_ui", runtimeProject.StartupUiPath.generic_string()}, + {"use_cooked_assets", runtimeProject.UseCookedAssets} + }} + }; + + std::ofstream output(outputPath, std::ios::binary | std::ios::trunc); + if (!output.is_open()) { + error = "Failed to create player config: " + outputPath.generic_string(); + return false; + } + output << config.dump(2) << '\n'; + if (!output.good()) { + error = "Failed to write player config: " + outputPath.generic_string(); + return false; + } + return true; +} + [[nodiscard]] std::filesystem::path MetaCoreBuildPackageOutputRoot( const MetaCoreProjectDescriptor& project, const MetaCoreRuntimeProjectDocument& runtimeProject, @@ -4696,7 +5006,7 @@ void MetaCoreApplyRuntimeProjectDefaults( ? request.OutputDirectory : runtimeProject.OutputDirectory; if (outputBase.empty()) { - outputBase = std::filesystem::path("Build") / "Windows"; + outputBase = std::filesystem::path("Build") / MetaCoreBuildPlatformToString(request.Target.Platform); } const std::filesystem::path absoluteOutputBase = outputBase.is_absolute() ? outputBase @@ -4745,6 +5055,243 @@ void MetaCoreCopyDirectoryForPackage( } } +[[nodiscard]] bool MetaCoreIsExternalGltfUri(std::string_view uri) { + return uri.starts_with("data:") || uri.starts_with("//") || uri.find("://") != std::string_view::npos; +} + +[[nodiscard]] int MetaCoreHexDigitValue(char character) { + if (character >= '0' && character <= '9') { + return character - '0'; + } + if (character >= 'a' && character <= 'f') { + return character - 'a' + 10; + } + if (character >= 'A' && character <= 'F') { + return character - 'A' + 10; + } + return -1; +} + +[[nodiscard]] std::string MetaCoreDecodeGltfUriPath(std::string_view uri) { + std::string decoded; + decoded.reserve(uri.size()); + for (std::size_t index = 0; index < uri.size(); ++index) { + if (uri[index] == '%' && index + 2 < uri.size()) { + const int high = MetaCoreHexDigitValue(uri[index + 1]); + const int low = MetaCoreHexDigitValue(uri[index + 2]); + if (high >= 0 && low >= 0) { + decoded.push_back(static_cast((high << 4) | low)); + index += 2; + continue; + } + } + decoded.push_back(uri[index]); + } + return decoded; +} + +[[nodiscard]] bool MetaCoreCollectGltfJsonLocalDependency( + const MetaCoreProjectDescriptor& project, + const std::filesystem::path& modelRelativePath, + std::string_view uri, + std::vector& dependencies, + std::string& error +) { + if (uri.empty() || MetaCoreIsExternalGltfUri(uri)) { + return true; + } + + const std::filesystem::path uriPath(MetaCoreDecodeGltfUriPath(uri)); + if (uriPath.is_absolute()) { + error = "GLTF dependency uses an absolute path: " + std::string(uri); + return false; + } + + const std::filesystem::path dependencyRelativePath = + (modelRelativePath.parent_path() / uriPath).lexically_normal(); + if (dependencyRelativePath.empty() || MetaCoreIsUnsafeRelativePath(dependencyRelativePath)) { + error = "Unsafe GLTF dependency path: " + std::string(uri); + return false; + } + + const std::filesystem::path dependencySourcePath = project.RootPath / dependencyRelativePath; + if (!MetaCoreRegularFileExistsNoThrow(dependencySourcePath)) { + error = "GLTF dependency not found: " + dependencyRelativePath.generic_string(); + return false; + } + + dependencies.push_back(dependencyRelativePath); + return true; +} + +[[nodiscard]] bool MetaCoreCollectGltfJsonLocalDependencies( + const MetaCoreProjectDescriptor& project, + const std::filesystem::path& modelRelativePath, + std::vector& dependencies, + std::string& error +) { + const std::filesystem::path modelSourcePath = project.RootPath / modelRelativePath; + std::ifstream input(modelSourcePath, std::ios::binary); + if (!input.is_open()) { + error = "Failed to open GLTF model source: " + modelRelativePath.generic_string(); + return false; + } + + nlohmann::json document; + try { + input >> document; + } catch (const std::exception& exception) { + error = "Failed to parse GLTF model source: " + modelRelativePath.generic_string() + " (" + exception.what() + ")"; + return false; + } + + const auto collectUriArray = [&](std::string_view key) -> bool { + const auto iterator = document.find(std::string(key)); + if (iterator == document.end() || !iterator->is_array()) { + return true; + } + for (const auto& item : *iterator) { + if (!item.is_object()) { + continue; + } + const auto uriIterator = item.find("uri"); + if (uriIterator == item.end() || !uriIterator->is_string()) { + continue; + } + if (!MetaCoreCollectGltfJsonLocalDependency( + project, + modelRelativePath, + uriIterator->get(), + dependencies, + error)) { + return false; + } + } + return true; + }; + + return collectUriArray("buffers") && collectUriArray("images"); +} + +[[nodiscard]] bool MetaCoreCopyCookedRuntimeModelSource( + const MetaCoreProjectDescriptor& project, + const std::filesystem::path& relativeSourcePath, + const std::filesystem::path& targetRelativePath, + MetaCoreBuildPlayerPackageResult& result, + std::unordered_set& copiedRelativePaths, + std::string& error +) { + const std::filesystem::path normalizedRelativePath = relativeSourcePath.lexically_normal(); + if (normalizedRelativePath.empty() || MetaCoreIsUnsafeRelativePath(normalizedRelativePath)) { + error = "Unsafe model runtime source path: " + relativeSourcePath.generic_string(); + return false; + } + const std::filesystem::path sourcePath = project.RootPath / normalizedRelativePath; + if (!MetaCoreRegularFileExistsNoThrow(sourcePath)) { + error = "Model runtime source not found: " + normalizedRelativePath.generic_string(); + return false; + } + const std::filesystem::path normalizedTargetPath = targetRelativePath.lexically_normal(); + if (normalizedTargetPath.empty() || MetaCoreIsUnsafeRelativePath(normalizedTargetPath)) { + error = "Unsafe model runtime target path: " + targetRelativePath.generic_string(); + return false; + } + const std::string copyKey = normalizedRelativePath.generic_string() + "->" + normalizedTargetPath.generic_string(); + if (!copiedRelativePaths.insert(copyKey).second) { + return true; + } + if (!MetaCoreCopyFileForPackage(sourcePath, result.OutputRoot / normalizedTargetPath, result)) { + error = "Failed to copy model runtime source: " + normalizedRelativePath.generic_string(); + return false; + } + return true; +} + +[[nodiscard]] bool MetaCoreCopyCookedRuntimeModelSources( + const MetaCoreProjectDescriptor& project, + const MetaCoreIAssetRegistryService& assetRegistryService, + const std::vector& dependencyReport, + MetaCoreBuildPlayerPackageResult& result, + std::string& error +) { + std::unordered_set cookedDependencyGuids; + for (const MetaCoreBuildDependencyReportEntry& dependency : dependencyReport) { + if (dependency.AssetGuid.IsValid() && dependency.Status == "Cooked") { + cookedDependencyGuids.insert(dependency.AssetGuid); + } + } + + std::unordered_set copiedRelativePaths; + for (const MetaCoreAssetRecord& record : assetRegistryService.GetAssetRegistry()) { + if (record.Type != "model") { + continue; + } + if (!cookedDependencyGuids.empty() && !cookedDependencyGuids.contains(record.Guid)) { + continue; + } + + const std::filesystem::path relativeSourcePath = !record.SourcePath.empty() + ? record.SourcePath + : record.RelativePath; + if (!MetaCoreIsGltfModelPath(relativeSourcePath)) { + continue; + } + + const auto dependencyIterator = std::find_if( + dependencyReport.begin(), + dependencyReport.end(), + [&](const MetaCoreBuildDependencyReportEntry& dependency) { + return dependency.AssetGuid == record.Guid; + } + ); + if (dependencyIterator == dependencyReport.end() || dependencyIterator->CookedPath.empty()) { + continue; + } + + if (relativeSourcePath.extension() == ".glb") { + continue; + } + + if (relativeSourcePath.extension() != ".gltf") { + continue; + } + + const std::filesystem::path cookedDependencyRoot = dependencyIterator->CookedPath.parent_path(); + if (cookedDependencyRoot.empty() || MetaCoreIsUnsafeRelativePath(cookedDependencyRoot)) { + error = "Unsafe cooked model dependency root: " + dependencyIterator->CookedPath.generic_string(); + return false; + } + + std::vector dependencies; + if (!MetaCoreCollectGltfJsonLocalDependencies(project, relativeSourcePath.lexically_normal(), dependencies, error)) { + return false; + } + const std::filesystem::path modelParentPath = relativeSourcePath.lexically_normal().parent_path(); + for (const std::filesystem::path& dependency : dependencies) { + std::filesystem::path dependencyTargetPath; + if (!modelParentPath.empty()) { + dependencyTargetPath = dependency.lexically_normal().lexically_relative(modelParentPath); + } else { + dependencyTargetPath = dependency.filename(); + } + if (dependencyTargetPath.empty() || MetaCoreIsUnsafeRelativePath(dependencyTargetPath)) { + error = "Unsafe glTF cooked dependency path: " + dependency.generic_string(); + return false; + } + if (!MetaCoreCopyCookedRuntimeModelSource( + project, + dependency, + cookedDependencyRoot / dependencyTargetPath, + result, + copiedRelativePaths, + error)) { + return false; + } + } + } + return true; +} + [[nodiscard]] bool MetaCoreCopyOptionalRelativeFileForPackage( const MetaCoreProjectDescriptor& project, const std::filesystem::path& relativePath, @@ -4925,11 +5472,21 @@ void MetaCoreCollectMaterialDependencies( void MetaCoreCollectMeshRendererDependencies( const MetaCoreMeshRendererComponent& meshRenderer, + const MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreCookDependencyQueueContext& context, const MetaCoreAssetGuid& sourceAssetGuid ) { MetaCoreEnqueueCookDependency(context, meshRenderer.MeshAssetGuid, sourceAssetGuid, "MeshRenderer.MeshAssetGuid"); - MetaCoreEnqueueCookDependency(context, meshRenderer.SourceModelAssetGuid, sourceAssetGuid, "MeshRenderer.SourceModelAssetGuid"); + if (meshRenderer.SourceModelAssetGuid.IsValid()) { + MetaCoreEnqueueCookDependency(context, meshRenderer.SourceModelAssetGuid, sourceAssetGuid, "MeshRenderer.SourceModelAssetGuid"); + } else if (!meshRenderer.SourceModelPath.empty()) { + const std::filesystem::path sourceModelPath(meshRenderer.SourceModelPath); + if (!MetaCoreIsUnsafeRelativePath(sourceModelPath)) { + if (const auto modelRecord = assetDatabaseService.FindAssetByRelativePath(sourceModelPath); modelRecord.has_value()) { + MetaCoreEnqueueCookDependency(context, modelRecord->Guid, sourceAssetGuid, "MeshRenderer.SourceModelPath"); + } + } + } for (const MetaCoreAssetGuid& materialGuid : meshRenderer.MaterialAssetGuids) { MetaCoreEnqueueCookDependency(context, materialGuid, sourceAssetGuid, "MeshRenderer.MaterialAssetGuids"); } @@ -4942,12 +5499,13 @@ void MetaCoreCollectMeshRendererDependencies( void MetaCoreCollectGameObjectDependencies( const std::vector& gameObjects, + const MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreCookDependencyQueueContext& context, const MetaCoreAssetGuid& sourceAssetGuid ) { for (const MetaCoreGameObjectData& gameObject : gameObjects) { if (gameObject.MeshRenderer.has_value()) { - MetaCoreCollectMeshRendererDependencies(*gameObject.MeshRenderer, context, sourceAssetGuid); + MetaCoreCollectMeshRendererDependencies(*gameObject.MeshRenderer, assetDatabaseService, context, sourceAssetGuid); } if (gameObject.PrefabInstance.has_value()) { MetaCoreEnqueueCookDependency(context, gameObject.PrefabInstance->PrefabAssetGuid, sourceAssetGuid, "PrefabInstance.PrefabAssetGuid"); @@ -5068,11 +5626,11 @@ void MetaCoreCollectCookDependenciesForAsset( if (assetRecord->Type == "scene") { if (const auto document = MetaCoreLoadSceneDocumentForCookDependencies(project, *assetRecord, registry); document.has_value()) { - MetaCoreCollectGameObjectDependencies(document->GameObjects, context, assetGuid); + MetaCoreCollectGameObjectDependencies(document->GameObjects, assetDatabaseService, context, assetGuid); } } else if (assetRecord->Type == "prefab") { if (const auto document = MetaCoreLoadPrefabDocumentForCookDependencies(project, *assetRecord, packageService, registry); document.has_value()) { - MetaCoreCollectGameObjectDependencies(document->GameObjects, context, assetGuid); + MetaCoreCollectGameObjectDependencies(document->GameObjects, assetDatabaseService, context, assetGuid); } } else if (assetRecord->Type == "material") { if (const auto document = MetaCoreLoadMaterialDocumentForCookDependencies(project, *assetRecord, packageService, registry); document.has_value()) { @@ -5471,19 +6029,20 @@ bool MetaCoreBuiltinCookService::CookAsset(const MetaCoreAssetGuid& assetGuid) { std::optional MetaCoreBuiltinAssetEditingService::LoadModelAsset( const MetaCoreAssetGuid& assetGuid ) const { - if (const auto cacheIterator = ModelCache_.find(assetGuid); cacheIterator != ModelCache_.end()) { - return cacheIterator->second; - } - if (AssetDatabaseService_ == nullptr || PackageService_ == nullptr || ReflectionRegistry_ == nullptr) { return std::nullopt; } const auto assetRecord = AssetDatabaseService_->FindAssetByGuid(assetGuid); if (!assetRecord.has_value()) { + ModelCache_.erase(assetGuid); return std::nullopt; } + if (const auto cacheIterator = ModelCache_.find(assetGuid); cacheIterator != ModelCache_.end()) { + return cacheIterator->second; + } + const std::filesystem::path relativePackagePath = !assetRecord->PackagePath.empty() ? assetRecord->PackagePath : assetRecord->RelativePath; const auto package = @@ -5831,6 +6390,19 @@ MetaCoreBuildPlayerPackageResult MetaCoreBuiltinBuildService::BuildPlayerPackage result.Error = "No MetaCore project is open"; return result; } + if (const std::optional targetError = MetaCoreValidateBuildTarget(request.Target); targetError.has_value()) { + result.Error = "Invalid build target: " + *targetError; + return result; + } + const std::filesystem::path playerExecutable = !request.PlayerExecutablePath.empty() + ? request.PlayerExecutablePath + : MetaCoreResolveDefaultPlayerExecutablePath(); + if (const std::optional playerExecutableError = + MetaCoreValidatePlayerExecutableForTarget(playerExecutable, request.Target); + playerExecutableError.has_value()) { + result.Error = *playerExecutableError; + return result; + } (void)AssetDatabaseService_->Refresh(); @@ -5912,13 +6484,6 @@ MetaCoreBuildPlayerPackageResult MetaCoreBuiltinBuildService::BuildPlayerPackage } } - const std::filesystem::path playerExecutable = !request.PlayerExecutablePath.empty() - ? request.PlayerExecutablePath - : MetaCoreResolveDefaultPlayerExecutablePath(); - if (!std::filesystem::exists(playerExecutable)) { - result.Error = "MetaCorePlayer executable was not found: " + playerExecutable.generic_string(); - return result; - } if (!MetaCoreCopyFileForPackage(playerExecutable, result.OutputRoot / "MetaCorePlayer.exe", result)) { result.Error = "Failed to copy MetaCorePlayer executable"; return result; @@ -5942,6 +6507,12 @@ MetaCoreBuildPlayerPackageResult MetaCoreBuiltinBuildService::BuildPlayerPackage if (request.CopyLooseProjectContent) { MetaCoreCopyDirectoryForPackage(project.ScenesPath, result.OutputRoot / "Scenes", result); MetaCoreCopyDirectoryForPackage(project.AssetsPath, result.OutputRoot / "Assets", result); + } else if (request.UseCookedAssetsInPackage) { + std::string modelSourceCopyError; + if (!MetaCoreCopyCookedRuntimeModelSources(project, *AssetDatabaseService_, result.DependencyReport, result, modelSourceCopyError)) { + result.Error = modelSourceCopyError; + return result; + } } if (request.CopyRuntimeConfig) { @@ -5998,6 +6569,17 @@ MetaCoreBuildPlayerPackageResult MetaCoreBuiltinBuildService::BuildPlayerPackage result ); + std::string playerConfigError; + if (!MetaCoreWritePlayerConfigForPackage( + result.OutputRoot / "MetaCorePlayer.config.json", + request.Target, + packagedRuntimeProject, + playerConfigError)) { + result.Error = playerConfigError; + return result; + } + result.CopiedFiles.push_back("MetaCorePlayer.config.json"); + result.Success = true; return result; } @@ -6006,10 +6588,25 @@ bool MetaCoreBuiltinImportPipelineService::RefreshImports() { if (AssetDatabaseService_ == nullptr || PackageService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) { return false; } + if (!MetaCoreDirectoryExistsNoThrow(AssetDatabaseService_->GetProjectDescriptor().AssetsPath)) { + return false; + } bool importedAnything = false; - for (const auto& entry : std::filesystem::recursive_directory_iterator(AssetDatabaseService_->GetProjectDescriptor().AssetsPath)) { - if (!entry.is_regular_file() || + std::error_code iteratorError; + for (std::filesystem::recursive_directory_iterator iterator( + AssetDatabaseService_->GetProjectDescriptor().AssetsPath, + std::filesystem::directory_options::skip_permission_denied, + iteratorError), + end; + iterator != end; + iterator.increment(iteratorError)) { + if (iteratorError) { + iteratorError.clear(); + continue; + } + const auto& entry = *iterator; + if (!MetaCoreEntryIsRegularFileNoThrow(entry) || MetaCoreIsMetaPath(entry.path()) || MetaCoreIsLegacyMetaPath(entry.path()) || MetaCoreIsPackagePath(entry.path())) { @@ -6047,18 +6644,33 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste ? relativeSourcePath : absolutePackagePath.lexically_relative(AssetDatabaseService_->GetProjectDescriptor().RootPath); const std::filesystem::path absoluteMetaPath = MetaCoreBuildMetaPath(absoluteSourcePath); + const std::string detectedAssetType = MetaCoreDetectAssetType(absoluteSourcePath); + const std::string detectedImporterId = MetaCoreDetectImporterId(absoluteSourcePath); MetaCoreAssetMetadataDocument metadata; if (std::filesystem::exists(absoluteMetaPath)) { (void)MetaCoreReadMetaJson(absoluteMetaPath, metadata); } + const bool metadataMatchesSource = + metadata.AssetGuid.IsValid() && + metadata.AssetType == detectedAssetType && + metadata.ImporterId == detectedImporterId && + metadata.SourceHash == *sourceHash && + metadata.SourcePath.lexically_normal() == relativeSourcePath.lexically_normal() && + metadata.PackagePath.lexically_normal() == relativePackagePath.lexically_normal() && + (metadata.AssetType != "model" || + (!metadata.SubAssets.empty() && MetaCorePathExistsNoThrow(absolutePackagePath))); + if (!forcePackageWrite && metadataMatchesSource) { + return false; + } + if (!metadata.AssetGuid.IsValid()) { metadata.AssetGuid = MetaCoreAssetGuid::Generate(); } - metadata.AssetType = MetaCoreDetectAssetType(absoluteSourcePath); - metadata.ImporterId = MetaCoreDetectImporterId(absoluteSourcePath); + metadata.AssetType = detectedAssetType; + metadata.ImporterId = detectedImporterId; metadata.SourcePath = relativeSourcePath; metadata.PackagePath = relativePackagePath; metadata.SourceHash = *sourceHash; @@ -6068,7 +6680,7 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste ? MetaCoreReadExistingModelImportSettings(*PackageService_, registry, absolutePackagePath) : MetaCoreBuildDefaultModelImportSettings(); - bool needsPackageWrite = forcePackageWrite || !std::filesystem::exists(absolutePackagePath); + bool needsPackageWrite = forcePackageWrite || !MetaCorePathExistsNoThrow(absolutePackagePath); if (!needsPackageWrite) { const auto package = PackageService_->ReadPackage(absolutePackagePath); needsPackageWrite = !package.has_value() || package->Header.SourceHash != *sourceHash; @@ -6142,17 +6754,11 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste assetPackage.PayloadSections.push_back(std::move(payload)); } - // 【敏捷开发决策】:开发迭代期暂不生成与烘焙体积沉重的 .mcasset 二进制包,仅写入 .mcmeta 索引,直接加载源 GLB 文件! - // 原始二进制烘焙代码将在下一个阶段(打包发版)由 Cooker 模块启用。 - /* - (void)MetaCoreWriteModelAssetPackageWithRuntimeCopy( - *PackageService_, - AssetDatabaseService_->GetProjectDescriptor().RootPath, - absolutePackagePath, - metadata.AssetGuid, - assetPackage - ); - */ + // 写入轻量模型文档 package,避免首次实例化时再次同步解析大型 GLB。 + // mesh/texture payload 仍不在编辑期烘焙,渲染继续直接加载源 GLB。 + if (!PackageService_->WritePackage(absolutePackagePath, std::move(assetPackage))) { + return false; + } if (MetaCoreWriteMetaJson(absoluteMetaPath, metadata)) { // 暂时忽略纹理等子资产运行时二进制包的物理生成 /* @@ -7700,6 +8306,98 @@ std::optional MetaCoreBuiltinSceneEditingService::CreateGameObject( return createdObjectId; } +void MetaCoreHydrateGeneratedMeshesFromMetadata( + MetaCoreModelAssetDocument& document, + const std::filesystem::path& absoluteMetaPath +) { + if (!document.GeneratedMeshAssets.empty()) { + return; + } + + MetaCoreAssetMetadataDocument metadata{}; + if (!MetaCoreReadMetaJson(absoluteMetaPath, metadata)) { + return; + } + + for (const MetaCoreSubAssetMetadata& subAsset : metadata.SubAssets) { + if (subAsset.Type != "mesh") { + continue; + } + + MetaCoreMeshAssetDocument meshAsset{}; + meshAsset.AssetGuid = subAsset.Guid; + meshAsset.Name = !subAsset.Name.empty() ? subAsset.Name : "Mesh_" + std::to_string(document.GeneratedMeshAssets.size()); + meshAsset.StableImportKey = MetaCoreBuildStableImportKey("mesh", meshAsset.Name, subAsset.Index); + document.GeneratedMeshAssets.push_back(std::move(meshAsset)); + + MetaCoreImportedGltfMeshDocument meshDocument{}; + meshDocument.Name = document.GeneratedMeshAssets.back().Name; + meshDocument.PrimitiveCount = 1; + document.Meshes.push_back(std::move(meshDocument)); + } +} + +void MetaCoreEnsureRenderableModelNode(MetaCoreModelAssetDocument& document, const std::filesystem::path& fallbackSourcePath) { + const bool hasRenderableNode = std::any_of( + document.Nodes.begin(), + document.Nodes.end(), + [](const MetaCoreImportedGltfNodeDocument& node) { return node.MeshIndex >= 0; } + ); + if (document.GeneratedMeshAssets.empty()) { + return; + } + + while (document.Meshes.size() < document.GeneratedMeshAssets.size()) { + const std::size_t meshIndex = document.Meshes.size(); + MetaCoreImportedGltfMeshDocument fallbackMesh{}; + fallbackMesh.Name = !document.GeneratedMeshAssets[meshIndex].Name.empty() + ? document.GeneratedMeshAssets[meshIndex].Name + : "Mesh_" + std::to_string(meshIndex); + fallbackMesh.PrimitiveCount = 1; + document.Meshes.push_back(std::move(fallbackMesh)); + } + + const bool shouldExpandSingleRenderableRoot = + hasRenderableNode && + document.Nodes.size() == 1 && + document.GeneratedMeshAssets.size() > 1; + + if (hasRenderableNode && !shouldExpandSingleRenderableRoot) { + return; + } + + MetaCoreImportedGltfNodeDocument fallbackNode{}; + fallbackNode.Name = !document.RootDisplayName.empty() + ? document.RootDisplayName + : (!fallbackSourcePath.stem().empty() ? fallbackSourcePath.stem().generic_string() : "Model"); + fallbackNode.StableNodePath = fallbackNode.Name; + fallbackNode.ParentIndex = -1; + fallbackNode.MeshIndex = document.GeneratedMeshAssets.size() == 1 ? 0 : -1; + document.Nodes.clear(); + document.Nodes.push_back(std::move(fallbackNode)); + + if (document.GeneratedMeshAssets.size() > 1) { + for (std::size_t meshIndex = 0; meshIndex < document.GeneratedMeshAssets.size(); ++meshIndex) { + MetaCoreImportedGltfNodeDocument meshNode{}; + meshNode.Name = !document.GeneratedMeshAssets[meshIndex].Name.empty() + ? document.GeneratedMeshAssets[meshIndex].Name + : "Mesh_" + std::to_string(meshIndex); + meshNode.StableNodePath = document.Nodes.front().Name + "/" + meshNode.Name; + meshNode.ParentIndex = 0; + meshNode.MeshIndex = static_cast(meshIndex); + document.Nodes.push_back(std::move(meshNode)); + } + } + + MetaCoreAddImportMessage( + document.ImportReport, + MetaCoreImportMessageSeverity::Warning, + "fallback_renderable_nodes", + "模型导入文档缺少可用层级,已根据 mesh 子资产生成实例化节点" + ); + MetaCoreFinalizeImportReport(document.ImportReport); +} + std::optional MetaCoreBuiltinSceneEditingService::InstantiateModelAsset( MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& assetGuid, @@ -7712,48 +8410,45 @@ std::optional MetaCoreBuiltinSceneEditingService::InstantiateModelAs return std::nullopt; } - const auto assetRecord = AssetDatabaseService_->FindAssetByGuid(assetGuid); + auto assetRecord = AssetDatabaseService_->FindAssetByGuid(assetGuid); if (!assetRecord.has_value() || assetRecord->Type != "model") { return std::nullopt; } - const std::filesystem::path relativePackagePath = - !assetRecord->PackagePath.empty() ? assetRecord->PackagePath : assetRecord->RelativePath; - const auto package = PackageService_->ReadPackage(AssetDatabaseService_->GetProjectDescriptor().RootPath / relativePackagePath); - std::optional importedDocument; + auto readModelPackage = [&]() -> std::optional { + if (!assetRecord.has_value()) { + return std::nullopt; + } - if (package.has_value()) { - importedDocument = MetaCoreReadModelAssetDocument(*package, ReflectionRegistry_->GetTypeRegistry()); - } + const std::filesystem::path relativePackagePath = + !assetRecord->PackagePath.empty() ? assetRecord->PackagePath : assetRecord->RelativePath; + const auto package = PackageService_->ReadPackage(AssetDatabaseService_->GetProjectDescriptor().RootPath / relativePackagePath); + if (!package.has_value()) { + return std::nullopt; + } + return MetaCoreReadModelAssetDocument(*package, ReflectionRegistry_->GetTypeRegistry()); + }; + + importedDocument = readModelPackage(); if (!importedDocument.has_value() || importedDocument->Nodes.empty()) { - // 【敏捷开发决策】:如果磁盘上不存在物理的 .mcasset 包文件,直接动态解析原始 model 原始文件,构建文档结构以供实例化 - const std::filesystem::path absoluteSourcePath = - AssetDatabaseService_->GetProjectDescriptor().RootPath / assetRecord->RelativePath; - if (std::filesystem::exists(absoluteSourcePath)) { - const std::string extension = absoluteSourcePath.extension().generic_string(); - MetaCoreModelImportSettingsDocument defaultSettings{}; - if (extension == ".glb" || extension == ".gltf") { - std::vector> meshPayloads; - importedDocument = MetaCoreBuildImportedGltfAssetDocument( - absoluteSourcePath, - assetRecord->RelativePath, - assetRecord->SourceHash, - meshPayloads, - defaultSettings - ); - } else if (extension == ".obj" || extension == ".fbx") { - importedDocument = MetaCoreBuildImportedGenericModelAssetDocument( - absoluteSourcePath, - assetRecord->RelativePath, - assetRecord->SourceHash, - defaultSettings - ); + // Missing or stale model packages must be repaired by the import pipeline once, + // not reparsed ad-hoc on every first hierarchy instantiation. + if (AssetDatabaseService_->ReimportAsset(assetGuid)) { + assetRecord = AssetDatabaseService_->FindAssetByGuid(assetGuid); + if (assetRecord.has_value() && assetRecord->Type == "model") { + importedDocument = readModelPackage(); } } } + if (importedDocument.has_value()) { + const std::filesystem::path absoluteMetaPath = AssetDatabaseService_->GetProjectDescriptor().RootPath / assetRecord->MetaPath; + MetaCoreHydrateGeneratedMeshesFromMetadata(*importedDocument, absoluteMetaPath); + MetaCoreEnsureRenderableModelNode(*importedDocument, assetRecord->RelativePath); + } + if (!importedDocument.has_value() || importedDocument->Nodes.empty()) { return std::nullopt; } diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index fb4945b..e491843 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -75,11 +76,16 @@ void MetaCoreRevealInExplorer(const std::filesystem::path& absolutePath) { void MetaCoreLaunchPlayer(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIScenePersistenceService& scenePersistenceService); void MetaCorePollPlayerProcess(MetaCoreEditorContext& editorContext); +[[nodiscard]] const char* MetaCoreImportResultLabel(MetaCoreModelImportResult result); +[[nodiscard]] const char* MetaCoreImportMessageSeverityLabel(MetaCoreImportMessageSeverity severity); +[[nodiscard]] int CountImportMessages(const MetaCoreModelImportReportDocument& report, MetaCoreImportMessageSeverity severity); static std::string GeneratedResourceFilter_{}; static std::string GeneratedResourceKindFilter_{}; static std::array AssetFilterBuffer_{}; static std::filesystem::path PendingRenamePath_{}; +static std::filesystem::path PendingDeletePath_{}; +static std::vector PendingDeleteImpactLines_{}; static std::array RenameBuffer_{}; static std::array NewFolderBuffer_{}; static float IconSize_ = 64.0f; @@ -645,6 +651,61 @@ void MetaCoreMoveProjectPath( } } +[[nodiscard]] MetaCoreLogLevel MetaCoreLogLevelForImportMessage(MetaCoreImportMessageSeverity severity) { + switch (severity) { + case MetaCoreImportMessageSeverity::Error: + return MetaCoreLogLevel::Error; + case MetaCoreImportMessageSeverity::Warning: + return MetaCoreLogLevel::Warning; + case MetaCoreImportMessageSeverity::Info: + default: + return MetaCoreLogLevel::Info; + } +} + +void MetaCoreLogReimportReport( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetRecord& assetRecord, + const MetaCoreModelImportReportDocument& report +) { + const int warningCount = CountImportMessages(report, MetaCoreImportMessageSeverity::Warning); + const int errorCount = CountImportMessages(report, MetaCoreImportMessageSeverity::Error); + const MetaCoreLogLevel summaryLevel = errorCount > 0 || report.Result == MetaCoreModelImportResult::Error + ? MetaCoreLogLevel::Error + : (warningCount > 0 || report.Result == MetaCoreModelImportResult::Warning || report.UsedFallback + ? MetaCoreLogLevel::Warning + : MetaCoreLogLevel::Info); + + editorContext.AddConsoleMessage( + summaryLevel, + "Import", + "Reimport " + assetRecord.RelativePath.generic_string() + ": " + + MetaCoreImportResultLabel(report.Result) + + " (Warnings: " + std::to_string(warningCount) + + ", Errors: " + std::to_string(errorCount) + + ", Fallback: " + (report.UsedFallback ? "Yes" : "No") + ")" + ); + + constexpr std::size_t maxMessagesToLog = 5; + for (std::size_t messageIndex = 0; messageIndex < report.Messages.size() && messageIndex < maxMessagesToLog; ++messageIndex) { + const auto& message = report.Messages[messageIndex]; + editorContext.AddConsoleMessage( + MetaCoreLogLevelForImportMessage(message.Severity), + "Import", + std::string(MetaCoreImportMessageSeverityLabel(message.Severity)) + + (message.Code.empty() ? "" : " [" + message.Code + "]") + + ": " + message.Message + ); + } + if (report.Messages.size() > maxMessagesToLog) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Import", + "Additional import messages: " + std::to_string(report.Messages.size() - maxMessagesToLog) + " hidden. Open the asset Inspector for full details." + ); + } +} + void MetaCoreReimportProjectAsset(MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& assetGuid) { const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); if (assetDatabaseService == nullptr || !assetGuid.IsValid()) { @@ -663,6 +724,14 @@ void MetaCoreReimportProjectAsset(MetaCoreEditorContext& editorContext, const Me "Assets", "Reimported asset: " + assetRecord->RelativePath.generic_string() ); + if (assetRecord->Type == "model") { + const auto assetEditingService = editorContext.GetModuleRegistry().ResolveService(); + if (assetEditingService != nullptr) { + if (const auto reimportedDocument = assetEditingService->LoadModelAsset(assetGuid)) { + MetaCoreLogReimportReport(editorContext, *assetRecord, reimportedDocument->ImportReport); + } + } + } } else { editorContext.AddConsoleMessage( MetaCoreLogLevel::Error, @@ -1830,17 +1899,58 @@ struct MetaCoreGeneratedAssetChoice { std::string Label{}; }; -[[nodiscard]] std::vector MetaCoreCollectTextureAssetChoices(MetaCoreEditorContext& editorContext) { - std::vector choices; - - // 基础版暂时返回空,避免每帧遍历资产数据库导致卡顿 - return choices; +struct MetaCoreTextureAssetChoicesCache { + std::uint64_t Fingerprint = 0; + std::vector Choices{}; + bool Valid = false; +}; +[[nodiscard]] std::uint64_t MetaCoreBuildTextureAssetChoicesFingerprint( + const MetaCoreIAssetDatabaseService& assetDatabaseService +) { + std::uint64_t fingerprint = MetaCoreHashString(assetDatabaseService.GetProjectDescriptor().RootPath.generic_string()); + for (const MetaCoreAssetRecord& record : assetDatabaseService.GetAssetRegistry()) { + if (record.Type != "model") { + continue; + } + + fingerprint = MetaCoreHashCombine(fingerprint, MetaCoreHashString(record.Guid.ToString())); + fingerprint = MetaCoreHashCombine(fingerprint, MetaCoreHashString(record.RelativePath.generic_string())); + fingerprint = MetaCoreHashCombine(fingerprint, MetaCoreHashString(record.PackagePath.generic_string())); + fingerprint = MetaCoreHashCombine(fingerprint, MetaCoreHashString(record.ImporterId)); + fingerprint = MetaCoreHashCombine(fingerprint, record.SourceHash); + + const std::filesystem::path relativePackagePath = + !record.PackagePath.empty() ? record.PackagePath : record.RelativePath; + const std::filesystem::path absolutePackagePath = + assetDatabaseService.GetProjectDescriptor().RootPath / relativePackagePath; + std::error_code errorCode; + const auto packageSize = std::filesystem::file_size(absolutePackagePath, errorCode); + fingerprint = MetaCoreHashCombine(fingerprint, errorCode ? 0 : packageSize); + errorCode.clear(); + const auto packageWriteTime = std::filesystem::last_write_time(absolutePackagePath, errorCode); + fingerprint = MetaCoreHashCombine( + fingerprint, + errorCode ? 0 : static_cast(packageWriteTime.time_since_epoch().count()) + ); + } + return fingerprint; +} + +[[nodiscard]] std::vector MetaCoreCollectTextureAssetChoices(MetaCoreEditorContext& editorContext) { + static MetaCoreTextureAssetChoicesCache cache; + std::vector choices; const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) { + cache = MetaCoreTextureAssetChoicesCache{}; return choices; } + const std::uint64_t fingerprint = MetaCoreBuildTextureAssetChoicesFingerprint(*assetDatabaseService); + if (cache.Valid && cache.Fingerprint == fingerprint) { + return cache.Choices; + } + for (const MetaCoreAssetRecord& record : assetDatabaseService->GetAssetRegistry()) { if (record.Type != "model") { continue; @@ -1862,6 +1972,9 @@ struct MetaCoreGeneratedAssetChoice { std::sort(choices.begin(), choices.end(), [](const MetaCoreGeneratedAssetChoice& lhs, const MetaCoreGeneratedAssetChoice& rhs) { return lhs.Label < rhs.Label; }); + cache.Fingerprint = fingerprint; + cache.Choices = choices; + cache.Valid = true; return choices; } @@ -2844,7 +2957,8 @@ private: ImGui::Separator(); ImGui::TextDisabled("Drag here to reparent to root"); const ImVec2 dropAreaMin = ImGui::GetCursorScreenPos(); - const ImVec2 dropAreaSize = ImVec2(ImGui::GetContentRegionAvail().x, 24.0F); + const float availableHeight = ImGui::GetContentRegionAvail().y; + const ImVec2 dropAreaSize = ImVec2(ImGui::GetContentRegionAvail().x, std::max(24.0F, availableHeight)); ImGui::InvisibleButton("MetaCoreHierarchyRootDrop", dropAreaSize); const ImVec2 dropAreaMax = ImVec2(dropAreaMin.x + dropAreaSize.x, dropAreaMin.y + dropAreaSize.y); @@ -3044,6 +3158,66 @@ void MetaCoreClearGeneratedResourceSelection(MetaCoreEditorContext& editorContex return objectIds; } +[[nodiscard]] std::vector FindSceneObjectsUsingSourceModelAsset( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetRecord& assetRecord +) { + std::vector objectIds; + for (const auto& gameObject : editorContext.GetScene().GetGameObjects()) { + if (!gameObject.HasComponent()) { + continue; + } + + const auto& meshRenderer = gameObject.GetComponent(); + bool isMatch = meshRenderer.SourceModelAssetGuid.IsValid() && meshRenderer.SourceModelAssetGuid == assetRecord.Guid; + if (!isMatch && !meshRenderer.SourceModelPath.empty()) { + isMatch = meshRenderer.SourceModelPath == assetRecord.RelativePath.generic_string() || + meshRenderer.SourceModelPath == assetRecord.SourcePath.generic_string(); + } + if (isMatch) { + objectIds.push_back(gameObject.GetId()); + } + } + return objectIds; +} + +[[nodiscard]] std::vector FindSceneObjectsUsingPrefabAsset( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& prefabAssetGuid +) { + std::vector objectIds; + if (!prefabAssetGuid.IsValid()) { + return objectIds; + } + + for (const auto& gameObject : editorContext.GetScene().GetGameObjects()) { + if (gameObject.HasComponent() && + gameObject.GetComponent().PrefabAssetGuid == prefabAssetGuid) { + objectIds.push_back(gameObject.GetId()); + } + } + return objectIds; +} + +void DrawReferencedObjectList(MetaCoreEditorContext& editorContext, const std::vector& objectIds, std::string_view idSuffix); + +void DrawSceneReferenceSummary( + MetaCoreEditorContext& editorContext, + const std::vector& objectIds, + std::string_view label, + std::string_view idSuffix +) { + ImGui::Text("%s: %d", std::string(label).c_str(), static_cast(objectIds.size())); + if (objectIds.empty()) { + return; + } + + if (ImGui::Button(("Highlight Scene Objects##" + std::string(idSuffix)).c_str())) { + editorContext.SetSelection(objectIds, objectIds.front()); + } + DrawReferencedObjectList(editorContext, objectIds, idSuffix); +} + void DrawReferencedObjectList(MetaCoreEditorContext& editorContext, const std::vector& objectIds, std::string_view idSuffix) { if (objectIds.empty()) return; if (ImGui::TreeNode(("引用对象列表##" + std::string(idSuffix)).c_str())) { @@ -3559,6 +3733,102 @@ void DrawModelNodeTreeRecursive( )); } +struct MetaCoreProjectAssetImportStatus { + bool HasStatus = false; + MetaCoreModelImportResult Result = MetaCoreModelImportResult::Success; + int WarningCount = 0; + int ErrorCount = 0; + bool UsedFallback = false; +}; + +struct MetaCoreProjectAssetImportStatusCacheEntry { + std::string Fingerprint{}; + MetaCoreProjectAssetImportStatus Status{}; +}; + +[[nodiscard]] bool MetaCoreProjectAssetImportStatusHasIssue(const MetaCoreProjectAssetImportStatus& status) { + return status.HasStatus && + (status.Result != MetaCoreModelImportResult::Success || status.WarningCount > 0 || status.ErrorCount > 0 || status.UsedFallback); +} + +[[nodiscard]] std::string MetaCoreBuildAssetRecordPackageFingerprint( + const std::filesystem::path& projectRoot, + const MetaCoreAssetRecord& assetRecord +) { + const std::filesystem::path packagePath = projectRoot / (!assetRecord.PackagePath.empty() ? assetRecord.PackagePath : assetRecord.RelativePath); + std::error_code ec; + const auto fileSize = std::filesystem::exists(packagePath, ec) && !ec ? std::filesystem::file_size(packagePath, ec) : 0U; + ec.clear(); + const auto writeTime = std::filesystem::exists(packagePath, ec) && !ec + ? std::filesystem::last_write_time(packagePath, ec).time_since_epoch().count() + : 0; + + return projectRoot.generic_string() + "|" + + assetRecord.Guid.ToString() + "|" + + assetRecord.RelativePath.generic_string() + "|" + + assetRecord.PackagePath.generic_string() + "|" + + std::to_string(assetRecord.SourceHash) + "|" + + std::to_string(static_cast(fileSize)) + "|" + + std::to_string(writeTime); +} + +[[nodiscard]] MetaCoreProjectAssetImportStatus GetProjectAssetImportStatus( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + const MetaCoreAssetRecord& assetRecord +) { + if (assetRecord.Type != "model") { + return {}; + } + + static std::unordered_map statusCache; + const std::string fingerprint = MetaCoreBuildAssetRecordPackageFingerprint(assetDatabaseService.GetProjectDescriptor().RootPath, assetRecord); + if (const auto cacheIterator = statusCache.find(assetRecord.Guid); + cacheIterator != statusCache.end() && cacheIterator->second.Fingerprint == fingerprint) { + return cacheIterator->second.Status; + } + + MetaCoreProjectAssetImportStatus status{}; + const auto assetEditingService = editorContext.GetModuleRegistry().ResolveService(); + if (assetEditingService != nullptr) { + if (const auto document = assetEditingService->LoadModelAsset(assetRecord.Guid)) { + status.HasStatus = true; + status.Result = document->ImportReport.Result; + status.WarningCount = CountImportMessages(document->ImportReport, MetaCoreImportMessageSeverity::Warning); + status.ErrorCount = CountImportMessages(document->ImportReport, MetaCoreImportMessageSeverity::Error); + status.UsedFallback = document->ImportReport.UsedFallback; + } + } + + statusCache[assetRecord.Guid] = MetaCoreProjectAssetImportStatusCacheEntry{fingerprint, status}; + return status; +} + +void DrawProjectAssetImportStatusTooltip(const MetaCoreProjectAssetImportStatus& status) { + if (!status.HasStatus) { + return; + } + ImGui::Text("Import: %s", MetaCoreImportResultLabel(status.Result)); + ImGui::Text("Warnings: %d", status.WarningCount); + ImGui::Text("Errors: %d", status.ErrorCount); + ImGui::Text("Fallback: %s", status.UsedFallback ? "Yes" : "No"); +} + +void DrawProjectAssetImportStatusBadge(const MetaCoreProjectAssetImportStatus& status, const ImVec2& min, const ImVec2& max) { + if (!MetaCoreProjectAssetImportStatusHasIssue(status)) { + return; + } + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const bool hasError = status.Result == MetaCoreModelImportResult::Error || status.ErrorCount > 0; + const ImU32 badgeColor = hasError ? IM_COL32(210, 70, 70, 230) : IM_COL32(210, 155, 45, 230); + const char* badgeText = hasError ? "E" : "W"; + const float radius = 8.0F; + const ImVec2 center(max.x - radius - 4.0F, min.y + radius + 4.0F); + drawList->AddCircleFilled(center, radius, badgeColor, 16); + drawList->AddText(ImVec2(center.x - 3.0F, center.y - 6.0F), IM_COL32(255, 255, 255, 255), badgeText); +} + void DrawModelAssetDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService) { const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset(); const auto assetRecord = assetDatabaseService.FindAssetByGuid(selectedAsset.Guid); @@ -3606,6 +3876,14 @@ void DrawModelAssetDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD ImGui::EndTable(); } + ImGui::Separator(); + DrawSceneReferenceSummary( + editorContext, + FindSceneObjectsUsingSourceModelAsset(editorContext, *assetRecord), + "Scene References", + "ModelAssetReferences" + ); + if (ImGui::BeginTabBar("ModelTabs")) { if (ImGui::BeginTabItem("Overview")) { ImGui::TextDisabled("Imported Structure"); @@ -4082,6 +4360,13 @@ void DrawPrefabDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatab ImGui::Text("Root Nodes:"); for (const auto& rootName : summary->RootNames) ImGui::BulletText("%s", rootName.c_str()); } + ImGui::Separator(); + DrawSceneReferenceSummary( + editorContext, + FindSceneObjectsUsingPrefabAsset(editorContext, selectedAsset.Guid), + "Scene References", + "PrefabAssetReferences" + ); ImGui::Spacing(); if (ImGui::Button("Open in Editor", ImVec2(-1, 0))) { /* Logic */ } } @@ -4440,6 +4725,15 @@ void DrawMaterialAssetDetails( ImGui::Separator(); ImGui::Spacing(); + DrawSceneReferenceSummary( + editorContext, + FindSceneObjectsUsingMaterial(editorContext, selectedAsset.Guid), + "Scene References", + "MaterialAssetReferences" + ); + ImGui::Separator(); + ImGui::Spacing(); + bool changed = false; float color[3] = { materialDoc.BaseColor.r, materialDoc.BaseColor.g, materialDoc.BaseColor.b }; @@ -4509,12 +4803,152 @@ void MetaCoreDrawAssetInspector(MetaCoreEditorContext& editorContext, MetaCoreIA return v.find(f) != std::string::npos; } +[[nodiscard]] bool HasAssetFilter() { + return AssetFilterBuffer_[0] != '\0'; +} + +[[nodiscard]] bool IsPathUnderDirectory( + const std::filesystem::path& path, + const std::filesystem::path& directory +) { + if (directory.empty()) { + return true; + } + + std::filesystem::path parent = path.lexically_normal().parent_path(); + const std::filesystem::path normalizedDirectory = directory.lexically_normal(); + while (!parent.empty()) { + if (parent == normalizedDirectory) { + return true; + } + const std::filesystem::path nextParent = parent.parent_path(); + if (nextParent == parent) { + break; + } + parent = nextParent; + } + return false; +} + +[[nodiscard]] bool MatchesAssetRecordFilter(const MetaCoreAssetRecord& assetRecord) { + return MatchesAssetFilter(assetRecord.RelativePath.filename().string()) || + MatchesAssetFilter(assetRecord.RelativePath.generic_string()) || + MatchesAssetFilter(assetRecord.Type) || + MatchesAssetFilter(assetRecord.Guid.ToString()); +} + [[nodiscard]] bool HasVisibleChildren(MetaCoreIAssetDatabaseService& assetDatabaseService, const std::filesystem::path& dir) { for (const auto& d : assetDatabaseService.GetDirectoriesUnder(dir)) if (d.parent_path() == dir && (MatchesAssetFilter(d.filename().string()) || HasVisibleChildren(assetDatabaseService, d))) return true; - for (const auto& a : assetDatabaseService.GetAssetsUnder(dir)) if (MatchesAssetFilter(a.RelativePath.filename().string()) || MatchesAssetFilter(a.Type)) return true; + for (const auto& a : assetDatabaseService.GetAssetsUnder(dir)) if (MatchesAssetRecordFilter(a)) return true; return false; } +[[nodiscard]] std::vector CollectAssetsForDeletePath( + MetaCoreIAssetDatabaseService& assetDatabaseService, + const std::filesystem::path& relativePath +) { + std::vector assets; + if (const auto directAsset = assetDatabaseService.FindAssetByRelativePath(relativePath)) { + assets.push_back(*directAsset); + return assets; + } + + for (const auto& assetRecord : assetDatabaseService.GetAssetRegistry()) { + if (IsPathUnderDirectory(assetRecord.RelativePath, relativePath)) { + assets.push_back(assetRecord); + } + } + return assets; +} + +[[nodiscard]] std::vector FindSceneObjectsUsingProjectAsset( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetRecord& assetRecord +) { + if (assetRecord.Type == "model") { + return FindSceneObjectsUsingSourceModelAsset(editorContext, assetRecord); + } + if (assetRecord.Type == "prefab") { + return FindSceneObjectsUsingPrefabAsset(editorContext, assetRecord.Guid); + } + if (assetRecord.Type == "material") { + return FindSceneObjectsUsingMaterial(editorContext, assetRecord.Guid); + } + return {}; +} + +void CopyProjectRelativePathToClipboard(const std::filesystem::path& relativePath) { + const std::string pathText = relativePath.generic_string(); + ImGui::SetClipboardText(pathText.c_str()); +} + +void CopyProjectAbsolutePathToClipboard( + const MetaCoreIAssetDatabaseService& assetDatabaseService, + const std::filesystem::path& relativePath +) { + const std::string pathText = (assetDatabaseService.GetProjectDescriptor().RootPath / relativePath).lexically_normal().string(); + ImGui::SetClipboardText(pathText.c_str()); +} + +[[nodiscard]] bool IsProjectAssetReferenceSelectable(const MetaCoreAssetRecord& assetRecord) { + return assetRecord.Type == "model" || assetRecord.Type == "prefab" || assetRecord.Type == "material"; +} + +void SelectProjectAssetReferencers(MetaCoreEditorContext& editorContext, const MetaCoreAssetRecord& assetRecord) { + const auto references = FindSceneObjectsUsingProjectAsset(editorContext, assetRecord); + if (references.empty()) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Assets", + "No current scene references found for: " + assetRecord.RelativePath.generic_string() + ); + return; + } + + editorContext.SetSelection(references, references.front()); + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Assets", + "Selected " + std::to_string(references.size()) + " scene object(s) referencing: " + assetRecord.RelativePath.generic_string() + ); +} + +[[nodiscard]] std::vector BuildDeleteImpactLines( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + const std::filesystem::path& relativePath +) { + std::vector impactLines; + for (const auto& assetRecord : CollectAssetsForDeletePath(assetDatabaseService, relativePath)) { + const auto references = FindSceneObjectsUsingProjectAsset(editorContext, assetRecord); + if (references.empty()) { + continue; + } + + impactLines.push_back( + assetRecord.RelativePath.generic_string() + " [" + assetRecord.Type + "] is referenced by " + + std::to_string(references.size()) + " scene object(s)." + ); + } + return impactLines; +} + +void RequestDeleteProjectPath( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + const std::filesystem::path& relativePath +) { + const auto impactLines = BuildDeleteImpactLines(editorContext, assetDatabaseService, relativePath); + if (impactLines.empty()) { + (void)assetDatabaseService.DeletePath(relativePath); + return; + } + + PendingDeletePath_ = relativePath; + PendingDeleteImpactLines_ = impactLines; + ImGui::OpenPopup("Delete Asset Impact"); +} + void DrawDirectoryTree( MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, @@ -4556,6 +4990,12 @@ public: ImGui::TextDisabled("%s | %s", assetDatabaseService->GetProjectDescriptor().Name.c_str(), assetDatabaseService->GetProjectDescriptor().RootPath.string().c_str()); ImGui::InputTextWithHint("##Filter", "Filter Assets...", AssetFilterBuffer_.data(), AssetFilterBuffer_.size()); + if (HasAssetFilter()) { + ImGui::SameLine(); + if (ImGui::SmallButton("Clear##ProjectFilter")) { + AssetFilterBuffer_[0] = '\0'; + } + } ImGui::Columns(2, "ProjectCols", true); DrawDirectoryTree(editorContext, *assetDatabaseService, "Assets", "Assets"); @@ -4622,12 +5062,15 @@ public: MetaCoreAssetGuid Guid{}; std::string Type{}; MetaCoreAssetStorageKind StorageKind = MetaCoreAssetStorageKind::SourcePackage; + std::filesystem::path PackagePath{}; + std::uint64_t SourceHash = 0; }; std::vector items; + const bool recursiveSearch = HasAssetFilter(); for (const auto& d : assetDatabaseService->GetDirectoriesUnder(selectedDirectory)) { - if (MatchesAssetFilter(d.filename().string())) { + if (MatchesAssetFilter(d.filename().string()) || (recursiveSearch && HasVisibleChildren(*assetDatabaseService, d))) { ProjectItem item{}; item.IsDirectory = true; item.Path = d; @@ -4635,15 +5078,38 @@ public: } } - for (const auto& a : assetDatabaseService->GetAssetsUnder(selectedDirectory)) { - if (!MatchesAssetFilter(a.RelativePath.filename().string()) && !MatchesAssetFilter(a.Type)) continue; - ProjectItem item{}; - item.IsDirectory = false; - item.Path = a.RelativePath; - item.Guid = a.Guid; - item.Type = a.Type; - item.StorageKind = a.StorageKind; - items.push_back(item); + if (recursiveSearch) { + for (const auto& a : assetDatabaseService->GetAssetRegistry()) { + if (!IsPathUnderDirectory(a.RelativePath, selectedDirectory) || !MatchesAssetRecordFilter(a)) { + continue; + } + ProjectItem item{}; + item.IsDirectory = false; + item.Path = a.RelativePath; + item.Guid = a.Guid; + item.Type = a.Type; + item.StorageKind = a.StorageKind; + item.PackagePath = a.PackagePath; + item.SourceHash = a.SourceHash; + items.push_back(item); + } + } else { + for (const auto& a : assetDatabaseService->GetAssetsUnder(selectedDirectory)) { + if (!MatchesAssetRecordFilter(a)) continue; + ProjectItem item{}; + item.IsDirectory = false; + item.Path = a.RelativePath; + item.Guid = a.Guid; + item.Type = a.Type; + item.StorageKind = a.StorageKind; + item.PackagePath = a.PackagePath; + item.SourceHash = a.SourceHash; + items.push_back(item); + } + } + + if (recursiveSearch) { + ImGui::TextDisabled("%zu matching item(s) under %s", items.size(), selectedDirectory.generic_string().c_str()); } // 4. 渲染视图项 @@ -4662,6 +5128,21 @@ public: labelText += " [" + item.Type + "]"; } + MetaCoreProjectAssetImportStatus importStatus{}; + if (!item.IsDirectory) { + MetaCoreAssetRecord statusRecord{}; + statusRecord.Guid = item.Guid; + statusRecord.RelativePath = item.Path; + statusRecord.Type = item.Type; + statusRecord.StorageKind = item.StorageKind; + statusRecord.PackagePath = item.PackagePath; + statusRecord.SourceHash = item.SourceHash; + importStatus = GetProjectAssetImportStatus(editorContext, *assetDatabaseService, statusRecord); + if (MetaCoreProjectAssetImportStatusHasIssue(importStatus)) { + labelText += importStatus.ErrorCount > 0 || importStatus.Result == MetaCoreModelImportResult::Error ? " [Import Error]" : " [Import Warning]"; + } + } + bool isSelected = false; if (item.IsDirectory) { isSelected = (selectedDirectory == item.Path); @@ -4669,8 +5150,14 @@ public: isSelected = (editorContext.GetSelectedAsset().Guid == item.Guid); } - ImGui::PushID(item.IsDirectory ? item.Path.string().c_str() : item.Guid.ToString().c_str()); + const std::string itemUiId = (item.IsDirectory ? "dir:" : "asset:") + item.Path.generic_string(); + ImGui::PushID(itemUiId.c_str()); const bool clicked = ImGui::Selectable(labelText.c_str(), isSelected); + if (ImGui::IsItemHovered() && MetaCoreProjectAssetImportStatusHasIssue(importStatus)) { + ImGui::BeginTooltip(); + DrawProjectAssetImportStatusTooltip(importStatus); + ImGui::EndTooltip(); + } // 2. 用 ImDrawList 在该行的左侧空白处绘制 10px 精美矢量微型图标 (避免缺失显示为问号) ImDrawList* drawList = ImGui::GetWindowDrawList(); @@ -4745,6 +5232,8 @@ public: a.RelativePath = item.Path; a.Type = item.Type; a.StorageKind = item.StorageKind; + a.PackagePath = item.PackagePath; + a.SourceHash = item.SourceHash; MetaCoreBeginProjectAssetDragDropSource(a); } @@ -4753,6 +5242,12 @@ public: if (ImGui::MenuItem("Reveal in Explorer")) { MetaCoreRevealInExplorer(assetDatabaseService->GetProjectDescriptor().RootPath / item.Path); } + if (ImGui::MenuItem("Copy Relative Path")) { + CopyProjectRelativePathToClipboard(item.Path); + } + if (ImGui::MenuItem("Copy Absolute Path")) { + CopyProjectAbsolutePathToClipboard(*assetDatabaseService, item.Path); + } ImGui::Separator(); if (ImGui::MenuItem("Rename...")) { PendingRenamePath_ = item.Path; @@ -4760,7 +5255,7 @@ public: openRenamePopup = true; } if (ImGui::MenuItem("Delete")) { - (void)assetDatabaseService->DeletePath(item.Path); + RequestDeleteProjectPath(editorContext, *assetDatabaseService, item.Path); } } else { if (ImGui::MenuItem("Open")) { @@ -4788,8 +5283,19 @@ public: const std::string guidText = item.Guid.ToString(); ImGui::SetClipboardText(guidText.c_str()); } + if (ImGui::MenuItem("Copy Relative Path")) { + CopyProjectRelativePathToClipboard(item.Path); + } + if (ImGui::MenuItem("Copy Absolute Path")) { + CopyProjectAbsolutePathToClipboard(*assetDatabaseService, item.Path); + } ImGui::Separator(); const auto fullRecord = assetDatabaseService->FindAssetByGuid(item.Guid); + const bool canSelectReferencers = fullRecord.has_value() && IsProjectAssetReferenceSelectable(*fullRecord); + if (ImGui::MenuItem("Select Scene Referencers", nullptr, false, canSelectReferencers)) { + SelectProjectAssetReferencers(editorContext, *fullRecord); + } + ImGui::Separator(); const bool canReimport = fullRecord.has_value() && !fullRecord->SourcePath.empty(); if (ImGui::MenuItem("Reimport", nullptr, false, canReimport)) { MetaCoreReimportProjectAsset(editorContext, item.Guid); @@ -4801,7 +5307,7 @@ public: openRenamePopup = true; } if (ImGui::MenuItem("Delete")) { - (void)assetDatabaseService->DeletePath(item.Path); + RequestDeleteProjectPath(editorContext, *assetDatabaseService, item.Path); } } ImGui::EndPopup(); @@ -4813,6 +5319,8 @@ public: a.RelativePath = item.Path; a.Type = item.Type; a.StorageKind = item.StorageKind; + a.PackagePath = item.PackagePath; + a.SourceHash = item.SourceHash; const auto modelDocument = MetaCoreLoadModelAssetDocument(editorContext, a); if (modelDocument.has_value()) { ImGui::Indent(18.0F); @@ -4858,7 +5366,20 @@ public: isSelected = (editorContext.GetSelectedAsset().Guid == item.Guid); } - ImGui::PushID(item.IsDirectory ? item.Path.string().c_str() : item.Guid.ToString().c_str()); + MetaCoreProjectAssetImportStatus importStatus{}; + if (!item.IsDirectory) { + MetaCoreAssetRecord statusRecord{}; + statusRecord.Guid = item.Guid; + statusRecord.RelativePath = item.Path; + statusRecord.Type = item.Type; + statusRecord.StorageKind = item.StorageKind; + statusRecord.PackagePath = item.PackagePath; + statusRecord.SourceHash = item.SourceHash; + importStatus = GetProjectAssetImportStatus(editorContext, *assetDatabaseService, statusRecord); + } + + const std::string itemUiId = (item.IsDirectory ? "dir:" : "asset:") + item.Path.generic_string(); + ImGui::PushID(itemUiId.c_str()); const ImVec2 cardSize(IconSize_ + 16.0f, IconSize_ + 36.0f); ImGui::BeginGroup(); @@ -4882,6 +5403,8 @@ public: } const bool clicked = ImGui::Selectable("##card", isSelected, 0, cardSize); + const ImVec2 cardMin = ImGui::GetItemRectMin(); + const ImVec2 cardMax = ImGui::GetItemRectMax(); ImGui::PopStyleColor(3); // 3. 用 ImDrawList 绘制大尺寸精美矢量图标 @@ -4986,6 +5509,7 @@ public: drawList->AddTriangleFilled(pFoldTop, pFoldCorner, pFoldRight, IM_COL32(180, 180, 185, 255)); drawList->AddPolyline(pts, 5, IM_COL32(150, 150, 160, 255), ImDrawFlags_Closed, 1.0f); } + DrawProjectAssetImportStatusBadge(importStatus, cardMin, cardMax); // 4. 绘制文件名 ImGui::SetCursorPos(startPos); @@ -5006,7 +5530,14 @@ public: ImGui::EndGroup(); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s\n类型: %s", item.Path.string().c_str(), item.IsDirectory ? "Folder" : item.Type.c_str()); + ImGui::BeginTooltip(); + ImGui::Text("%s", item.Path.string().c_str()); + ImGui::Text("Type: %s", item.IsDirectory ? "Folder" : item.Type.c_str()); + if (MetaCoreProjectAssetImportStatusHasIssue(importStatus)) { + ImGui::Separator(); + DrawProjectAssetImportStatusTooltip(importStatus); + } + ImGui::EndTooltip(); } if (clicked) { @@ -5038,6 +5569,8 @@ public: a.RelativePath = item.Path; a.Type = item.Type; a.StorageKind = item.StorageKind; + a.PackagePath = item.PackagePath; + a.SourceHash = item.SourceHash; MetaCoreBeginProjectAssetDragDropSource(a); } @@ -5046,6 +5579,12 @@ public: if (ImGui::MenuItem("Reveal in Explorer")) { MetaCoreRevealInExplorer(assetDatabaseService->GetProjectDescriptor().RootPath / item.Path); } + if (ImGui::MenuItem("Copy Relative Path")) { + CopyProjectRelativePathToClipboard(item.Path); + } + if (ImGui::MenuItem("Copy Absolute Path")) { + CopyProjectAbsolutePathToClipboard(*assetDatabaseService, item.Path); + } ImGui::Separator(); if (ImGui::MenuItem("Rename...")) { PendingRenamePath_ = item.Path; @@ -5053,7 +5592,7 @@ public: openRenamePopup = true; } if (ImGui::MenuItem("Delete")) { - (void)assetDatabaseService->DeletePath(item.Path); + RequestDeleteProjectPath(editorContext, *assetDatabaseService, item.Path); } } else { if (ImGui::MenuItem("Open")) { @@ -5081,8 +5620,19 @@ public: const std::string guidText = item.Guid.ToString(); ImGui::SetClipboardText(guidText.c_str()); } + if (ImGui::MenuItem("Copy Relative Path")) { + CopyProjectRelativePathToClipboard(item.Path); + } + if (ImGui::MenuItem("Copy Absolute Path")) { + CopyProjectAbsolutePathToClipboard(*assetDatabaseService, item.Path); + } ImGui::Separator(); const auto fullRecord = assetDatabaseService->FindAssetByGuid(item.Guid); + const bool canSelectReferencers = fullRecord.has_value() && IsProjectAssetReferenceSelectable(*fullRecord); + if (ImGui::MenuItem("Select Scene Referencers", nullptr, false, canSelectReferencers)) { + SelectProjectAssetReferencers(editorContext, *fullRecord); + } + ImGui::Separator(); const bool canReimport = fullRecord.has_value() && !fullRecord->SourcePath.empty(); if (ImGui::MenuItem("Reimport", nullptr, false, canReimport)) { MetaCoreReimportProjectAsset(editorContext, item.Guid); @@ -5094,7 +5644,7 @@ public: openRenamePopup = true; } if (ImGui::MenuItem("Delete")) { - (void)assetDatabaseService->DeletePath(item.Path); + RequestDeleteProjectPath(editorContext, *assetDatabaseService, item.Path); } } ImGui::EndPopup(); @@ -5171,6 +5721,43 @@ public: ImGui::EndPopup(); } + if (ImGui::BeginPopupModal("Delete Asset Impact", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Delete: %s", PendingDeletePath_.generic_string().c_str()); + ImGui::Separator(); + ImGui::TextWrapped("This path contains assets referenced by the current scene. Deleting it may clear viewport content or leave missing asset references."); + if (!PendingDeleteImpactLines_.empty()) { + ImGui::BeginChild("DeleteImpactList", ImVec2(560.0F, 120.0F), true); + for (const auto& line : PendingDeleteImpactLines_) { + ImGui::BulletText("%s", line.c_str()); + } + ImGui::EndChild(); + } + + if (ImGui::Button("Delete Anyway", ImVec2(140, 0))) { + const bool deleted = assetDatabaseService->DeletePath(PendingDeletePath_); + if (deleted) { + const auto& selectedAsset = editorContext.GetSelectedAsset(); + if (selectedAsset.RelativePath == PendingDeletePath_ || IsPathUnderDirectory(selectedAsset.RelativePath, PendingDeletePath_)) { + editorContext.ClearSelectedAssetSubId(); + editorContext.SelectAsset({}); + } + editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "Assets", "已删除存在场景引用的资源: " + PendingDeletePath_.generic_string()); + } else { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Assets", "删除失败: " + PendingDeletePath_.generic_string()); + } + PendingDeletePath_.clear(); + PendingDeleteImpactLines_.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + PendingDeletePath_.clear(); + PendingDeleteImpactLines_.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + if (ImGui::BeginPopupModal("New Folder", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Enter folder name:"); ImGui::InputText("##FolderName", NewFolderBuffer_.data(), NewFolderBuffer_.size()); @@ -6127,24 +6714,62 @@ public: ImGui::Text("Platform"); if (ImGui::BeginChild("PlatformList", ImVec2(0, 180.0f), true)) { - ImGui::Selectable("💻 PC, Mac & Linux", true); - - ImGui::Selectable("📱 Android", false); - ImGui::Selectable("🍏 iOS", false); - ImGui::Selectable("🌐 WebGL", false); + DrawPlatformSelectable("PC, Mac & Linux", "Windows"); + DrawPlatformSelectable("Linux", "Linux"); + DrawPlatformSelectable("Android", "Android"); + DrawPlatformSelectable("Web", "Web"); } ImGui::EndChild(); ImGui::NextColumn(); ImGui::Text("Build Configuration"); + MetaCoreBuildTargetUiStatus targetStatus = BuildTargetUiStatusFromBuffers(); + MetaCoreBuildTargetUiStatus playerExecutableStatus = PlayerExecutableUiStatusFromBuffer(); + MetaCoreBuildTargetUiStatus startupSceneStatus = StartupSceneUiStatusFromBuffer(*assetDatabaseService); if (ImGui::BeginChild("ConfigDetails", ImVec2(0, 180.0f), false)) { ImGui::InputText("Startup Scene", StartupScenePathBuffer_.data(), StartupScenePathBuffer_.size()); + ImGui::SameLine(); + if (ImGui::Button("Use First Scene##StartupScene")) { + ApplyFirstEnabledBuildScene(*assetDatabaseService); + } + startupSceneStatus = StartupSceneUiStatusFromBuffer(*assetDatabaseService); + ImGui::TextColored( + startupSceneStatus.IsError ? ImVec4(1.0F, 0.35F, 0.25F, 1.0F) : ImVec4(0.35F, 0.85F, 0.45F, 1.0F), + "%s", + startupSceneStatus.Message.c_str() + ); ImGui::InputText("Startup UI", StartupUiPathBuffer_.data(), StartupUiPathBuffer_.size()); ImGui::InputText("Build Profile", BuildProfileNameBuffer_.data(), BuildProfileNameBuffer_.size()); - ImGui::InputText("Target Platform", TargetPlatformBuffer_.data(), TargetPlatformBuffer_.size()); + static constexpr const char* kPlatformOptions[] = {"Windows", "Linux", "Android", "Web"}; + static constexpr const char* kGraphicsOptions[] = {"OpenGL", "Vulkan", "WebGL", "WebGPU"}; + static constexpr const char* kArchitectureOptions[] = {"x64", "arm64", "wasm32"}; + if (DrawStringCombo("Target Platform", TargetPlatformBuffer_, kPlatformOptions, sizeof(kPlatformOptions) / sizeof(kPlatformOptions[0]))) { + ApplyPlatformPreset(TargetPlatformBuffer_.data()); + } + DrawStringCombo("Graphics Backend", GraphicsBackendBuffer_, kGraphicsOptions, sizeof(kGraphicsOptions) / sizeof(kGraphicsOptions[0])); + DrawStringCombo("Architecture", ArchitectureBuffer_, kArchitectureOptions, sizeof(kArchitectureOptions) / sizeof(kArchitectureOptions[0])); + targetStatus = BuildTargetUiStatusFromBuffers(); + ImGui::TextColored( + targetStatus.IsError + ? ImVec4(1.0F, 0.35F, 0.25F, 1.0F) + : (targetStatus.IsWarning ? ImVec4(0.95F, 0.75F, 0.25F, 1.0F) : ImVec4(0.35F, 0.85F, 0.45F, 1.0F)), + "%s", + targetStatus.Message.c_str() + ); ImGui::InputText("Player Exe", PlayerExecutablePathBuffer_.data(), PlayerExecutablePathBuffer_.size()); + ImGui::SameLine(); + if (ImGui::Button("Use Default##PlayerExe")) { + CopyPathToBuffer(PlayerExecutablePathBuffer_, ResolveDefaultPlayerExecutablePath()); + } + playerExecutableStatus = PlayerExecutableUiStatusFromBuffer(); + ImGui::TextColored( + playerExecutableStatus.IsError ? ImVec4(1.0F, 0.35F, 0.25F, 1.0F) : ImVec4(0.35F, 0.85F, 0.45F, 1.0F), + "%s", + playerExecutableStatus.Message.c_str() + ); ImGui::InputText("Output Dir", OutputDirectoryBuffer_.data(), OutputDirectoryBuffer_.size()); + ImGui::TextDisabled("Output Root: %s", BuildOutputRootPreview(project).generic_string().c_str()); ImGui::InputText("Cooked Assets Dir", CookedAssetsDirectoryBuffer_.data(), CookedAssetsDirectoryBuffer_.size()); ImGui::Checkbox("Use cooked assets", &UseCookedAssetsInPackage_); @@ -6166,17 +6791,20 @@ public: ImGui::Spacing(); // 3. 底部主按钮 (右对齐) - const bool canBuild = buildService != nullptr; - if (!canBuild) { - ImGui::BeginDisabled(); - } - // 保存配置 if (ImGui::Button("Save Settings", ImVec2(120.0f, 26.0f))) { (void)SaveBuildSettings(editorContext, project, true); } ImGui::SameLine(); + const bool targetCanBePackaged = !targetStatus.IsError && !targetStatus.IsWarning; + const bool playerExecutableCanBePackaged = !playerExecutableStatus.IsError; + const bool startupSceneCanBePackaged = !startupSceneStatus.IsError; + const bool canBuild = buildService != nullptr && targetCanBePackaged && playerExecutableCanBePackaged && startupSceneCanBePackaged; + if (!canBuild) { + ImGui::BeginDisabled(); + } + // 打包 (Build) if (ImGui::Button("Build", ImVec2(100.0f, 26.0f))) { BuildPackage(editorContext, *buildService, false); @@ -6190,6 +6818,10 @@ public: if (!canBuild) { ImGui::EndDisabled(); + ImGui::TextDisabled( + "Build disabled: %s", + BuildDisabledReason(buildService.get(), targetStatus, playerExecutableStatus, startupSceneStatus).c_str() + ); } if (!LastOutputRoot_.empty()) { @@ -6207,6 +6839,264 @@ private: std::snprintf(buffer.data(), buffer.size(), "%s", value.c_str()); } + static bool DrawStringCombo( + const char* label, + std::array& buffer, + const char* const* options, + std::size_t optionCount + ) { + std::size_t selectedIndex = optionCount; + for (std::size_t index = 0; index < optionCount; ++index) { + if (std::strcmp(buffer.data(), options[index]) == 0) { + selectedIndex = index; + break; + } + } + + const char* preview = selectedIndex < optionCount ? options[selectedIndex] : buffer.data(); + bool changed = false; + if (ImGui::BeginCombo(label, preview != nullptr && preview[0] != '\0' ? preview : "")) { + for (std::size_t index = 0; index < optionCount; ++index) { + const bool selected = selectedIndex == index; + if (ImGui::Selectable(options[index], selected)) { + CopyStringToBuffer(buffer, options[index]); + changed = true; + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + return changed; + } + + static MetaCoreBuildPlatform BuildPlatformFromBuffer(const std::array& buffer) { + const std::string value = buffer.data(); + if (value == "Linux") { + return MetaCoreBuildPlatform::Linux; + } + if (value == "Android") { + return MetaCoreBuildPlatform::Android; + } + if (value == "Web") { + return MetaCoreBuildPlatform::Web; + } + return MetaCoreBuildPlatform::Windows; + } + + static MetaCoreGraphicsBackend GraphicsBackendFromBuffer(const std::array& buffer) { + const std::string value = buffer.data(); + if (value == "Vulkan") { + return MetaCoreGraphicsBackend::Vulkan; + } + if (value == "WebGL") { + return MetaCoreGraphicsBackend::WebGL; + } + if (value == "WebGPU") { + return MetaCoreGraphicsBackend::WebGPU; + } + return MetaCoreGraphicsBackend::OpenGL; + } + + static MetaCoreBuildArchitecture ArchitectureFromBuffer(const std::array& buffer) { + const std::string value = buffer.data(); + if (value == "arm64") { + return MetaCoreBuildArchitecture::Arm64; + } + if (value == "wasm32") { + return MetaCoreBuildArchitecture::Wasm32; + } + return MetaCoreBuildArchitecture::X64; + } + + struct MetaCoreBuildTargetUiStatus { + bool IsError = false; + bool IsWarning = false; + std::string Message{}; + }; + + MetaCoreBuildTargetUiStatus BuildTargetUiStatusFromBuffers() const { + const MetaCoreBuildPlatform platform = BuildPlatformFromBuffer(TargetPlatformBuffer_); + const MetaCoreGraphicsBackend graphics = GraphicsBackendFromBuffer(GraphicsBackendBuffer_); + const MetaCoreBuildArchitecture architecture = ArchitectureFromBuffer(ArchitectureBuffer_); + const bool webGraphics = graphics == MetaCoreGraphicsBackend::WebGL || graphics == MetaCoreGraphicsBackend::WebGPU; + + if (platform == MetaCoreBuildPlatform::Web) { + if (architecture != MetaCoreBuildArchitecture::Wasm32) { + return {true, false, "Invalid target: Web builds require wasm32 architecture."}; + } + if (!webGraphics) { + return {true, false, "Invalid target: Web builds require WebGL or WebGPU."}; + } + return {false, true, "Target is valid, but Web packaging is not implemented yet."}; + } + + if (architecture == MetaCoreBuildArchitecture::Wasm32) { + return {true, false, "Invalid target: wasm32 is only valid for Web builds."}; + } + if (webGraphics) { + return {true, false, "Invalid target: WebGL/WebGPU are only valid for Web builds."}; + } + if (platform != MetaCoreBuildPlatform::Windows) { + return {false, true, "Target is valid, but this packager currently supports Windows only."}; + } + return {false, false, "Target can be packaged by the current Windows Player packager."}; + } + + static std::string ToLowerAscii(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) { + return static_cast(std::tolower(character)); + }); + return value; + } + + static bool RegularFileExistsNoThrow(const std::filesystem::path& path) { + std::error_code errorCode; + return std::filesystem::is_regular_file(path, errorCode) && !errorCode; + } + + static std::filesystem::path LexicallyNormalNoThrow(const char* value) { + try { + return std::filesystem::path(value != nullptr ? value : "").lexically_normal(); + } catch (...) { + return {}; + } + } + + static std::filesystem::path ResolveDefaultPlayerExecutablePath() { +#if defined(_WIN32) + std::array executablePath{}; + const DWORD length = GetModuleFileNameW( + nullptr, + executablePath.data(), + static_cast(executablePath.size()) + ); + if (length > 0 && length < executablePath.size()) { + return std::filesystem::path(executablePath.data()).parent_path() / "MetaCorePlayer.exe"; + } +#endif + return std::filesystem::current_path() / "MetaCorePlayer.exe"; + } + + MetaCoreBuildTargetUiStatus PlayerExecutableUiStatusFromBuffer() const { + std::filesystem::path playerExecutable; + try { + playerExecutable = PlayerExecutablePathBuffer_[0] != '\0' + ? std::filesystem::path(PlayerExecutablePathBuffer_.data()) + : ResolveDefaultPlayerExecutablePath(); + } catch (...) { + return {true, false, "Player executable path is invalid."}; + } + if (!RegularFileExistsNoThrow(playerExecutable)) { + return {true, false, "Player executable was not found: " + playerExecutable.generic_string()}; + } + if (BuildPlatformFromBuffer(TargetPlatformBuffer_) == MetaCoreBuildPlatform::Windows && + ToLowerAscii(playerExecutable.extension().generic_string()) != ".exe") { + return {true, false, "Windows Player package requires a .exe player executable."}; + } + return {false, false, "Player executable is valid: " + playerExecutable.generic_string()}; + } + + MetaCoreBuildTargetUiStatus StartupSceneUiStatusFromBuffer( + const MetaCoreIAssetDatabaseService& assetDatabaseService + ) const { + if (StartupScenePathBuffer_[0] == '\0') { + return {true, false, "Startup scene is not set."}; + } + const std::filesystem::path startupScenePath = LexicallyNormalNoThrow(StartupScenePathBuffer_.data()); + if (startupScenePath.empty()) { + return {true, false, "Startup scene path is invalid."}; + } + const auto sceneRecord = assetDatabaseService.FindAssetByRelativePath(startupScenePath); + if (!sceneRecord.has_value() || sceneRecord->Type != "scene") { + return {true, false, "Startup scene is not registered in AssetDatabase: " + startupScenePath.generic_string()}; + } + return {false, false, "Startup scene is registered: " + startupScenePath.generic_string()}; + } + + void ApplyFirstEnabledBuildScene(const MetaCoreIAssetDatabaseService& assetDatabaseService) { + for (const auto& scene : BuildScenes_) { + if (scene.second) { + CopyPathToBuffer(StartupScenePathBuffer_, std::filesystem::path(scene.first)); + return; + } + } + for (const auto& asset : assetDatabaseService.GetAssetRegistry()) { + if (asset.Type == "scene") { + CopyPathToBuffer(StartupScenePathBuffer_, asset.RelativePath); + return; + } + } + } + + static std::string BuildDisabledReason( + const MetaCoreIBuildService* buildService, + const MetaCoreBuildTargetUiStatus& targetStatus, + const MetaCoreBuildTargetUiStatus& playerExecutableStatus, + const MetaCoreBuildTargetUiStatus& startupSceneStatus + ) { + if (buildService == nullptr) { + return "Build service is unavailable."; + } + if (targetStatus.IsError || targetStatus.IsWarning) { + return targetStatus.Message; + } + if (playerExecutableStatus.IsError) { + return playerExecutableStatus.Message; + } + if (startupSceneStatus.IsError) { + return startupSceneStatus.Message; + } + return "Build is unavailable."; + } + + void DrawPlatformSelectable(const char* label, const char* platformValue) { + const bool selected = std::strcmp(TargetPlatformBuffer_.data(), platformValue) == 0; + if (ImGui::Selectable(label, selected)) { + CopyStringToBuffer(TargetPlatformBuffer_, platformValue); + ApplyPlatformPreset(platformValue); + } + } + + void ApplyPlatformPreset(const char* platformValue) { + if (std::strcmp(platformValue, "Web") == 0) { + CopyStringToBuffer(GraphicsBackendBuffer_, "WebGPU"); + CopyStringToBuffer(ArchitectureBuffer_, "wasm32"); + } else if (std::strcmp(platformValue, "Windows") == 0 || std::strcmp(platformValue, "Linux") == 0) { + if (std::strcmp(GraphicsBackendBuffer_.data(), "WebGL") == 0 || + std::strcmp(GraphicsBackendBuffer_.data(), "WebGPU") == 0) { + CopyStringToBuffer(GraphicsBackendBuffer_, "OpenGL"); + } + if (std::strcmp(ArchitectureBuffer_.data(), "wasm32") == 0) { + CopyStringToBuffer(ArchitectureBuffer_, "x64"); + } + } else if (std::strcmp(platformValue, "Android") == 0) { + if (std::strcmp(GraphicsBackendBuffer_.data(), "WebGL") == 0 || + std::strcmp(GraphicsBackendBuffer_.data(), "WebGPU") == 0) { + CopyStringToBuffer(GraphicsBackendBuffer_, "Vulkan"); + } + if (std::strcmp(ArchitectureBuffer_.data(), "wasm32") == 0) { + CopyStringToBuffer(ArchitectureBuffer_, "arm64"); + } + } + } + + std::filesystem::path BuildOutputRootPreview(const MetaCoreProjectDescriptor& project) const { + std::filesystem::path outputBase; + try { + outputBase = OutputDirectoryBuffer_[0] != '\0' + ? std::filesystem::path(OutputDirectoryBuffer_.data()).lexically_normal() + : (std::filesystem::path("Build") / TargetPlatformBuffer_.data()); + if (!outputBase.is_absolute()) { + outputBase = project.RootPath / outputBase; + } + return outputBase / (project.Name.empty() ? "MetaCoreProject" : project.Name); + } catch (...) { + return project.RootPath / "Build" / "Windows" / (project.Name.empty() ? "MetaCoreProject" : project.Name); + } + } + static std::filesystem::path BuildRuntimeDirectory(const MetaCoreProjectDescriptor& project) { return !project.RuntimePath.empty() ? project.RuntimePath : (project.RootPath / "Runtime"); } @@ -6261,6 +7151,8 @@ private: CopyPathToBuffer(StartupUiPathBuffer_, document.StartupUiPath); CopyStringToBuffer(BuildProfileNameBuffer_, document.BuildProfileName); CopyStringToBuffer(TargetPlatformBuffer_, document.TargetPlatform); + CopyStringToBuffer(GraphicsBackendBuffer_, document.GraphicsBackend); + CopyStringToBuffer(ArchitectureBuffer_, document.Architecture); CopyPathToBuffer(OutputDirectoryBuffer_, document.OutputDirectory); CopyPathToBuffer(CookedAssetsDirectoryBuffer_, document.CookedAssetsDirectory); CopyPathToBuffer(DataSourcesPathBuffer_, document.DataSourcesPath); @@ -6291,6 +7183,8 @@ private: document.StartupUiPath = std::filesystem::path(StartupUiPathBuffer_.data()).lexically_normal(); document.BuildProfileName = BuildProfileNameBuffer_.data(); document.TargetPlatform = TargetPlatformBuffer_.data(); + document.GraphicsBackend = GraphicsBackendBuffer_.data(); + document.Architecture = ArchitectureBuffer_.data(); document.OutputDirectory = std::filesystem::path(OutputDirectoryBuffer_.data()).lexically_normal(); document.CookedAssetsDirectory = std::filesystem::path(CookedAssetsDirectoryBuffer_.data()).lexically_normal(); document.DataSourcesPath = std::filesystem::path(DataSourcesPathBuffer_.data()).lexically_normal(); @@ -6361,6 +7255,9 @@ private: if (OutputDirectoryBuffer_[0] != '\0') { request.OutputDirectory = OutputDirectoryBuffer_.data(); } + request.Target.Platform = BuildPlatformFromBuffer(TargetPlatformBuffer_); + request.Target.GraphicsBackend = GraphicsBackendFromBuffer(GraphicsBackendBuffer_); + request.Target.Architecture = ArchitectureFromBuffer(ArchitectureBuffer_); request.CookBeforePackage = CookBeforePackage_; request.CopyLooseProjectContent = cookedOnly ? false : CopyLooseContent_; request.CopyRuntimeConfig = CopyRuntimeConfig_; @@ -6393,6 +7290,8 @@ private: std::array StartupUiPathBuffer_{}; std::array BuildProfileNameBuffer_{}; std::array TargetPlatformBuffer_{}; + std::array GraphicsBackendBuffer_{}; + std::array ArchitectureBuffer_{}; std::array PlayerExecutablePathBuffer_{}; std::array OutputDirectoryBuffer_{}; std::array CookedAssetsDirectoryBuffer_{}; diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index 8f47a17..86bc68d 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -2,6 +2,7 @@ #include "MetaCoreEditor/MetaCoreBuiltinModules.h" #include "MetaCoreEditor/MetaCoreEditorServices.h" +#include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCoreEditorCameraController.h" #include @@ -20,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -74,6 +76,57 @@ constexpr bool GMetaCoreEnableImGuizmo = true; constexpr const char* GMetaCoreEditorLayoutRelativePath = "Library/Editor/imgui.ini"; +[[nodiscard]] bool MetaCoreTrySetProjectPathFromArgument(const std::filesystem::path& inputPath) { + if (inputPath.empty()) { + return false; + } + + const std::filesystem::path projectFilePath = inputPath.filename() == "MetaCore.project.json" + ? inputPath + : MetaCoreGetProjectFilePath(inputPath); + if (!std::filesystem::is_regular_file(projectFilePath)) { + return false; + } + + _putenv_s("METACORE_PROJECT_PATH", projectFilePath.parent_path().string().c_str()); + return true; +} + +void MetaCoreApplyProjectPathFromCommandLine(int argc, char* argv[]) { + for (int index = 1; index < argc; ++index) { + if (argv[index] == nullptr) { + continue; + } + + const std::string argument(argv[index]); + if (argument == "--project") { + if (index + 1 < argc && argv[index + 1] != nullptr) { + if (MetaCoreTrySetProjectPathFromArgument(std::filesystem::path(argv[index + 1]))) { + return; + } + } + ++index; + continue; + } + + constexpr std::string_view projectPrefix = "--project="; + if (argument.rfind(projectPrefix.data(), 0) == 0) { + if (MetaCoreTrySetProjectPathFromArgument(std::filesystem::path(argument.substr(projectPrefix.size())))) { + return; + } + continue; + } + + if (argument.rfind("--", 0) == 0) { + continue; + } + + if (MetaCoreTrySetProjectPathFromArgument(std::filesystem::path(argument))) { + return; + } + } +} + [[nodiscard]] const char* MetaCoreGetInfernuxDockWindowId(std::string_view panelId) { if (panelId == "Hierarchy") { return "hierarchy"; @@ -644,6 +697,8 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) { return true; } + MetaCoreApplyProjectPathFromCommandLine(argc, argv); + MetaCoreTraceStartup("metacore.app: initialize window"); if (!Window_.Initialize(1600, 900, "MetaCore Editor")) { MetaCoreTraceStartup("metacore.app: initialize window failed"); @@ -830,6 +885,7 @@ int MetaCoreEditorApp::Run() { while (!Window_.ShouldClose()) { Window_.BeginFrame(); SyncImGuiLayoutIniPath(); + SyncViewportProjectRootPath(); // Filament 接管,不再使用 OpenGL3 后端的 NewFrame ImGui_ImplWin32_NewFrame(); @@ -954,6 +1010,21 @@ void MetaCoreEditorApp::SyncImGuiLayoutIniPath() { EditorContext_->SetDockLayoutBuilt(hasSavedLayout); } +void MetaCoreEditorApp::SyncViewportProjectRootPath() { + std::filesystem::path projectRoot{}; + if (const auto assetDatabaseService = ModuleRegistry_.ResolveService(); + assetDatabaseService != nullptr && assetDatabaseService->HasProject()) { + projectRoot = assetDatabaseService->GetProjectDescriptor().RootPath.lexically_normal(); + } + + if (projectRoot == ViewportProjectRoot_) { + return; + } + + ViewportProjectRoot_ = projectRoot; + ViewportRenderer_.SetProjectRootPath(ViewportProjectRoot_); +} + void MetaCoreEditorApp::DrawEditorFrame() { SceneInteractionService_.HandleShortcuts(*EditorContext_); @@ -1025,7 +1096,20 @@ void MetaCoreEditorApp::DrawEditorFrame() { const ImGuiWindowFlags panelWindowFlags = static_cast(panelProvider->GetWindowFlags()); const std::string panelWindowName = MetaCoreBuildDockWindowName(*panelProvider); ImGui::Begin(panelWindowName.c_str(), &panelOpen, panelWindowFlags); - panelProvider->DrawPanel(*EditorContext_); + try { + panelProvider->DrawPanel(*EditorContext_); + } catch (const std::exception& exception) { + ImGui::TextColored( + ImVec4(1.0F, 0.35F, 0.25F, 1.0F), + "Panel failed to draw: %s", + exception.what() + ); + } catch (...) { + ImGui::TextColored( + ImVec4(1.0F, 0.35F, 0.25F, 1.0F), + "Panel failed to draw: unknown error" + ); + } ImGui::End(); if (pushedPadding) { @@ -1829,4 +1913,3 @@ void MetaCoreEditorApp::EnsureDefaultDockLayout(unsigned int dockSpaceId) { } } // namespace MetaCore - diff --git a/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.cpp b/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.cpp index 9b151a9..445c13b 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.cpp @@ -1,7 +1,20 @@ +#define GLM_ENABLE_EXPERIMENTAL #include "MetaCoreGltfImporter.h" #include "MetaCoreFoundation/MetaCoreHash.h" -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include #include @@ -701,262 +714,151 @@ void MetaCoreAppendUtf8Codepoint(std::string& output, std::uint32_t codepoint) { document.GeneratedMaterialAssets.push_back(std::move(materialAsset)); }; - MetaCoreGltfTrace("使用降级机制解析普通 GLTF 文件..."); - addFallbackSkeleton(); - MetaCoreFinalizeImportReport(document.ImportReport); - MetaCoreApplyModelImportSettings(document); - return document; - cgltf_data* data = nullptr; - if (data->images_count > 0) { - for (std::size_t i = 0; i < data->images_count; ++i) { - const cgltf_image& image = data->images[i]; - MetaCoreImportedGltfTextureDocument textureDoc; - textureDoc.SourcePath = image.uri ? std::filesystem::path(image.uri).lexically_normal() : std::filesystem::path{}; - textureDoc.UsageHint = "Unknown"; - - MetaCoreTextureAssetDocument genTex; - const std::string imageName = MetaCoreGltfDisplayName(image.name, "Texture_" + std::to_string(i)); - genTex.AssetGuid = MetaCoreMakeDeterministicGeneratedAssetGuid(relativeSourcePath, "texture", imageName, i); - genTex.Name = imageName; - genTex.StableImportKey = MetaCoreBuildStableImportKey("texture", imageName, i); - genTex.SourcePath = textureDoc.SourcePath; - genTex.UsageHint = "Unknown"; - - document.Textures.push_back(std::move(textureDoc)); - document.GeneratedTextureAssets.push_back(std::move(genTex)); - } - } + MetaCoreGltfTrace("使用 gltfio 引擎后端离线提取 GLTF 层级结构..."); - if (data->materials_count > 0) { - for (std::size_t i = 0; i < data->materials_count; ++i) { - const cgltf_material& mat = data->materials[i]; - MetaCoreImportedGltfMaterialDocument matDoc; - matDoc.Name = MetaCoreGltfDisplayName(mat.name, "Material_" + std::to_string(i)); - matDoc.DoubleSided = mat.double_sided; - - MetaCoreMaterialAssetDocument genMat; - genMat.AssetGuid = MetaCoreMakeDeterministicGeneratedAssetGuid(relativeSourcePath, "material", matDoc.Name, i); - genMat.Name = matDoc.Name; - genMat.StableImportKey = MetaCoreBuildStableImportKey("material", matDoc.Name, i); - genMat.DoubleSided = mat.double_sided; - genMat.AlphaCutoff = mat.alpha_cutoff; - genMat.AlphaMode = mat.alpha_mode == cgltf_alpha_mode_blend ? MetaCoreMaterialAlphaMode::Blend : (mat.alpha_mode == cgltf_alpha_mode_mask ? MetaCoreMaterialAlphaMode::Mask : MetaCoreMaterialAlphaMode::Opaque); - - if (mat.has_pbr_metallic_roughness) { - genMat.BaseColor = glm::vec3(mat.pbr_metallic_roughness.base_color_factor[0], mat.pbr_metallic_roughness.base_color_factor[1], mat.pbr_metallic_roughness.base_color_factor[2]); - genMat.Metallic = mat.pbr_metallic_roughness.metallic_factor; - genMat.Roughness = mat.pbr_metallic_roughness.roughness_factor; - - if (mat.pbr_metallic_roughness.base_color_texture.texture) { - const std::int32_t texIdx = static_cast(mat.pbr_metallic_roughness.base_color_texture.texture->image - data->images); - if (texIdx >= 0 && static_cast(texIdx) < document.GeneratedTextureAssets.size()) { - matDoc.TextureIndices.push_back(texIdx); - genMat.BaseColorTexture = document.GeneratedTextureAssets[static_cast(texIdx)].AssetGuid; - } else { - MetaCoreAddImportMessage( - document.ImportReport, - MetaCoreImportMessageSeverity::Warning, - "missing_texture_reference", - "材质引用了超出范围的贴图,已跳过该贴图槽位" - ); - } - } - } - if (matDoc.TextureIndices.empty() && !document.GeneratedTextureAssets.empty()) { - matDoc.TextureIndices.push_back(0); - genMat.BaseColorTexture = document.GeneratedTextureAssets.front().AssetGuid; - MetaCoreAddImportMessage( - document.ImportReport, - MetaCoreImportMessageSeverity::Info, - "default_texture_binding", - "材质缺少贴图引用时,已回退到首个可用贴图资源" - ); - } - - document.Materials.push_back(std::move(matDoc)); - document.GeneratedMaterialAssets.push_back(std::move(genMat)); - } - } - - if (data->meshes_count > 0) { - for (std::size_t i = 0; i < data->meshes_count; ++i) { - const cgltf_mesh& mesh = data->meshes[i]; - MetaCoreImportedGltfMeshDocument meshDoc; - meshDoc.Name = MetaCoreGltfDisplayName(mesh.name, "Mesh_" + std::to_string(i)); - meshDoc.PrimitiveCount = static_cast(mesh.primitives_count); - - MetaCoreGltfTrace(("处理 Mesh [" + std::to_string(i) + "]: " + meshDoc.Name + ", Primitives: " + std::to_string(mesh.primitives_count)).c_str()); - - MetaCoreMeshAssetDocument genMesh; - genMesh.AssetGuid = MetaCoreMakeDeterministicGeneratedAssetGuid(relativeSourcePath, "mesh", meshDoc.Name, i); - genMesh.Name = meshDoc.Name; - genMesh.StableImportKey = MetaCoreBuildStableImportKey("mesh", meshDoc.Name, i); - - std::vector meshBinaryData; - struct Vertex { - float px, py, pz; - float nx, ny, nz; - float u, v; - }; - - std::vector vertices; - std::vector indices; - - glm::vec3 bmin{1e30, 1e30, 1e30}; - glm::vec3 bmax{-1e30, -1e30, -1e30}; - - for (std::size_t p = 0; p < mesh.primitives_count; ++p) { - const cgltf_primitive& prim = mesh.primitives[p]; - - const std::uint32_t vertexOffset = static_cast(vertices.size()); - - const cgltf_accessor* posAcc = nullptr; - const cgltf_accessor* normAcc = nullptr; - const cgltf_accessor* uvAcc = nullptr; - - for (std::size_t a = 0; a < prim.attributes_count; ++a) { - const cgltf_attribute& attr = prim.attributes[a]; - if (attr.type == cgltf_attribute_type_position) posAcc = attr.data; - if (attr.type == cgltf_attribute_type_normal) normAcc = attr.data; - if (attr.type == cgltf_attribute_type_texcoord) uvAcc = attr.data; - } - - if (!posAcc) continue; - - for (std::size_t v = 0; v < posAcc->count; ++v) { - Vertex vertex{}; - float pos[3]; - cgltf_accessor_read_float(posAcc, v, pos, 3); - vertex.px = pos[0]; vertex.py = pos[1]; vertex.pz = pos[2]; - - bmin = glm::min(bmin, glm::vec3(pos[0], pos[1], pos[2])); - bmax = glm::max(bmax, glm::vec3(pos[0], pos[1], pos[2])); - - if (normAcc) { - float norm[3]; - cgltf_accessor_read_float(normAcc, v, norm, 3); - vertex.nx = norm[0]; vertex.ny = norm[1]; vertex.nz = norm[2]; - } - if (uvAcc) { - float uv[2]; - cgltf_accessor_read_float(uvAcc, v, uv, 2); - vertex.u = uv[0]; vertex.v = uv[1]; - } - vertices.push_back(vertex); - } - - const std::uint32_t firstIndex = static_cast(indices.size()); - - if (prim.indices) { - for (std::size_t idx = 0; idx < prim.indices->count; ++idx) { - indices.push_back(vertexOffset + static_cast(cgltf_accessor_read_index(prim.indices, idx))); - } - } else { - for (std::size_t idx = 0; idx < posAcc->count; ++idx) { - indices.push_back(vertexOffset + static_cast(idx)); - } - } - - std::int32_t matIdx = -1; - if (prim.material) { - matIdx = static_cast(prim.material - data->materials); - meshDoc.MaterialSlots.push_back(matIdx); - } - - MetaCoreMeshSubMeshDocument subMesh; - subMesh.Name = "Primitive_" + std::to_string(p); - subMesh.MaterialSlotIndex = matIdx; - subMesh.FirstIndex = firstIndex; - subMesh.IndexCount = static_cast(indices.size()) - firstIndex; - genMesh.SubMeshes.push_back(std::move(subMesh)); - } - if (meshDoc.MaterialSlots.empty() && !document.GeneratedMaterialAssets.empty()) { - meshDoc.MaterialSlots.push_back(0); - MetaCoreMeshSubMeshDocument defaultSubMesh; - defaultSubMesh.Name = "DefaultSubMesh"; - defaultSubMesh.MaterialSlotIndex = 0; - defaultSubMesh.FirstIndex = 0; - defaultSubMesh.IndexCount = static_cast(indices.size()); - genMesh.SubMeshes.push_back(std::move(defaultSubMesh)); - } - - genMesh.VertexCount = static_cast(vertices.size()); - genMesh.IndexCount = static_cast(indices.size()); - genMesh.BoundingBoxMin = bmin; - genMesh.BoundingBoxMax = bmax; - - MetaCoreGltfTrace((" Mesh [" + std::to_string(i) + "] 顶点数: " + std::to_string(genMesh.VertexCount) + ", 索引数: " + std::to_string(genMesh.IndexCount)).c_str()); - - const std::size_t payloadSize = sizeof(std::uint32_t) * 2 + vertices.size() * sizeof(Vertex) + indices.size() * sizeof(std::uint32_t); - meshBinaryData.resize(payloadSize); - std::byte* ptr = meshBinaryData.data(); - - std::uint32_t vCount = static_cast(vertices.size()); - std::uint32_t iCount = static_cast(indices.size()); - - std::memcpy(ptr, &vCount, sizeof(std::uint32_t)); ptr += sizeof(std::uint32_t); - std::memcpy(ptr, &iCount, sizeof(std::uint32_t)); ptr += sizeof(std::uint32_t); - std::memcpy(ptr, vertices.data(), vertices.size() * sizeof(Vertex)); ptr += vertices.size() * sizeof(Vertex); - std::memcpy(ptr, indices.data(), indices.size() * sizeof(std::uint32_t)); - - genMesh.PayloadIndex = 1 + outMeshPayloads.size(); - genMesh.PayloadSize = payloadSize; - outMeshPayloads.push_back(std::move(meshBinaryData)); - - document.Meshes.push_back(std::move(meshDoc)); - document.GeneratedMeshAssets.push_back(std::move(genMesh)); - } - } - - if (data->nodes_count > 0) { - for (std::size_t i = 0; i < data->nodes_count; ++i) { - const cgltf_node& node = data->nodes[i]; - MetaCoreImportedGltfNodeDocument nodeDoc; - if (node.name && strlen(node.name) > 0) { - nodeDoc.Name = MetaCoreGltfDisplayName(node.name, "Node_" + std::to_string(i)); - } else if (node.mesh && node.mesh->name && strlen(node.mesh->name) > 0) { - nodeDoc.Name = MetaCoreGltfDisplayName(node.mesh->name, "Mesh_" + std::to_string(i)) + "_Node"; - } else { - nodeDoc.Name = "Node_" + std::to_string(i); - } - - nodeDoc.ParentIndex = node.parent ? static_cast(node.parent - data->nodes) : -1; - nodeDoc.MeshIndex = node.mesh ? static_cast(node.mesh - data->meshes) : -1; - - if (node.has_translation) { - nodeDoc.Position = glm::vec3(node.translation[0], node.translation[1], node.translation[2]); - } - if (node.has_rotation) { - const glm::quat q(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]); - nodeDoc.RotationEulerDegrees = glm::degrees(glm::eulerAngles(q)); - } - if (node.has_scale) { - nodeDoc.Scale = glm::vec3(node.scale[0], node.scale[1], node.scale[2]); - } - document.Nodes.push_back(std::move(nodeDoc)); - } - for (std::size_t nodeIndex = 0; nodeIndex < document.Nodes.size(); ++nodeIndex) { - document.Nodes[nodeIndex].StableNodePath = MetaCoreBuildNodeStablePath(document.Nodes, nodeIndex); - } + std::vector fileBuffer; + if (FILE* file = _wfopen(absoluteSourcePath.c_str(), L"rb")) { + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + fileBuffer.resize(size); + fread(fileBuffer.data(), 1, size, file); + fclose(file); } else { - MetaCoreImportedGltfNodeDocument rootNode; - rootNode.Name = absoluteSourcePath.stem().string(); - rootNode.StableNodePath = rootNode.Name; - rootNode.MeshIndex = document.Meshes.empty() ? -1 : 0; - document.Nodes.push_back(std::move(rootNode)); - MetaCoreAddImportMessage( - document.ImportReport, - MetaCoreImportMessageSeverity::Info, - "generated_root_node", - "源模型未提供节点层级,已生成默认根节点" - ); + MetaCoreAddImportMessage(document.ImportReport, MetaCoreImportMessageSeverity::Error, "file_read_error", "Failed to read file."); + return document; + } + + static filament::Engine* NOOP_Engine = nullptr; + static utils::NameComponentManager* NameManager = nullptr; + static filament::gltfio::MaterialProvider* MaterialProvider = nullptr; + static filament::gltfio::AssetLoader* Loader = nullptr; + + if (!NOOP_Engine) { + NOOP_Engine = filament::Engine::create(filament::Engine::Backend::NOOP); + NameManager = new utils::NameComponentManager(utils::EntityManager::get()); + MaterialProvider = filament::gltfio::createJitShaderProvider(NOOP_Engine); + Loader = filament::gltfio::AssetLoader::create({NOOP_Engine, MaterialProvider, NameManager}); + } + + filament::gltfio::FilamentAsset* asset = Loader->createAsset((const uint8_t*)fileBuffer.data(), fileBuffer.size()); + if (!asset) { + MetaCoreAddImportMessage(document.ImportReport, MetaCoreImportMessageSeverity::Error, "gltfio_error", "gltfio AssetLoader failed to parse asset."); + MetaCoreFinalizeImportReport(document.ImportReport); + return document; + } + + std::unordered_map entityToMeshIndex; + + const utils::Entity* renderables = asset->getRenderableEntities(); + size_t numRenderables = asset->getRenderableEntityCount(); + + for (size_t i = 0; i < numRenderables; ++i) { + utils::Entity e = renderables[i]; + entityToMeshIndex[e] = static_cast(i); + + auto inst = NOOP_Engine->getRenderableManager().getInstance(e); + const filament::Box& aabb = NOOP_Engine->getRenderableManager().getAxisAlignedBoundingBox(inst); + const filament::math::float3 aabbMin = aabb.getMin(); + const filament::math::float3 aabbMax = aabb.getMax(); + + MetaCoreMeshAssetDocument meshAsset; + meshAsset.Name = "Mesh_" + std::to_string(i); + const char* renderableName = asset->getName(e); + if (renderableName != nullptr && renderableName[0] != '\0') { + meshAsset.Name = renderableName; + } + meshAsset.AssetGuid = MetaCoreMakeDeterministicGeneratedAssetGuid(relativeSourcePath, "mesh", meshAsset.Name, i); + meshAsset.StableImportKey = MetaCoreBuildStableImportKey("mesh", meshAsset.Name, i); + meshAsset.BoundingBoxMin = glm::vec3(aabbMin.x, aabbMin.y, aabbMin.z); + meshAsset.BoundingBoxMax = glm::vec3(aabbMax.x, aabbMax.y, aabbMax.z); + meshAsset.VertexCount = 0; + meshAsset.IndexCount = 0; + + MetaCoreImportedGltfMeshDocument meshDocument; + meshDocument.Name = meshAsset.Name; + meshDocument.PrimitiveCount = 1; + document.Meshes.push_back(std::move(meshDocument)); + + document.GeneratedMeshAssets.push_back(std::move(meshAsset)); + } + + auto& tm = NOOP_Engine->getTransformManager(); + utils::Entity rootEntity = asset->getRoot(); + std::unordered_map, utils::Entity::Hasher> parentToChildren; + + const utils::Entity* allEntities = asset->getEntities(); + size_t allEntityCount = asset->getEntityCount(); + for (size_t i = 0; i < allEntityCount; ++i) { + utils::Entity e = allEntities[i]; + auto inst = tm.getInstance(e); + utils::Entity parentE = tm.getParent(inst); + if (parentE) { + parentToChildren[parentE].push_back(e); + } else { + if (e != rootEntity) { + parentToChildren[rootEntity].push_back(e); + } + } + } + + std::function addNode = [&](utils::Entity e, int parentIdx) { + MetaCoreImportedGltfNodeDocument node; + const char* name = asset->getName(e); + node.Name = name && name[0] != '\0' ? name : "Node_" + std::to_string(document.Nodes.size()); + node.StableNodePath = node.Name; + node.ParentIndex = parentIdx; + + auto itMesh = entityToMeshIndex.find(e); + if (itMesh != entityToMeshIndex.end()) { + node.MeshIndex = itMesh->second; + } else { + node.MeshIndex = -1; + } + + auto inst = tm.getInstance(e); + filament::math::mat4f localMat = tm.getTransform(inst); + glm::mat4 m( + localMat[0][0], localMat[0][1], localMat[0][2], localMat[0][3], + localMat[1][0], localMat[1][1], localMat[1][2], localMat[1][3], + localMat[2][0], localMat[2][1], localMat[2][2], localMat[2][3], + localMat[3][0], localMat[3][1], localMat[3][2], localMat[3][3] + ); + + glm::vec3 scale; + glm::quat rotation; + glm::vec3 translation; + glm::vec3 skew; + glm::vec4 perspective; + glm::decompose(m, scale, rotation, translation, skew, perspective); + + node.Position = translation; + glm::vec3 euler = glm::degrees(glm::eulerAngles(rotation)); + node.RotationEulerDegrees = euler; + node.Scale = scale; + + int myIdx = static_cast(document.Nodes.size()); + document.Nodes.push_back(node); + + for (utils::Entity child : parentToChildren[e]) { + addNode(child, myIdx); + } + }; + + addNode(rootEntity, -1); + + Loader->destroyAsset(asset); + + for (std::size_t nodeIndex = 0; nodeIndex < document.Nodes.size(); ++nodeIndex) { + document.Nodes[nodeIndex].StableNodePath = MetaCoreBuildNodeStablePath(document.Nodes, nodeIndex); } - cgltf_free(data); MetaCoreFinalizeImportReport(document.ImportReport); MetaCoreApplyModelImportSettings(document); return document; + + } } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h index bf9858f..5f70816 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h @@ -38,6 +38,7 @@ private: void DrawGameViewWindow(); void DrawPlaceholderDockWindow(const char* label, const char* caption); void SyncImGuiLayoutIniPath(); + void SyncViewportProjectRootPath(); void EnsureDefaultDockLayout(unsigned int dockSpaceId); void QueueDockTabSelection(std::string windowId); void ApplyPendingDockTabSelections(); @@ -53,6 +54,7 @@ private: std::vector PendingDockTabSelections_{}; std::unique_ptr EditorContext_{}; std::filesystem::path ImGuiLayoutProjectRoot_{}; + std::filesystem::path ViewportProjectRoot_{}; std::string ImGuiIniPath_{}; bool Initialized_ = false; }; diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index 7e075ae..3456687 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -216,9 +216,36 @@ public: [[nodiscard]] virtual std::filesystem::path GetCookedPathForAsset(const MetaCoreAssetGuid& assetGuid) const = 0; }; +enum class MetaCoreBuildPlatform { + Windows, + Linux, + Android, + Web +}; + +enum class MetaCoreGraphicsBackend { + OpenGL, + Vulkan, + WebGL, + WebGPU +}; + +enum class MetaCoreBuildArchitecture { + X64, + Arm64, + Wasm32 +}; + +struct MetaCoreBuildTarget { + MetaCoreBuildPlatform Platform = MetaCoreBuildPlatform::Windows; + MetaCoreGraphicsBackend GraphicsBackend = MetaCoreGraphicsBackend::OpenGL; + MetaCoreBuildArchitecture Architecture = MetaCoreBuildArchitecture::X64; +}; + struct MetaCoreBuildPlayerPackageRequest { std::filesystem::path PlayerExecutablePath{}; std::filesystem::path OutputDirectory{}; + MetaCoreBuildTarget Target{}; bool CookBeforePackage = true; bool CopyLooseProjectContent = true; bool CopyRuntimeConfig = true; diff --git a/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp b/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp index 3587740..df2c942 100644 --- a/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp +++ b/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace MetaCore { @@ -12,13 +13,23 @@ MetaCoreAssetRegistry& MetaCoreAssetRegistry::Get() { } void MetaCoreAssetRegistry::ScanDirectory(const std::filesystem::path& rootPath) { - if (!std::filesystem::exists(rootPath) || !std::filesystem::is_directory(rootPath)) { + std::error_code error; + if (!std::filesystem::is_directory(rootPath, error)) { return; } // 遍历指定目录下的所有文件,寻找 .mcmeta 文件 - for (const auto& entry : std::filesystem::recursive_directory_iterator(rootPath)) { - if (!entry.is_regular_file() || entry.path().extension() != ".mcmeta") { + std::error_code iteratorError; + for (std::filesystem::recursive_directory_iterator iterator(rootPath, std::filesystem::directory_options::skip_permission_denied, iteratorError), end; + iterator != end; + iterator.increment(iteratorError)) { + if (iteratorError) { + iteratorError.clear(); + continue; + } + const auto& entry = *iterator; + std::error_code entryError; + if (!entry.is_regular_file(entryError) || entry.path().extension() != ".mcmeta") { continue; } @@ -36,6 +47,11 @@ void MetaCoreAssetRegistry::ScanDirectory(const std::filesystem::path& rootPath) const std::string guidStr = json["guid"].get(); const std::string sourcePathStr = json["source_path"].get(); + const std::filesystem::path sourcePath = rootPath.parent_path() / std::filesystem::path(sourcePathStr); + std::error_code sourceError; + if (!std::filesystem::exists(sourcePath, sourceError)) { + continue; + } auto parsedGuid = MetaCoreAssetGuid::Parse(guidStr); if (parsedGuid.has_value()) { diff --git a/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp b/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp index 35942d6..805e5e2 100644 --- a/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp @@ -52,7 +52,7 @@ bool MetaCoreEditorViewportRenderer::Initialize(MetaCoreRenderDevice& renderDevi } */ - if (!FilamentSceneBridge_.Initialize(window, offscreen)) { + if (!FilamentSceneBridge_.Initialize(window, offscreen, renderDevice.GetBackend())) { MetaCoreTrace("metacore.render: viewport filament scene runtime bind failed"); return false; } diff --git a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp index 8011d1b..c7ab652 100644 --- a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -45,19 +46,206 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include // 引入 Windows 和 OpenGL 头文件以创建纹理 +#ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif #include #include namespace MetaCore { +namespace { + +[[nodiscard]] filament::backend::Backend MetaCoreToFilamentBackend(MetaCoreRenderBackend backend) { + switch (backend) { + case MetaCoreRenderBackend::OpenGL: + return filament::backend::Backend::OPENGL; + case MetaCoreRenderBackend::Vulkan: + return filament::backend::Backend::VULKAN; + case MetaCoreRenderBackend::WebGPU: + return filament::backend::Backend::WEBGPU; + } + return filament::backend::Backend::OPENGL; +} + +[[nodiscard]] const char* MetaCoreRenderBackendToString(MetaCoreRenderBackend backend) { + switch (backend) { + case MetaCoreRenderBackend::OpenGL: + return "OpenGL"; + case MetaCoreRenderBackend::Vulkan: + return "Vulkan"; + case MetaCoreRenderBackend::WebGPU: + return "WebGPU"; + } + return "OpenGL"; +} + +class MetaCoreViewportMaterialProvider final : public filament::gltfio::MaterialProvider { +public: + explicit MetaCoreViewportMaterialProvider(filament::Engine* engine) + : Inner_(filament::gltfio::createJitShaderProvider(engine)) {} + + ~MetaCoreViewportMaterialProvider() override { + delete Inner_; + } + + filament::MaterialInstance* createMaterialInstance( + filament::gltfio::MaterialKey* config, + filament::gltfio::UvMap* uvmap, + const char* label = "material", + const char* extras = nullptr + ) override { + CoerceFoliageAlphaMode(config); + return Inner_->createMaterialInstance(config, uvmap, label, extras); + } + + filament::Material* getMaterial( + filament::gltfio::MaterialKey* config, + filament::gltfio::UvMap* uvmap, + const char* label = "material" + ) override { + CoerceFoliageAlphaMode(config); + return Inner_->getMaterial(config, uvmap, label); + } + + const filament::Material* const* getMaterials() const noexcept override { + return Inner_->getMaterials(); + } + + size_t getMaterialsCount() const noexcept override { + return Inner_->getMaterialsCount(); + } + + void destroyMaterials() override { + Inner_->destroyMaterials(); + } + + bool needsDummyData(filament::VertexAttribute attrib) const noexcept override { + return Inner_->needsDummyData(attrib); + } + +private: + static void CoerceFoliageAlphaMode(filament::gltfio::MaterialKey* config) { + if (config != nullptr && config->alphaMode == filament::gltfio::AlphaMode::BLEND) { + config->alphaMode = filament::gltfio::AlphaMode::MASK; + } + } + + filament::gltfio::MaterialProvider* Inner_ = nullptr; +}; + +[[nodiscard]] std::string MetaCoreBuildModelLoadFailureKey(const std::filesystem::path& absolutePath) { + std::string key = absolutePath.lexically_normal().generic_string(); + std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return key; +} + +[[nodiscard]] bool MetaCoreIsExternalGltfResourceUri(std::string_view uri) { + if (uri.empty() || uri.starts_with("data:") || uri.starts_with("//")) { + return false; + } + return uri.find("://") == std::string_view::npos; +} + +[[nodiscard]] std::optional MetaCoreDecodeGltfResourceUri(std::string_view uri) { + std::string decoded; + decoded.reserve(uri.size()); + for (std::size_t index = 0; index < uri.size(); ++index) { + const char character = uri[index]; + if (character == '%' && index + 2 < uri.size()) { + const auto hexToInt = [](char value) -> int { + if (value >= '0' && value <= '9') return value - '0'; + if (value >= 'a' && value <= 'f') return 10 + (value - 'a'); + if (value >= 'A' && value <= 'F') return 10 + (value - 'A'); + return -1; + }; + const int high = hexToInt(uri[index + 1]); + const int low = hexToInt(uri[index + 2]); + if (high >= 0 && low >= 0) { + decoded.push_back(static_cast((high << 4) | low)); + index += 2; + continue; + } + } + decoded.push_back(character); + } + + std::filesystem::path decodedPath(decoded); + if (decodedPath.empty() || decodedPath.is_absolute()) { + return std::nullopt; + } + for (const auto& part : decodedPath) { + if (part == "..") { + return std::nullopt; + } + } + return decodedPath.lexically_normal(); +} + +void MetaCoreReleaseResourceBuffer(void*, size_t, void* userData) { + delete static_cast*>(userData); +} + +void MetaCoreAddExternalGltfResourceData( + filament::gltfio::ResourceLoader& resourceLoader, + const filament::gltfio::FilamentAsset& asset, + const std::filesystem::path& modelPath +) { + const std::filesystem::path resourceRoot = modelPath.parent_path(); + const char* const* resourceUris = asset.getResourceUris(); + const std::size_t resourceUriCount = asset.getResourceUriCount(); + for (std::size_t resourceIndex = 0; resourceIndex < resourceUriCount; ++resourceIndex) { + const char* resourceUri = resourceUris[resourceIndex]; + if (resourceUri == nullptr || !MetaCoreIsExternalGltfResourceUri(resourceUri) || resourceLoader.hasResourceData(resourceUri)) { + continue; + } + + const auto decodedUri = MetaCoreDecodeGltfResourceUri(resourceUri); + if (!decodedUri.has_value()) { + continue; + } + + std::ifstream input(resourceRoot / *decodedUri, std::ios::binary); + if (!input.is_open()) { + std::cerr << "FilamentSceneBridge: Missing GLTF external resource: " + << (resourceRoot / *decodedUri) << " uri=" << resourceUri << std::endl; + continue; + } + + auto* buffer = new std::vector( + (std::istreambuf_iterator(input)), + std::istreambuf_iterator() + ); + resourceLoader.addResourceData( + resourceUri, + filament::gltfio::ResourceLoader::BufferDescriptor( + buffer->data(), + buffer->size(), + MetaCoreReleaseResourceBuffer, + buffer + ) + ); + } +} + +} // namespace + class MetaCoreFilamentSceneBridge::MetaCoreFilamentSceneBridgeImpl { public: MetaCoreFilamentSceneBridgeImpl() = default; @@ -71,14 +259,19 @@ public: return RenderSync_.BuildSnapshot(scene); } - bool Initialize(MetaCoreWindow& window, bool offscreen = true) { - std::cout << "FilamentSceneBridge: Initializing..." << std::endl; + bool Initialize(MetaCoreWindow& window, bool offscreen = true, MetaCoreRenderBackend backend = MetaCoreRenderBackend::OpenGL) { + std::cout << "FilamentSceneBridge: Initializing with backend=" << MetaCoreRenderBackendToString(backend) << std::endl; // 创建 Filament Engine (不共享上下文,避免闪退) - Engine_ = filament::Engine::create(); - + Engine_ = filament::Engine::create(MetaCoreToFilamentBackend(backend)); + if (!Engine_ && backend != MetaCoreRenderBackend::OpenGL) { + std::cerr << "FilamentSceneBridge: backend " << MetaCoreRenderBackendToString(backend) + << " is not available; falling back to OpenGL." << std::endl; + Engine_ = filament::Engine::create(MetaCoreToFilamentBackend(MetaCoreRenderBackend::OpenGL)); + } + if (!Engine_) { - std::cerr << "Failed to create Filament Engine with shared context!" << std::endl; + std::cerr << "Failed to create Filament Engine!" << std::endl; return false; } @@ -170,7 +363,7 @@ public: View_->setPostProcessingEnabled(true); // 初始化 gltfio - MaterialProvider_ = filament::gltfio::createJitShaderProvider(Engine_); + MaterialProvider_ = new MetaCoreViewportMaterialProvider(Engine_); NameManager_ = new utils::NameComponentManager(utils::EntityManager::get()); AssetLoader_ = filament::gltfio::AssetLoader::create({ Engine_, MaterialProvider_, NameManager_ }); ResourceLoader_ = new filament::gltfio::ResourceLoader({ Engine_ }); @@ -250,6 +443,8 @@ public: } LoadedAssets_.clear(); AssetBuffers_.clear(); + FailedModelLoads_.clear(); + FailedModelLoadPaths_.clear(); // 2. 销毁所有 pivotEntity 中间辅助坐标变换实体 for (auto& [objectId, assetEntity] : ObjectToFilamentEntity_) { @@ -540,9 +735,7 @@ public: // 1. 收集当前 snapshot 中所有存在的模型 Root ID std::unordered_set activeHostRootIds; for (const auto& renderable : snapshot.Renderables) { - if (renderable.MeshSource == MetaCoreMeshSourceKind::Asset || - (renderable.MeshSource == MetaCoreMeshSourceKind::Builtin && - (renderable.BuiltinMesh == MetaCoreBuiltinMeshType::Cube || renderable.BuiltinMesh == MetaCoreBuiltinMeshType::Plane))) { + if (IsEngineLoadedModelRenderable(renderable)) { if (renderable.HostRootId != 0) { activeHostRootIds.insert(renderable.HostRootId); } @@ -604,6 +797,7 @@ public: // 从 LoadedAssets_ 中移除 AssetBuffers_.erase(hostRootId); + FailedModelLoads_.erase(hostRootId); it = LoadedAssets_.erase(it); } else { ++it; @@ -611,8 +805,7 @@ public: } for (const auto& renderable : snapshot.Renderables) { - if (renderable.MeshSource != MetaCoreMeshSourceKind::Asset && - renderable.MeshSource != MetaCoreMeshSourceKind::Builtin) { + if (!IsEngineLoadedModelRenderable(renderable)) { continue; } @@ -638,11 +831,25 @@ public: MetaCoreId hostRootId = renderable.HostRootId; std::string hostRootName = renderable.HostRootName; + const std::string failureKey = MetaCoreBuildModelLoadFailureKey(absolutePath); + const bool sourceExists = std::filesystem::exists(absolutePath); + if (sourceExists) { + FailedModelLoadPaths_.erase(failureKey); + } // 如果该模型的顶级根宿主已经被加载过,则不需要重复加载,直接更新变换 if (LoadedAssets_.contains(hostRootId)) { continue; } + if (FailedModelLoads_.contains(hostRootId) || FailedModelLoadPaths_.contains(failureKey)) { + continue; + } + if (!sourceExists) { + std::cerr << "FilamentSceneBridge: Missing GLTF source: " << absolutePath << " (Host: " << hostRootName << ")" << std::endl; + FailedModelLoads_.insert(hostRootId); + FailedModelLoadPaths_.insert(failureKey); + continue; + } // 加载新模型,以顶级根宿主 hostRootId 进行注册存储 std::cout << "FilamentSceneBridge: Loading GLTF: " << absolutePath << " (Host: " << hostRootName << ")" << std::endl; @@ -650,6 +857,8 @@ public: std::ifstream file(absolutePath, std::ios::binary); if (!file.is_open()) { std::cerr << "Failed to open file: " << absolutePath << std::endl; + FailedModelLoads_.insert(hostRootId); + FailedModelLoadPaths_.insert(failureKey); continue; } @@ -662,13 +871,17 @@ public: if (!asset) { std::cerr << "Failed to create asset!" << std::endl; + FailedModelLoads_.insert(hostRootId); + FailedModelLoadPaths_.insert(failureKey); continue; } // 加载贴图等资源 if (ResourceLoader_) { + MetaCoreAddExternalGltfResourceData(*ResourceLoader_, *asset, absolutePath); ResourceLoader_->loadResources(asset); ResourceLoader_->asyncUpdateLoad(); // 触发底层的纹理同步 + ResourceLoader_->evictResourceData(); } // 添加到场景 @@ -907,36 +1120,71 @@ public: } } - // 5. 同步所有存活网格的材质属性,例如 BaseColor + // 5. 同步所有存活网格的材质属性。 auto& rm = Engine_->getRenderableManager(); - for (const auto& renderable : snapshot.Renderables) { - auto itEntity = ObjectToFilamentEntity_.find(renderable.ObjectId); - if (itEntity == ObjectToFilamentEntity_.end()) { - continue; - } - utils::Entity entity = itEntity->second.second; - if (!entity) { - continue; - } - - auto instance = rm.getInstance(entity); + const auto applyMaterialParameters = [&](auto instance, const MetaCoreRenderSyncRenderable& renderable) -> bool { if (!instance) { - continue; + return false; } - // 遍历所有 primitive,获取材质实例,并设置其 baseColorFactor 属性 size_t primCount = rm.getPrimitiveCount(instance); for (size_t primIndex = 0; primIndex < primCount; ++primIndex) { filament::MaterialInstance* matInst = rm.getMaterialInstanceAt(instance, primIndex); if (matInst) { - // BaseColor 包含 RGB,以 float4 传递,Alpha 设为 1.0 + const filament::Material* material = matInst->getMaterial(); filament::math::float4 baseColorVec{ renderable.BaseColor.x, renderable.BaseColor.y, renderable.BaseColor.z, 1.0f }; - matInst->setParameter("baseColorFactor", baseColorVec); + if (material != nullptr && material->hasParameter("baseColorFactor")) { + matInst->setParameter("baseColorFactor", baseColorVec); + } + if (material != nullptr && material->hasParameter("metallicFactor")) { + matInst->setParameter("metallicFactor", renderable.Metallic); + } + if (material != nullptr && material->hasParameter("roughnessFactor")) { + matInst->setParameter("roughnessFactor", renderable.Roughness); + } + if (material != nullptr && material->hasParameter("emissiveFactor")) { + matInst->setParameter( + "emissiveFactor", + filament::math::float3{ + renderable.EmissiveColor.x, + renderable.EmissiveColor.y, + renderable.EmissiveColor.z + } + ); + } + matInst->setDoubleSided(renderable.DoubleSided); + } + } + return true; + }; + + for (const auto& renderable : snapshot.Renderables) { + bool applied = false; + auto itEntity = ObjectToFilamentEntity_.find(renderable.ObjectId); + if (itEntity != ObjectToFilamentEntity_.end()) { + utils::Entity entity = itEntity->second.second; + if (entity) { + applied = applyMaterialParameters(rm.getInstance(entity), renderable); + } + } + + if (!applied) { + const auto loadedAsset = LoadedAssets_.find(renderable.HostRootId); + if (loadedAsset == LoadedAssets_.end() || loadedAsset->second == nullptr) { + continue; + } + + const utils::Entity* entities = loadedAsset->second->getEntities(); + const size_t entityCount = loadedAsset->second->getEntityCount(); + for (size_t entityIndex = 0; entityIndex < entityCount; ++entityIndex) { + if (entities[entityIndex] && applyMaterialParameters(rm.getInstance(entities[entityIndex]), renderable)) { + applied = true; + } } } } @@ -1357,6 +1605,12 @@ private: auto it = ObjectToFilamentEntity_.find(objectId); if (it == ObjectToFilamentEntity_.end()) return; + // glTF child entities keep their authored local transforms. The ECS child objects are + // editor-facing handles; only the loaded model host/pivot drives the imported asset. + if (!LoadedAssets_.contains(objectId)) { + return; + } + utils::Entity entity = it->second.second; auto& tm = Engine_->getTransformManager(); @@ -1364,20 +1618,20 @@ private: if (instance) { auto parent = tm.getParent(instance); glm::mat4 matrix = parent ? localMatrix : worldMatrix; - - // 【关键修复】:如果当前 entity 是子节点(即不是顶级映射的 pivotEntity) - // 由于它的父级(assetRoot)带有了 -90X 旋转,我们必须对子节点应用基变换来抵消这个旋转。 - // 公式:L_filament = R(90X) * L_ecs * R(-90X) - if (LoadedAssets_.count(objectId) == 0) { - glm::mat4 R_plus90X = glm::rotate(glm::mat4(1.0f), glm::radians(90.0f), {1, 0, 0}); - glm::mat4 R_minus90X = glm::rotate(glm::mat4(1.0f), glm::radians(-90.0f), {1, 0, 0}); - matrix = R_plus90X * matrix * R_minus90X; - } - tm.setTransform(instance, *reinterpret_cast(glm::value_ptr(matrix))); } } + static bool IsEngineLoadedModelRenderable(const MetaCoreRenderSyncRenderable& renderable) { + if (renderable.MeshSource == MetaCoreMeshSourceKind::Asset) { + return renderable.SourceModelAssetGuid.IsValid() || !renderable.SourceModelPath.empty(); + } + + return renderable.MeshSource == MetaCoreMeshSourceKind::Builtin && + (renderable.BuiltinMesh == MetaCoreBuiltinMeshType::Cube || + renderable.BuiltinMesh == MetaCoreBuiltinMeshType::Plane); + } + filament::Engine* Engine_ = nullptr; filament::SwapChain* SwapChain_ = nullptr; filament::Renderer* Renderer_ = nullptr; @@ -1399,6 +1653,8 @@ private: std::unordered_map> ObjectToFilamentEntity_; std::unordered_map LoadedAssets_; std::unordered_map> AssetBuffers_; + std::unordered_set FailedModelLoads_; + std::unordered_set FailedModelLoadPaths_; std::unordered_map SceneLightEntities_; std::unordered_set EntitiesInScene_; @@ -1427,8 +1683,8 @@ MetaCoreFilamentSceneBridge::MetaCoreFilamentSceneBridge() MetaCoreFilamentSceneBridge::~MetaCoreFilamentSceneBridge() = default; -bool MetaCoreFilamentSceneBridge::Initialize(MetaCoreWindow& window, bool offscreen) { - return Impl_->Initialize(window, offscreen); +bool MetaCoreFilamentSceneBridge::Initialize(MetaCoreWindow& window, bool offscreen, MetaCoreRenderBackend backend) { + return Impl_->Initialize(window, offscreen, backend); } void MetaCoreFilamentSceneBridge::Shutdown() { diff --git a/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp b/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp index f9904df..6dc3488 100644 --- a/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp @@ -2,6 +2,7 @@ #include "MetaCoreRender/MetaCoreRuntimeUiRenderer.h" #include +#include #include #include #include @@ -11,6 +12,14 @@ #include #include +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + #include #include @@ -36,23 +45,56 @@ using MagFilter = TextureSampler::MagFilter; namespace MetaCore { +namespace { + +[[nodiscard]] std::filesystem::path MetaCoreGetExecutableDirectory() { + std::array executablePath{}; + const DWORD length = GetModuleFileNameW( + nullptr, + executablePath.data(), + static_cast(executablePath.size()) + ); + if (length == 0 || length >= executablePath.size()) { + return {}; + } + return std::filesystem::path(executablePath.data()).parent_path(); +} + +[[nodiscard]] std::ifstream MetaCoreOpenUiBlitMaterial() { + std::vector candidates{ + "uiBlit.filamat", + "../uiBlit.filamat", + "build/uiBlit.filamat" + }; + + const std::filesystem::path executableDirectory = MetaCoreGetExecutableDirectory(); + if (!executableDirectory.empty()) { + candidates.push_back(executableDirectory / "uiBlit.filamat"); + } + + for (const std::filesystem::path& candidate : candidates) { + std::ifstream file(candidate, std::ios::binary); + if (file.is_open()) { + return file; + } + } + return {}; +} + +} // namespace + MetaCoreImGuiHelper::MetaCoreImGuiHelper(Engine* engine, filament::View* view, const std::filesystem::path& fontPath, ImGuiContext *imGuiContext) : mEngine(engine), mView(view), mScene(engine->createScene()), mImGuiContext(imGuiContext ? imGuiContext : ImGui::CreateContext()), - mOwnsImGuiContext(imGuiContext == nullptr) { + mOwnsImGuiContext(imGuiContext == nullptr), + mPreviousImGuiContext(ImGui::GetCurrentContext()) { ImGui::SetCurrentContext(mImGuiContext); ImGuiIO& io = ImGui::GetIO(); // 加载我们编译好的材质 - std::ifstream file("uiBlit.filamat", std::ios::binary); - if (!file.is_open()) { - file.open("../uiBlit.filamat", std::ios::binary); - } - if (!file.is_open()) { - file.open("build/uiBlit.filamat", std::ios::binary); - } + std::ifstream file = MetaCoreOpenUiBlitMaterial(); if (!file.is_open()) { std::cerr << "MetaCoreImGuiHelper: Failed to open uiBlit.filamat from any expected location!" << std::endl; @@ -198,11 +240,10 @@ MetaCoreImGuiHelper::~MetaCoreImGuiHelper() { em.destroy(mRenderable); em.destroy(mCameraEntity); - if (mImGuiContext == ImGui::GetCurrentContext()) { - ImGui::SetCurrentContext(nullptr); - } if (mOwnsImGuiContext) { ImGui::DestroyContext(mImGuiContext); + } else if (mImGuiContext == ImGui::GetCurrentContext()) { + ImGui::SetCurrentContext(mPreviousImGuiContext); } } diff --git a/Source/MetaCoreRender/Private/MetaCorePlayerRenderer.cpp b/Source/MetaCoreRender/Private/MetaCorePlayerRenderer.cpp new file mode 100644 index 0000000..91f2d71 --- /dev/null +++ b/Source/MetaCoreRender/Private/MetaCorePlayerRenderer.cpp @@ -0,0 +1,82 @@ +#include "MetaCoreRender/MetaCorePlayerRenderer.h" + +#include "MetaCoreRender/MetaCoreRenderDevice.h" +#include "MetaCoreScene/MetaCoreScene.h" + +#include +#include +#include + +namespace MetaCore { + +namespace { + +void MetaCoreTracePlayerRenderer(const char* message) { + if (message == nullptr) { + return; + } + + const auto now = std::chrono::system_clock::now(); + const auto time = std::chrono::system_clock::to_time_t(now); + struct tm timeInfo {}; + localtime_s(&timeInfo, &time); + + char timeBuffer[32]{}; + std::strftime(timeBuffer, sizeof(timeBuffer), "[%H:%M:%S] ", &timeInfo); + std::printf("%s%s\n", timeBuffer, message); +} + +} // namespace + +bool MetaCorePlayerRenderer::Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window) { + MetaCoreTracePlayerRenderer("metacore.render: player init enter"); + Shutdown(); + + if (!FilamentSceneBridge_.Initialize(window, false, renderDevice.GetBackend())) { + MetaCoreTracePlayerRenderer("metacore.render: player filament scene bind failed"); + return false; + } + + RenderDevice_ = &renderDevice; + ViewportRect_ = MetaCoreViewportRect{}; + MetaCoreTracePlayerRenderer("metacore.render: player init success"); + return true; +} + +void MetaCorePlayerRenderer::Shutdown() { + FilamentSceneBridge_.Shutdown(); + RenderDevice_ = nullptr; + ViewportRect_ = MetaCoreViewportRect{}; +} + +void MetaCorePlayerRenderer::SetViewportRect(const MetaCoreViewportRect& viewportRect) { + ViewportRect_ = viewportRect; + if (RenderDevice_ != nullptr) { + RenderDevice_->SetSceneViewportRect(ViewportRect_); + } +} + +void MetaCorePlayerRenderer::SetProjectRootPath(const std::filesystem::path& projectRootPath) { + FilamentSceneBridge_.SetProjectRootPath(projectRootPath); +} + +void MetaCorePlayerRenderer::RenderScene(MetaCoreScene& scene, const MetaCoreSceneView& sceneView) { + if (RenderDevice_ == nullptr) { + return; + } + + RenderDevice_->SetSceneViewportRect(ViewportRect_); + FilamentSceneBridge_.Resize(static_cast(ViewportRect_.Width), static_cast(ViewportRect_.Height)); + FilamentSceneBridge_.ApplySceneView(sceneView); + FilamentSceneBridge_.SyncScene(scene, false, true); +} + +void MetaCorePlayerRenderer::SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height) { + FilamentSceneBridge_.SetRuntimeUiOverlayFrame(frame, width, height); +} + +void MetaCorePlayerRenderer::RenderAll() { + FilamentSceneBridge_.RenderAll(); +} + +} // namespace MetaCore diff --git a/Source/MetaCoreRender/Private/MetaCoreRenderDevice.cpp b/Source/MetaCoreRender/Private/MetaCoreRenderDevice.cpp index cfdfeb2..a7ac287 100644 --- a/Source/MetaCoreRender/Private/MetaCoreRenderDevice.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreRenderDevice.cpp @@ -9,6 +9,7 @@ namespace MetaCore { class MetaCoreRenderDevice::MetaCoreRenderDeviceImpl { public: MetaCoreWindow* Window = nullptr; + MetaCoreRenderDeviceConfig Config{}; bool Initialized = false; }; @@ -20,11 +21,12 @@ MetaCoreRenderDevice::~MetaCoreRenderDevice() { Shutdown(); } -bool MetaCoreRenderDevice::Initialize(MetaCoreWindow& window) { +bool MetaCoreRenderDevice::Initialize(MetaCoreWindow& window, MetaCoreRenderDeviceConfig config) { if (Impl_->Initialized) { return true; } Impl_->Window = &window; + Impl_->Config = config; Impl_->Initialized = true; return true; } @@ -47,4 +49,8 @@ void MetaCoreRenderDevice::SetSceneViewportRect(const MetaCoreViewportRect& view (void)viewportRect; } +MetaCoreRenderBackend MetaCoreRenderDevice::GetBackend() const { + return Impl_->Config.Backend; +} + } // namespace MetaCore diff --git a/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp b/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp index bd64c51..3a178c8 100644 --- a/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp @@ -138,7 +138,13 @@ MetaCoreSceneRenderSyncSnapshot MetaCoreSceneRenderSync::BuildSnapshot(const Met hostRoot.GetId(), hostRoot.GetName(), hostRoot.HasComponent(), - meshRenderer.BaseColor + meshRenderer.BaseColor, + meshRenderer.Metallic, + meshRenderer.Roughness, + meshRenderer.EmissiveColor, + meshRenderer.AlphaMode, + meshRenderer.AlphaCutoff, + meshRenderer.DoubleSided }); } diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h index db25b2f..d1a731a 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h @@ -1,6 +1,7 @@ #pragma once #include "MetaCoreFoundation/MetaCoreId.h" +#include "MetaCoreRender/MetaCoreRenderTypes.h" #include @@ -24,7 +25,7 @@ public: MetaCoreFilamentSceneBridge(); ~MetaCoreFilamentSceneBridge(); - bool Initialize(MetaCoreWindow& window, bool offscreen = true); + bool Initialize(MetaCoreWindow& window, bool offscreen = true, MetaCoreRenderBackend backend = MetaCoreRenderBackend::OpenGL); void Shutdown(); void Resize(int width, int height); void SetProjectRootPath(const std::filesystem::path& projectRootPath); diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h index 12f3656..e06d207 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h @@ -79,6 +79,7 @@ private: filament::TextureSampler mSampler; bool mFlipVertical = false; bool mOwnsImGuiContext = false; + ImGuiContext* mPreviousImGuiContext = nullptr; std::filesystem::path mSettingsPath; std::unordered_set mImGuiTextures; std::unordered_map mRuntimeUiTextures; diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCorePlayerRenderer.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCorePlayerRenderer.h new file mode 100644 index 0000000..3a9db84 --- /dev/null +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCorePlayerRenderer.h @@ -0,0 +1,31 @@ +#pragma once + +#include "MetaCoreRender/MetaCoreFilamentSceneBridge.h" +#include "MetaCoreRender/MetaCoreRenderTypes.h" + +#include + +namespace MetaCore { + +class MetaCoreRenderDevice; +class MetaCoreScene; +class MetaCoreWindow; +struct MetaCoreRuntimeUiFrame; + +class MetaCorePlayerRenderer { +public: + bool Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window); + void Shutdown(); + void SetViewportRect(const MetaCoreViewportRect& viewportRect); + void SetProjectRootPath(const std::filesystem::path& projectRootPath); + void RenderScene(MetaCoreScene& scene, const MetaCoreSceneView& sceneView); + void SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height); + void RenderAll(); + +private: + MetaCoreRenderDevice* RenderDevice_ = nullptr; + MetaCoreFilamentSceneBridge FilamentSceneBridge_{}; + MetaCoreViewportRect ViewportRect_{}; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRenderDevice.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRenderDevice.h index 1e34155..3871012 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRenderDevice.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRenderDevice.h @@ -16,11 +16,12 @@ public: MetaCoreRenderDevice(); ~MetaCoreRenderDevice(); - bool Initialize(MetaCoreWindow& window); + bool Initialize(MetaCoreWindow& window, MetaCoreRenderDeviceConfig config = {}); void Shutdown(); void RenderFrame() const; void PresentFrame() const; void SetSceneViewportRect(const MetaCoreViewportRect& viewportRect); + [[nodiscard]] MetaCoreRenderBackend GetBackend() const; private: class MetaCoreRenderDeviceImpl; diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRenderTypes.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRenderTypes.h index 86afaa2..8e0d888 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRenderTypes.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRenderTypes.h @@ -6,6 +6,16 @@ namespace MetaCore { +enum class MetaCoreRenderBackend { + OpenGL, + Vulkan, + WebGPU +}; + +struct MetaCoreRenderDeviceConfig { + MetaCoreRenderBackend Backend = MetaCoreRenderBackend::OpenGL; +}; + struct MetaCoreViewportRect { float Left = 0.0F; float Top = 0.0F; diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h index 61071c1..a079cda 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h @@ -35,6 +35,12 @@ struct MetaCoreRenderSyncRenderable { std::string HostRootName{}; bool HostRootHasModelRootTag = false; glm::vec3 BaseColor{1.0F, 1.0F, 1.0F}; + float Metallic = 0.0F; + float Roughness = 1.0F; + glm::vec3 EmissiveColor{0.0F, 0.0F, 0.0F}; + MetaCoreMeshAlphaMode AlphaMode = MetaCoreMeshAlphaMode::Opaque; + float AlphaCutoff = 0.5F; + bool DoubleSided = false; }; struct MetaCoreRenderSyncCamera { diff --git a/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp index 72bbb17..0fc242e 100644 --- a/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp +++ b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp @@ -168,6 +168,8 @@ MetaCoreRuntimeProjectDocument MetaCoreBuildDefaultRuntimeProjectDocument( document.StartupUiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; document.BuildProfileName = "Development"; document.TargetPlatform = "Windows"; + document.GraphicsBackend = "OpenGL"; + document.Architecture = "x64"; document.OutputDirectory = std::filesystem::path("Build") / "Windows"; document.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; document.UseCookedAssets = false; @@ -195,6 +197,12 @@ void MetaCoreApplyRuntimeProjectDefaults( if (document.TargetPlatform.empty()) { document.TargetPlatform = defaults.TargetPlatform; } + if (document.GraphicsBackend.empty()) { + document.GraphicsBackend = defaults.GraphicsBackend; + } + if (document.Architecture.empty()) { + document.Architecture = defaults.Architecture; + } if (document.OutputDirectory.empty()) { document.OutputDirectory = defaults.OutputDirectory; } diff --git a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h index fdde12e..fdb4929 100644 --- a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h +++ b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h @@ -141,6 +141,12 @@ struct MetaCoreRuntimeProjectDocument { MC_PROPERTY() std::string TargetPlatform{"Windows"}; + MC_PROPERTY() + std::string GraphicsBackend{"OpenGL"}; + + MC_PROPERTY() + std::string Architecture{"x64"}; + MC_PROPERTY() std::filesystem::path OutputDirectory{}; diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index c5eaea1..030af2b 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -1821,6 +1821,12 @@ void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() { rootMesh.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset; rootMesh.SourceModelPath = "Assets/Models/SyncRoot.glb"; rootMesh.BaseColor = glm::vec3(0.5F, 0.6F, 0.7F); + rootMesh.Metallic = 0.35F; + rootMesh.Roughness = 0.65F; + rootMesh.EmissiveColor = glm::vec3(0.05F, 0.10F, 0.15F); + rootMesh.AlphaMode = MetaCore::MetaCoreMeshAlphaMode::Mask; + rootMesh.AlphaCutoff = 0.42F; + rootMesh.DoubleSided = true; MetaCore::MetaCoreGameObject child = scene.CreateGameObject("RenderChild", root.GetId()); child.GetComponent().Position = glm::vec3(4.0F, 5.0F, 6.0F); @@ -1853,6 +1859,12 @@ void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() { if (renderable.ObjectId == root.GetId()) { MetaCoreExpect(renderable.Visible == true, "RenderSync 根对象 Visible 应为 true"); 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.65F) <= 0.0001F, "RenderSync 根对象 Roughness 应正确"); + MetaCoreExpectVec3Near(renderable.EmissiveColor, glm::vec3(0.05F, 0.10F, 0.15F), "RenderSync 根对象 EmissiveColor 应正确"); + MetaCoreExpect(renderable.AlphaMode == MetaCore::MetaCoreMeshAlphaMode::Mask, "RenderSync 根对象 AlphaMode 应正确"); + MetaCoreExpect(std::abs(renderable.AlphaCutoff - 0.42F) <= 0.0001F, "RenderSync 根对象 AlphaCutoff 应正确"); + MetaCoreExpect(renderable.DoubleSided == true, "RenderSync 根对象 DoubleSided 应正确"); } else if (renderable.ObjectId == child.GetId()) { MetaCoreExpect(renderable.Visible == false, "RenderSync 子对象 Visible 应为 false"); MetaCoreExpectVec3Near(renderable.BaseColor, glm::vec3(0.1F, 0.2F, 0.3F), "RenderSync 子对象 BaseColor 应正确"); @@ -2179,6 +2191,73 @@ void MetaCoreTestImportPipelineAndCook() { std::filesystem::remove_all(tempProjectRoot); } +void MetaCoreTestMissingModelSourceDoesNotRemainRegistered() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreMissingModelSourceProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"MissingModelSourceProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + const std::filesystem::path modelPath = tempProjectRoot / "Assets" / "Tree.gltf"; + { + std::ofstream gltfFile(modelPath, std::ios::trunc); + gltfFile << "{\n" + << " \"asset\": {\"version\": \"2.0\"},\n" + << " \"scenes\": [{\"nodes\": [0]}],\n" + << " \"nodes\": [{\"mesh\": 0, \"name\": \"Tree\"}],\n" + << " \"meshes\": [{\"name\": \"TreeMesh\", \"primitives\": [{}]}]\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto assetDatabase = moduleRegistry.ResolveService(); + const auto assetEditingService = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr, "AssetDatabaseService should be available"); + MetaCoreExpect(assetEditingService != nullptr, "AssetEditingService should be available"); + + const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Tree.gltf"); + MetaCoreExpect(modelRecord.has_value(), "Imported glTF source should be registered"); + MetaCoreExpect(assetEditingService->LoadModelAsset(modelRecord->Guid).has_value(), "Imported glTF package should be loadable"); + + const MetaCore::MetaCoreAssetGuid modelGuid = modelRecord->Guid; + const std::filesystem::path packagePath = tempProjectRoot / "Assets" / "Tree.gltf.mcasset"; + const std::filesystem::path metaPath = tempProjectRoot / "Assets" / "Tree.gltf.mcmeta"; + MetaCoreExpect(std::filesystem::exists(packagePath), "Model import should write a lightweight mcasset package"); + MetaCoreExpect(std::filesystem::exists(metaPath), "Model import should write mcmeta"); + + std::filesystem::remove(modelPath); + MetaCoreExpect(assetDatabase->Refresh(), "Refreshing after external source deletion should not crash"); + MetaCoreExpect(!assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Tree.gltf").has_value(), "Missing source model should not remain registered"); + MetaCoreExpect(!assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Tree.gltf.mcasset").has_value(), "Orphan model package should not be registered as an asset"); + MetaCoreExpect(!assetEditingService->LoadModelAsset(modelGuid).has_value(), "Deleted model should not be returned from the model cache"); + + MetaCore::MetaCoreAssetRegistry::Get().Clear(); + MetaCore::MetaCoreAssetRegistry::Get().ScanDirectory(tempProjectRoot / "Assets"); + MetaCoreExpect(MetaCore::MetaCoreAssetRegistry::Get().ResolveGuidToPath(modelGuid).empty(), "Orphan mcmeta should not register a missing source GUID"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + MetaCore::MetaCoreAssetRegistry::Get().Clear(); + std::filesystem::remove_all(tempProjectRoot); +} + void MetaCoreTestProjectDescriptorAndGltfImporterSkeleton() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreProjectGltfImporterProject"; @@ -3315,6 +3394,7 @@ void MetaCoreTestBuildServiceCreatesPlayerPackageLayout() { std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets" / "UI"); std::filesystem::create_directories(tempProjectRoot / "Assets" / "Materials"); + std::filesystem::create_directories(tempProjectRoot / "Assets" / "Models"); std::filesystem::create_directories(tempProjectRoot / "Config" / "RuntimeData"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Runtime"); @@ -3335,6 +3415,10 @@ void MetaCoreTestBuildServiceCreatesPlayerPackageLayout() { std::ofstream fakePlayer(tempProjectRoot / "Tools" / "MetaCorePlayer.exe", std::ios::binary | std::ios::trunc); fakePlayer << "fake player"; } + { + std::ofstream fakeNonExecutablePlayer(tempProjectRoot / "Tools" / "MetaCorePlayer.txt", std::ios::binary | std::ios::trunc); + fakeNonExecutablePlayer << "not a Windows executable"; + } _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); @@ -3376,6 +3460,35 @@ void MetaCoreTestBuildServiceCreatesPlayerPackageLayout() { "Build package test should write dependency material" ); + const std::filesystem::path modelPath = std::filesystem::path("Assets") / "Models" / "Tree.gltf"; + const std::filesystem::path modelTexturePath = std::filesystem::path("Assets") / "Models" / "TreeBase.ppm"; + const std::filesystem::path glbModelPath = std::filesystem::path("Assets") / "Models" / "CookedBox.glb"; + { + const unsigned char modelTextureData[] = { + 0x50, 0x36, 0x0A, 0x31, 0x20, 0x31, 0x0A, 0x32, + 0x35, 0x35, 0x0A, 0x30, 0x80, 0x30 + }; + std::ofstream textureFile(tempProjectRoot / modelTexturePath, std::ios::binary | std::ios::trunc); + textureFile.write(reinterpret_cast(modelTextureData), sizeof(modelTextureData)); + } + { + std::ofstream gltfFile(tempProjectRoot / modelPath, std::ios::trunc); + gltfFile << "{\n" + << " \"asset\": {\"version\": \"2.0\"},\n" + << " \"scenes\": [{\"nodes\": [0]}],\n" + << " \"nodes\": [{\"mesh\": 0, \"name\": \"Tree\"}],\n" + << " \"meshes\": [{\"name\": \"TreeMesh\", \"primitives\": [{\"material\": 0}]}],\n" + << " \"materials\": [{\"name\": \"TreeMaterial\", \"pbrMetallicRoughness\": {\"baseColorTexture\": {\"index\": 0}}}],\n" + << " \"textures\": [{\"source\": 0}],\n" + << " \"images\": [{\"uri\": \"TreeBase.ppm\"}]\n" + << "}\n"; + } + { + const std::filesystem::path sandboxGlbPath = std::filesystem::path("D:/MetaCore/SandboxProject/Assets/Models/box1.glb"); + MetaCoreExpect(std::filesystem::exists(sandboxGlbPath), "Build package test requires SandboxProject box1.glb fixture"); + std::filesystem::copy_file(sandboxGlbPath, tempProjectRoot / glbModelPath, std::filesystem::copy_options::overwrite_existing); + } + const std::filesystem::path uiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; MetaCore::MetaCoreUiDocument uiDocument; uiDocument.Name = "Hud"; @@ -3395,6 +3508,8 @@ void MetaCoreTestBuildServiceCreatesPlayerPackageLayout() { runtimeProject.StartupUiPath = uiPath; runtimeProject.BuildProfileName = "BuildSmokeProfile"; runtimeProject.TargetPlatform = "Windows"; + runtimeProject.GraphicsBackend = "Vulkan"; + runtimeProject.Architecture = "x64"; runtimeProject.OutputDirectory = std::filesystem::path("BuildSettingsOutput"); runtimeProject.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; runtimeProject.UseCookedAssets = false; @@ -3447,15 +3562,29 @@ void MetaCoreTestBuildServiceCreatesPlayerPackageLayout() { MetaCoreExpect(assetDatabase->Refresh(), "Build package test should refresh asset database"); const auto materialRecord = assetDatabase->FindAssetByRelativePath(materialPath); MetaCoreExpect(materialRecord.has_value(), "Build package test should register dependency material"); + const auto modelRecord = assetDatabase->FindAssetByRelativePath(modelPath); + MetaCoreExpect(modelRecord.has_value(), "Build package test should register glTF model source"); + const auto glbModelRecord = assetDatabase->FindAssetByRelativePath(glbModelPath); + MetaCoreExpect(glbModelRecord.has_value(), "Build package test should register GLB model source"); bool assignedMaterialDependency = false; for (MetaCore::MetaCoreGameObjectData& gameObject : sceneDocument.GameObjects) { if (gameObject.MeshRenderer.has_value()) { gameObject.MeshRenderer->MaterialAssetGuids = {materialRecord->Guid}; + gameObject.MeshRenderer->SourceModelPath = modelPath.generic_string(); assignedMaterialDependency = true; break; } } MetaCoreExpect(assignedMaterialDependency, "Build package test startup scene should have a MeshRenderer"); + + MetaCore::MetaCoreGameObjectData glbGameObject; + glbGameObject.Id = 9001; + glbGameObject.Name = "CookedBox"; + glbGameObject.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; + glbGameObject.MeshRenderer->MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset; + glbGameObject.MeshRenderer->SourceModelAssetGuid = glbModelRecord->Guid; + sceneDocument.GameObjects.push_back(std::move(glbGameObject)); + MetaCoreExpect( MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(tempProjectRoot / scenePath, sceneDocument, registry), "Build package test should rewrite startup scene with material dependency" @@ -3464,11 +3593,22 @@ void MetaCoreTestBuildServiceCreatesPlayerPackageLayout() { MetaCore::MetaCoreBuildPlayerPackageRequest request; request.PlayerExecutablePath = tempProjectRoot / "Tools" / "MetaCorePlayer.exe"; + request.Target.GraphicsBackend = MetaCore::MetaCoreGraphicsBackend::Vulkan; const MetaCore::MetaCoreBuildPlayerPackageResult result = buildService->BuildPlayerPackage(request); MetaCoreExpect(result.Success, ("BuildService should create player package: " + result.Error).c_str()); const std::filesystem::path outputRoot = tempProjectRoot / "BuildSettingsOutput" / "BuildPackageProject"; MetaCoreExpect(result.OutputRoot == outputRoot, "BuildService should use runtime project output root"); MetaCoreExpect(std::filesystem::exists(outputRoot / "MetaCorePlayer.exe"), "Build output should include MetaCorePlayer.exe"); + MetaCoreExpect(std::filesystem::exists(outputRoot / "MetaCorePlayer.config.json"), "Build output should include Player launch config"); + { + std::ifstream playerConfigInput(outputRoot / "MetaCorePlayer.config.json", std::ios::binary); + nlohmann::json playerConfigJson; + playerConfigInput >> playerConfigJson; + MetaCoreExpect(playerConfigJson["target"]["platform"] == "Windows", "Player config should record target platform"); + MetaCoreExpect(playerConfigJson["target"]["graphics_backend"] == "Vulkan", "Player config should record graphics backend"); + MetaCoreExpect(playerConfigJson["target"]["architecture"] == "x64", "Player config should record architecture"); + MetaCoreExpect(playerConfigJson["runtime"]["build_profile"] == "BuildSmokeProfile", "Player config should record build profile"); + } MetaCoreExpect(std::filesystem::exists(outputRoot / "MetaCore.project.json"), "Build output should include project descriptor"); const auto packagedProjectFile = MetaCore::MetaCoreReadProjectFile(outputRoot / "MetaCore.project.json"); MetaCoreExpect(packagedProjectFile.has_value(), "Build output project descriptor should be readable"); @@ -3488,6 +3628,8 @@ void MetaCoreTestBuildServiceCreatesPlayerPackageLayout() { ); MetaCoreExpect(packagedLooseRuntimeProject.has_value(), "Build output runtime project should be readable"); MetaCoreExpect(packagedLooseRuntimeProject->BuildProfileName == "BuildSmokeProfile", "Build output should preserve saved build profile"); + MetaCoreExpect(packagedLooseRuntimeProject->GraphicsBackend == "Vulkan", "Build output should preserve graphics backend"); + MetaCoreExpect(packagedLooseRuntimeProject->Architecture == "x64", "Build output should preserve architecture"); MetaCoreExpect(packagedLooseRuntimeProject->OutputDirectory == runtimeProject.OutputDirectory, "Build output should preserve saved output directory"); MetaCoreExpect(std::filesystem::exists(outputRoot / "Library" / "Cooked" / "Windows" / "CookManifest.bin"), "Build output should include CookManifest"); const std::filesystem::path cookedMaterialPath = cookService->GetCookedPathForAsset(materialRecord->Guid); @@ -3525,8 +3667,21 @@ void MetaCoreTestBuildServiceCreatesPlayerPackageLayout() { ); const std::filesystem::path cookedOnlyOutputRoot = tempProjectRoot / "BuildCooked" / "BuildPackageProject"; + const std::filesystem::path cookedGlbPath = cookService->GetCookedPathForAsset(glbModelRecord->Guid); + const std::filesystem::path cookedGltfPath = cookService->GetCookedPathForAsset(modelRecord->Guid); + MetaCoreExpect(!cookedGlbPath.empty(), "Cooked-only build should cook GLB model source"); + MetaCoreExpect(!cookedGltfPath.empty(), "Cooked-only build should cook glTF model source"); MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / "MetaCorePlayer.exe"), "Cooked-only build output should include MetaCorePlayer.exe"); MetaCoreExpect(!std::filesystem::exists(cookedOnlyOutputRoot / scenePath), "Cooked-only build output should not copy loose startup scene"); + MetaCoreExpect(!std::filesystem::exists(cookedOnlyOutputRoot / modelPath), "Cooked-only build output should not copy loose glTF model source"); + MetaCoreExpect(!std::filesystem::exists(cookedOnlyOutputRoot / modelTexturePath), "Cooked-only build output should not copy loose glTF local texture dependency"); + MetaCoreExpect(!std::filesystem::exists(cookedOnlyOutputRoot / glbModelPath), "Cooked-only build output should not copy loose GLB model source"); + MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / cookedGltfPath), "Cooked-only build output should include cooked glTF model binary"); + MetaCoreExpect( + std::filesystem::exists(cookedOnlyOutputRoot / cookedGltfPath.parent_path() / "TreeBase.ppm"), + "Cooked-only build output should include glTF texture beside cooked model" + ); + MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / cookedGlbPath), "Cooked-only build output should include cooked GLB model binary"); MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / runtimeProjectPath), "Cooked-only build output should include runtime project config"); MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / runtimeProject.DataSourcesPath), "Cooked-only build output should include custom runtime data sources path"); MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / runtimeProject.BindingsPath), "Cooked-only build output should include custom runtime bindings path"); @@ -3543,6 +3698,90 @@ void MetaCoreTestBuildServiceCreatesPlayerPackageLayout() { ); MetaCoreExpect(!cookedOnlyResult.CookedAssets.empty(), "Cooked-only build result should record cooked assets"); + MetaCore::MetaCoreBuildPlayerPackageRequest invalidWebVulkanRequest; + invalidWebVulkanRequest.PlayerExecutablePath = tempProjectRoot / "Tools" / "MetaCorePlayer.exe"; + invalidWebVulkanRequest.Target.Platform = MetaCore::MetaCoreBuildPlatform::Web; + invalidWebVulkanRequest.Target.GraphicsBackend = MetaCore::MetaCoreGraphicsBackend::Vulkan; + invalidWebVulkanRequest.Target.Architecture = MetaCore::MetaCoreBuildArchitecture::Wasm32; + const MetaCore::MetaCoreBuildPlayerPackageResult invalidWebVulkanResult = + buildService->BuildPlayerPackage(invalidWebVulkanRequest); + MetaCoreExpect(!invalidWebVulkanResult.Success, "BuildService should reject Web targets with Vulkan graphics"); + MetaCoreExpect( + invalidWebVulkanResult.Error.find("Web build target requires WebGL or WebGPU") != std::string::npos, + "BuildService should explain invalid Web graphics backend" + ); + + MetaCore::MetaCoreBuildPlayerPackageRequest invalidWindowsWebGpuRequest; + invalidWindowsWebGpuRequest.PlayerExecutablePath = tempProjectRoot / "Tools" / "MetaCorePlayer.exe"; + invalidWindowsWebGpuRequest.Target.Platform = MetaCore::MetaCoreBuildPlatform::Windows; + invalidWindowsWebGpuRequest.Target.GraphicsBackend = MetaCore::MetaCoreGraphicsBackend::WebGPU; + invalidWindowsWebGpuRequest.Target.Architecture = MetaCore::MetaCoreBuildArchitecture::X64; + const MetaCore::MetaCoreBuildPlayerPackageResult invalidWindowsWebGpuResult = + buildService->BuildPlayerPackage(invalidWindowsWebGpuRequest); + MetaCoreExpect(!invalidWindowsWebGpuResult.Success, "BuildService should reject native targets with WebGPU graphics"); + MetaCoreExpect( + invalidWindowsWebGpuResult.Error.find("only valid for Web build targets") != std::string::npos, + "BuildService should explain invalid native WebGPU backend" + ); + + MetaCore::MetaCoreBuildPlayerPackageRequest invalidWebX64Request; + invalidWebX64Request.PlayerExecutablePath = tempProjectRoot / "Tools" / "MetaCorePlayer.exe"; + invalidWebX64Request.Target.Platform = MetaCore::MetaCoreBuildPlatform::Web; + invalidWebX64Request.Target.GraphicsBackend = MetaCore::MetaCoreGraphicsBackend::WebGL; + invalidWebX64Request.Target.Architecture = MetaCore::MetaCoreBuildArchitecture::X64; + const MetaCore::MetaCoreBuildPlayerPackageResult invalidWebX64Result = + buildService->BuildPlayerPackage(invalidWebX64Request); + MetaCoreExpect(!invalidWebX64Result.Success, "BuildService should reject Web targets without wasm32 architecture"); + MetaCoreExpect( + invalidWebX64Result.Error.find("requires wasm32 architecture") != std::string::npos, + "BuildService should explain invalid Web architecture" + ); + + MetaCore::MetaCoreBuildPlayerPackageRequest unsupportedWebRequest; + unsupportedWebRequest.PlayerExecutablePath = tempProjectRoot / "Tools" / "MetaCorePlayer.exe"; + unsupportedWebRequest.Target.Platform = MetaCore::MetaCoreBuildPlatform::Web; + unsupportedWebRequest.Target.GraphicsBackend = MetaCore::MetaCoreGraphicsBackend::WebGPU; + unsupportedWebRequest.Target.Architecture = MetaCore::MetaCoreBuildArchitecture::Wasm32; + const MetaCore::MetaCoreBuildPlayerPackageResult unsupportedWebResult = + buildService->BuildPlayerPackage(unsupportedWebRequest); + MetaCoreExpect(!unsupportedWebResult.Success, "BuildService should reject Web packaging until implemented"); + MetaCoreExpect( + unsupportedWebResult.Error.find("Web build packaging is not implemented yet") != std::string::npos, + "BuildService should explain that Web packaging is not implemented" + ); + + MetaCore::MetaCoreBuildPlayerPackageRequest unsupportedLinuxRequest; + unsupportedLinuxRequest.PlayerExecutablePath = tempProjectRoot / "Tools" / "MetaCorePlayer.exe"; + unsupportedLinuxRequest.Target.Platform = MetaCore::MetaCoreBuildPlatform::Linux; + unsupportedLinuxRequest.Target.GraphicsBackend = MetaCore::MetaCoreGraphicsBackend::Vulkan; + unsupportedLinuxRequest.Target.Architecture = MetaCore::MetaCoreBuildArchitecture::X64; + const MetaCore::MetaCoreBuildPlayerPackageResult unsupportedLinuxResult = + buildService->BuildPlayerPackage(unsupportedLinuxRequest); + MetaCoreExpect(!unsupportedLinuxResult.Success, "BuildService should reject non-Windows packaging until implemented"); + MetaCoreExpect( + unsupportedLinuxResult.Error.find("Windows Player packages only") != std::string::npos, + "BuildService should explain that only Windows packages are currently implemented" + ); + + MetaCore::MetaCoreBuildPlayerPackageRequest invalidWindowsPlayerPathRequest; + invalidWindowsPlayerPathRequest.PlayerExecutablePath = tempProjectRoot / "Tools" / "MetaCorePlayer.txt"; + invalidWindowsPlayerPathRequest.Target.Platform = MetaCore::MetaCoreBuildPlatform::Windows; + invalidWindowsPlayerPathRequest.Target.GraphicsBackend = MetaCore::MetaCoreGraphicsBackend::OpenGL; + invalidWindowsPlayerPathRequest.Target.Architecture = MetaCore::MetaCoreBuildArchitecture::X64; + invalidWindowsPlayerPathRequest.OutputDirectory = tempProjectRoot / "InvalidPlayerPathOutput"; + invalidWindowsPlayerPathRequest.CookBeforePackage = false; + const MetaCore::MetaCoreBuildPlayerPackageResult invalidWindowsPlayerPathResult = + buildService->BuildPlayerPackage(invalidWindowsPlayerPathRequest); + MetaCoreExpect(!invalidWindowsPlayerPathResult.Success, "BuildService should reject non-exe Windows Player paths"); + MetaCoreExpect( + invalidWindowsPlayerPathResult.Error.find("requires a .exe player executable") != std::string::npos, + "BuildService should explain invalid Windows Player executable extension" + ); + MetaCoreExpect( + !std::filesystem::exists(tempProjectRoot / "InvalidPlayerPathOutput"), + "BuildService should reject invalid Player paths before creating output directories" + ); + runtimeProject.DataSourcesPath = std::filesystem::path("..") / "OutsideDataSources.mcruntime"; MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeProjectDocument(tempProjectRoot / runtimeProjectPath, runtimeProject, registry), @@ -3852,6 +4091,8 @@ void MetaCoreTestRuntimeProjectDocumentIo() { writtenDocument.StartupUiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; writtenDocument.BuildProfileName = "Shipping"; writtenDocument.TargetPlatform = "Windows"; + writtenDocument.GraphicsBackend = "Vulkan"; + writtenDocument.Architecture = "x64"; writtenDocument.OutputDirectory = std::filesystem::path("Build") / "Windows"; writtenDocument.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; writtenDocument.UseCookedAssets = true; @@ -3869,6 +4110,8 @@ void MetaCoreTestRuntimeProjectDocumentIo() { MetaCoreExpect(loadedDocument->StartupUiPath == writtenDocument.StartupUiPath, "StartupUiPath should round trip"); MetaCoreExpect(loadedDocument->BuildProfileName == writtenDocument.BuildProfileName, "BuildProfileName should round trip"); MetaCoreExpect(loadedDocument->TargetPlatform == writtenDocument.TargetPlatform, "TargetPlatform should round trip"); + MetaCoreExpect(loadedDocument->GraphicsBackend == writtenDocument.GraphicsBackend, "GraphicsBackend should round trip"); + MetaCoreExpect(loadedDocument->Architecture == writtenDocument.Architecture, "Architecture should round trip"); MetaCoreExpect(loadedDocument->OutputDirectory == writtenDocument.OutputDirectory, "OutputDirectory should round trip"); MetaCoreExpect(loadedDocument->CookedAssetsDirectory == writtenDocument.CookedAssetsDirectory, "CookedAssetsDirectory should round trip"); MetaCoreExpect(loadedDocument->UseCookedAssets == writtenDocument.UseCookedAssets, "UseCookedAssets should round trip"); @@ -3890,6 +4133,8 @@ void MetaCoreTestRuntimeProjectDocumentIo() { defaultRuntimeProject.StartupScenePath == std::filesystem::path("Scenes") / "Main.mcscene.json", "RuntimeProject defaults should use the JSON scene asset extension" ); + MetaCoreExpect(defaultRuntimeProject.GraphicsBackend == "OpenGL", "RuntimeProject defaults should use OpenGL graphics"); + MetaCoreExpect(defaultRuntimeProject.Architecture == "x64", "RuntimeProject defaults should use x64 architecture"); MetaCoreExpect( defaultRuntimeProject.DataSourcesPath == std::filesystem::path("ConfigRuntime") / "DataSources.mcruntime", "RuntimeProject defaults should place DataSources under runtime_directory" @@ -6147,6 +6392,8 @@ int main() { MetaCoreTestStaticCppComponentScenePersistenceAndUndo(); std::cout << "[RUN] MetaCoreTestImportPipelineAndCook..." << std::endl; MetaCoreTestImportPipelineAndCook(); + std::cout << "[RUN] MetaCoreTestMissingModelSourceDoesNotRemainRegistered..." << std::endl; + MetaCoreTestMissingModelSourceDoesNotRemainRegistered(); std::cout << "[RUN] MetaCoreTestProjectDescriptorAndGltfImporterSkeleton..." << std::endl; MetaCoreTestProjectDescriptorAndGltfImporterSkeleton(); std::cout << "[RUN] MetaCoreTestInstantiateImportedModelAssetIntoScene..." << std::endl; diff --git a/tools/MetaCoreBuildPackageTool/main.cpp b/tools/MetaCoreBuildPackageTool/main.cpp index 0a101cd..aa778f1 100644 --- a/tools/MetaCoreBuildPackageTool/main.cpp +++ b/tools/MetaCoreBuildPackageTool/main.cpp @@ -16,12 +16,67 @@ void MetaCorePrintUsage() { << "Options:\n" << " --player Path to MetaCorePlayer.exe\n" << " --output Build output base directory\n" + << " --platform \n" + << " --graphics \n" + << " --arch \n" << " --no-cook Package without cooking first\n" << " --no-loose-content Do not copy loose Assets/Scenes\n" << " --no-runtime-config Do not copy Runtime configuration\n" << " --use-cooked-assets Enable cooked asset loading in packaged runtime config\n"; } +[[nodiscard]] bool MetaCoreParseBuildPlatform( + std::string_view value, + MetaCore::MetaCoreBuildPlatform& outputPlatform +) { + if (value == "windows") { + outputPlatform = MetaCore::MetaCoreBuildPlatform::Windows; + } else if (value == "linux") { + outputPlatform = MetaCore::MetaCoreBuildPlatform::Linux; + } else if (value == "android") { + outputPlatform = MetaCore::MetaCoreBuildPlatform::Android; + } else if (value == "web") { + outputPlatform = MetaCore::MetaCoreBuildPlatform::Web; + } else { + return false; + } + return true; +} + +[[nodiscard]] bool MetaCoreParseGraphicsBackend( + std::string_view value, + MetaCore::MetaCoreGraphicsBackend& outputBackend +) { + if (value == "opengl") { + outputBackend = MetaCore::MetaCoreGraphicsBackend::OpenGL; + } else if (value == "vulkan") { + outputBackend = MetaCore::MetaCoreGraphicsBackend::Vulkan; + } else if (value == "webgl") { + outputBackend = MetaCore::MetaCoreGraphicsBackend::WebGL; + } else if (value == "webgpu") { + outputBackend = MetaCore::MetaCoreGraphicsBackend::WebGPU; + } else { + return false; + } + return true; +} + +[[nodiscard]] bool MetaCoreParseBuildArchitecture( + std::string_view value, + MetaCore::MetaCoreBuildArchitecture& outputArchitecture +) { + if (value == "x64") { + outputArchitecture = MetaCore::MetaCoreBuildArchitecture::X64; + } else if (value == "arm64") { + outputArchitecture = MetaCore::MetaCoreBuildArchitecture::Arm64; + } else if (value == "wasm32") { + outputArchitecture = MetaCore::MetaCoreBuildArchitecture::Wasm32; + } else { + return false; + } + return true; +} + [[nodiscard]] bool MetaCoreSetProjectPathEnvironment(const std::filesystem::path& projectRoot) { #if defined(_WIN32) return _putenv_s("METACORE_PROJECT_PATH", projectRoot.string().c_str()) == 0; @@ -103,6 +158,21 @@ int main(int argc, char* argv[]) { std::cerr << "--output requires a directory\n"; return 1; } + } else if (argument == "--platform") { + if (index + 1 >= argc || !MetaCoreParseBuildPlatform(argv[++index], request.Target.Platform)) { + std::cerr << "--platform requires one of: windows, linux, android, web\n"; + return 1; + } + } else if (argument == "--graphics") { + if (index + 1 >= argc || !MetaCoreParseGraphicsBackend(argv[++index], request.Target.GraphicsBackend)) { + std::cerr << "--graphics requires one of: opengl, vulkan, webgl, webgpu\n"; + return 1; + } + } else if (argument == "--arch") { + if (index + 1 >= argc || !MetaCoreParseBuildArchitecture(argv[++index], request.Target.Architecture)) { + std::cerr << "--arch requires one of: x64, arm64, wasm32\n"; + return 1; + } } else if (argument == "--no-cook") { request.CookBeforePackage = false; } else if (argument == "--no-loose-content") { diff --git a/vcpkg.json b/vcpkg.json index 280c13d..086e26c 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,7 @@ { "name": "metacore", "version-string": "1.0.0", + "builtin-baseline": "717f2c1db552beed995736e41accdf4f6f4ca986", "dependencies": [ "glm", "entt", @@ -16,14 +17,21 @@ { "name": "rmlui", "default-features": false - }, - { - "name": "qtbase", - "default-features": false, - "features": [ - "gui", - "widgets" + } + ], + "features": { + "launcher": { + "description": "Build the Qt-based MetaCore launcher.", + "dependencies": [ + { + "name": "qtbase", + "default-features": false, + "features": [ + "gui", + "widgets" + ] + } ] } - ] + } }