diff --git a/Apps/MetaCorePlayer/main.cpp b/Apps/MetaCorePlayer/main.cpp index 0d4a039..c496651 100644 --- a/Apps/MetaCorePlayer/main.cpp +++ b/Apps/MetaCorePlayer/main.cpp @@ -133,6 +133,18 @@ MetaCore::MetaCoreSceneView MetaCoreBuildPlayerSceneView() { } // namespace int main(int argc, char* argv[]) { + std::filesystem::path customProjectRoot{}; + std::filesystem::path customScenePath{}; + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--project") == 0 && i + 1 < argc) { + customProjectRoot = argv[i + 1]; + ++i; + } else if (std::strcmp(argv[i], "--scene") == 0 && i + 1 < argc) { + customScenePath = argv[i + 1]; + ++i; + } + } + if (argc > 0) { std::filesystem::current_path(std::filesystem::path(argv[0]).parent_path()); } @@ -150,29 +162,39 @@ int main(int argc, char* argv[]) { } MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; - if (!viewportRenderer.Initialize(renderDevice, window)) { + if (!viewportRenderer.Initialize(renderDevice, window, false)) { std::cerr << "MetaCorePlayer: viewport renderer initialize failed\n"; return 1; } - const auto projectRoot = MetaCore::MetaCoreDiscoverProjectRoot(); - if (!projectRoot.has_value()) { - std::cerr << "MetaCorePlayer: failed to discover project root\n"; - return 1; + std::filesystem::path projectRoot; + if (!customProjectRoot.empty()) { + projectRoot = std::filesystem::absolute(customProjectRoot); + } else { + const auto discovered = MetaCore::MetaCoreDiscoverProjectRoot(); + if (!discovered.has_value()) { + std::cerr << "MetaCorePlayer: failed to discover project root\n"; + return 1; + } + projectRoot = *discovered; } - const std::filesystem::path projectPath = MetaCore::MetaCoreGetProjectFilePath(*projectRoot); - viewportRenderer.SetProjectRootPath(*projectRoot); + const std::filesystem::path projectPath = MetaCore::MetaCoreGetProjectFilePath(projectRoot); + viewportRenderer.SetProjectRootPath(projectRoot); MetaCore::MetaCoreTypeRegistry typeRegistry; MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(typeRegistry); const auto loadedRuntimeProjectDocument = MetaCore::MetaCoreReadRuntimeProjectDocument( - *projectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", + projectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", typeRegistry ); const auto runtimeProjectDocument = loadedRuntimeProjectDocument.value_or(MetaCoreBuildDefaultRuntimeProjectDocument()); std::optional startupSceneDocument; - if (!runtimeProjectDocument.StartupScenePath.empty()) { - startupSceneDocument = MetaCore::MetaCoreReadScenePackage(*projectRoot / runtimeProjectDocument.StartupScenePath); + if (!customScenePath.empty()) { + const auto absoluteScene = customScenePath.is_absolute() ? customScenePath : (projectRoot / customScenePath); + startupSceneDocument = MetaCore::MetaCoreReadScenePackage(absoluteScene); + } + if (!startupSceneDocument.has_value() && !runtimeProjectDocument.StartupScenePath.empty()) { + startupSceneDocument = MetaCore::MetaCoreReadScenePackage(projectRoot / runtimeProjectDocument.StartupScenePath); } if (!startupSceneDocument.has_value()) { startupSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(projectPath); @@ -190,9 +212,9 @@ int main(int argc, char* argv[]) { } MetaCore::MetaCoreRuntimeDataDispatcher runtimeDataDispatcher(scene); - const auto sourcesPath = (*projectRoot / runtimeProjectDocument.DataSourcesPath).lexically_normal(); - const auto bindingsPath = (*projectRoot / runtimeProjectDocument.BindingsPath).lexically_normal(); - const auto diagnosticsPath = (*projectRoot / runtimeProjectDocument.DiagnosticsPath).lexically_normal(); + const auto sourcesPath = (projectRoot / runtimeProjectDocument.DataSourcesPath).lexically_normal(); + const auto bindingsPath = (projectRoot / runtimeProjectDocument.BindingsPath).lexically_normal(); + const auto diagnosticsPath = (projectRoot / runtimeProjectDocument.DiagnosticsPath).lexically_normal(); const auto loadedSourcesDocument = MetaCore::MetaCoreReadRuntimeDataSourcesDocument(sourcesPath, typeRegistry); const auto loadedBindingsDocument = MetaCore::MetaCoreReadRuntimeBindingsDocument(bindingsPath, typeRegistry); const auto sourcesDocument = loadedSourcesDocument.value_or(MetaCoreBuildDefaultRuntimeDataSourcesDocument()); @@ -279,6 +301,7 @@ int main(int argc, char* argv[]) { } } viewportRenderer.RenderSceneToViewport(scene, MetaCoreBuildPlayerSceneView(), true); + viewportRenderer.RenderAll(); renderDevice.RenderFrame(); renderDevice.PresentFrame(); window.EndFrame(); diff --git a/SandboxProject/Scenes/Main.mcscene b/SandboxProject/Scenes/Main.mcscene index fbd2154..e3d82b6 100644 Binary files a/SandboxProject/Scenes/Main.mcscene and b/SandboxProject/Scenes/Main.mcscene differ diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index 7b936f5..7e9bc97 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -1792,6 +1792,14 @@ void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorCon materialGuid = materialPayload->MaterialAssetGuid; } } + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(MetaCoreProjectAssetDragDropPayloadType); + payload != nullptr && payload->DataSize == sizeof(MetaCoreProjectAssetDragDropPayload)) { + const auto* assetPayload = static_cast(payload->Data); + if (assetPayload != nullptr && std::strcmp(assetPayload->AssetType, "material") == 0 && + MetaCoreApplyDraggedMaterialToSelectedObjects(editorContext, materialIndex, assetPayload->AssetGuid)) { + materialGuid = assetPayload->AssetGuid; + } + } ImGui::EndDragDropTarget(); } const std::string label = "Material Guid " + std::to_string(materialIndex); @@ -2794,7 +2802,8 @@ void MetaCoreWriteEmbeddedTextureRuntimePackages( const std::string extension = path.extension().string(); if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga" || - extension == ".ppm" || extension == ".pnm" || extension == ".pgm") { + extension == ".ppm" || extension == ".pnm" || extension == ".pgm" || + extension == ".ktx" || extension == ".hdr") { return "TextureImporter"; } if (MetaCoreIsGltfModelPath(path)) { @@ -2841,7 +2850,8 @@ void MetaCoreWriteEmbeddedTextureRuntimePackages( return "ui_document"; } if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga" || - extension == ".ppm" || extension == ".pnm" || extension == ".pgm") { + extension == ".ppm" || extension == ".pnm" || extension == ".pgm" || + extension == ".ktx" || extension == ".hdr") { return "texture"; } if (extension == ".fbx" || extension == ".obj" || MetaCoreIsGltfModelPath(path)) { @@ -3194,6 +3204,7 @@ public: [[nodiscard]] bool OpenProject(const std::filesystem::path& projectRoot) override; [[nodiscard]] bool RenamePath(const std::filesystem::path& relativePath, const std::filesystem::path& newName) override; [[nodiscard]] bool MovePath(const std::filesystem::path& relativePath, const std::filesystem::path& targetDirectory) override; + [[nodiscard]] bool DeletePath(const std::filesystem::path& relativePath) override; [[nodiscard]] const std::vector& GetAssetRegistry() const override { return AssetRecords_; } [[nodiscard]] std::optional FindAssetByGuid(const MetaCoreAssetGuid& assetGuid) const override { @@ -3321,6 +3332,10 @@ void MetaCoreBuiltinAssetDatabaseService::LoadProjectDescriptor() { (void)std::filesystem::create_directories(Project_.UiPath); (void)std::filesystem::create_directories(Project_.LibraryPath); (void)std::filesystem::create_directories(Project_.BuildPath); + (void)std::filesystem::create_directories(Project_.AssetsPath / "Models"); + (void)std::filesystem::create_directories(Project_.AssetsPath / "Materials"); + (void)std::filesystem::create_directories(Project_.AssetsPath / "Textures"); + (void)std::filesystem::create_directories(Project_.AssetsPath / "Prefabs"); // 首次打开项目时,自动初始化扫描全局资产 GUID 注册表 (对标 Unity 升级) MetaCoreAssetRegistry::Get().Clear(); @@ -3343,6 +3358,10 @@ bool MetaCoreBuiltinAssetDatabaseService::CreateProject(const std::filesystem::p (void)std::filesystem::create_directories(normalizedRoot / "Ui"); (void)std::filesystem::create_directories(normalizedRoot / "Library"); (void)std::filesystem::create_directories(normalizedRoot / "Build"); + (void)std::filesystem::create_directories(normalizedRoot / "Assets" / "Models"); + (void)std::filesystem::create_directories(normalizedRoot / "Assets" / "Materials"); + (void)std::filesystem::create_directories(normalizedRoot / "Assets" / "Textures"); + (void)std::filesystem::create_directories(normalizedRoot / "Assets" / "Prefabs"); MetaCoreProjectFileDocument document; if (!projectName.empty()) { @@ -3509,6 +3528,55 @@ bool MetaCoreBuiltinAssetDatabaseService::MovePath( return Refresh(); } +bool MetaCoreBuiltinAssetDatabaseService::DeletePath(const std::filesystem::path& relativePath) { + if (!HasProject() || MetaCoreIsUnsafeRelativePath(relativePath) || relativePath.empty()) { + return false; + } + + const std::filesystem::path normalizedSourcePath = relativePath.lexically_normal(); + const std::filesystem::path absoluteSourcePath = Project_.RootPath / normalizedSourcePath; + if (!std::filesystem::exists(absoluteSourcePath)) { + return false; + } + + std::error_code errorCode; + std::filesystem::remove_all(absoluteSourcePath, errorCode); + if (errorCode) { + return false; + } + + const std::filesystem::path metaPath = Project_.RootPath / MetaCoreBuildMetaPath(normalizedSourcePath); + if (std::filesystem::exists(metaPath)) { + std::filesystem::remove_all(metaPath, errorCode); + } + + const std::filesystem::path packagePath = Project_.RootPath / MetaCoreBuildPackagePathFromSource(normalizedSourcePath); + if (std::filesystem::exists(packagePath)) { + std::filesystem::remove_all(packagePath, errorCode); + } + + auto isUnderDeleted = [&](const std::filesystem::path& path) { + if (path == normalizedSourcePath) { + return true; + } + const auto pathStr = path.lexically_normal().generic_string(); + const auto sourceStr = normalizedSourcePath.generic_string() + "/"; + return pathStr.rfind(sourceStr, 0) == 0; + }; + + if (isUnderDeleted(Project_.StartupScenePath)) { + Project_.StartupScenePath.clear(); + } + + Project_.ScenePaths.erase( + std::remove_if(Project_.ScenePaths.begin(), Project_.ScenePaths.end(), isUnderDeleted), + Project_.ScenePaths.end() + ); + + SaveProjectDescriptor(); + return Refresh(); +} + void MetaCoreBuiltinAssetDatabaseService::SaveProjectDescriptor() const { if (!HasProject()) { return; @@ -3558,9 +3626,13 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { } MetaCoreAssetGuid sceneGuid{}; + bool hasSceneMeta = false; std::filesystem::path metaPath = MetaCoreBuildMetaPath(entry.path()); if (!std::filesystem::exists(metaPath)) { - metaPath = entry.path().string() + ".meta"; + const auto legacyMetaPath = entry.path().string() + ".meta"; + if (std::filesystem::exists(legacyMetaPath)) { + metaPath = legacyMetaPath; + } } if (std::filesystem::exists(metaPath)) { std::ifstream metaFile(metaPath); @@ -3570,6 +3642,7 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { metaFile >> json; if (json.contains("guid") && json["guid"].is_string()) { sceneGuid = MetaCoreAssetGuid::Parse(json["guid"].get()).value_or(MetaCoreAssetGuid{}); + hasSceneMeta = true; } } catch (...) {} } @@ -3579,6 +3652,20 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { sceneGuid = MetaCoreAssetGuid::Generate(); } + if (!hasSceneMeta) { + metaPath = MetaCoreBuildMetaPath(entry.path()); + MetaCoreAssetMetadataDocument sceneMetadata; + sceneMetadata.AssetGuid = sceneGuid; + sceneMetadata.AssetType = "scene"; + sceneMetadata.ImporterId = "SceneImporter"; + sceneMetadata.SourcePath = entry.path().lexically_relative(Project_.RootPath); + sceneMetadata.PackagePath = sceneMetadata.SourcePath; + sceneMetadata.SourceHash = MetaCoreHashFile(entry.path()).value_or(0); + if (MetaCoreWriteMetaJson(metaPath, sceneMetadata)) { + hasSceneMeta = true; + } + } + const auto relativePath = entry.path().lexically_relative(Project_.RootPath); const std::uint64_t fileHash = MetaCoreHashFile(entry.path()).value_or(0); @@ -3589,7 +3676,7 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { MetaCoreAssetStorageKind::SourceFile, {}, relativePath, - std::filesystem::exists(metaPath) ? metaPath.lexically_relative(Project_.RootPath) : std::filesystem::path{}, + hasSceneMeta ? metaPath.lexically_relative(Project_.RootPath) : std::filesystem::path{}, "SceneImporter", fileHash }); @@ -3662,6 +3749,17 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { } } } + if (!hasMetadata) { + metadata.AssetGuid = MetaCoreAssetGuid::Generate(); + metadata.AssetType = MetaCoreDetectAssetType(entry.path()); + metadata.ImporterId = MetaCoreDetectImporterId(entry.path()); + metadata.SourcePath = relativePath; + metadata.PackagePath = MetaCoreIsJsonSourceAssetPath(relativePath) ? relativePath : MetaCoreBuildPackagePathFromSource(relativePath); + metadata.SourceHash = MetaCoreHashFile(entry.path()).value_or(0); + if (MetaCoreWriteMetaJson(metaPath, metadata)) { + hasMetadata = true; + } + } AssetRecords_.push_back(MetaCoreAssetRecord{ hasMetadata ? metadata.AssetGuid : MetaCoreAssetGuid{}, diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index a6104b0..ba1f77e 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -1,8 +1,10 @@ #include "MetaCoreBuiltinEditorModule.h" +#include #include "MetaCoreEditor/MetaCoreEditorContext.h" #include "MetaCoreEditor/MetaCoreEditorAssetTypes.h" #include "MetaCoreEditor/MetaCoreEditorServices.h" +#include "MetaCoreScene/MetaCoreSceneSerializer.h" #include "MetaCoreEditorCameraController.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreFoundation/MetaCoreHash.h" @@ -28,6 +30,8 @@ #include #include #include +#include +#include namespace MetaCore { @@ -38,9 +42,21 @@ namespace MetaCore { namespace { +[[nodiscard]] std::filesystem::path MetaCoreBuildMetaPath(const std::filesystem::path& sourcePath) { + return std::filesystem::path(sourcePath.string() + ".mcmeta"); +} + +void MetaCoreLaunchPlayer(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIScenePersistenceService& scenePersistenceService); +void MetaCorePollPlayerProcess(MetaCoreEditorContext& editorContext); + static std::string GeneratedResourceFilter_{}; static std::string GeneratedResourceKindFilter_{}; static std::array AssetFilterBuffer_{}; +static std::filesystem::path PendingRenamePath_{}; +static std::array RenameBuffer_{}; +static std::array NewFolderBuffer_{}; +static HANDLE PlayerProcessHandle_ = NULL; +static HANDLE PlayerStdoutRead_ = NULL; [[nodiscard]] MetaCoreTypeRegistry MetaCoreBuildRuntimePanelTypeRegistry() { MetaCoreTypeRegistry registry; @@ -104,7 +120,6 @@ struct MetaCoreUiDocumentSummary { std::size_t ButtonCount = 0; }; -static std::filesystem::path PendingRenamePath_{}; static std::array FolderNameBuffer_{}; static std::array RenamePathBuffer_{}; @@ -691,6 +706,14 @@ void MetaCoreHandleGlobalEditorShortcuts(MetaCoreEditorContext& editorContext) { if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) { editorContext.RequestRenameActiveObject(); } + + if (ImGui::IsKeyPressed(ImGuiKey_F5, false)) { + const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + if (scenePersistenceService != nullptr && assetDatabaseService != nullptr && assetDatabaseService->HasProject()) { + MetaCoreLaunchPlayer(editorContext, *assetDatabaseService, *scenePersistenceService); + } + } } void MetaCoreApplyHierarchySelection(MetaCoreEditorContext& editorContext, MetaCoreId objectId, bool allowToggle) { @@ -1777,6 +1800,7 @@ public: std::string GetProviderId() const override { return "MetaCoreDefaultMenuProvider"; } void DrawMenuBar(MetaCoreEditorContext& editorContext) override { + MetaCorePollPlayerProcess(editorContext); MetaCoreHandleGlobalEditorShortcuts(editorContext); const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); @@ -1804,6 +1828,9 @@ public: if (ImGui::MenuItem("保存场景", "Ctrl+S", false, scenePersistenceService != nullptr)) { MetaCoreHandleSaveScene(editorContext); } + if (ImGui::MenuItem("一键保存并运行", "F5", false, scenePersistenceService != nullptr && assetDatabaseService != nullptr && assetDatabaseService->HasProject())) { + MetaCoreLaunchPlayer(editorContext, *assetDatabaseService, *scenePersistenceService); + } if (ImGui::MenuItem("刷新 Asset 数据库", nullptr, false, assetDatabaseService != nullptr)) { MetaCoreHandleReloadAssets(editorContext); } @@ -3275,7 +3302,7 @@ void DrawModelAssetDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD } [[nodiscard]] bool MetaCoreCanDragProjectAsset(const MetaCoreAssetRecord& assetRecord) { - return assetRecord.Type == "model" || assetRecord.Type == "prefab"; + return assetRecord.Type == "model" || assetRecord.Type == "prefab" || assetRecord.Type == "material" || assetRecord.Type == "texture"; } void MetaCoreBeginProjectAssetDragDropSource(const MetaCoreAssetRecord& assetRecord) { @@ -3318,6 +3345,24 @@ void MetaCoreBeginProjectAssetDragDropSource(const MetaCoreAssetRecord& assetRec editorContext.SelectOnly(*instantiatedObjectId); return true; } + if (assetType == "material") { + if (forcedParentId.has_value()) { + const MetaCoreId targetEntityId = *forcedParentId; + auto gameObject = editorContext.GetScene().FindGameObject(targetEntityId); + if (gameObject && gameObject.HasComponent()) { + const MetaCoreEditorStateSnapshot beforeSnapshot = editorContext.CaptureStateSnapshot(); + auto& meshRenderer = gameObject.GetComponent(); + if (meshRenderer.MaterialAssetGuids.empty()) { + meshRenderer.MaterialAssetGuids.resize(1); + } + meshRenderer.MaterialAssetGuids[0] = assetPayload->AssetGuid; + const MetaCoreEditorStateSnapshot afterSnapshot = editorContext.CaptureStateSnapshot(); + (void)editorContext.CommitStateTransition("绑定材质槽位", beforeSnapshot, afterSnapshot, false); + return true; + } + } + return false; + } if (assetType == "prefab") { const auto instantiatedObjectId = MetaCoreInstantiatePrefab(editorContext, assetPayload->AssetGuid, forcedParentId); if (!instantiatedObjectId.has_value()) { @@ -3331,6 +3376,59 @@ void MetaCoreBeginProjectAssetDragDropSource(const MetaCoreAssetRecord& assetRec namespace { +void CreateNewMaterial(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService) { + const std::filesystem::path selectedDir = editorContext.GetSelectedProjectDirectory(); + const std::filesystem::path absoluteDir = assetDatabaseService.GetProjectDescriptor().RootPath / selectedDir; + if (!std::filesystem::exists(absoluteDir)) { + return; + } + + std::filesystem::path finalPath; + int index = 0; + while (true) { + std::string filename = "NewMaterial"; + if (index > 0) { + filename += " " + std::to_string(index); + } + filename += ".mcmaterial.json"; + finalPath = absoluteDir / filename; + if (!std::filesystem::exists(finalPath)) { + break; + } + index++; + } + + MetaCoreMaterialAssetDocument materialDoc; + materialDoc.AssetGuid = MetaCoreAssetGuid::Generate(); + materialDoc.Name = finalPath.stem().stem().string(); + + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (!reflectionRegistry) { + return; + } + + if (MetaCoreSceneSerializer::SaveMaterialToJson(finalPath, materialDoc, reflectionRegistry->GetTypeRegistry())) { + const std::filesystem::path relativePath = finalPath.lexically_relative(assetDatabaseService.GetProjectDescriptor().RootPath); + const std::filesystem::path metaPath = assetDatabaseService.GetProjectDescriptor().RootPath / MetaCoreBuildMetaPath(relativePath); + + nlohmann::json json; + json["guid"] = materialDoc.AssetGuid.ToString(); + json["asset_type"] = "material"; + json["importer_id"] = "MaterialImporter"; + json["source_path"] = relativePath.generic_string(); + json["package_path"] = relativePath.generic_string(); + json["source_hash"] = MetaCoreHashFile(finalPath).value_or(0); + + std::ofstream output(metaPath); + if (output.is_open()) { + output << json.dump(4); + output.close(); + } + + assetDatabaseService.Refresh(); + } +} + void DrawPrefabDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIPackageService& packageService, MetaCoreIReflectionRegistry& reflectionRegistry) { const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset(); const auto assetRecord = assetDatabaseService.FindAssetByGuid(selectedAsset.Guid); @@ -3381,11 +3479,253 @@ void DrawUiDocumentDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD ImGui::PopStyleVar(); } +void MetaCoreLaunchPlayer(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIScenePersistenceService& scenePersistenceService) { + if (PlayerProcessHandle_ != NULL) { + TerminateProcess(PlayerProcessHandle_, 0); + CloseHandle(PlayerProcessHandle_); + PlayerProcessHandle_ = NULL; + if (PlayerStdoutRead_ != NULL) { + CloseHandle(PlayerStdoutRead_); + PlayerStdoutRead_ = NULL; + } + editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Info, "Editor", "正在重启 MetaCore Player 实例..."); + } + + (void)scenePersistenceService.SaveCurrentScene(editorContext); + + const auto& project = assetDatabaseService.GetProjectDescriptor(); + std::filesystem::path projectPath = project.RootPath; + std::filesystem::path scenePath = scenePersistenceService.GetCurrentScenePath(); + + HANDLE hStdoutWrite = NULL; + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + if (!CreatePipe(&PlayerStdoutRead_, &hStdoutWrite, &saAttr, 0)) { + editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Error, "Editor", "创建 Player 日志重定向管道失败"); + return; + } + SetHandleInformation(PlayerStdoutRead_, HANDLE_FLAG_INHERIT, 0); + + char exePath[MAX_PATH]; + GetModuleFileNameA(NULL, exePath, MAX_PATH); + std::filesystem::path currentExe(exePath); + std::filesystem::path playerExe = currentExe.parent_path() / "MetaCorePlayer.exe"; + + std::string cmdLine = "\"" + playerExe.string() + "\" --project \"" + projectPath.string() + "\""; + if (!scenePath.empty()) { + cmdLine += " --scene \"" + scenePath.string() + "\""; + } + + STARTUPINFOA si; + PROCESS_INFORMATION pi; + ZeroMemory(&si, sizeof(STARTUPINFOA)); + si.cb = sizeof(STARTUPINFOA); + si.hStdOutput = hStdoutWrite; + si.hStdError = hStdoutWrite; + si.dwFlags |= STARTF_USESTDHANDLES; + + ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); + + std::vector cmdLineWritable(cmdLine.begin(), cmdLine.end()); + cmdLineWritable.push_back('\0'); + + BOOL success = CreateProcessA( + NULL, + cmdLineWritable.data(), + NULL, + NULL, + TRUE, + CREATE_NO_WINDOW, + NULL, + NULL, + &si, + &pi + ); + + CloseHandle(hStdoutWrite); + + if (!success) { + CloseHandle(PlayerStdoutRead_); + PlayerStdoutRead_ = NULL; + editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Error, "Editor", "启动 MetaCore Player 失败,请检查 MetaCorePlayer.exe 是否在同级目录"); + return; + } + + PlayerProcessHandle_ = pi.hProcess; + CloseHandle(pi.hThread); + + editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Info, "Editor", "MetaCore Player 已启动:" + cmdLine); +} + +void MetaCorePollPlayerProcess(MetaCoreEditorContext& editorContext) { + if (PlayerProcessHandle_ == NULL) { + return; + } + + DWORD exitCode = 0; + if (GetExitCodeProcess(PlayerProcessHandle_, &exitCode)) { + if (exitCode != STILL_ACTIVE) { + editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Info, "Editor", "MetaCore Player 已退出,退出码:" + std::to_string(exitCode)); + CloseHandle(PlayerProcessHandle_); + PlayerProcessHandle_ = NULL; + } + } + + if (PlayerStdoutRead_ != NULL) { + DWORD bytesAvailable = 0; + if (PeekNamedPipe(PlayerStdoutRead_, NULL, 0, NULL, &bytesAvailable, NULL) && bytesAvailable > 0) { + std::vector buffer(bytesAvailable + 1, '\0'); + DWORD bytesRead = 0; + if (ReadFile(PlayerStdoutRead_, buffer.data(), bytesAvailable, &bytesRead, NULL) && bytesRead > 0) { + std::string logStr(buffer.data(), bytesRead); + std::size_t start = 0; + while (true) { + std::size_t pos = logStr.find('\n', start); + if (pos == std::string::npos) { + std::string line = logStr.substr(start); + if (!line.empty() && line != "\r") { + if (line.back() == '\r') line.pop_back(); + editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Info, "Player", line); + } + break; + } + std::string line = logStr.substr(start, pos - start); + if (!line.empty() && line.back() == '\r') line.pop_back(); + editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Info, "Player", line); + start = pos + 1; + } + } + } + + if (PlayerProcessHandle_ == NULL) { + CloseHandle(PlayerStdoutRead_); + PlayerStdoutRead_ = NULL; + } + } +} + +void DrawMaterialAssetDetails( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + MetaCoreIReflectionRegistry& reflectionRegistry +) { + const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset(); + const auto assetRecord = assetDatabaseService.FindAssetByGuid(selectedAsset.Guid); + if (!assetRecord.has_value()) return; + + const std::filesystem::path absolutePath = assetDatabaseService.GetProjectDescriptor().RootPath / assetRecord->RelativePath; + + auto materialDocOpt = MetaCoreSceneSerializer::LoadMaterialFromJson(absolutePath, reflectionRegistry.GetTypeRegistry()); + if (!materialDocOpt.has_value()) { + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "加载材质失败!"); + return; + } + MetaCoreMaterialAssetDocument materialDoc = std::move(*materialDocOpt); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4)); + if (ImGui::BeginChild("MaterialInspector", ImVec2(0, 0), true, 0)) { + ImGui::TextDisabled("MATERIAL ASSET"); + ImGui::Separator(); + ImGui::Spacing(); + + bool changed = false; + + float color[3] = { materialDoc.BaseColor.r, materialDoc.BaseColor.g, materialDoc.BaseColor.b }; + if (ImGui::ColorEdit3("基础颜色 (Base Color)", color)) { + materialDoc.BaseColor = glm::vec3(color[0], color[1], color[2]); + changed = true; + } + + float metallic = materialDoc.Metallic; + if (ImGui::SliderFloat("金属度 (Metallic)", &metallic, 0.0f, 1.0f)) { + materialDoc.Metallic = metallic; + changed = true; + } + + float roughness = materialDoc.Roughness; + if (ImGui::SliderFloat("粗糙度 (Roughness)", &roughness, 0.0f, 1.0f)) { + materialDoc.Roughness = roughness; + changed = true; + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Text("贴图绑定 (Textures)"); + + auto drawTextureCombo = [&](const char* label, MetaCoreAssetGuid& currentGuid) { + std::string previewName = "None"; + if (currentGuid.IsValid()) { + const auto record = assetDatabaseService.FindAssetByGuid(currentGuid); + if (record.has_value()) { + previewName = record->RelativePath.filename().string(); + } else { + previewName = "Missing (" + currentGuid.ToString() + ")"; + } + } + + if (ImGui::BeginCombo(label, previewName.c_str())) { + if (ImGui::Selectable("None", !currentGuid.IsValid())) { + if (currentGuid.IsValid()) { + currentGuid = MetaCoreAssetGuid{}; + changed = true; + } + } + + for (const auto& a : assetDatabaseService.GetAssetRegistry()) { + if (a.Type == "texture") { + const bool selected = (currentGuid == a.Guid); + const std::string name = a.RelativePath.filename().string(); + if (ImGui::Selectable(name.c_str(), selected)) { + if (currentGuid != a.Guid) { + currentGuid = a.Guid; + changed = true; + } + } + } + } + ImGui::EndCombo(); + } + }; + + drawTextureCombo("基础颜色贴图 (Base Color Texture)", materialDoc.BaseColorTexture); + drawTextureCombo("法线贴图 (Normal Texture)", materialDoc.NormalTexture); + drawTextureCombo("金属粗糙度贴图 (Metallic Roughness)", materialDoc.MetallicRoughnessTexture); + drawTextureCombo("遮蔽贴图 (Occlusion/Ao Texture)", materialDoc.AoTexture); + + if (changed) { + if (MetaCoreSceneSerializer::SaveMaterialToJson(absolutePath, materialDoc, reflectionRegistry.GetTypeRegistry())) { + const std::filesystem::path metaPath = assetDatabaseService.GetProjectDescriptor().RootPath / MetaCoreBuildMetaPath(assetRecord->RelativePath); + const std::uint64_t newHash = MetaCoreHashFile(absolutePath).value_or(0); + std::error_code ec; + if (std::filesystem::exists(metaPath, ec)) { + try { + std::ifstream input(metaPath); + nlohmann::json json; + input >> json; + input.close(); + json["source_hash"] = newHash; + std::ofstream output(metaPath); + output << json.dump(4); + output.close(); + } catch (...) {} + } + assetDatabaseService.Refresh(); + } + } + } + ImGui::EndChild(); + ImGui::PopStyleVar(); +} + void MetaCoreDrawAssetInspector(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIPackageService& packageService, MetaCoreIReflectionRegistry& reflectionRegistry) { if (!editorContext.HasSelectedAsset()) return; const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset(); if (selectedAsset.Type == "model") DrawModelAssetDetails(editorContext, assetDatabaseService); else if (selectedAsset.Type == "prefab") DrawPrefabDetails(editorContext, assetDatabaseService, packageService, reflectionRegistry); + else if (selectedAsset.Type == "material") DrawMaterialAssetDetails(editorContext, assetDatabaseService, reflectionRegistry); else if (selectedAsset.Type == "ui_document") DrawUiDocumentDetails(editorContext, assetDatabaseService, packageService, reflectionRegistry); else { ImGui::Separator(); @@ -3443,6 +3783,7 @@ public: const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); if (!assetDatabaseService || !assetDatabaseService->HasProject()) return; if (editorContext.GetSelectedProjectDirectory().empty()) editorContext.SetSelectedProjectDirectory("Assets"); + bool openRenamePopup = false; ImGui::TextDisabled("%s | %s", assetDatabaseService->GetProjectDescriptor().Name.c_str(), assetDatabaseService->GetProjectDescriptor().RootPath.string().c_str()); if (ImGui::Button("刷新")) { MetaCoreHandleReloadAssets(editorContext); assetDatabaseService->Refresh(); } @@ -3462,6 +3803,17 @@ public: if (MatchesAssetFilter(d.filename().string()) && ImGui::Selectable(("[Dir] " + d.filename().string()).c_str(), selectedDirectory == d)) { editorContext.SetSelectedProjectDirectory(d); } + if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) { + if (ImGui::MenuItem("重命名...")) { + PendingRenamePath_ = d; + std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", d.filename().string().c_str()); + openRenamePopup = true; + } + if (ImGui::MenuItem("删除")) { + assetDatabaseService->DeletePath(d); + } + ImGui::EndPopup(); + } } const MetaCoreAssetGuid selectedAssetGuid = editorContext.GetSelectedAsset().Guid; for (const auto& a : assetDatabaseService->GetAssetsUnder(selectedDirectory)) { @@ -3484,6 +3836,17 @@ public: } } MetaCoreBeginProjectAssetDragDropSource(a); + if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) { + if (ImGui::MenuItem("重命名...")) { + PendingRenamePath_ = a.RelativePath; + std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", a.RelativePath.filename().string().c_str()); + openRenamePopup = true; + } + if (ImGui::MenuItem("删除")) { + assetDatabaseService->DeletePath(a.RelativePath); + } + ImGui::EndPopup(); + } if (selected && a.Type == "model") { const auto modelDocument = MetaCoreLoadModelAssetDocument(editorContext, a); @@ -3523,6 +3886,62 @@ public: } } ImGui::Columns(1); + + if (openRenamePopup) { + ImGui::OpenPopup("重命名资源"); + } + + if (ImGui::BeginPopupContextWindow("ProjectBlankContextMenu", ImGuiPopupFlags_MouseButtonRight)) { + if (ImGui::MenuItem("刷新")) { + MetaCoreHandleReloadAssets(editorContext); + assetDatabaseService->Refresh(); + } + if (ImGui::MenuItem("新建文件夹")) { + std::snprintf(NewFolderBuffer_.data(), NewFolderBuffer_.size(), "NewFolder"); + ImGui::OpenPopup("新建文件夹"); + } + if (ImGui::MenuItem("新建材质")) { + CreateNewMaterial(editorContext, *assetDatabaseService); + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopupModal("重命名资源", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("请输入新名称:"); + ImGui::InputText("##NewName", RenameBuffer_.data(), RenameBuffer_.size()); + if (ImGui::Button("确定", ImVec2(120, 0))) { + std::string newName(RenameBuffer_.data()); + if (!newName.empty()) { + assetDatabaseService->RenamePath(PendingRenamePath_, newName); + } + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("取消", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopupModal("新建文件夹", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("请输入文件夹名称:"); + ImGui::InputText("##FolderName", NewFolderBuffer_.data(), NewFolderBuffer_.size()); + if (ImGui::Button("确定", ImVec2(120, 0))) { + std::string folderName(NewFolderBuffer_.data()); + if (!folderName.empty()) { + std::filesystem::path newDirPath = assetDatabaseService->GetProjectDescriptor().RootPath / selectedDirectory / folderName; + std::error_code ec; + std::filesystem::create_directories(newDirPath, ec); + assetDatabaseService->Refresh(); + } + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("取消", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } } }; diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index 25424fe..c898b4d 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -140,6 +140,7 @@ public: [[nodiscard]] virtual bool OpenProject(const std::filesystem::path& projectRoot) = 0; [[nodiscard]] virtual bool RenamePath(const std::filesystem::path& relativePath, const std::filesystem::path& newName) = 0; [[nodiscard]] virtual bool MovePath(const std::filesystem::path& relativePath, const std::filesystem::path& targetDirectory) = 0; + [[nodiscard]] virtual bool DeletePath(const std::filesystem::path& relativePath) = 0; [[nodiscard]] virtual std::vector GetDirectoriesUnder(const std::filesystem::path& relativeDirectory) const = 0; [[nodiscard]] virtual std::vector GetAssetsUnder(const std::filesystem::path& relativeDirectory) const = 0; [[nodiscard]] virtual std::optional FindAssetByRelativePath(const std::filesystem::path& relativePath) const = 0; diff --git a/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp b/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp index 6b45e89..02e9256 100644 --- a/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp @@ -39,7 +39,7 @@ void MetaCoreTrace(const char* message) { } } // namespace -bool MetaCoreEditorViewportRenderer::Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window) { +bool MetaCoreEditorViewportRenderer::Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window, bool offscreen) { MetaCoreTrace("metacore.render: viewport init enter"); Shutdown(); @@ -52,7 +52,7 @@ bool MetaCoreEditorViewportRenderer::Initialize(MetaCoreRenderDevice& renderDevi } */ - if (!FilamentSceneBridge_.Initialize(window)) { + if (!FilamentSceneBridge_.Initialize(window, offscreen)) { 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 f8dc24a..69d6db7 100644 --- a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp @@ -65,7 +65,7 @@ public: return RenderSync_.BuildSnapshot(scene); } - bool Initialize(MetaCoreWindow& window) { + bool Initialize(MetaCoreWindow& window, bool offscreen = true) { std::cout << "FilamentSceneBridge: Initializing..." << std::endl; // 创建 Filament Engine (不共享上下文,避免闪退) @@ -98,6 +98,7 @@ public: .castShadows(true) .build(*Engine_, Light_); Scene_->addEntity(Light_); + EntitiesInScene_.insert(Light_); // 创建相机 utils::Entity cameraEntity = utils::EntityManager::get().create(); @@ -108,40 +109,45 @@ public: View_->setScene(Scene_); View_->setCamera(Camera_); - // 创建 UI 视图 - UIView_ = Engine_->createView(); - - // 创建 ImGuiHelper - ImGuiHelper_ = new MetaCoreImGuiHelper(Engine_, UIView_, "", ImGui::GetCurrentContext()); + if (offscreen) { + // 创建 UI 视图 + UIView_ = Engine_->createView(); + + // 创建 ImGuiHelper + ImGuiHelper_ = new MetaCoreImGuiHelper(Engine_, UIView_, "", ImGui::GetCurrentContext()); - // 初始化离屏渲染纹理 - auto [width, height] = window.GetFramebufferSize(); - - // 1. 创建 Filament 颜色纹理 - FilamentTexture_ = filament::Texture::Builder() - .width(static_cast(width)) - .height(static_cast(height)) - .usage(filament::Texture::Usage::COLOR_ATTACHMENT | filament::Texture::Usage::SAMPLEABLE) - .format(filament::Texture::InternalFormat::RGBA8) - .build(*Engine_); + // 初始化离屏渲染纹理 + auto [width, height] = window.GetFramebufferSize(); - // 2. 创建深度纹理 - DepthTexture_ = filament::Texture::Builder() - .width(static_cast(width)) - .height(static_cast(height)) - .usage(filament::Texture::Usage::DEPTH_ATTACHMENT) - .format(filament::Texture::InternalFormat::DEPTH24) - .build(*Engine_); - - // 3. 创建 RenderTarget 并绑定 - RenderTarget_ = filament::RenderTarget::Builder() - .texture(filament::RenderTarget::AttachmentPoint::COLOR, FilamentTexture_) - .texture(filament::RenderTarget::AttachmentPoint::DEPTH, DepthTexture_) - .build(*Engine_); - - // 4. 设置到 View - View_->setRenderTarget(RenderTarget_); - View_->setViewport({0, 0, static_cast(width), static_cast(height)}); + // 1. 创建 Filament 颜色纹理 + FilamentTexture_ = filament::Texture::Builder() + .width(static_cast(width)) + .height(static_cast(height)) + .usage(filament::Texture::Usage::COLOR_ATTACHMENT | filament::Texture::Usage::SAMPLEABLE) + .format(filament::Texture::InternalFormat::RGBA8) + .build(*Engine_); + + // 2. 创建深度纹理 + DepthTexture_ = filament::Texture::Builder() + .width(static_cast(width)) + .height(static_cast(height)) + .usage(filament::Texture::Usage::DEPTH_ATTACHMENT) + .format(filament::Texture::InternalFormat::DEPTH24) + .build(*Engine_); + + // 3. 创建 RenderTarget 并绑定 + RenderTarget_ = filament::RenderTarget::Builder() + .texture(filament::RenderTarget::AttachmentPoint::COLOR, FilamentTexture_) + .texture(filament::RenderTarget::AttachmentPoint::DEPTH, DepthTexture_) + .build(*Engine_); + + // 4. 设置到 View + View_->setRenderTarget(RenderTarget_); + View_->setViewport({0, 0, static_cast(width), static_cast(height)}); + } else { + auto [width, height] = window.GetFramebufferSize(); + View_->setViewport({0, 0, static_cast(width), static_cast(height)}); + } // 确保开启后处理(为了HDR) View_->setPostProcessingEnabled(true); @@ -151,7 +157,7 @@ public: NameManager_ = new utils::NameComponentManager(utils::EntityManager::get()); AssetLoader_ = filament::gltfio::AssetLoader::create({ Engine_, MaterialProvider_, NameManager_ }); - std::cout << "FilamentSceneBridge: Initialized with Offscreen Texture ID: " << GLTextureId_ << std::endl; + std::cout << "FilamentSceneBridge: Initialized with Offscreen=" << (offscreen ? "true" : "false") << std::endl; return true; } @@ -160,8 +166,13 @@ public: std::cout << "FilamentSceneBridge: Shutting down..." << std::endl; // 销毁所有加载的资产 + std::cout << "[DEBUG] LoadedAssets_ size: " << LoadedAssets_.size() << std::endl; for (auto& [id, asset] : LoadedAssets_) { - AssetLoader_->destroyAsset(asset); + std::cout << "[DEBUG] Destroying asset for ID: " << id << ", asset pointer: " << asset << std::endl; + if (asset) { + AssetLoader_->destroyAsset(asset); + } + std::cout << "[DEBUG] Destroyed asset for ID: " << id << std::endl; } LoadedAssets_.clear(); ObjectToFilamentEntity_.clear(); @@ -170,13 +181,16 @@ public: for (auto& [id, entity] : SceneLightEntities_) { if (Scene_) { Scene_->remove(entity); + EntitiesInScene_.erase(entity); } Engine_->destroy(entity); utils::EntityManager::get().destroy(entity); } SceneLightEntities_.clear(); + EntitiesInScene_.clear(); // 销毁 gltfio 资源 + std::cout << "[DEBUG] Destroying gltfio resources..." << std::endl; if (AssetLoader_) { filament::gltfio::AssetLoader::destroy(&AssetLoader_); AssetLoader_ = nullptr; @@ -204,10 +218,17 @@ public: glDeleteTextures(1, &GLTextureId_); GLTextureId_ = 0; } - // 销毁 View Engine_->destroy(View_); - + if (UIView_) { + Engine_->destroy(UIView_); + UIView_ = nullptr; + } + if (ImGuiHelper_) { + delete ImGuiHelper_; + ImGuiHelper_ = nullptr; + } + // 销毁相机组件和实体 if (Camera_) { utils::Entity cameraEntity = Camera_->getEntity(); @@ -376,6 +397,75 @@ 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)) { + if (renderable.HostRootId != 0) { + activeHostRootIds.insert(renderable.HostRootId); + } + } + } + + // 2. 检查所有已加载的资产,若其 Root ID 不在 activeHostRootIds 中,则说明已被删除,执行清理 + for (auto it = LoadedAssets_.begin(); it != LoadedAssets_.end(); ) { + MetaCoreId hostRootId = it->first; + filament::gltfio::FilamentAsset* asset = it->second; + + if (!activeHostRootIds.contains(hostRootId)) { + std::cout << "FilamentSceneBridge: Unloading deleted asset: " << hostRootId << std::endl; + + // 从场景中移除该资产关联的所有实体 + if (Scene_ && asset) { + const utils::Entity* entities = asset->getEntities(); + size_t entityCount = asset->getEntityCount(); + for (size_t i = 0; i < entityCount; ++i) { + Scene_->remove(entities[i]); + EntitiesInScene_.erase(entities[i]); + } + } + + // 查找并清理对应的 pivotEntity + auto pivotIt = ObjectToFilamentEntity_.find(hostRootId); + if (pivotIt != ObjectToFilamentEntity_.end()) { + utils::Entity pivotEntity = pivotIt->second.second; + bool isPivotNode = (asset == nullptr || pivotEntity != asset->getRoot()); + if (Scene_ && pivotEntity) { + Scene_->remove(pivotEntity); + EntitiesInScene_.erase(pivotEntity); + } + if (isPivotNode && pivotEntity) { + utils::EntityManager::get().destroy(pivotEntity); + } + } + + // 销毁资产本身 + if (asset) { + AssetLoader_->destroyAsset(asset); + } + + // 从 ObjectToFilamentEntity_ 和 ObjectWorldMatrices_ 中清除所有指向该 asset 的映射 + if (asset != nullptr) { + for (auto entIt = ObjectToFilamentEntity_.begin(); entIt != ObjectToFilamentEntity_.end(); ) { + if (entIt->second.first == asset) { + ObjectWorldMatrices_.erase(entIt->first); + entIt = ObjectToFilamentEntity_.erase(entIt); + } else { + ++entIt; + } + } + } + // 确保顶级父 ID 的变换缓存也被清除 + ObjectWorldMatrices_.erase(hostRootId); + + // 从 LoadedAssets_ 中移除 + it = LoadedAssets_.erase(it); + } else { + ++it; + } + } + for (const auto& renderable : snapshot.Renderables) { if (renderable.MeshSource != MetaCoreMeshSourceKind::Asset && renderable.MeshSource != MetaCoreMeshSourceKind::Builtin) { @@ -433,6 +523,13 @@ public: // 添加到场景 Scene_->addEntities(asset->getEntities(), asset->getEntityCount()); + { + const utils::Entity* entities = asset->getEntities(); + size_t entityCount = asset->getEntityCount(); + for (size_t i = 0; i < entityCount; ++i) { + EntitiesInScene_.insert(entities[i]); + } + } LoadedAssets_[hostRootId] = asset; @@ -543,6 +640,7 @@ public: ObjectToFilamentEntity_[hostRootId] = { asset, pivotEntity }; Scene_->addEntity(pivotEntity); + EntitiesInScene_.insert(pivotEntity); // 更新初始位置 glm::mat4 initLocalMat{1.0f}; @@ -553,11 +651,108 @@ public: UpdateTransform(renderable.ObjectId, initLocalMat); } - // 全量同步所有已映射对象的 Transform + // 1. 构建反向查找映射,方便反查 entity 对应的 objectId + std::unordered_map entityToObjectId; for (const auto& [objectId, assetEntity] : ObjectToFilamentEntity_) { - auto itMat = snapshot.LocalMatrices.find(objectId); - if (itMat != snapshot.LocalMatrices.end()) { - UpdateTransform(objectId, itMat->second); + entityToObjectId[assetEntity.second] = objectId; + } + + // 2. 控制所有已加载 glTF 资产内部所有实体(包括无直接映射的内部渲染实体)的可见性 + auto& tm = Engine_->getTransformManager(); + for (const auto& [hostRootId, asset] : LoadedAssets_) { + if (!asset) continue; + const utils::Entity* entities = asset->getEntities(); + size_t entityCount = asset->getEntityCount(); + for (size_t i = 0; i < entityCount; ++i) { + utils::Entity entity = entities[i]; + if (!entity) continue; + + bool shouldBeInScene = false; + auto it = entityToObjectId.find(entity); + if (it != entityToObjectId.end()) { + // 若是直接映射节点,其可见性由快照决定 + shouldBeInScene = snapshot.WorldMatrices.contains(it->second); + } else { + // 若是没有直接映射关系的内部渲染/辅助实体,上溯其在 Filament 层级树中的父节点链, + // 找到最近的具有映射关系的祖先节点,并继承其可见性状态。 + utils::Entity current = entity; + utils::Entity mappedAncestor; + while (current) { + auto instance = tm.getInstance(current); + if (!instance) break; + utils::Entity parent = tm.getParent(instance); + if (!parent) break; + if (entityToObjectId.contains(parent)) { + mappedAncestor = parent; + break; + } + current = parent; + } + + if (mappedAncestor) { + shouldBeInScene = snapshot.WorldMatrices.contains(entityToObjectId[mappedAncestor]); + } else { + // 回退保护:如果没有找到,则与该资产对应的顶级根宿主保持状态一致 + shouldBeInScene = snapshot.WorldMatrices.contains(hostRootId); + } + } + + // 同步可见性状态到 Filament Scene + if (shouldBeInScene) { + if (Scene_ && !EntitiesInScene_.contains(entity)) { + Scene_->addEntity(entity); + EntitiesInScene_.insert(entity); + } + } else { + if (Scene_ && EntitiesInScene_.contains(entity)) { + Scene_->remove(entity); + EntitiesInScene_.erase(entity); + } + } + } + } + + // 3. 控制 ObjectToFilamentEntity_ 中所有非资产实体(如 pivotEntity 辅助实体)的可见性 + for (const auto& [objectId, assetEntity] : ObjectToFilamentEntity_) { + utils::Entity entity = assetEntity.second; + if (!entity) continue; + + // 检查该实体是否为资产内部实体,如果不是(例如 pivotEntity),则直接控制可见性 + bool isAssetEntity = false; + if (assetEntity.first) { + const utils::Entity* entities = assetEntity.first->getEntities(); + size_t entityCount = assetEntity.first->getEntityCount(); + for (size_t i = 0; i < entityCount; ++i) { + if (entities[i] == entity) { + isAssetEntity = true; + break; + } + } + } + + if (!isAssetEntity) { + bool shouldBeInScene = snapshot.WorldMatrices.contains(objectId); + if (shouldBeInScene) { + if (Scene_ && !EntitiesInScene_.contains(entity)) { + Scene_->addEntity(entity); + EntitiesInScene_.insert(entity); + } + } else { + if (Scene_ && EntitiesInScene_.contains(entity)) { + Scene_->remove(entity); + EntitiesInScene_.erase(entity); + } + } + } + } + + // 4. 同步所有存活实体的本地和全局变换矩阵 + for (const auto& [objectId, assetEntity] : ObjectToFilamentEntity_) { + if (snapshot.WorldMatrices.contains(objectId)) { + auto itMat = snapshot.LocalMatrices.find(objectId); + if (itMat != snapshot.LocalMatrices.end()) { + UpdateTransform(objectId, itMat->second); + } } } } @@ -582,33 +777,44 @@ public: filament::Camera::Fov::VERTICAL ); } - void RenderAll() { - if (!Renderer_ || !SwapChain_ || !View_ || !UIView_ || !ImGuiHelper_) { + if (!Renderer_ || !SwapChain_ || !View_) { return; } if (Renderer_->beginFrame(SwapChain_)) { - // 1. 渲染 3D 离屏视口 (输出到 RenderTarget) - filament::Renderer::ClearOptions options; - options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 深灰色 - options.clear = true; - Renderer_->setClearOptions(options); - - Renderer_->render(View_); + if (RenderTarget_) { + // 1. 渲染 3D 离屏视口 (输出到 RenderTarget) + filament::Renderer::ClearOptions options; + options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 深灰色 + options.clear = true; + Renderer_->setClearOptions(options); + + Renderer_->render(View_); - // 2. 准备并渲染 UI 视口 (输出到 SwapChain) - ImGuiIO& io = ImGui::GetIO(); - ImGuiHelper_->setDisplaySize(io.DisplaySize.x, io.DisplaySize.y); - UIView_->setViewport({0, 0, static_cast(io.DisplaySize.x), static_cast(io.DisplaySize.y)}); - - options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 灰色 - options.clear = true; - Renderer_->setClearOptions(options); - - ImGuiHelper_->processImGuiCommands(ImGui::GetDrawData(), io); - - Renderer_->render(UIView_); + if (UIView_ && ImGuiHelper_) { + // 2. 准备并渲染 UI 视口 (输出到 SwapChain) + ImGuiIO& io = ImGui::GetIO(); + ImGuiHelper_->setDisplaySize(io.DisplaySize.x, io.DisplaySize.y); + UIView_->setViewport({0, 0, static_cast(io.DisplaySize.x), static_cast(io.DisplaySize.y)}); + + options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 灰色 + options.clear = true; + Renderer_->setClearOptions(options); + + ImGuiHelper_->processImGuiCommands(ImGui::GetDrawData(), io); + + Renderer_->render(UIView_); + } + } else { + // 3. 直接上屏渲染 + filament::Renderer::ClearOptions options; + options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 深灰色 + options.clear = true; + Renderer_->setClearOptions(options); + + Renderer_->render(View_); + } Renderer_->endFrame(); @@ -627,6 +833,11 @@ public: void Resize(int width, int height) { if (width <= 0 || height <= 0) return; + if (!RenderTarget_) { + View_->setViewport({0, 0, static_cast(width), static_cast(height)}); + return; + } + // 【关键修复】:如果大小没有改变,不要重新创建纹理! // 否则会导致上一帧传给 ImGui 的纹理指针在这一帧被提前销毁,引发 Use-after-free 崩溃。 if (FilamentTexture_ && @@ -728,6 +939,12 @@ public: return 0.0F; } + bool VerifyEntityInSceneForTesting(MetaCoreId objectId) const { + auto it = ObjectToFilamentEntity_.find(objectId); + if (it == ObjectToFilamentEntity_.end()) return false; + return EntitiesInScene_.contains(it->second.second); + } + private: static filament::math::float3 ToFilamentFloat3(const glm::vec3& value) { return filament::math::float3{ value.x, value.y, value.z }; @@ -788,6 +1005,7 @@ private: .castShadows(true) .build(*Engine_, lightEntity); Scene_->addEntity(lightEntity); + EntitiesInScene_.insert(lightEntity); lightIt = SceneLightEntities_.emplace(light.ObjectId, lightEntity).first; } @@ -806,6 +1024,7 @@ private: } Scene_->remove(it->second); + EntitiesInScene_.erase(it->second); Engine_->destroy(it->second); utils::EntityManager::get().destroy(it->second); it = SceneLightEntities_.erase(it); @@ -853,6 +1072,7 @@ private: std::unordered_map> ObjectToFilamentEntity_; std::unordered_map LoadedAssets_; std::unordered_map SceneLightEntities_; + std::unordered_set EntitiesInScene_; filament::View* UIView_ = nullptr; MetaCoreImGuiHelper* ImGuiHelper_ = nullptr; @@ -873,8 +1093,8 @@ MetaCoreFilamentSceneBridge::MetaCoreFilamentSceneBridge() MetaCoreFilamentSceneBridge::~MetaCoreFilamentSceneBridge() = default; -bool MetaCoreFilamentSceneBridge::Initialize(MetaCoreWindow& window) { - return Impl_->Initialize(window); +bool MetaCoreFilamentSceneBridge::Initialize(MetaCoreWindow& window, bool offscreen) { + return Impl_->Initialize(window, offscreen); } void MetaCoreFilamentSceneBridge::Shutdown() { @@ -938,4 +1158,8 @@ float MetaCoreFilamentSceneBridge::GetDefaultLightIntensityForTesting() const { return Impl_->GetDefaultLightIntensityForTesting(); } +bool MetaCoreFilamentSceneBridge::VerifyEntityInSceneForTesting(MetaCoreId objectId) const { + return Impl_->VerifyEntityInSceneForTesting(objectId); +} + } // namespace MetaCore diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h index ea0e7ce..768626d 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h @@ -15,7 +15,7 @@ class MetaCoreScene; class MetaCoreEditorViewportRenderer { public: - bool Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window); + bool Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window, bool offscreen = true); void Shutdown(); void SetViewportRect(const MetaCoreViewportRect& viewportRect); void RenderSceneToViewport(MetaCoreScene& scene, const MetaCoreSceneView& sceneView, bool useScenePrimaryCamera = false); diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h index 2227123..6f30872 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h @@ -23,7 +23,7 @@ public: MetaCoreFilamentSceneBridge(); ~MetaCoreFilamentSceneBridge(); - bool Initialize(MetaCoreWindow& window); + bool Initialize(MetaCoreWindow& window, bool offscreen = true); void Shutdown(); void Resize(int width, int height); void SetProjectRootPath(const std::filesystem::path& projectRootPath); @@ -42,6 +42,7 @@ public: [[nodiscard]] bool VerifyTransformForTesting(MetaCoreId objectId, const glm::mat4& expectedMatrix) const; [[nodiscard]] bool VerifyLightExistsForTesting(MetaCoreId objectId) const; [[nodiscard]] float GetDefaultLightIntensityForTesting() const; + [[nodiscard]] bool VerifyEntityInSceneForTesting(MetaCoreId objectId) const; private: class MetaCoreFilamentSceneBridgeImpl; diff --git a/docs/任务计划.md b/docs/任务计划.md new file mode 100644 index 0000000..2b5d0e6 --- /dev/null +++ b/docs/任务计划.md @@ -0,0 +1,1089 @@ +下面是按你最新判断重新整理后的 **MetaCore 完整任务计划 v2**。 +核心调整是:**Runtime UI MVP 提前到 Build Settings / 项目打包之前**。 + +--- + +# MetaCore 当前总路线 + +```text +P0 迁移基线:Panda3D 清理 / JSON 资产链路稳定 + ↓ +P1 渲染主链路:ECS → SceneRenderSync → Filament + ↓ +P2 编辑器基础闭环:Hierarchy / Inspector / Gizmo / Save Reload + ↓ +P3 编辑器生产力:Project / Import / Prefab / Material / Launch Player + ↓ +P4 Runtime UI MVP:RmlUi 最小运行时 UI 闭环 + ↓ +P5 Build Settings:项目打包入口 + ↓ +P6 Cooked Runtime:真正发布期运行 + ↓ +P7 多平台 / 国产化 / Web + ↓ +P8 稳定化 / 文档 / 示例项目 / 版本发布 +``` + +--- + +# 当前状态 + +## 已完成 + +```text +P0 迁移基线:已完成 +P1 渲染主链路:已完成 +P2 编辑器基础闭环:已完成 +``` + +也就是现在 MetaCore 已经具备: + +```text +JSON Scene +→ ECS Scene +→ SceneRenderSync +→ Filament 渲染 +→ Editor Hierarchy +→ Inspector +→ Gizmo +→ Save / Reload +→ Player 主相机运行 +``` + +接下来正式进入 **P3 编辑器生产力阶段**。 + +--- + +# P0:迁移基线【已完成】 + +## 目标 + +清理 Panda3D 历史层,确立 Filament + JSON 资产路线。 + +## 已完成 + +```text +P0.1 移除 Panda3D / simplepbr 构建引用 +P0.2 删除 MetaCorePandaSceneBridge +P0.3 删除 cmake/MetaCorePanda3D.cmake +P0.4 清空 PANDA3D_ROOT 后可独立 configure +P0.5 Release 构建 Editor / Player / SmokeTests 通过 +P0.6 默认场景切到 .mcscene.json +P0.7 Prefab 切到 .mcprefab.json +P0.8 .mcmeta 不再被 ImportPipeline 当成源资产 +P0.9 Cook 支持 JSON 场景 / Prefab / Material / UI +``` + +## 仍建议补充 + +```text +P0-T01 整理 SandboxProject 历史模型资源改动 +P0-T02 确认 Library/Cooked 是否进入 .gitignore +P0-T03 给当前迁移状态打 milestone commit +``` + +建议里程碑: + +```text +milestone: remove Panda3D backend and stabilize JSON asset pipeline +``` + +--- + +# P1:渲染主链路【已完成】 + +## 目标 + +让 Filament 真正由 ECS 场景数据驱动,而不是只跑 Demo。 + +## 已完成 + +```text +P1.1 新增 SceneRenderSync 快照层 +P1.2 收集 MeshRenderer / Camera / Light / WorldMatrix +P1.3 Filament bridge 读取 SceneRenderSync 快照 +P1.4 Scene Light 同步到 Filament Light +P1.5 Player 优先使用场景主相机 +P1.6 Editor Gizmo 保持 ImGui / ImGuizmo overlay +P1.7 Smoke test 通过 +P1.8 Editor / Player / Tests Release 构建通过 +``` + +## 后续技术债 + +```text +T-DEP-001 解决 Filament Debug / Release runtime mismatch +``` + +当前可以先明确支持: + +```text +Release +RelWithDebInfo +``` + +Debug 链接失败暂时不要阻塞主线。 + +--- + +# P2:编辑器基础闭环【已完成】 + +## 目标 + +让 MetaCoreEditorApp 具备最小 Unity-like 场景编辑能力。 + +## 已完成 + +```text +P2.1 Hierarchy 显示实体 +P2.2 Entity Selection +P2.3 Inspector 编辑 Transform +P2.4 Inspector 编辑 Camera +P2.5 Inspector 编辑 Light +P2.6 SceneView 渲染 ECS 场景 +P2.7 Gizmo 操作 Transform +P2.8 保存 .mcscene.json +P2.9 关闭重开后恢复场景 +P2.10 Player 使用主相机场景运行 +``` + +## P2 验收标准 + +```text +打开 Editor +→ 打开 Main.mcscene.json +→ 选择实体 +→ Inspector 修改 Transform / Camera / Light +→ Gizmo 移动物体 +→ 保存场景 +→ 关闭 Editor +→ 重新打开 Editor +→ 场景恢复 +→ 启动 Player +→ Player 使用主相机渲染 +``` + +建议里程碑: + +```text +milestone: complete editor authoring loop +``` + +--- + +# P3:编辑器生产力【下一阶段重点】 + +## 阶段目标 + +让编辑器从“能编辑场景”升级为“能组织项目资产、导入资源、创建 Prefab、编辑材质、一键运行”。 + +P3 不做正式打包,不做 cooked-only runtime。 + +--- + +## P3.1 Project 面板增强 + +### 任务 + +```text +P3.1-T01 显示 Assets 目录树 +P3.1-T02 显示 Scenes / Prefabs / Models / Materials / Textures / UI +P3.1-T03 识别 .mcscene.json +P3.1-T04 识别 .mcprefab.json +P3.1-T05 识别 .gltf / .glb +P3.1-T06 识别材质 JSON +P3.1-T07 识别贴图 png / jpg / ktx / hdr +P3.1-T08 支持刷新 Project 面板 +P3.1-T09 支持右键菜单 +P3.1-T10 支持重命名资产 +P3.1-T11 支持删除资产 +P3.1-T12 支持双击打开 Scene +``` + +### 验收标准 + +```text +Project 面板能浏览 Assets +双击 .mcscene.json 可打开场景 +右键可刷新、重命名、删除基础资产 +``` + +--- + +## P3.2 资产导入体验 + +### 任务 + +```text +P3.2-T01 拖入 glTF / GLB 到 Assets +P3.2-T02 自动生成 .mcmeta +P3.2-T03 自动分配 AssetGuid +P3.2-T04 Project 面板自动刷新 +P3.2-T05 导入后可在 Inspector 查看资产信息 +P3.2-T06 模型资产可拖入 Scene +P3.2-T07 拖入 Scene 后生成 Entity + Transform + MeshRenderer +P3.2-T08 MeshRenderer 引用模型 AssetGuid +P3.2-T09 导入失败时 Console 输出错误 +P3.2-T10 防止 .mcmeta 被重复导入 +``` + +### 验收标准 + +```text +把 GLB 放入 Assets/Models +→ 自动生成 .mcmeta +→ Project 面板出现模型 +→ 拖到 Scene +→ Hierarchy 出现实体 +→ SceneView 显示模型 +→ 保存重开后恢复 +``` + +--- + +## P3.3 Prefab 最小工作流 + +### 任务 + +```text +P3.3-T01 选中 Entity 后支持 Create Prefab +P3.3-T02 保存为 .mcprefab.json +P3.3-T03 Prefab 保存 Entity 层级、Transform、MeshRenderer、Camera、Light +P3.3-T04 Project 面板显示 Prefab +P3.3-T05 从 Project 拖 Prefab 到 Scene +P3.3-T06 实例化 Prefab Entity +P3.3-T07 Prefab Instance 保存 prefabGuid +P3.3-T08 Inspector 显示 Prefab Instance 状态 +P3.3-T09 支持 Apply Prefab +P3.3-T10 支持 Revert Prefab +P3.3-T11 支持断开 Prefab 连接 +``` + +### 暂不做 + +```text +Prefab Variant +Nested Prefab 深度编辑 +Prefab Override 可视化 Diff +多人协作冲突解决 +``` + +### 验收标准 + +```text +Entity → Save as Prefab +→ 删除原 Entity +→ 从 Project 拖 Prefab 回场景 +→ 修改 Transform +→ Apply +→ 重新拖一个 Prefab 实例 +→ 应用后的数据正确 +``` + +--- + +## P3.4 Material 基础编辑 + +### 任务 + +```text +P3.4-T01 定义 Material 资产格式 +P3.4-T02 支持创建 Material +P3.4-T03 保存为 .mcmaterial.json 或当前已有材质 JSON +P3.4-T04 Inspector 编辑 BaseColor +P3.4-T05 Inspector 编辑 Metallic +P3.4-T06 Inspector 编辑 Roughness +P3.4-T07 支持 BaseColor Texture 引用 +P3.4-T08 MeshRenderer 可绑定 Material AssetGuid +P3.4-T09 SceneRenderSync 收集 Material 引用 +P3.4-T10 Filament bridge 应用 MaterialInstance +P3.4-T11 保存重开后材质恢复 +``` + +### 暂不做 + +```text +Shader Graph +材质节点编辑器 +复杂透明排序 +完整 PBR 参数集 +``` + +### 验收标准 + +```text +创建 Material +→ 修改 BaseColor / Roughness / Metallic +→ 绑定到模型 +→ SceneView 更新 +→ 保存重开恢复 +``` + +--- + +## P3.5 一键 Launch Player + +### 任务 + +```text +P3.5-T01 菜单增加 Run / Launch Player +P3.5-T02 Launch 前自动保存当前场景 +P3.5-T03 启动 MetaCorePlayer +P3.5-T04 传入 project path +P3.5-T05 传入 scene path +P3.5-T06 Player 加载当前场景 +P3.5-T07 Player 使用主相机运行 +P3.5-T08 Console 显示启动日志 +P3.5-T09 Player 启动失败时 Editor 输出错误 +``` + +### 验收标准 + +```text +Editor 中编辑场景 +→ 点击 Run / Launch Player +→ 自动保存 +→ 打开 Player +→ Player 显示同一个场景 +``` + +--- + +## P3 完成标志 + +```text +Project 面板可用 +资产导入可用 +Prefab 最小闭环可用 +Material 基础编辑可用 +Editor 可一键启动 Player +``` + +建议里程碑: + +```text +milestone: editor asset prefab material workflow +``` + +--- + +# P4:Runtime UI MVP【提前到打包前】 + +## 阶段目标 + +先让 MetaCore 运行时具备最小 UI 能力。 +不做完整 UI 编辑器,只做能显示、保存、加载、响应基础输入的 RmlUi 运行时闭环。 + +--- + +## P4.1 RmlUi 基础集成 + +### 任务 + +```text +P4.1-T01 接入 RmlUi 依赖 +P4.1-T02 初始化 RmlUi Context +P4.1-T03 建立 RmlUi 渲染后端 +P4.1-T04 建立输入事件转发 +P4.1-T05 Player 中显示第一个 .rml 页面 +P4.1-T06 支持字体加载 +P4.1-T07 支持图片资源加载 +``` + +### 验收标准 + +```text +启动 Player +→ 3D 场景正常渲染 +→ 屏幕上显示一个 RmlUi 面板 +``` + +--- + +## P4.2 UI 资产识别 + +### 任务 + +```text +P4.2-T01 Project 面板识别 .rml +P4.2-T02 Project 面板识别 .rcss +P4.2-T03 生成 UI .mcmeta +P4.2-T04 UI 资产拥有 AssetGuid +P4.2-T05 ImportPipeline 不误处理 UI 依赖文件 +P4.2-T06 双击 .rml 可预览或打开外部编辑器 +``` + +### 验收标准 + +```text +Assets/UI/MainHUD.rml +Assets/UI/MainHUD.rcss +→ Project 面板正确显示 +→ 有 GUID +→ 不被误导入成模型/场景 +``` + +--- + +## P4.3 UIComponent + +### 任务 + +```text +P4.3-T01 新增 UIComponent +P4.3-T02 UIComponent 引用 RML AssetGuid +P4.3-T03 UIComponent 支持 Visible +P4.3-T04 UIComponent 支持 SortOrder +P4.3-T05 Inspector 可编辑 UIComponent +P4.3-T06 场景保存 UIComponent +P4.3-T07 场景重开恢复 UIComponent +P4.3-T08 Player 加载 UIComponent 并显示 UI +``` + +建议结构: + +```cpp +struct UIComponent +{ + AssetGuid Document; + bool Visible = true; + int SortOrder = 0; +}; +``` + +### 验收标准 + +```text +创建 Entity +→ 添加 UIComponent +→ 指向 MainHUD.rml +→ 保存场景 +→ Launch Player +→ UI 显示 +``` + +--- + +## P4.4 UI 输入与基础事件 + +### 任务 + +```text +P4.4-T01 鼠标移动输入 +P4.4-T02 鼠标点击输入 +P4.4-T03 键盘输入 +P4.4-T04 Button click 回调 +P4.4-T05 Console 输出 UI 事件 +P4.4-T06 可通过简单事件名触发 Runtime Action +P4.4-T07 UI 可向 ECS/EventBus 派发事件 +``` + +### 暂不做 + +```text +完整脚本系统 +复杂数据绑定 +可视化 UI Designer +UI 动画系统 +主题编辑器 +控件库市场 +``` + +### 验收标准 + +```text +Player 显示 HUD +→ 点击按钮 +→ Console 输出事件 +→ 或派发一个 UIEvent 到 Runtime EventBus +``` + +--- + +## P4 完成标志 + +```text +RmlUi 能在 Player 中显示 +.rml / .rcss 作为资产被 Project 面板识别 +UIComponent 能随场景保存和加载 +Player 能响应基础鼠标键盘输入 +按钮点击能触发基础事件 +``` + +建议里程碑: + +```text +milestone: runtime UI MVP with RmlUi +``` + +--- + +# P5:Build Settings / 项目打包入口 + +## 阶段目标 + +在编辑器中提供正式打包入口,把场景、资产、UI、Player 组合成一个可运行发布目录。 + +--- + +## P5.1 Project Settings + +### 任务 + +```text +P5.1-T01 新增 ProjectSettings.json +P5.1-T02 保存项目名称 +P5.1-T03 保存默认启动场景 +P5.1-T04 保存窗口大小 +P5.1-T05 保存公司名 / 应用名 +P5.1-T06 保存目标平台配置 +P5.1-T07 保存默认 UI / 启动 UI 配置 +P5.1-T08 Editor 可读写 Project Settings +P5.1-T09 Player 可读取 Project Settings +``` + +### 验收标准 + +```text +Project Settings 可编辑 +关闭重开后配置保持 +Player 可读取项目名、默认场景、窗口大小 +``` + +--- + +## P5.2 Build Settings 面板 + +### 任务 + +```text +P5.2-T01 新增 File / Build Settings 菜单 +P5.2-T02 显示 Scenes In Build +P5.2-T03 可添加当前场景到 Build +P5.2-T04 可移除 Build 场景 +P5.2-T05 可设置主启动场景 +P5.2-T06 显示 UI Assets / Runtime UI 依赖 +P5.2-T07 可选择目标平台 Windows +P5.2-T08 可选择输出目录 +P5.2-T09 可勾选 Development Build +P5.2-T10 可勾选 Use Cooked Assets +P5.2-T11 点击 Build 触发打包流程 +``` + +### 验收标准 + +```text +打开 Build Settings +→ 添加 Main.mcscene.json +→ 确认 UI 资源依赖 +→ 选择 Windows +→ 设置输出目录 +→ 点击 Build +``` + +--- + +## P5.3 Build Pipeline 第一版 + +### 任务 + +```text +P5.3-T01 Build 前保存所有 dirty scene +P5.3-T02 扫描 Scenes In Build +P5.3-T03 收集模型 / 材质 / 贴图依赖 +P5.3-T04 收集 .rml / .rcss / 字体 / UI 图片依赖 +P5.3-T05 调用已有 Cook 流程 +P5.3-T06 生成 CookManifest +P5.3-T07 拷贝 MetaCorePlayer.exe +P5.3-T08 拷贝 Filament 运行时依赖 DLL +P5.3-T09 拷贝 RmlUi 运行时依赖 DLL +P5.3-T10 拷贝 cooked 资产 +P5.3-T11 生成 RuntimeConfig.json +P5.3-T12 可将 MetaCorePlayer.exe 重命名为项目名.exe +P5.3-T13 输出 Build 日志 +``` + +### 输出目录建议 + +```text +Builds/Windows/MyProject/ + MyProject.exe + RuntimeConfig.json + Data/ + CookManifest.bin + Assets/ + *.mccooked + UI/ + *.rml + *.rcss + Fonts/ + Textures/ + Plugins/ + Logs/ +``` + +### 验收标准 + +```text +点击 Build +→ 生成 Builds/Windows/MyProject +→ 双击 MyProject.exe +→ 主场景正常显示 +→ Runtime UI 正常显示 +``` + +--- + +## P5.4 Development Build + +### 任务 + +```text +P5.4-T01 Development Build 显示 Console 日志 +P5.4-T02 输出 runtime log 文件 +P5.4-T03 支持命令行参数覆盖主场景 +P5.4-T04 支持窗口大小配置 +P5.4-T05 支持显示 FPS / frame time +P5.4-T06 支持 UI debug overlay +``` + +--- + +## P5 完成标志 + +```text +Editor 中有 Build Settings +可以选择场景、UI、平台和输出目录 +点击 Build 能生成可双击运行的 Windows 发布目录 +发布目录中 3D 场景和 Runtime UI 都能运行 +``` + +建议里程碑: + +```text +milestone: build settings and first packaged runtime +``` + +--- + +# P6:Cooked Runtime 深化 + +## 阶段目标 + +让发布期 Player 真正脱离编辑期 JSON 源资产,依靠 CookManifest + RuntimeAssetRegistry + .mccooked 运行。 + +--- + +## P6.1 CookManifest 稳定化 + +### 任务 + +```text +P6.1-T01 明确 CookManifest 数据结构 +P6.1-T02 记录 AssetGuid +P6.1-T03 记录 AssetType +P6.1-T04 记录 SourcePath +P6.1-T05 记录 CookedPath +P6.1-T06 记录 ContentHash +P6.1-T07 记录 Dependencies +P6.1-T08 支持 UI 依赖记录 +P6.1-T09 支持 JSON manifest 调试格式 +P6.1-T10 支持 binary manifest 发布格式 +``` + +--- + +## P6.2 RuntimeAssetRegistry + +### 任务 + +```text +P6.2-T01 新增 RuntimeAssetRegistry +P6.2-T02 LoadManifest +P6.2-T03 GetCookedPath +P6.2-T04 GetAssetType +P6.2-T05 GetDependencies +P6.2-T06 HasAsset +P6.2-T07 Resolve UI Asset +P6.2-T08 Resolve Material / Texture / Model Asset +P6.2-T09 运行时错误日志 +``` + +--- + +## P6.3 Cooked Scene Runtime Data + +### 任务 + +```text +P6.3-T01 Scene JSON 转 RuntimeSceneData +P6.3-T02 保存实体列表 +P6.3-T03 保存 Transform +P6.3-T04 保存 MeshRenderer +P6.3-T05 保存 Camera +P6.3-T06 保存 Light +P6.3-T07 保存 Prefab Instance +P6.3-T08 保存 UIComponent +P6.3-T09 保存资产依赖 GUID +P6.3-T10 Player 从 cooked scene 构建 ECS +``` + +--- + +## P6.4 Cooked-only Player + +### 任务 + +```text +P6.4-T01 Player 支持 --cooked 参数 +P6.4-T02 Player 支持 --main-scene-guid 参数 +P6.4-T03 Player 启动时加载 CookManifest +P6.4-T04 Player 不扫描 Assets 源目录 +P6.4-T05 Player 不读取 .mcscene.json +P6.4-T06 Player 不读取 .mcprefab.json +P6.4-T07 Player 不读取 .rml 源路径,除非以 loose file 模式运行 +P6.4-T08 Player 只通过 cooked asset 启动 +``` + +### 验收标准 + +```text +删除或重命名 Assets 源目录 +保留 Builds/Windows/MyProject/Data +双击 MyProject.exe +3D 场景和 UI 仍然可以正常运行 +``` + +--- + +## P6.5 增量 Cook + +### 任务 + +```text +P6.5-T01 基于 ContentHash 判断是否需要重 Cook +P6.5-T02 修改场景只 Cook 场景 +P6.5-T03 修改材质只 Cook 材质和依赖 +P6.5-T04 修改模型只 Cook 模型相关依赖 +P6.5-T05 修改 UI 只 Cook UI 相关依赖 +P6.5-T06 输出 Cook 统计信息 +``` + +--- + +## P6 完成标志 + +```text +Build 后的 Player 可以完全脱离 Assets 源目录运行 +CookManifest / RuntimeAssetRegistry 成为运行期资产入口 +Scene / Prefab / Material / Model / Texture / UI 都能通过 cooked 资产加载 +``` + +建议里程碑: + +```text +milestone: cooked-only runtime +``` + +--- + +# P7:多平台 / 国产化 / Web + +## 阶段目标 + +从 Windows 原型走向跨平台和国产化环境适配。 + +--- + +## P7.1 Windows 稳定化 + +```text +P7.1-T01 Release / RelWithDebInfo 构建稳定 +P7.1-T02 DLL 拷贝规则稳定 +P7.1-T03 打包目录稳定 +P7.1-T04 日志系统稳定 +P7.1-T05 安装包或压缩包发布策略 +``` + +--- + +## P7.2 Linux 支持 + +```text +P7.2-T01 Linux CMake configure +P7.2-T02 Linux build Editor / Player +P7.2-T03 Filament Linux 后端验证 +P7.2-T04 RmlUi Linux 输入和字体验证 +P7.2-T05 Linux 打包目录 +``` + +--- + +## P7.3 国产系统适配 + +```text +P7.3-T01 统信 UOS 构建验证 +P7.3-T02 麒麟系统构建验证 +P7.3-T03 国产 CPU 环境兼容性检查 +P7.3-T04 国产 GPU / OpenGL / Vulkan 兼容性检查 +P7.3-T05 字体 / 输入法 / 路径编码问题处理 +P7.3-T06 RmlUi 中文字体和输入验证 +``` + +--- + +## P7.4 Web / B/S 方向 + +```text +P7.4-T01 评估 Filament WebGPU / WebGL 路线 +P7.4-T02 Player Web 版本技术验证 +P7.4-T03 Web 资产加载方式调整 +P7.4-T04 浏览器 UI 与 RmlUi 关系梳理 +P7.4-T05 明确 Web Runtime 是否复用 RmlUi +``` + +--- + +## P7 完成标志 + +```text +Windows 可发布 +Linux 可运行 +国产桌面系统有验证路线 +Web 方向完成技术预研 +``` + +--- + +# P8:稳定化 / 文档 / 示例项目 / 版本发布 + +## 阶段目标 + +让 MetaCore 从快速开发状态进入可维护、可演示、可交付状态。 + +--- + +## P8.1 测试体系 + +```text +P8.1-T01 SmokeTests 分类 +P8.1-T02 AssetPipeline 测试 +P8.1-T03 SceneSerialization 测试 +P8.1-T04 Prefab 测试 +P8.1-T05 Material 测试 +P8.1-T06 RmlUi UIComponent 测试 +P8.1-T07 Cook 测试 +P8.1-T08 RuntimeAssetRegistry 测试 +P8.1-T09 Editor authoring loop 手动验收清单 +``` + +--- + +## P8.2 日志系统 + +```text +P8.2-T01 Editor Console 日志统一 +P8.2-T02 Player 日志文件 +P8.2-T03 Cook 日志 +P8.2-T04 Import 日志 +P8.2-T05 UI 日志 +P8.2-T06 错误分级 Info / Warning / Error / Fatal +``` + +--- + +## P8.3 崩溃与错误恢复 + +```text +P8.3-T01 场景加载失败 fallback +P8.3-T02 资产缺失 fallback +P8.3-T03 材质缺失 fallback +P8.3-T04 模型导入失败提示 +P8.3-T05 UI 文件缺失 fallback +P8.3-T06 JSON 解析失败定位到文件和字段 +``` + +--- + +## P8.4 文档 + +```text +P8.4-T01 README 更新 +P8.4-T02 Architecture/Rendering.md +P8.4-T03 Architecture/SceneRenderSync.md +P8.4-T04 Architecture/AssetPipeline.md +P8.4-T05 Architecture/SceneSerialization.md +P8.4-T06 Architecture/Prefab.md +P8.4-T07 Architecture/Material.md +P8.4-T08 Architecture/RuntimeUI.md +P8.4-T09 Architecture/CookedRuntime.md +P8.4-T10 Roadmap/MetaCore_v0.1.md +P8.4-T11 DeveloperGuide/Build.md +P8.4-T12 UserGuide/Editor.md +``` + +--- + +## P8.5 示例项目 + +```text +P8.5-T01 清理 SandboxProject +P8.5-T02 新增 MinimalProject +P8.5-T03 新增 GLB Viewer 示例 +P8.5-T04 新增 Industrial Dashboard 示例 +P8.5-T05 新增 Runtime UI Demo +P8.5-T06 新增 Digital Twin Demo 雏形 +``` + +--- + +# 推荐版本节点 + +## v0.1:编辑器最小可用版 + +包含: + +```text +P0 Panda3D 清理 +P1 ECS → Filament +P2 编辑器基础闭环 +``` + +状态:你现在基本已经达到。 + +目标: + +```text +能打开编辑器 +能编辑场景 +能保存重开 +能启动 Player 看场景 +``` + +--- + +## v0.2:资产与 Prefab 工作流版 + +包含: + +```text +P3 Project 面板增强 +P3 资产导入体验 +P3 Prefab 创建 / 实例化 / Apply +P3 Material 基础编辑 +P3 一键 Launch Player +``` + +目标: + +```text +具备 Unity-like 最小编辑体验 +``` + +--- + +## v0.3:Runtime UI MVP 版 + +包含: + +```text +P4 RmlUi 集成 +P4 .rml / .rcss 资产识别 +P4 UIComponent +P4 Player 显示 UI +P4 基础输入和按钮事件 +``` + +目标: + +```text +3D 场景 + 运行时 UI 可以同时运行 +能做简单业务界面和演示面板 +``` + +--- + +## v0.4:项目打包版 + +包含: + +```text +P5 Project Settings +P5 Build Settings +P5 Build Pipeline +P5 拷贝 Player +P5 打包场景、模型、材质、贴图、UI +``` + +目标: + +```text +能从 Editor 打包出可双击运行的 Windows 项目 +``` + +--- + +## v0.5:Cooked Runtime 版 + +包含: + +```text +P6 RuntimeAssetRegistry +P6 CookManifest +P6 Cooked-only Player +P6 增量 Cook +P6 运行时完全脱离源资产 +``` + +目标: + +```text +形成真正发布期运行架构 +``` + +--- + +## v0.6:平台化与演示版 + +包含: + +```text +P7 Windows 稳定 +P7 Linux 验证 +P7 国产化预研 +P8 示例项目 +P8 文档和测试体系 +``` + +目标: + +```text +可以对外演示、交付 Demo、验证行业场景 +``` + +--- + +# 现在最推荐的执行顺序 + +你当前 P2 已完成,建议接下来严格按这个顺序: + +```text +1. P3.1 Project 面板增强 +2. P3.2 资产导入体验 +3. P3.3 Prefab 最小闭环 +4. P3.4 Material 基础编辑 +5. P3.5 一键 Launch Player +6. P4.1 RmlUi 基础集成 +7. P4.2 UI 资产识别 +8. P4.3 UIComponent +9. P4.4 UI 输入与基础事件 +10. P5.1 Project Settings +11. P5.2 Build Settings 面板 +12. P5.3 Build Pipeline 第一版 +13. P6 Cooked Runtime 深化 +``` + +一句话总结: + +> 新路线应该是:先把编辑器生产力做好,再补 Runtime UI MVP,然后再做 Build Settings 打包。这样 MetaCore 打包出来的不是一个单纯 3D 场景播放器,而是一个能展示模型、材质、场景和业务 UI 的真正运行时应用。 diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 07d9b47..b671610 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -20,6 +20,8 @@ #include "MetaCoreScene/MetaCoreScene.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" #include "MetaCoreScene/MetaCoreTransformUtils.h" +#include "MetaCoreFoundation/MetaCoreHash.h" +#include #include #include @@ -1107,6 +1109,265 @@ void MetaCoreTestInstantiateMultiNodeModelAssetIntoScene() { std::filesystem::remove_all(tempProjectRoot); } +void MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport() { + const std::filesystem::path projectRoot = "d:/MetaCore/SandboxProject"; + _putenv_s("METACORE_PROJECT_PATH", projectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + const auto assetDatabase = moduleRegistry.ResolveService(); + const auto sceneEditingService = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); + MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService"); + + const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets/Models/box1.glb")); + MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的 box1.glb 模型资源"); + + const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt); + MetaCoreExpect(instantiatedRootId.has_value(), "应能将导入模型实例化到场景"); + + // 初始化窗口和 FilamentSceneBridge + MetaCore::MetaCoreWindow testWindow; + bool winOk = testWindow.Initialize(800, 600, "DeleteModelAssetTestWindow"); + MetaCoreExpect(winOk, "测试窗口初始化应成功"); + + MetaCore::MetaCoreFilamentSceneBridge bridge; + bridge.SetProjectRootPath(projectRoot); + bool bridgeOk = bridge.Initialize(testWindow); + MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功"); + + // 同步场景,将模型加载到 FilamentSceneBridge 中 + bridge.SyncScene(scene, false, false); + + // 验证模型已被桥接器加载和映射 + glm::mat4 modelWorldMatrix; + bool hasModel = bridge.TryGetObjectWorldMatrix(*instantiatedRootId, modelWorldMatrix); + MetaCoreExpect(hasModel, "同步场景后,桥接器中应包含已实例化模型的变换映射"); + + // 在场景中删除该模型 GameObject + const std::vector deletedIds = scene.DeleteGameObjects({*instantiatedRootId}); + MetaCoreExpect(!deletedIds.empty(), "删除模型对象应返回被删除的节点ID"); + + // 再次同步场景,此时应触发卸载 + bridge.SyncScene(scene, false, false); + + // 验证删除后,桥接器中是否还存在该模型的映射 + bool hasModelAfterDelete = bridge.TryGetObjectWorldMatrix(*instantiatedRootId, modelWorldMatrix); + MetaCoreExpect(!hasModelAfterDelete, "删除模型后,桥接器映射中不应再包含该模型"); + + bridge.Shutdown(); + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); +} + + +void MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreDeleteModelSubChildProject"; + 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\": \"DeleteModelSubChildProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + { + std::ofstream gltfFile(tempProjectRoot / "Assets" / "Assembly.gltf", std::ios::trunc); + gltfFile << "{\n" + << " \"asset\": { \"version\": \"2.0\" },\n" + << " \"scenes\": [{ \"nodes\": [0] }],\n" + << " \"nodes\": [\n" + << " { \"name\": \"AssemblyRoot\", \"children\": [1, 2] },\n" + << " { \"name\": \"Valve_A\" },\n" + << " { \"name\": \"Valve_B\" }\n" + << " ]\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + const auto assetDatabase = moduleRegistry.ResolveService(); + const auto sceneEditingService = moduleRegistry.ResolveService(); + const auto packageService = moduleRegistry.ResolveService(); + const auto reflectionRegistry = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); + MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService"); + MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); + MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); + + const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Assembly.gltf"); + MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的多节点模型资源"); + + MetaCore::MetaCoreModelAssetDocument document; + document.AssetType = "model"; + document.ImporterId = "GltfModelImporter"; + document.SourcePath = std::filesystem::path("Assets") / "Assembly.gltf"; + document.SourceHash = 1234; + document.ModelKind = MetaCore::MetaCoreModelAssetKind::Gltf; + document.SourceFormat = "gltf"; + + MetaCore::MetaCoreImportedGltfNodeDocument rootNode; + rootNode.Name = "AssemblyRoot"; + rootNode.ParentIndex = -1; + rootNode.MeshIndex = -1; + document.Nodes.push_back(rootNode); + + MetaCore::MetaCoreImportedGltfNodeDocument childNode; + childNode.Name = "Valve_A"; + childNode.ParentIndex = 0; + childNode.MeshIndex = 0; + document.Nodes.push_back(childNode); + + MetaCore::MetaCoreImportedGltfNodeDocument childNodeB; + childNodeB.Name = "Valve_B"; + childNodeB.ParentIndex = 0; + childNodeB.MeshIndex = 0; + document.Nodes.push_back(childNodeB); + + MetaCore::MetaCoreMeshAssetDocument meshAsset; + meshAsset.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); + meshAsset.Name = "ValveMesh"; + MetaCore::MetaCoreMeshSubMeshDocument firstSubMesh; + firstSubMesh.Name = "Primitive_0"; + firstSubMesh.MaterialSlotIndex = 0; + firstSubMesh.FirstIndex = 0; + firstSubMesh.IndexCount = 3; + meshAsset.SubMeshes.push_back(firstSubMesh); + document.GeneratedMeshAssets.push_back(meshAsset); + + const auto payload = MetaCore::MetaCoreSerializeToBytes(document, reflectionRegistry->GetTypeRegistry()); + MetaCoreExpect(payload.has_value(), "多节点模型资源应能序列化"); + + MetaCore::MetaCorePackageDocument package; + package.Header.PackageType = MetaCore::MetaCorePackageType::Asset; + package.Header.PackageGuid = modelRecord->Guid; + package.Header.SourceHash = document.SourceHash; + package.NameTable.emplace_back("MetaCoreModelAssetDocument"); + package.Exports.push_back(MetaCore::MetaCoreExportEntry{ + modelRecord->Guid, + modelRecord->RelativePath.filename().string(), + MetaCore::MetaCoreMakeTypeId("MetaCoreModelAssetDocument"), + 0, + static_cast(payload->size()) + }); + package.PayloadSections.push_back(*payload); + MetaCoreExpect( + packageService->WritePackage(tempProjectRoot / modelRecord->PackagePath, std::move(package)), + "应能写入多节点模型资源包" + ); + + const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt); + MetaCoreExpect(instantiatedRootId.has_value(), "应能将多节点模型实例化到场景"); + + MetaCore::MetaCoreId childId = 0; + MetaCore::MetaCoreId childIdB = 0; + for (const auto& obj : scene.GetGameObjects()) { + if (obj.GetName() == "Valve_A") { + childId = obj.GetId(); + } else if (obj.GetName() == "Valve_B") { + childIdB = obj.GetId(); + } + } + MetaCoreExpect(childId != 0, "应能找到子节点 Valve_A"); + MetaCoreExpect(childIdB != 0, "应能找到子节点 Valve_B"); + + // 初始化窗口和 FilamentSceneBridge + MetaCore::MetaCoreWindow testWindow; + bool winOk = testWindow.Initialize(800, 600, "DeleteModelSubChildTestWindow"); + MetaCoreExpect(winOk, "测试窗口初始化应成功"); + + MetaCore::MetaCoreFilamentSceneBridge bridge; + bridge.SetProjectRootPath(tempProjectRoot); + bool bridgeOk = bridge.Initialize(testWindow); + MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功"); + + // 同步场景,将模型加载到 FilamentSceneBridge 中 + bridge.SyncScene(scene, false, false); + + // 验证子节点对应的实体是否已经在场景中渲染 + bool isChildInScene = bridge.VerifyEntityInSceneForTesting(childId); + bool isChildBInScene = bridge.VerifyEntityInSceneForTesting(childIdB); + MetaCoreExpect(isChildInScene, "同步场景后,子节点 Valve_A 实体应在场景中被渲染"); + MetaCoreExpect(isChildBInScene, "同步场景后,子节点 Valve_B 实体应在场景中被渲染"); + + // 在场景中删除子节点 Valve_A GameObject(在 Command 中执行,为了后续 Undo) + const bool deleted = editorContext.ExecuteSnapshotCommand("删除子节点 Valve_A", [&]() { + scene.DeleteGameObjects({childId}); + return true; + }); + MetaCoreExpect(deleted, "删除子节点 Valve_A 快照命令应成功"); + + // 再次同步场景 + bridge.SyncScene(scene, false, false); + + // 验证删除子节点 Valve_A 后,桥接器中该子节点对应的实体是否已被移出场景渲染 + bool isChildInSceneAfterDelete = bridge.VerifyEntityInSceneForTesting(childId); + bool isChildBInSceneAfterDelete = bridge.VerifyEntityInSceneForTesting(childIdB); + MetaCoreExpect(!isChildInSceneAfterDelete, "删除子节点 Valve_A 后,其实体应被移出场景渲染"); + MetaCoreExpect(isChildBInSceneAfterDelete, "删除子节点 Valve_A 后,Valve_B 实体应依然保留在场景中渲染(没有卸载资产)"); + + // 撤销删除子节点 + const bool undoSucceeded = editorContext.UndoCommand(); + MetaCoreExpect(undoSucceeded, "撤销删除应成功"); + + // 再次同步场景 + bridge.SyncScene(scene, false, false); + + // 验证撤销后,该子节点实体是否已被重新恢复渲染 + bool isChildInSceneAfterUndo = bridge.VerifyEntityInSceneForTesting(childId); + MetaCoreExpect(isChildInSceneAfterUndo, "撤销删除后,子节点 Valve_A 实体应重新被加回场景渲染"); + + bridge.Shutdown(); + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + + void MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreModelReimportStableProject"; @@ -2485,21 +2746,27 @@ void MetaCoreTestDecoupledSnapshotSync() { MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功"); // 5. 在 scene == nullptr 的快照渲染模式下执行同步 + std::cout << "[DEBUG] SyncScene starting..." << std::endl; bridge.SyncScene(snapshot, nullptr, false, false); + std::cout << "[DEBUG] SyncScene completed." << std::endl; // 6. 验证映射重建和变换同步 glm::mat4 cubeWorldMatrix; bool hasCube = bridge.TryGetObjectWorldMatrix(cubeId, cubeWorldMatrix); + std::cout << "[DEBUG] hasCube: " << hasCube << std::endl; MetaCoreExpect(hasCube, "无 scene 模式下重建映射后应包含 Cube 变换"); bool transOk = bridge.VerifyTransformForTesting(cubeId, customLocalMatrix); + std::cout << "[DEBUG] transOk: " << transOk << std::endl; MetaCoreExpect(transOk, "快照中的自定义局部变换应正确同步到 Filament"); // 7. 验证 Enabled 状态为 false 的光源被过滤排除,且默认灯光起降机制生效 (自动亮起) bool hasLight = bridge.VerifyLightExistsForTesting(lightId); + std::cout << "[DEBUG] hasLight: " << hasLight << std::endl; MetaCoreExpect(!hasLight, "Enabled 为 false 的自定义光源不应被实例化"); float defaultLightIntensity = bridge.GetDefaultLightIntensityForTesting(); + std::cout << "[DEBUG] defaultLightIntensity: " << defaultLightIntensity << std::endl; MetaCoreExpect(defaultLightIntensity > 99000.0F, "无有效自定义光源时,默认灯光应起飞作为兜底"); // 8. 动态开启光源属性,验证默认灯光动态变暗 @@ -2508,12 +2775,16 @@ void MetaCoreTestDecoupledSnapshotSync() { light.Enabled = true; } } + std::cout << "[DEBUG] SyncScene 2 starting..." << std::endl; bridge.SyncScene(snapshot, nullptr, false, false); + std::cout << "[DEBUG] SyncScene 2 completed." << std::endl; bool hasLightNow = bridge.VerifyLightExistsForTesting(lightId); + std::cout << "[DEBUG] hasLightNow: " << hasLightNow << std::endl; MetaCoreExpect(hasLightNow, "启用光源后自定义光源应被实例化"); float defaultLightIntensityNow = bridge.GetDefaultLightIntensityForTesting(); + std::cout << "[DEBUG] defaultLightIntensityNow: " << defaultLightIntensityNow << std::endl; MetaCoreExpect(defaultLightIntensityNow < 1.0F, "启用自定义光源时,默认灯光强度应自动降为 0"); // 9. 再次禁用光源,验证自定义光源被销毁并且默认灯光复亮 @@ -2522,19 +2793,205 @@ void MetaCoreTestDecoupledSnapshotSync() { light.Enabled = false; } } + std::cout << "[DEBUG] SyncScene 3 starting..." << std::endl; bridge.SyncScene(snapshot, nullptr, false, false); + std::cout << "[DEBUG] SyncScene 3 completed." << std::endl; bool hasLightFinal = bridge.VerifyLightExistsForTesting(lightId); + std::cout << "[DEBUG] hasLightFinal: " << hasLightFinal << std::endl; MetaCoreExpect(!hasLightFinal, "再次禁用光源后自定义光源实体应被正确销毁移出"); float defaultLightIntensityFinal = bridge.GetDefaultLightIntensityForTesting(); + std::cout << "[DEBUG] defaultLightIntensityFinal: " << defaultLightIntensityFinal << std::endl; MetaCoreExpect(defaultLightIntensityFinal > 99000.0F, "再次无激活光源时,默认灯光应重新亮起兜底"); + // 9.5 模拟删除网格对象,验证其在 FilamentSceneBridge 中被成功卸载 + std::cout << "[DEBUG] Deleting mesh object..." << std::endl; + for (auto it = snapshot.Renderables.begin(); it != snapshot.Renderables.end(); ) { + if (it->ObjectId == cubeId) { + it = snapshot.Renderables.erase(it); + } else { + ++it; + } + } + std::cout << "[DEBUG] SyncScene 4 starting..." << std::endl; + bridge.SyncScene(snapshot, nullptr, false, false); + std::cout << "[DEBUG] SyncScene 4 completed." << std::endl; + + glm::mat4 deletedWorldMatrix; + bool hasCubeAfterDelete = bridge.TryGetObjectWorldMatrix(cubeId, deletedWorldMatrix); + std::cout << "[DEBUG] hasCubeAfterDelete: " << hasCubeAfterDelete << std::endl; + MetaCoreExpect(!hasCubeAfterDelete, "删除网格对象后,其在桥接器映射中不应再存在"); + // 10. 资源清理 + std::cout << "[DEBUG] bridge.Shutdown starting..." << std::endl; bridge.Shutdown(); + std::cout << "[DEBUG] bridge.Shutdown completed." << std::endl; window.Shutdown(); } +void MetaCoreTestP3ProductivityStage() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreTestP3Project"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + // 1. 写入默认的 MetaCore.project.json + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"P3Project\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\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(); + MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); + MetaCoreExpect(assetDatabase->HasProject(), "应成功打开项目"); + + // 2. 测试资产自动导入与默认目录 (P3.2) + // 写入一个没有 .mcmeta 的 .ktx 贴图文件 + const std::filesystem::path texturePath = tempProjectRoot / "Assets" / "test_texture.ktx"; + { + std::ofstream textureFile(texturePath, std::ios::trunc); + textureFile << "dummy-ktx-data"; + } + + assetDatabase->Refresh(); + + // 验证是否在 Assets 下自动生成了 test_texture.ktx.mcmeta 物理元文件 + const std::filesystem::path metaPath = tempProjectRoot / "Assets" / "test_texture.ktx.mcmeta"; + MetaCoreExpect(std::filesystem::exists(metaPath), "Refresh 后应生成 test_texture.ktx.mcmeta 物理元文件"); + + // 读取该 .mcmeta 文件内容,校验结构 + std::ifstream metaFile(metaPath); + nlohmann::json metaJson; + metaFile >> metaJson; + metaFile.close(); + + MetaCoreExpect(metaJson.contains("guid"), "元文件应包含 guid"); + MetaCoreExpect(metaJson.contains("asset_type") && metaJson["asset_type"] == "texture", "元文件 asset_type 应为 texture"); + MetaCoreExpect(metaJson.contains("importer_id") && metaJson["importer_id"] == "TextureImporter", "元文件 importer_id 应为 TextureImporter"); + + const std::string textureGuidStr = metaJson["guid"].get(); + MetaCore::MetaCoreAssetGuid textureGuid = MetaCore::MetaCoreAssetGuid::Parse(textureGuidStr).value_or(MetaCore::MetaCoreAssetGuid{}); + MetaCoreExpect(textureGuid.IsValid(), "自动分配的材质 GUID 应为有效 GUID"); + + // 3. 测试资产重命名与删除 (P3.1, P3.4) + // 重命名该贴图文件 + MetaCoreExpect(assetDatabase->RenamePath(std::filesystem::path("Assets") / "test_texture.ktx", "renamed_texture.ktx"), "应能重命名贴图文件"); + const std::filesystem::path renamedTexturePath = tempProjectRoot / "Assets" / "renamed_texture.ktx"; + const std::filesystem::path renamedMetaPath = tempProjectRoot / "Assets" / "renamed_texture.ktx.mcmeta"; + MetaCoreExpect(std::filesystem::exists(renamedTexturePath), "重命名后新贴图文件应存在"); + MetaCoreExpect(!std::filesystem::exists(texturePath), "重命名后旧贴图文件不应存在"); + MetaCoreExpect(std::filesystem::exists(renamedMetaPath), "重命名后新 mcmeta 文件应存在"); + MetaCoreExpect(!std::filesystem::exists(metaPath), "重命名后旧 mcmeta 文件不应存在"); + + // 删除重命名后的贴图文件 + MetaCoreExpect(assetDatabase->DeletePath(std::filesystem::path("Assets") / "renamed_texture.ktx"), "应能删除贴图文件"); + MetaCoreExpect(!std::filesystem::exists(renamedTexturePath), "删除后贴图文件应不存在"); + MetaCoreExpect(!std::filesystem::exists(renamedMetaPath), "删除后 mcmeta 文件应不存在"); + + // 4. 测试材质创建、加载、编辑保存及物理 Hash 更新 (P3.4) + // 保存一个初始材质 + const std::filesystem::path materialPath = tempProjectRoot / "Assets" / "test_material.mcmaterial.json"; + MetaCore::MetaCoreMaterialAssetDocument materialDoc; + materialDoc.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); + materialDoc.Name = "test_material"; + materialDoc.BaseColor = glm::vec3(1.0F, 0.0F, 0.0F); + materialDoc.Metallic = 0.5F; + materialDoc.Roughness = 0.8F; + + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterFoundationGeneratedTypes(registry); + MetaCoreRegisterSceneGeneratedTypes(registry); + + MetaCoreExpect( + MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(materialPath, materialDoc, registry), + "应能保存材质文件" + ); + + // 自动刷新生成 .mcmeta 物理元文件 + assetDatabase->Refresh(); + const std::filesystem::path materialMetaPath = tempProjectRoot / "Assets" / "test_material.mcmaterial.json.mcmeta"; + MetaCoreExpect(std::filesystem::exists(materialMetaPath), "Refresh 后应生成材质的 mcmeta 元文件"); + + // 编辑材质属性并重新保存 + materialDoc.BaseColor = glm::vec3(0.0F, 1.0F, 0.0F); + materialDoc.Metallic = 0.9F; + materialDoc.Roughness = 0.1F; + MetaCoreExpect( + MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(materialPath, materialDoc, registry), + "应能重新保存修改后的材质文件" + ); + + // 模拟材质编辑器的物理 Hash 保存更新逻辑 + std::uint64_t initialHash = 0; + { + std::ifstream input(materialMetaPath); + nlohmann::json json; + input >> json; + input.close(); + if (json.contains("source_hash")) { + initialHash = json["source_hash"].get(); + } + } + + // 更新 Hash + const std::uint64_t newHash = MetaCore::MetaCoreHashFile(materialPath).value_or(0); + { + std::ifstream input(materialMetaPath); + nlohmann::json json; + input >> json; + input.close(); + json["source_hash"] = newHash; + std::ofstream output(materialMetaPath); + output << json.dump(4); + output.close(); + } + + // 读回以校验 + std::uint64_t savedHash = 0; + { + std::ifstream input(materialMetaPath); + nlohmann::json json; + input >> json; + input.close(); + if (json.contains("source_hash")) { + savedHash = json["source_hash"].get(); + } + } + MetaCoreExpect(savedHash == newHash, "保存并更新后,材质 mcmeta 物理哈希值应被更新且一致"); + + // 5. 测试拖拽绑定到 Mesh 槽位 (P3.3) + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreGameObject gameObject = scene.CreateGameObject("MeshObject"); + auto& meshRenderer = gameObject.AddComponent(); + + // 模拟槽位绑定 + if (meshRenderer.MaterialAssetGuids.empty()) { + meshRenderer.MaterialAssetGuids.resize(1); + } + meshRenderer.MaterialAssetGuids[0] = materialDoc.AssetGuid; + + MetaCoreExpect(meshRenderer.MaterialAssetGuids[0] == materialDoc.AssetGuid, "拖拽绑定后槽位 0 材质 Guid 应正确"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + } // namespace int main() { @@ -2598,6 +3055,10 @@ int main() { MetaCoreTestInstantiateImportedModelAssetIntoScene(); std::cout << "[RUN] MetaCoreTestInstantiateMultiNodeModelAssetIntoScene..." << std::endl; MetaCoreTestInstantiateMultiNodeModelAssetIntoScene(); + std::cout << "[RUN] MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport..." << std::endl; + MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport(); + std::cout << "[RUN] MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport..." << std::endl; + MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport(); std::cout << "[RUN] MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable..." << std::endl; MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable(); std::cout << "[RUN] MetaCoreTestBootstrapStartupSceneCreation..." << std::endl; @@ -2654,6 +3115,9 @@ int main() { std::cout << "[RUN] MetaCoreTestDecoupledSnapshotSync..." << std::endl; MetaCoreTestDecoupledSnapshotSync(); + std::cout << "[RUN] MetaCoreTestP3ProductivityStage..." << std::endl; + MetaCoreTestP3ProductivityStage(); + std::cout << "MetaCoreSmokeTests passed\n"; return 0; }