From 7729a21fe982dff4ce3b9cbfb12badb4be6a77b8 Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Wed, 17 Jun 2026 15:11:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=99=A8=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=92=8C=E6=9D=90=E8=B4=A8=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Apps/MetaCoreLauncher/main.cpp | 1055 + CMakeLists.txt | 28 + .../Materials/Material 1.mcmaterial.json | 4 +- .../Material 1.mcmaterial.json.mcmeta | 2 +- .../Materials/Material 7.mcmaterial.json | 26 + .../Material 7.mcmaterial.json.mcmeta | 8 + SandboxProject/Assets/Models/Cube.glb.mcasset | Bin 0 -> 3966 bytes SandboxProject/Scenes/Main.mcscene | 18584 +--------------- .../MetaCoreBuiltinCoreServicesModule.cpp | 439 +- .../Private/MetaCoreBuiltinEditorModule.cpp | 141 +- .../Private/MetaCoreEditorApp.cpp | 60 +- .../Private/MetaCoreProject.cpp | 94 + .../MetaCoreFoundation/MetaCoreProject.h | 10 + .../Private/MetaCoreFilamentSceneBridge.cpp | 310 +- .../Private/MetaCoreSceneRenderSync.cpp | 1 + .../MetaCoreRender/MetaCoreSceneRenderSync.h | 1 + .../MetaCoreScene/Private/MetaCoreScene.cpp | 10 +- tests/MetaCoreSmokeTests.cpp | 73 +- 18 files changed, 2104 insertions(+), 18742 deletions(-) create mode 100644 Apps/MetaCoreLauncher/main.cpp create mode 100644 SandboxProject/Assets/Materials/Material 7.mcmaterial.json create mode 100644 SandboxProject/Assets/Materials/Material 7.mcmaterial.json.mcmeta create mode 100644 SandboxProject/Assets/Models/Cube.glb.mcasset diff --git a/Apps/MetaCoreLauncher/main.cpp b/Apps/MetaCoreLauncher/main.cpp new file mode 100644 index 0000000..157e8fd --- /dev/null +++ b/Apps/MetaCoreLauncher/main.cpp @@ -0,0 +1,1055 @@ +#include "MetaCoreFoundation/MetaCoreProject.h" +#include "MetaCorePlatform/MetaCoreWindow.h" +#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" +#include "MetaCoreRender/MetaCoreRenderDevice.h" + +#include +#if defined(_WIN32) +#include +#else +#include +#include +#endif +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#include +#include +#endif + +#if defined(_WIN32) +extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); +#else +extern char** environ; +#endif + +namespace { + +struct MetaCoreLauncherProjectRecord { + std::string Name{}; + std::filesystem::path Root{}; + std::string LastOpened{}; +}; + +enum class MetaCoreLauncherPathPickerTarget { + NewProjectParent, + ExistingProject +}; + +struct MetaCoreLauncherPathPickerEntry { + std::filesystem::path Path{}; + std::string Label{}; + bool IsDirectory = false; + bool IsProjectFile = false; +}; + +[[nodiscard]] std::string MetaCoreToLower(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) { + return static_cast(std::tolower(character)); + }); + return value; +} + +[[nodiscard]] std::string MetaCoreNowIsoString() { + const auto now = std::chrono::system_clock::now(); + const auto timeValue = std::chrono::system_clock::to_time_t(now); + std::tm timeInfo{}; +#if defined(_WIN32) + localtime_s(&timeInfo, &timeValue); +#else + localtime_r(&timeValue, &timeInfo); +#endif + + std::ostringstream stream; + stream << std::put_time(&timeInfo, "%Y-%m-%dT%H:%M:%S"); + return stream.str(); +} + +[[nodiscard]] std::filesystem::path MetaCoreGetExecutableDirectory() { +#if defined(_WIN32) + std::vector buffer(MAX_PATH, L'\0'); + while (true) { + const DWORD copied = GetModuleFileNameW(nullptr, buffer.data(), static_cast(buffer.size())); + if (copied == 0) { + return {}; + } + if (copied < buffer.size() - 1) { + return std::filesystem::path(std::wstring(buffer.data(), copied)).parent_path(); + } + buffer.resize(buffer.size() * 2, L'\0'); + } +#else + std::vector buffer(PATH_MAX, '\0'); + while (true) { + const ssize_t copied = readlink("/proc/self/exe", buffer.data(), buffer.size()); + if (copied < 0) { + return {}; + } + if (static_cast(copied) < buffer.size()) { + return std::filesystem::path(std::string(buffer.data(), static_cast(copied))).parent_path(); + } + buffer.resize(buffer.size() * 2, '\0'); + } +#endif +} + +[[nodiscard]] std::filesystem::path MetaCoreGetLauncherStatePath() { +#if defined(_WIN32) + if (const char* appData = std::getenv("APPDATA"); appData != nullptr && appData[0] != '\0') { + return std::filesystem::path(appData) / "MetaCore" / "launcher_projects.json"; + } +#else + if (const char* home = std::getenv("HOME"); home != nullptr && home[0] != '\0') { + return std::filesystem::path(home) / ".metacore" / "launcher_projects.json"; + } +#endif + return std::filesystem::current_path() / ".metacore" / "launcher_projects.json"; +} + +[[nodiscard]] std::filesystem::path MetaCoreGetHomeDirectory() { +#if defined(_WIN32) + if (const char* userProfile = std::getenv("USERPROFILE"); userProfile != nullptr && userProfile[0] != '\0') { + return std::filesystem::path(userProfile); + } +#else + if (const char* home = std::getenv("HOME"); home != nullptr && home[0] != '\0') { + return std::filesystem::path(home); + } +#endif + return std::filesystem::current_path(); +} + +[[nodiscard]] std::string MetaCoreReadProjectDisplayName(const std::filesystem::path& projectRoot) { + if (const auto projectDocument = MetaCore::MetaCoreReadProjectFile(MetaCore::MetaCoreGetProjectFilePath(projectRoot)); + projectDocument.has_value() && !projectDocument->Name.empty()) { + return projectDocument->Name; + } + return projectRoot.filename().string(); +} + +[[nodiscard]] std::string MetaCoreProjectRootKey(const std::filesystem::path& projectRoot) { + if (const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(projectRoot); resolvedRoot.has_value()) { + return resolvedRoot->generic_string(); + } + return projectRoot.lexically_normal().generic_string(); +} + +void MetaCoreCopyToBuffer(std::array& buffer, const std::string& value) { + std::memset(buffer.data(), 0, buffer.size()); + std::strncpy(buffer.data(), value.c_str(), buffer.size() - 1); +} + +void MetaCoreCopyToBuffer(std::array& buffer, const std::string& value) { + std::memset(buffer.data(), 0, buffer.size()); + std::strncpy(buffer.data(), value.c_str(), buffer.size() - 1); +} + +[[nodiscard]] std::vector MetaCoreCollectPathPickerEntries( + const std::filesystem::path& directory, + bool includeProjectFile +) { + std::vector entries; + std::error_code errorCode; + const std::filesystem::directory_options options = std::filesystem::directory_options::skip_permission_denied; + for (auto iterator = std::filesystem::directory_iterator(directory, options, errorCode); + iterator != std::filesystem::directory_iterator(); + iterator.increment(errorCode)) { + if (errorCode) { + errorCode.clear(); + continue; + } + + const std::filesystem::path entryPath = iterator->path(); + errorCode.clear(); + const bool isDirectory = iterator->is_directory(errorCode); + errorCode.clear(); + const bool isProjectFile = + includeProjectFile && + iterator->is_regular_file(errorCode) && + entryPath.filename() == "MetaCore.project.json"; + if (!isDirectory && !isProjectFile) { + continue; + } + + MetaCoreLauncherPathPickerEntry entry; + entry.Path = entryPath; + entry.Label = entryPath.filename().string(); + entry.IsDirectory = isDirectory; + entry.IsProjectFile = isProjectFile; + entries.push_back(std::move(entry)); + } + + std::sort(entries.begin(), entries.end(), [](const auto& lhs, const auto& rhs) { + if (lhs.IsDirectory != rhs.IsDirectory) { + return lhs.IsDirectory; + } + return MetaCoreToLower(lhs.Label) < MetaCoreToLower(rhs.Label); + }); + return entries; +} + +[[nodiscard]] std::filesystem::path MetaCoreEditorExecutablePath() { + std::filesystem::path editorPath = MetaCoreGetExecutableDirectory() / "MetaCoreEditorApp"; +#if defined(_WIN32) + editorPath += ".exe"; +#endif + return editorPath; +} + +bool MetaCoreLaunchEditorForProject( + const std::filesystem::path& projectRoot, + std::string& outError, + bool removeLauncherOwnedBackend = false +) { + const std::filesystem::path editorPath = MetaCoreEditorExecutablePath(); + if (!std::filesystem::exists(editorPath)) { + outError = "启动器旁边找不到 MetaCoreEditorApp。"; + return false; + } + +#if defined(_WIN32) + SetEnvironmentVariableW(L"METACORE_PROJECT_PATH", projectRoot.wstring().c_str()); + std::wstring commandLine = + L"\"" + editorPath.wstring() + L"\" --project \"" + projectRoot.wstring() + L"\""; + + STARTUPINFOW startupInfo{}; + startupInfo.cb = sizeof(startupInfo); + PROCESS_INFORMATION processInfo{}; + std::vector mutableCommandLine(commandLine.begin(), commandLine.end()); + mutableCommandLine.push_back(L'\0'); + + const BOOL created = CreateProcessW( + nullptr, + mutableCommandLine.data(), + nullptr, + nullptr, + FALSE, + 0, + nullptr, + editorPath.parent_path().wstring().c_str(), + &startupInfo, + &processInfo + ); + if (!created) { + outError = "启动 MetaCoreEditorApp 失败。"; + return false; + } + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); + return true; +#else + const std::string editorPathString = editorPath.string(); + const std::string editorNameString = editorPath.filename().string(); + const std::string projectRootString = projectRoot.string(); + + std::vector environmentStrings; + for (char** iterator = environ; iterator != nullptr && *iterator != nullptr; ++iterator) { + const std::string_view entry(*iterator); + if (entry.rfind("METACORE_PROJECT_PATH=", 0) == 0) { + continue; + } + if (removeLauncherOwnedBackend && entry.rfind("METACORE_FILAMENT_BACKEND=", 0) == 0) { + continue; + } + environmentStrings.emplace_back(*iterator); + } + environmentStrings.emplace_back("METACORE_PROJECT_PATH=" + projectRootString); + + std::vector environmentPointers; + environmentPointers.reserve(environmentStrings.size() + 1); + for (std::string& entry : environmentStrings) { + environmentPointers.push_back(entry.data()); + } + environmentPointers.push_back(nullptr); + + std::array arguments = { + const_cast(editorNameString.c_str()), + const_cast("--project"), + const_cast(projectRootString.c_str()), + nullptr + }; + + pid_t childPid = 0; + const int result = posix_spawn( + &childPid, + editorPathString.c_str(), + nullptr, + nullptr, + arguments.data(), + environmentPointers.data() + ); + if (result != 0) { + outError = "启动 MetaCoreEditorApp 失败。"; + return false; + } + return true; +#endif +} + +void MetaCoreConfigureLauncherFont() { + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->AddFontDefault(); + + std::vector candidatePaths; + if (const char* overrideFontPath = std::getenv("METACORE_IMGUI_FONT"); overrideFontPath != nullptr && overrideFontPath[0] != '\0') { + candidatePaths.emplace_back(overrideFontPath); + } + +#if defined(_WIN32) + char* windowsDirectory = nullptr; + std::size_t windowsDirectoryLength = 0; + if (_dupenv_s(&windowsDirectory, &windowsDirectoryLength, "WINDIR") == 0 && windowsDirectory != nullptr) { + const std::filesystem::path fontsPath = std::filesystem::path(windowsDirectory) / "Fonts"; + candidatePaths.push_back(fontsPath / "NotoSansSC-VF.ttf"); + candidatePaths.push_back(fontsPath / "msyh.ttc"); + candidatePaths.push_back(fontsPath / "msyhbd.ttc"); + candidatePaths.push_back(fontsPath / "Deng.ttf"); + candidatePaths.push_back(fontsPath / "simhei.ttf"); + free(windowsDirectory); + } +#else + candidatePaths.emplace_back("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"); + candidatePaths.emplace_back("/usr/share/fonts/opentype/noto/NotoSansCJK-Medium.ttc"); + candidatePaths.emplace_back("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"); + candidatePaths.emplace_back("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"); + candidatePaths.emplace_back("/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf"); + candidatePaths.emplace_back("/usr/share/fonts/truetype/arphic/uming.ttc"); +#endif + + for (const auto& candidatePath : candidatePaths) { + if (!std::filesystem::exists(candidatePath)) { + continue; + } + + try { + ImFont* font = io.Fonts->AddFontFromFileTTF( + candidatePath.string().c_str(), + 17.0F, + nullptr, + io.Fonts->GetGlyphRangesChineseSimplifiedCommon() + ); + if (font != nullptr) { + io.FontDefault = font; + return; + } + } catch (const std::bad_alloc&) { + } + } +} + +void MetaCoreApplyLauncherStyle() { + ImGuiStyle& style = ImGui::GetStyle(); + ImGui::StyleColorsDark(); + style.WindowRounding = 0.0F; + style.ChildRounding = 6.0F; + style.PopupRounding = 6.0F; + style.FrameRounding = 6.0F; + style.GrabRounding = 6.0F; + style.WindowBorderSize = 0.0F; + style.FrameBorderSize = 1.0F; + style.WindowPadding = ImVec2(0.0F, 0.0F); + style.FramePadding = ImVec2(10.0F, 7.0F); + style.ItemSpacing = ImVec2(8.0F, 8.0F); + style.ScrollbarSize = 8.0F; + + style.Colors[ImGuiCol_Text] = ImVec4(0.81F, 0.81F, 0.81F, 1.0F); + style.Colors[ImGuiCol_TextDisabled] = ImVec4(0.42F, 0.42F, 0.42F, 1.0F); + style.Colors[ImGuiCol_WindowBg] = ImVec4(0.098F, 0.098F, 0.098F, 1.0F); + style.Colors[ImGuiCol_ChildBg] = ImVec4(0.125F, 0.125F, 0.125F, 1.0F); + style.Colors[ImGuiCol_PopupBg] = ImVec4(0.125F, 0.125F, 0.125F, 0.98F); + style.Colors[ImGuiCol_Border] = ImVec4(0.18F, 0.18F, 0.18F, 1.0F); + style.Colors[ImGuiCol_FrameBg] = ImVec4(0.125F, 0.125F, 0.125F, 1.0F); + style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(0.165F, 0.165F, 0.165F, 1.0F); + style.Colors[ImGuiCol_FrameBgActive] = ImVec4(0.20F, 0.20F, 0.20F, 1.0F); + style.Colors[ImGuiCol_Button] = ImVec4(0.125F, 0.125F, 0.125F, 1.0F); + style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.165F, 0.165F, 0.165F, 1.0F); + style.Colors[ImGuiCol_ButtonActive] = ImVec4(0.20F, 0.20F, 0.20F, 1.0F); + style.Colors[ImGuiCol_Header] = ImVec4(0.20F, 0.20F, 0.20F, 1.0F); + style.Colors[ImGuiCol_HeaderHovered] = ImVec4(0.24F, 0.24F, 0.24F, 1.0F); + style.Colors[ImGuiCol_HeaderActive] = ImVec4(1.0F, 1.0F, 1.0F, 0.18F); + style.Colors[ImGuiCol_Separator] = ImVec4(0.18F, 0.18F, 0.18F, 1.0F); +} + +class MetaCoreLauncherApp { +public: + ~MetaCoreLauncherApp() { + Shutdown(); + } + + bool Initialize() { + if (std::getenv("METACORE_FILAMENT_BACKEND") == nullptr) { +#if defined(_WIN32) + _putenv_s("METACORE_FILAMENT_BACKEND", "opengl"); +#else + setenv("METACORE_FILAMENT_BACKEND", "opengl", 0); +#endif + LauncherSetFilamentBackend_ = true; + } + + if (!Window_.Initialize(1080, 720, "MetaCore 启动器")) { + return false; + } + +#if defined(_WIN32) + Window_.SetNativeWindowMessageHandler([](void* nativeWindowHandle, unsigned int message, std::uintptr_t wparam, std::intptr_t lparam) { + return ImGui_ImplWin32_WndProcHandler(static_cast(nativeWindowHandle), message, static_cast(wparam), static_cast(lparam)) != 0; + }); +#endif + + if (!RenderDevice_.Initialize(Window_)) { + return false; + } + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.IniFilename = nullptr; + io.LogFilename = nullptr; + MetaCoreConfigureLauncherFont(); + MetaCoreApplyLauncherStyle(); + +#if defined(_WIN32) + if (!ImGui_ImplWin32_InitForOpenGL(Window_.GetNativeWindowHandle())) { +#else + if (!ImGui_ImplGlfw_InitForOther(static_cast(Window_.GetNativeWindowHandle()), true)) { +#endif + return false; + } + + if (!ViewportRenderer_.Initialize(RenderDevice_, Window_, true)) { + return false; + } + + LoadState(); + MetaCoreCopyToBuffer(NewProjectParentBuffer_, std::filesystem::current_path().string()); + MetaCoreCopyToBuffer(NewProjectNameBuffer_, "新项目"); + Initialized_ = true; + return true; + } + + int Run() { + while (!Window_.ShouldClose()) { + Window_.BeginFrame(); +#if defined(_WIN32) + ImGui_ImplWin32_NewFrame(); +#else + ImGui_ImplGlfw_NewFrame(); +#endif + ImGui::NewFrame(); + DrawFrame(); + ImGui::Render(); + ViewportRenderer_.RenderAll(); + Window_.EndFrame(); + } + return 0; + } + + void Shutdown() { + if (!Initialized_) { + return; + } + ViewportRenderer_.Shutdown(); +#if defined(_WIN32) + ImGui_ImplWin32_Shutdown(); +#else + ImGui_ImplGlfw_Shutdown(); +#endif + ImGui::DestroyContext(); + RenderDevice_.Shutdown(); + Window_.Shutdown(); + Initialized_ = false; + } + +private: + void LoadState() { + Projects_.clear(); + const std::filesystem::path statePath = MetaCoreGetLauncherStatePath(); + std::ifstream input(statePath); + if (!input.is_open()) { + return; + } + + try { + nlohmann::json json; + input >> json; + if (!json.contains("recent_projects") || !json["recent_projects"].is_array()) { + return; + } + for (const auto& item : json["recent_projects"]) { + if (!item.contains("root") || !item["root"].is_string()) { + continue; + } + MetaCoreLauncherProjectRecord record; + record.Root = item["root"].get(); + if (item.contains("name") && item["name"].is_string()) { + record.Name = item["name"].get(); + } + if (item.contains("last_opened") && item["last_opened"].is_string()) { + record.LastOpened = item["last_opened"].get(); + } + if (const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(record.Root); resolvedRoot.has_value()) { + record.Root = *resolvedRoot; + record.Name = MetaCoreReadProjectDisplayName(record.Root); + } else if (record.Name.empty()) { + record.Name = record.Root.filename().string(); + } + Projects_.push_back(std::move(record)); + } + } catch (const std::exception& exceptionObject) { + StatusMessage_ = std::string("读取启动器状态失败:") + exceptionObject.what(); + } + } + + void SaveState() const { + const std::filesystem::path statePath = MetaCoreGetLauncherStatePath(); + std::error_code errorCode; + std::filesystem::create_directories(statePath.parent_path(), errorCode); + if (errorCode) { + return; + } + + nlohmann::json json; + json["recent_projects"] = nlohmann::json::array(); + for (const auto& project : Projects_) { + json["recent_projects"].push_back({ + {"name", project.Name}, + {"root", project.Root.generic_string()}, + {"last_opened", project.LastOpened} + }); + } + + std::ofstream output(statePath, std::ios::trunc); + if (output.is_open()) { + output << json.dump(2); + } + } + + bool UpsertProject(const std::filesystem::path& projectRoot) { + const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(projectRoot); + if (!resolvedRoot.has_value()) { + StatusMessage_ = "该路径不是有效的 MetaCore 项目。"; + return false; + } + + const std::string key = MetaCoreProjectRootKey(*resolvedRoot); + auto iterator = std::find_if(Projects_.begin(), Projects_.end(), [&](const MetaCoreLauncherProjectRecord& record) { + return MetaCoreProjectRootKey(record.Root) == key; + }); + + MetaCoreLauncherProjectRecord record; + record.Root = *resolvedRoot; + record.Name = MetaCoreReadProjectDisplayName(*resolvedRoot); + record.LastOpened = MetaCoreNowIsoString(); + + if (iterator != Projects_.end()) { + *iterator = std::move(record); + const auto index = static_cast(std::distance(Projects_.begin(), iterator)); + SelectedIndex_ = index; + std::rotate(Projects_.begin(), Projects_.begin() + index, Projects_.begin() + index + 1); + SelectedIndex_ = 0; + } else { + Projects_.insert(Projects_.begin(), std::move(record)); + SelectedIndex_ = 0; + } + SaveState(); + return true; + } + + void LaunchSelectedProject() { + if (SelectedIndex_ < 0 || SelectedIndex_ >= static_cast(Projects_.size())) { + StatusMessage_ = "请先选择一个项目。"; + return; + } + + auto& project = Projects_[static_cast(SelectedIndex_)]; + const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(project.Root); + if (!resolvedRoot.has_value()) { + StatusMessage_ = "项目文件夹缺失或无效。"; + return; + } + + std::string error; + if (!MetaCoreLaunchEditorForProject(*resolvedRoot, error, LauncherSetFilamentBackend_)) { + StatusMessage_ = error; + return; + } + + project.Root = *resolvedRoot; + project.Name = MetaCoreReadProjectDisplayName(*resolvedRoot); + project.LastOpened = MetaCoreNowIsoString(); + SaveState(); + StatusMessage_ = "已启动编辑器:" + project.Name; + } + + void RemoveSelectedProjectRecord() { + if (SelectedIndex_ < 0 || SelectedIndex_ >= static_cast(Projects_.size())) { + StatusMessage_ = "请先选择一条项目记录。"; + return; + } + Projects_.erase(Projects_.begin() + SelectedIndex_); + if (Projects_.empty()) { + SelectedIndex_ = -1; + } else if (SelectedIndex_ >= static_cast(Projects_.size())) { + SelectedIndex_ = static_cast(Projects_.size()) - 1; + } + SaveState(); + } + + bool SetPathPickerDirectory(const std::filesystem::path& requestedPath) { + std::filesystem::path directory = requestedPath; + std::error_code errorCode; + + if (directory.filename() == "MetaCore.project.json" || std::filesystem::is_regular_file(directory, errorCode)) { + directory = directory.parent_path(); + } + + errorCode.clear(); + if (!std::filesystem::exists(directory, errorCode) || !std::filesystem::is_directory(directory, errorCode)) { + PathPickerError_ = "目录不存在或无法访问。"; + return false; + } + + errorCode.clear(); + const std::filesystem::path canonicalDirectory = std::filesystem::weakly_canonical(directory, errorCode); + PathPickerCurrentDirectory_ = !errorCode && !canonicalDirectory.empty() + ? canonicalDirectory + : directory.lexically_normal(); + MetaCoreCopyToBuffer(PathPickerPathBuffer_, PathPickerCurrentDirectory_.string()); + PathPickerSelectedPath_.clear(); + PathPickerError_.clear(); + return true; + } + + void OpenPathPicker(MetaCoreLauncherPathPickerTarget target) { + PathPickerTarget_ = target; + PathPickerSelectedPath_.clear(); + PathPickerError_.clear(); + + std::filesystem::path initialPath = target == MetaCoreLauncherPathPickerTarget::NewProjectParent + ? std::filesystem::path(NewProjectParentBuffer_.data()) + : std::filesystem::path(AddExistingPathBuffer_.data()); + if (initialPath.empty()) { + initialPath = std::filesystem::current_path(); + } + + if (!SetPathPickerDirectory(initialPath)) { + PathPickerError_.clear(); + if (!SetPathPickerDirectory(std::filesystem::current_path())) { + (void)SetPathPickerDirectory(MetaCoreGetHomeDirectory()); + } + } + OpenPathPickerPopup_ = true; + } + + void ApplyPickedPath(const std::filesystem::path& pickedPath) { + if (PathPickerTarget_ == MetaCoreLauncherPathPickerTarget::NewProjectParent) { + MetaCoreCopyToBuffer(NewProjectParentBuffer_, pickedPath.string()); + } else { + MetaCoreCopyToBuffer(AddExistingPathBuffer_, pickedPath.string()); + } + } + + void DrawFrame() { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + ImGui::SetNextWindowViewport(viewport->ID); + + constexpr ImGuiWindowFlags rootFlags = + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoBringToFrontOnFocus; + + ImGui::Begin("MetaCoreLauncherRoot", nullptr, rootFlags); + DrawSidebar(); + ImGui::SameLine(0.0F, 0.0F); + DrawProjectsPage(); + DrawNewProjectPopup(); + DrawAddExistingPopup(); + ImGui::End(); + } + + void DrawSidebar() { + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.078F, 0.078F, 0.078F, 1.0F)); + ImGui::BeginChild("Sidebar", ImVec2(220.0F, 0.0F), false, ImGuiWindowFlags_NoScrollbar); + ImGui::SetCursorPos(ImVec2(20.0F, 24.0F)); + ImGui::TextUnformatted("MetaCore"); + ImGui::SetCursorPosX(20.0F); + ImGui::TextDisabled("启动器"); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.145F, 0.145F, 0.145F, 1.0F)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.16F, 0.16F, 0.16F, 1.0F)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.18F, 0.18F, 0.18F, 1.0F)); + ImGui::SetCursorPosX(12.0F); + if (ImGui::Button("项目", ImVec2(196.0F, 38.0F))) { + StatusMessage_ = "当前页面:项目"; + } + ImGui::PopStyleColor(3); + ImGui::EndChild(); + ImGui::PopStyleColor(); + } + + void DrawProjectsPage() { + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.098F, 0.098F, 0.098F, 1.0F)); + ImGui::BeginChild("ProjectsPage", ImVec2(0.0F, 0.0F), false); + ImGui::SetCursorPos(ImVec2(28.0F, 24.0F)); + ImGui::BeginGroup(); + + constexpr float pageRightPadding = 28.0F; + const float headerStartX = ImGui::GetCursorPosX(); + const float pageContentWidth = std::max(1.0F, ImGui::GetContentRegionAvail().x - pageRightPadding); + const float headerButtonsWidth = + 112.0F + 112.0F + 76.0F + 112.0F + ImGui::GetStyle().ItemSpacing.x * 3.0F; + ImGui::TextUnformatted("项目"); + ImGui::SameLine(headerStartX + std::max(0.0F, pageContentWidth - headerButtonsWidth)); + if (ImGui::Button("新建项目", ImVec2(112.0F, 36.0F))) { + MetaCoreCopyToBuffer(NewProjectNameBuffer_, "新项目"); + if (NewProjectParentBuffer_[0] == '\0') { + MetaCoreCopyToBuffer(NewProjectParentBuffer_, std::filesystem::current_path().string()); + } + OpenNewProjectPopup_ = true; + } + ImGui::SameLine(); + if (ImGui::Button("添加已有", ImVec2(112.0F, 36.0F))) { + MetaCoreCopyToBuffer(AddExistingPathBuffer_, std::filesystem::current_path().string()); + OpenAddExistingPopup_ = true; + } + ImGui::SameLine(); + if (ImGui::Button("启动", ImVec2(76.0F, 36.0F))) { + LaunchSelectedProject(); + } + ImGui::SameLine(); + if (ImGui::Button("移除记录", ImVec2(112.0F, 36.0F))) { + RemoveSelectedProjectRecord(); + } + + ImGui::SetCursorPosX(headerStartX); + ImGui::SetNextItemWidth(pageContentWidth); + ImGui::InputTextWithHint("##SearchProjects", "搜索项目...", SearchBuffer_.data(), SearchBuffer_.size()); + + if (!StatusMessage_.empty()) { + ImGui::SetCursorPosX(headerStartX); + ImGui::TextDisabled("%s", StatusMessage_.c_str()); + } + + ImGui::SetCursorPosX(headerStartX); + ImGui::BeginChild("ProjectCards", ImVec2(pageContentWidth, 0.0F), false); + DrawProjectCards(); + ImGui::EndChild(); + + ImGui::EndGroup(); + ImGui::EndChild(); + ImGui::PopStyleColor(); + } + + void DrawProjectCards() { + const std::string searchNeedle = MetaCoreToLower(SearchBuffer_.data()); + int visibleCount = 0; + for (std::size_t index = 0; index < Projects_.size(); ++index) { + const auto& project = Projects_[index]; + const std::string haystack = MetaCoreToLower(project.Name + " " + project.Root.string()); + if (!searchNeedle.empty() && haystack.find(searchNeedle) == std::string::npos) { + continue; + } + + ++visibleCount; + const bool selected = SelectedIndex_ == static_cast(index); + const bool missing = !MetaCore::MetaCoreResolveProjectRootCandidate(project.Root).has_value(); + const ImVec4 cardColor = selected + ? ImVec4(0.20F, 0.20F, 0.20F, 1.0F) + : ImVec4(0.125F, 0.125F, 0.125F, 1.0F); + ImGui::PushID(static_cast(index)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, cardColor); + ImGui::PushStyleColor(ImGuiCol_Border, selected ? ImVec4(1.0F, 1.0F, 1.0F, 0.65F) : ImVec4(0.18F, 0.18F, 0.18F, 1.0F)); + ImGui::BeginChild("ProjectCard", ImVec2(0.0F, 96.0F), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImGui::SetCursorPos(ImVec2(14.0F, 12.0F)); + ImGui::BeginGroup(); + if (missing) { + ImGui::TextDisabled("%s", project.Name.c_str()); + } else { + ImGui::TextUnformatted(project.Name.c_str()); + } + ImGui::TextDisabled("%s", project.Root.string().c_str()); + if (missing) { + ImGui::TextColored(ImVec4(0.92F, 0.34F, 0.34F, 1.0F), "项目缺失"); + } else if (!project.LastOpened.empty()) { + ImGui::TextDisabled("上次打开 %s", project.LastOpened.c_str()); + } + ImGui::EndGroup(); + ImGui::EndChild(); + + if (ImGui::IsItemHovered()) { + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + SelectedIndex_ = static_cast(index); + } + if (!missing && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + SelectedIndex_ = static_cast(index); + LaunchSelectedProject(); + } + } + ImGui::PopStyleColor(2); + ImGui::PopID(); + } + + if (visibleCount == 0) { + ImGui::Dummy(ImVec2(1.0F, 48.0F)); + ImGui::TextDisabled("没有项目"); + } + } + + void DrawNewProjectPopup() { + if (OpenNewProjectPopup_) { + ImGui::OpenPopup("新建项目"); + OpenNewProjectPopup_ = false; + } + ImGui::SetNextWindowSize(ImVec2(620.0F, 300.0F), ImGuiCond_Always); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0F, 24.0F)); + if (ImGui::BeginPopupModal("新建项目", nullptr, ImGuiWindowFlags_NoResize)) { + const float popupInputWidth = ImGui::GetContentRegionAvail().x; + + ImGui::TextUnformatted("项目名称"); + ImGui::SetNextItemWidth(popupInputWidth); + ImGui::InputText("##NewProjectName", NewProjectNameBuffer_.data(), NewProjectNameBuffer_.size()); + ImGui::Spacing(); + ImGui::TextUnformatted("父目录"); + const float browseButtonWidth = 88.0F; + ImGui::SetNextItemWidth(std::max(1.0F, popupInputWidth - browseButtonWidth - ImGui::GetStyle().ItemSpacing.x)); + ImGui::InputText("##NewProjectParent", NewProjectParentBuffer_.data(), NewProjectParentBuffer_.size()); + ImGui::SameLine(); + if (ImGui::Button("选择...", ImVec2(browseButtonWidth, 0.0F))) { + OpenPathPicker(MetaCoreLauncherPathPickerTarget::NewProjectParent); + } + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::SetCursorPosX(std::max(28.0F, ImGui::GetWindowWidth() - 28.0F - 96.0F * 2.0F - ImGui::GetStyle().ItemSpacing.x)); + if (ImGui::Button("创建", ImVec2(96.0F, 32.0F))) { + const std::string name = NewProjectNameBuffer_.data(); + const std::filesystem::path parent = NewProjectParentBuffer_.data(); + if (name.empty() || parent.empty()) { + StatusMessage_ = "项目名称和父目录不能为空。"; + } else { + const std::filesystem::path projectRoot = parent / name; + if (MetaCore::MetaCoreCreateProjectSkeleton(projectRoot, name)) { + if (UpsertProject(projectRoot)) { + StatusMessage_ = "已创建项目:" + name; + ImGui::CloseCurrentPopup(); + } + } else { + StatusMessage_ = "创建项目失败。"; + } + } + } + ImGui::SameLine(); + if (ImGui::Button("取消", ImVec2(96.0F, 32.0F))) { + ImGui::CloseCurrentPopup(); + } + DrawPathPickerPopup(); + ImGui::EndPopup(); + } + ImGui::PopStyleVar(); + } + + void DrawAddExistingPopup() { + if (OpenAddExistingPopup_) { + ImGui::OpenPopup("添加已有项目"); + OpenAddExistingPopup_ = false; + } + ImGui::SetNextWindowSize(ImVec2(620.0F, 240.0F), ImGuiCond_Always); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0F, 24.0F)); + if (ImGui::BeginPopupModal("添加已有项目", nullptr, ImGuiWindowFlags_NoResize)) { + const float popupInputWidth = ImGui::GetContentRegionAvail().x; + + ImGui::TextUnformatted("项目路径"); + const float browseButtonWidth = 88.0F; + ImGui::SetNextItemWidth(std::max(1.0F, popupInputWidth - browseButtonWidth - ImGui::GetStyle().ItemSpacing.x)); + ImGui::InputText("##ExistingProjectPath", AddExistingPathBuffer_.data(), AddExistingPathBuffer_.size()); + ImGui::SameLine(); + if (ImGui::Button("选择...", ImVec2(browseButtonWidth, 0.0F))) { + OpenPathPicker(MetaCoreLauncherPathPickerTarget::ExistingProject); + } + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::SetCursorPosX(std::max(28.0F, ImGui::GetWindowWidth() - 28.0F - 96.0F * 2.0F - ImGui::GetStyle().ItemSpacing.x)); + if (ImGui::Button("添加", ImVec2(96.0F, 32.0F))) { + if (UpsertProject(std::filesystem::path(AddExistingPathBuffer_.data()))) { + StatusMessage_ = "已添加项目:" + Projects_[static_cast(SelectedIndex_)].Name; + ImGui::CloseCurrentPopup(); + } + } + ImGui::SameLine(); + if (ImGui::Button("取消", ImVec2(96.0F, 32.0F))) { + ImGui::CloseCurrentPopup(); + } + DrawPathPickerPopup(); + ImGui::EndPopup(); + } + ImGui::PopStyleVar(); + } + + void DrawPathPickerPopup() { + if (OpenPathPickerPopup_) { + ImGui::OpenPopup("选择路径"); + OpenPathPickerPopup_ = false; + } + + ImGui::SetNextWindowSize(ImVec2(760.0F, 520.0F), ImGuiCond_Always); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0F, 24.0F)); + if (ImGui::BeginPopupModal("选择路径", nullptr, ImGuiWindowFlags_NoResize)) { + const bool pickingNewProjectParent = PathPickerTarget_ == MetaCoreLauncherPathPickerTarget::NewProjectParent; + ImGui::TextUnformatted(pickingNewProjectParent ? "选择新项目父目录" : "选择已有项目"); + ImGui::TextDisabled(pickingNewProjectParent ? "选择项目将要创建到的父目录。" : "选择项目根目录,或选择 MetaCore.project.json。"); + ImGui::Spacing(); + + const float popupWidth = ImGui::GetContentRegionAvail().x; + const float actionButtonWidth = 76.0F; + const float pathInputWidth = std::max( + 1.0F, + popupWidth - actionButtonWidth * 3.0F - ImGui::GetStyle().ItemSpacing.x * 3.0F + ); + ImGui::SetNextItemWidth(pathInputWidth); + ImGui::InputText("##PathPickerPath", PathPickerPathBuffer_.data(), PathPickerPathBuffer_.size()); + ImGui::SameLine(); + if (ImGui::Button("前往", ImVec2(actionButtonWidth, 0.0F))) { + (void)SetPathPickerDirectory(std::filesystem::path(PathPickerPathBuffer_.data())); + } + ImGui::SameLine(); + if (ImGui::Button("上一级", ImVec2(actionButtonWidth, 0.0F))) { + const std::filesystem::path parentPath = PathPickerCurrentDirectory_.parent_path(); + if (!parentPath.empty() && parentPath != PathPickerCurrentDirectory_) { + (void)SetPathPickerDirectory(parentPath); + } + } + ImGui::SameLine(); + if (ImGui::Button("主目录", ImVec2(actionButtonWidth, 0.0F))) { + (void)SetPathPickerDirectory(MetaCoreGetHomeDirectory()); + } + + ImGui::Spacing(); + ImGui::TextDisabled("%s", PathPickerCurrentDirectory_.string().c_str()); + ImGui::Spacing(); + + ImGui::BeginChild("PathPickerEntries", ImVec2(0.0F, 300.0F), true); + const std::vector entries = + MetaCoreCollectPathPickerEntries(PathPickerCurrentDirectory_, !pickingNewProjectParent); + if (entries.empty()) { + ImGui::TextDisabled("没有可选择的目录或项目文件。"); + } + for (const auto& entry : entries) { + const std::string label = entry.IsDirectory + ? std::string("[目录] ") + entry.Label + : std::string("[项目文件] ") + entry.Label; + const bool selected = !PathPickerSelectedPath_.empty() && PathPickerSelectedPath_ == entry.Path; + if (ImGui::Selectable(label.c_str(), selected)) { + PathPickerSelectedPath_ = entry.Path; + } + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + if (entry.IsDirectory) { + (void)SetPathPickerDirectory(entry.Path); + } else { + ApplyPickedPath(entry.Path); + ImGui::CloseCurrentPopup(); + } + } + } + ImGui::EndChild(); + + if (!PathPickerError_.empty()) { + ImGui::TextColored(ImVec4(0.92F, 0.34F, 0.34F, 1.0F), "%s", PathPickerError_.c_str()); + } else if (!pickingNewProjectParent && std::filesystem::exists(PathPickerCurrentDirectory_ / "MetaCore.project.json")) { + ImGui::TextDisabled("当前目录包含 MetaCore.project.json。"); + } else { + ImGui::TextDisabled("双击目录进入,单击目录后可选择该目录。"); + } + + ImGui::Spacing(); + const float footerButtonWidth = 128.0F; + ImGui::SetCursorPosX(std::max( + 28.0F, + ImGui::GetWindowWidth() - 28.0F - footerButtonWidth * 2.0F - ImGui::GetStyle().ItemSpacing.x + )); + if (ImGui::Button( + PathPickerSelectedPath_.empty() ? "选择当前目录" : "选择选中项", + ImVec2(footerButtonWidth, 32.0F) + )) { + ApplyPickedPath(PathPickerSelectedPath_.empty() ? PathPickerCurrentDirectory_ : PathPickerSelectedPath_); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("取消", ImVec2(footerButtonWidth, 32.0F))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + ImGui::PopStyleVar(); + } + + MetaCore::MetaCoreWindow Window_{}; + MetaCore::MetaCoreRenderDevice RenderDevice_{}; + MetaCore::MetaCoreEditorViewportRenderer ViewportRenderer_{}; + std::vector Projects_{}; + std::array SearchBuffer_{}; + std::array NewProjectNameBuffer_{}; + std::array NewProjectParentBuffer_{}; + std::array AddExistingPathBuffer_{}; + std::array PathPickerPathBuffer_{}; + std::string StatusMessage_{}; + std::string PathPickerError_{}; + std::filesystem::path PathPickerCurrentDirectory_{}; + std::filesystem::path PathPickerSelectedPath_{}; + int SelectedIndex_ = -1; + bool OpenNewProjectPopup_ = false; + bool OpenAddExistingPopup_ = false; + bool OpenPathPickerPopup_ = false; + MetaCoreLauncherPathPickerTarget PathPickerTarget_ = MetaCoreLauncherPathPickerTarget::NewProjectParent; + bool LauncherSetFilamentBackend_ = false; + bool Initialized_ = false; +}; + +} // namespace + +int main() { +#if !defined(_WIN32) + std::signal(SIGCHLD, SIG_IGN); +#endif + + MetaCoreLauncherApp app; + if (!app.Initialize()) { + std::cerr << "MetaCoreLauncher initialize failed\n"; + return 1; + } + return app.Run(); +} diff --git a/CMakeLists.txt b/CMakeLists.txt index da3b58d..82e8e6e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -548,6 +548,34 @@ target_link_libraries(MetaCoreEditorApp metacore_stage_ui_blit_material(MetaCoreEditorApp) +set(METACORE_LAUNCHER_SOURCES + Apps/MetaCoreLauncher/main.cpp + Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp +) + +if(NOT WIN32 AND METACORE_IMGUI_USES_PKG_CONFIG AND EXISTS "${METACORE_IMGUI_GLFW_BACKEND_SOURCE}") + list(APPEND METACORE_LAUNCHER_SOURCES "${METACORE_IMGUI_GLFW_BACKEND_SOURCE}") +endif() + +add_executable(MetaCoreLauncher + ${METACORE_LAUNCHER_SOURCES} +) + +target_link_libraries(MetaCoreLauncher + PRIVATE + MetaCoreFoundation + MetaCorePlatform + MetaCoreRender + imgui::imgui +) + +target_compile_options(MetaCoreLauncher PRIVATE ${METACORE_COMMON_WARNINGS}) +if(WIN32) + target_compile_definitions(MetaCoreLauncher PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) +endif() +metacore_stage_ui_blit_material(MetaCoreLauncher) + + add_executable(MetaCorePlayer Apps/MetaCorePlayer/main.cpp diff --git a/SandboxProject/Assets/Materials/Material 1.mcmaterial.json b/SandboxProject/Assets/Materials/Material 1.mcmaterial.json index 3f50a86..b5c3f0c 100644 --- a/SandboxProject/Assets/Materials/Material 1.mcmaterial.json +++ b/SandboxProject/Assets/Materials/Material 1.mcmaterial.json @@ -4,8 +4,8 @@ "AoTexture": "", "AssetGuid": "fc3fd465-4f39-44af-907a-4fc0494c2502", "BaseColor": [ - 1.0, - 0.9686274528503418, + 0.46666666865348816, + 0.9529411792755127, 0.16862745583057404 ], "BaseColorTexture": "", diff --git a/SandboxProject/Assets/Materials/Material 1.mcmaterial.json.mcmeta b/SandboxProject/Assets/Materials/Material 1.mcmaterial.json.mcmeta index 224ee15..4e8638a 100644 --- a/SandboxProject/Assets/Materials/Material 1.mcmaterial.json.mcmeta +++ b/SandboxProject/Assets/Materials/Material 1.mcmaterial.json.mcmeta @@ -3,6 +3,6 @@ "guid": "fc3fd465-4f39-44af-907a-4fc0494c2502", "importer_id": "MaterialImporter", "package_path": "Assets/Materials/Material 1.mcmaterial.json", - "source_hash": 12648564269874715657, + "source_hash": 4176055334418841915, "source_path": "Assets/Materials/Material 1.mcmaterial.json" } \ No newline at end of file diff --git a/SandboxProject/Assets/Materials/Material 7.mcmaterial.json b/SandboxProject/Assets/Materials/Material 7.mcmaterial.json new file mode 100644 index 0000000..f735c8e --- /dev/null +++ b/SandboxProject/Assets/Materials/Material 7.mcmaterial.json @@ -0,0 +1,26 @@ +{ + "AlphaCutoff": 0.5, + "AlphaMode": 0, + "AoTexture": "", + "AssetGuid": "b13c413e-0350-4152-9f1e-41552f9823af", + "BaseColor": [ + 0.27450981736183167, + 0.19607843458652496, + 1.0 + ], + "BaseColorTexture": "", + "DoubleSided": false, + "EmissiveColor": [ + 0.0, + 0.0, + 0.0 + ], + "EmissiveTexture": "", + "Metallic": 0.0, + "MetallicRoughnessTexture": "", + "Name": "Material 7", + "NormalTexture": "", + "Roughness": 1.0, + "ShaderModel": 0, + "StableImportKey": "" +} \ No newline at end of file diff --git a/SandboxProject/Assets/Materials/Material 7.mcmaterial.json.mcmeta b/SandboxProject/Assets/Materials/Material 7.mcmaterial.json.mcmeta new file mode 100644 index 0000000..31f7c4c --- /dev/null +++ b/SandboxProject/Assets/Materials/Material 7.mcmaterial.json.mcmeta @@ -0,0 +1,8 @@ +{ + "asset_type": "material", + "guid": "b13c413e-0350-4152-9f1e-41552f9823af", + "importer_id": "MaterialImporter", + "package_path": "Assets/Materials/Material 7.mcmaterial.json", + "source_hash": 12622570808854545607, + "source_path": "Assets/Materials/Material 7.mcmaterial.json" +} \ No newline at end of file diff --git a/SandboxProject/Assets/Models/Cube.glb.mcasset b/SandboxProject/Assets/Models/Cube.glb.mcasset new file mode 100644 index 0000000000000000000000000000000000000000..83c4a93c1be0ae913269f69e0ebd450cb66dc450 GIT binary patch literal 3966 zcmbtXO>7%Q6y87yfwn;Thf16P2_)L|0Eb9mCC*Qy;>6e~H(HIo&-TLZj=M8k*&r1p zxB(|5TEve;;DSKn#2F;GaVbT;a4AwHZd^E4NM+vc`_}8m35hb&YS#PS%=_N=er7!7 zdEPzlxx+m?YN{Kw59IG&W4&5CEbq%YiIjbn5)+hdw{d53UnmSF| zbW)k#jA+iRN8eumW%?j=erp_Zr z+azm)xNE-4KNbmvh0V4KX<4;hUxY2sd#8f9pYG2Of}(Nr9$=-OkfG!}oeW%8P1?O7 zqBez+hQ?CVx5G6T8dIn|Bxs;gQzPmLh}Rcfg5r*lh1tT#dCs>TsI0}?wt#- zz{l-r*PZaQtB2>W%|3nej|+bu{o(C%&zD&O-T20lLlbFQz6s|3J8-LDF}dp361oUls`hX;L^-@RllGIwIIgkuC9N? zGf7hoN}3g?nLXgUnYQv`xa9Ei04|=HI?%_7cPmdt0i2yh|3EHs$!DZsJ?{;+mV?A> zFP+TgBfNndqnwhG$@6MfxGKZna=7d|uP%Rc?XSkKKTp5=-prZJPdX`_*s1!q$*rR~ zY!BQ;0(%ZjjdHBnQ$8ixApwKD7@=BF#&w!dCy3`)o0~6v_`#vdmET^e;2bQJq&9bT z`xc1Bg!Z+@{}ICd?1WCiSEDc-{Szts)zE3TSN& materialGuids) { - return std::all_of(materialGuids.begin(), materialGuids.end(), [](const MetaCoreAssetGuid& guid) { - return !guid.IsValid(); - }); -} - [[nodiscard]] std::string MetaCoreResolveSceneObjectModelPath( const MetaCoreScene& scene, MetaCoreGameObject object @@ -1162,6 +1178,34 @@ void MetaCoreSyncGeneratedMaterialPreviewToMeshRenderer( return std::nullopt; } +[[nodiscard]] bool MetaCoreMaterialGuidResolves( + const MetaCoreModelAssetDocument* modelDocument, + const MetaCoreIAssetEditingService* assetEditingService, + const MetaCoreAssetGuid& materialGuid +) { + if (!materialGuid.IsValid()) { + return false; + } + + if (modelDocument != nullptr && + MetaCoreFindGeneratedMaterialIndexByGuid(*modelDocument, materialGuid).has_value()) { + return true; + } + + return assetEditingService != nullptr && + assetEditingService->LoadMaterialAsset(materialGuid).has_value(); +} + +[[nodiscard]] bool MetaCoreMaterialGuidListHasResolvableMaterial( + const std::vector& materialGuids, + const MetaCoreModelAssetDocument* modelDocument, + const MetaCoreIAssetEditingService* assetEditingService +) { + return std::any_of(materialGuids.begin(), materialGuids.end(), [&](const MetaCoreAssetGuid& guid) { + return MetaCoreMaterialGuidResolves(modelDocument, assetEditingService, guid); + }); +} + [[nodiscard]] std::optional MetaCoreResolveFirstGeneratedMaterialIndexForMeshRenderer( const MetaCoreModelAssetDocument& document, const MetaCoreMeshRendererComponent& meshRenderer @@ -1185,64 +1229,73 @@ void MetaCoreSyncGeneratedMaterialPreviewToMeshRenderer( return std::nullopt; } -void MetaCoreRepairLegacyModelMaterialBinding( +[[nodiscard]] bool MetaCoreRepairLegacyModelMaterialBinding( MetaCoreMeshRendererComponent& meshRenderer, const MetaCoreModelAssetDocument& document, - const MetaCoreAssetGuid& modelGuid + const MetaCoreAssetGuid& modelGuid, + const MetaCoreIAssetEditingService* assetEditingService = nullptr ) { + bool changed = false; if (meshRenderer.SourceModelPath.empty()) { meshRenderer.SourceModelPath = document.SourcePath.generic_string(); + changed = true; } if (!meshRenderer.SourceModelAssetGuid.IsValid() && modelGuid.IsValid()) { meshRenderer.SourceModelAssetGuid = modelGuid; + changed = true; } - if (!MetaCoreMaterialGuidListIsEmpty(meshRenderer.MaterialAssetGuids)) { - return; + if (MetaCoreMaterialGuidListHasResolvableMaterial( + meshRenderer.MaterialAssetGuids, + &document, + assetEditingService + )) { + return changed; } if (meshRenderer.ModelNodeIndex < 0 || static_cast(meshRenderer.ModelNodeIndex) >= document.Nodes.size()) { - return; + return changed; } const MetaCoreImportedGltfNodeDocument& node = document.Nodes[static_cast(meshRenderer.ModelNodeIndex)]; if (node.MeshIndex < 0 || static_cast(node.MeshIndex) >= document.Meshes.size()) { - return; + return changed; } const auto& materialSlots = document.Meshes[static_cast(node.MeshIndex)].MaterialSlots; - meshRenderer.MaterialAssetGuids.clear(); + std::vector repairedMaterialGuids; + repairedMaterialGuids.reserve(materialSlots.size()); for (const std::int32_t materialIndex : materialSlots) { if (materialIndex >= 0 && static_cast(materialIndex) < document.GeneratedMaterialAssets.size()) { - meshRenderer.MaterialAssetGuids.push_back(document.GeneratedMaterialAssets[static_cast(materialIndex)].AssetGuid); + repairedMaterialGuids.push_back(document.GeneratedMaterialAssets[static_cast(materialIndex)].AssetGuid); } else { - meshRenderer.MaterialAssetGuids.push_back(MetaCoreAssetGuid{}); + repairedMaterialGuids.push_back(MetaCoreAssetGuid{}); } } + + if (meshRenderer.MaterialAssetGuids != repairedMaterialGuids) { + meshRenderer.MaterialAssetGuids = std::move(repairedMaterialGuids); + changed = true; + } + return changed; } void MetaCoreApplyGeneratedMaterialPreviewToScene( MetaCoreEditorContext& editorContext, const MetaCoreModelAssetDocument& document ) { + const auto assetEditingService = editorContext.GetModuleRegistry().ResolveService(); const std::string documentSourcePath = MetaCoreNormalizeModelRelativePath(document.SourcePath.generic_string()); const MetaCoreAssetGuid modelGuid = documentSourcePath.empty() ? MetaCoreAssetGuid{} : MetaCoreAssetRegistry::Get().ResolvePathToGuid(documentSourcePath); bool changed = false; + std::size_t matchedObjects = 0; for (MetaCoreGameObject& sceneObject : editorContext.GetScene().GetGameObjects()) { if (!sceneObject.HasComponent()) { continue; } auto& meshRenderer = sceneObject.GetComponent(); - std::optional materialIndex; - for (const MetaCoreAssetGuid& materialGuid : meshRenderer.MaterialAssetGuids) { - materialIndex = MetaCoreFindGeneratedMaterialIndexByGuid(document, materialGuid); - if (materialIndex.has_value()) { - break; - } - } - const bool sameModelByGuid = modelGuid.IsValid() && meshRenderer.SourceModelAssetGuid.IsValid() && @@ -1251,7 +1304,24 @@ void MetaCoreApplyGeneratedMaterialPreviewToScene( const bool sameModelByPath = !documentSourcePath.empty() && sceneObjectModelPath == documentSourcePath; const bool sameModel = sameModelByGuid || sameModelByPath; - if (!materialIndex.has_value() && sameModel && MetaCoreMaterialGuidListIsEmpty(meshRenderer.MaterialAssetGuids)) { + if (sameModel) { + changed = MetaCoreRepairLegacyModelMaterialBinding( + meshRenderer, + document, + modelGuid, + assetEditingService.get() + ) || changed; + } + + std::optional materialIndex; + for (const MetaCoreAssetGuid& materialGuid : meshRenderer.MaterialAssetGuids) { + materialIndex = MetaCoreFindGeneratedMaterialIndexByGuid(document, materialGuid); + if (materialIndex.has_value()) { + break; + } + } + + if (!materialIndex.has_value() && sameModel) { materialIndex = MetaCoreResolveFirstGeneratedMaterialIndexForMeshRenderer(document, meshRenderer); } @@ -1259,13 +1329,26 @@ void MetaCoreApplyGeneratedMaterialPreviewToScene( continue; } - if (sameModel) { - MetaCoreRepairLegacyModelMaterialBinding(meshRenderer, document, modelGuid); - } MetaCoreSyncGeneratedMaterialPreviewToMeshRenderer(meshRenderer, document, *materialIndex); + ++matchedObjects; + MetaCoreDebugMaterialLog( + "Apply generated material preview objectId=" + std::to_string(sceneObject.GetId()) + + " name=\"" + sceneObject.GetName() + "\"" + + " modelGuid=" + modelGuid.ToString() + + " materialIndex=" + std::to_string(*materialIndex) + + " baseColor=" + MetaCoreDebugVec3String(meshRenderer.BaseColor) + + " metallic=" + std::to_string(meshRenderer.Metallic) + + " roughness=" + std::to_string(meshRenderer.Roughness) + ); changed = true; } + if (changed) { + MetaCoreDebugMaterialLog( + "Apply generated material preview model=\"" + documentSourcePath + + "\" matchedObjects=" + std::to_string(matchedObjects) + ); + } if (changed) { editorContext.GetScene().IncrementRevision(); } @@ -1328,6 +1411,7 @@ void MetaCoreApplyMaterialPreviewToSceneUsers( const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); const auto assetEditingService = editorContext.GetModuleRegistry().ResolveService(); bool changed = false; + std::size_t matchedObjects = 0; for (MetaCoreGameObject& sceneObject : editorContext.GetScene().GetGameObjects()) { if (!sceneObject.HasComponent()) { continue; @@ -1342,14 +1426,54 @@ void MetaCoreApplyMaterialPreviewToSceneUsers( meshRenderer, materialAsset ); + ++matchedObjects; + MetaCoreDebugMaterialLog( + "Apply material resource preview objectId=" + std::to_string(sceneObject.GetId()) + + " name=\"" + sceneObject.GetName() + "\"" + + " materialGuid=" + materialGuid.ToString() + + " baseColor=" + MetaCoreDebugVec3String(meshRenderer.BaseColor) + + " metallic=" + std::to_string(meshRenderer.Metallic) + + " roughness=" + std::to_string(meshRenderer.Roughness) + ); changed = true; } + MetaCoreDebugMaterialLog( + "Apply material resource preview materialGuid=" + materialGuid.ToString() + + " matchedObjects=" + std::to_string(matchedObjects) + ); if (changed) { editorContext.GetScene().IncrementRevision(); } } +[[nodiscard]] std::unordered_map& +MetaCoreGetInlineMaterialEditDrafts() { + static std::unordered_map drafts; + return drafts; +} + +void MetaCoreApplyMaterialAssetPreviewToScene( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetEditingService& assetEditingService, + const MetaCoreAssetGuid& materialGuid, + const MetaCoreMaterialAssetDocument& materialAsset +) { + const auto resolvedMaterial = assetEditingService.ResolveGeneratedAsset(materialGuid); + if (resolvedMaterial.has_value() && resolvedMaterial->GeneratedKind == "material") { + auto modelDocument = assetEditingService.LoadModelAsset(resolvedMaterial->SourceAsset.Guid); + if (modelDocument.has_value() && + resolvedMaterial->GeneratedIndex < modelDocument->GeneratedMaterialAssets.size()) { + modelDocument->GeneratedMaterialAssets[resolvedMaterial->GeneratedIndex] = materialAsset; + modelDocument->GeneratedMaterialAssets[resolvedMaterial->GeneratedIndex].AssetGuid = materialGuid; + MetaCoreApplyGeneratedMaterialPreviewToScene(editorContext, *modelDocument); + return; + } + } + + MetaCoreApplyMaterialPreviewToSceneUsers(editorContext, materialGuid, materialAsset); +} + [[nodiscard]] bool MetaCoreDrawGeneratedAssetCombo( const char* label, const std::vector& choices, @@ -1394,8 +1518,19 @@ void MetaCoreForEachSelectedGameObject(MetaCoreEditorContext& editorContext, TAc auto materialAsset = assetEditingService->LoadMaterialAsset(materialGuid); if (!materialAsset.has_value()) { + MetaCoreDebugMaterialLog( + "Apply material failed: materialGuid=" + materialGuid.ToString() + " could not be loaded" + ); return false; } + MetaCoreDebugMaterialLog( + "Apply material begin slot=" + std::to_string(materialSlotIndex) + + " materialGuid=" + materialGuid.ToString() + + " name=\"" + materialAsset->Name + "\"" + + " baseColor=" + MetaCoreDebugVec3String(materialAsset->BaseColor) + + " metallic=" + std::to_string(materialAsset->Metallic) + + " roughness=" + std::to_string(materialAsset->Roughness) + ); const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); const auto resolvedMaterial = assetEditingService->ResolveGeneratedAsset(materialGuid); @@ -1431,10 +1566,24 @@ void MetaCoreForEachSelectedGameObject(MetaCoreEditorContext& editorContext, TAc *materialAsset ); } + const auto& meshRenderer = selectedObject.GetComponent(); + MetaCoreDebugMaterialLog( + "Apply material objectId=" + std::to_string(selectedObject.GetId()) + + " name=\"" + selectedObject.GetName() + "\"" + + " slot=" + std::to_string(materialSlotIndex) + + " materialGuid=" + materialGuid.ToString() + + " meshBaseColor=" + MetaCoreDebugVec3String(meshRenderer.BaseColor) + + " meshMetallic=" + std::to_string(meshRenderer.Metallic) + + " meshRoughness=" + std::to_string(meshRenderer.Roughness) + + " sceneRevisionBeforeIncrement=" + std::to_string(editorContext.GetScene().GetRevision()) + ); changed = true; }); if (changed) { editorContext.GetScene().IncrementRevision(); + MetaCoreDebugMaterialLog( + "Apply material scene revision=" + std::to_string(editorContext.GetScene().GetRevision()) + ); } return changed; } @@ -1574,6 +1723,63 @@ void MetaCoreClearMaterialSlotOnSelectedObjects( return modelDocument->GeneratedMaterialAssets[static_cast(materialIndex)].AssetGuid; } +[[nodiscard]] bool MetaCoreRepairMeshRendererMaterialBindingsFromSourceModel( + MetaCoreEditorContext& editorContext, + MetaCoreMeshRendererComponent& meshRenderer +) { + if (meshRenderer.MeshSource != MetaCoreMeshSourceKind::Asset || meshRenderer.ModelNodeIndex < 0) { + return false; + } + + const auto assetEditingService = editorContext.GetModuleRegistry().ResolveService(); + if (assetEditingService == nullptr) { + return false; + } + + MetaCoreAssetGuid modelGuid = meshRenderer.SourceModelAssetGuid; + std::optional modelDocument; + if (modelGuid.IsValid()) { + modelDocument = assetEditingService->LoadModelAsset(modelGuid); + } + + if (!modelDocument.has_value()) { + const auto sourceAsset = MetaCoreFindSourceModelAssetForMeshRenderer(editorContext, meshRenderer); + if (sourceAsset.has_value()) { + modelGuid = sourceAsset->Guid; + modelDocument = assetEditingService->LoadModelAsset(sourceAsset->Guid); + } + } + + if (!modelDocument.has_value()) { + return false; + } + + const bool repaired = MetaCoreRepairLegacyModelMaterialBinding( + meshRenderer, + *modelDocument, + modelGuid, + assetEditingService.get() + ); + if (!repaired) { + return false; + } + + std::optional materialIndex; + for (const MetaCoreAssetGuid& materialGuid : meshRenderer.MaterialAssetGuids) { + materialIndex = MetaCoreFindGeneratedMaterialIndexByGuid(*modelDocument, materialGuid); + if (materialIndex.has_value()) { + break; + } + } + if (!materialIndex.has_value()) { + materialIndex = MetaCoreResolveFirstGeneratedMaterialIndexForMeshRenderer(*modelDocument, meshRenderer); + } + if (materialIndex.has_value()) { + MetaCoreSyncGeneratedMaterialPreviewToMeshRenderer(meshRenderer, *modelDocument, *materialIndex); + } + return true; +} + [[nodiscard]] bool MetaCoreRestoreDefaultMaterialSlotOnSelectedObjects( MetaCoreEditorContext& editorContext, std::size_t materialSlotIndex @@ -2495,6 +2701,10 @@ void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorCon } MetaCoreMeshRendererComponent& meshRenderer = gameObject.GetComponent(); + if (MetaCoreRepairMeshRendererMaterialBindingsFromSourceModel(editorContext, meshRenderer)) { + editorContext.GetScene().IncrementRevision(); + } + const MetaCoreGameObjectData* prefabObject = nullptr; if (const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, gameObject); prefabDocument.has_value() && gameObject.HasComponent()) { @@ -3045,9 +3255,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; @@ -3093,35 +3300,65 @@ void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorCon MetaCoreClearMaterialSlotOnSelectedObjects(editorContext, materialIndex); } } else { - MetaCoreMaterialAssetDocument materialAsset = *materialDocument; - bool modified = false; + auto& materialDrafts = MetaCoreGetInlineMaterialEditDrafts(); + const auto draftIterator = materialDrafts.find(materialGuid); + MetaCoreMaterialAssetDocument materialAsset = + draftIterator != materialDrafts.end() ? draftIterator->second : *materialDocument; + bool previewModified = false; + bool commitModified = false; + + const auto markContinuousEdit = [&](bool changed) { + if (changed) { + previewModified = true; + } + if (ImGui::IsItemDeactivatedAfterEdit()) { + commitModified = true; + } + }; + + const auto markDiscreteEdit = [&](bool changed) { + if (changed) { + previewModified = true; + commitModified = true; + } + }; // 材质基本属性编辑 float baseColorRes[3] = {materialAsset.BaseColor.x, materialAsset.BaseColor.y, materialAsset.BaseColor.z}; - if (ImGui::ColorEdit3(("Base Color##Res" + std::to_string(materialIndex)).c_str(), baseColorRes)) { + const bool baseColorChanged = ImGui::ColorEdit3(("Base Color##Res" + std::to_string(materialIndex)).c_str(), baseColorRes); + if (baseColorChanged) { materialAsset.BaseColor = {baseColorRes[0], baseColorRes[1], baseColorRes[2]}; - modified = true; - } - if (ImGui::SliderFloat(("Metallic##Res" + std::to_string(materialIndex)).c_str(), &materialAsset.Metallic, 0.0F, 1.0F)) { - modified = true; - } - if (ImGui::SliderFloat(("Roughness##Res" + std::to_string(materialIndex)).c_str(), &materialAsset.Roughness, 0.0F, 1.0F)) { - modified = true; - } - if (ImGui::Checkbox(("Double Sided##Res" + std::to_string(materialIndex)).c_str(), &materialAsset.DoubleSided)) { - modified = true; } + markContinuousEdit(baseColorChanged); + + markContinuousEdit(ImGui::SliderFloat( + ("Metallic##Res" + std::to_string(materialIndex)).c_str(), + &materialAsset.Metallic, + 0.0F, + 1.0F + )); + markContinuousEdit(ImGui::SliderFloat( + ("Roughness##Res" + std::to_string(materialIndex)).c_str(), + &materialAsset.Roughness, + 0.0F, + 1.0F + )); + markDiscreteEdit(ImGui::Checkbox(("Double Sided##Res" + std::to_string(materialIndex)).c_str(), &materialAsset.DoubleSided)); const char* alphaModeItems[] = {"Opaque", "Mask", "Blend"}; int alphaModeIndex = static_cast(materialAsset.AlphaMode); if (ImGui::Combo(("Alpha Mode##Res" + std::to_string(materialIndex)).c_str(), &alphaModeIndex, alphaModeItems, IM_ARRAYSIZE(alphaModeItems))) { materialAsset.AlphaMode = static_cast(alphaModeIndex); - modified = true; + previewModified = true; + commitModified = true; } if (materialAsset.AlphaMode == MetaCoreMaterialAlphaMode::Mask) { - if (ImGui::SliderFloat(("Alpha Cutoff##Res" + std::to_string(materialIndex)).c_str(), &materialAsset.AlphaCutoff, 0.0F, 1.0F)) { - modified = true; - } + markContinuousEdit(ImGui::SliderFloat( + ("Alpha Cutoff##Res" + std::to_string(materialIndex)).c_str(), + &materialAsset.AlphaCutoff, + 0.0F, + 1.0F + )); } // 贴图通道配置 @@ -3129,46 +3366,70 @@ void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorCon MetaCoreAssetGuid baseColorTexture = materialAsset.BaseColorTexture; if (MetaCoreDrawGeneratedAssetCombo(("Base Color Texture##Res" + std::to_string(materialIndex)).c_str(), textureChoices, baseColorTexture)) { materialAsset.BaseColorTexture = baseColorTexture; - modified = true; + previewModified = true; + commitModified = true; } MetaCoreAssetGuid metalRoughTexture = materialAsset.MetallicRoughnessTexture; if (MetaCoreDrawGeneratedAssetCombo(("MetalRough Texture##Res" + std::to_string(materialIndex)).c_str(), textureChoices, metalRoughTexture)) { materialAsset.MetallicRoughnessTexture = metalRoughTexture; - modified = true; + previewModified = true; + commitModified = true; } MetaCoreAssetGuid normalTexture = materialAsset.NormalTexture; if (MetaCoreDrawGeneratedAssetCombo(("Normal Texture##Res" + std::to_string(materialIndex)).c_str(), textureChoices, normalTexture)) { materialAsset.NormalTexture = normalTexture; - modified = true; + previewModified = true; + commitModified = true; } float emissiveColor[3] = {materialAsset.EmissiveColor.x, materialAsset.EmissiveColor.y, materialAsset.EmissiveColor.z}; - if (ImGui::ColorEdit3(("Emissive Color##Res" + std::to_string(materialIndex)).c_str(), emissiveColor)) { + const bool emissiveColorChanged = ImGui::ColorEdit3(("Emissive Color##Res" + std::to_string(materialIndex)).c_str(), emissiveColor); + if (emissiveColorChanged) { materialAsset.EmissiveColor = {emissiveColor[0], emissiveColor[1], emissiveColor[2]}; - modified = true; } + markContinuousEdit(emissiveColorChanged); MetaCoreAssetGuid emissiveTexture = materialAsset.EmissiveTexture; if (MetaCoreDrawGeneratedAssetCombo(("Emissive Texture##Res" + std::to_string(materialIndex)).c_str(), textureChoices, emissiveTexture)) { materialAsset.EmissiveTexture = emissiveTexture; - modified = true; + previewModified = true; + commitModified = true; } MetaCoreAssetGuid aoTexture = materialAsset.AoTexture; if (MetaCoreDrawGeneratedAssetCombo(("AO Texture##Res" + std::to_string(materialIndex)).c_str(), textureChoices, aoTexture)) { materialAsset.AoTexture = aoTexture; - modified = true; + previewModified = true; + commitModified = true; } - if (modified) { + if (previewModified) { + materialDrafts[materialGuid] = materialAsset; + MetaCoreApplyMaterialAssetPreviewToScene( + editorContext, + *assetEditingService, + materialGuid, + materialAsset + ); + } + + if (commitModified) { + MetaCoreDebugMaterialLog( + "Save material edit begin slot=" + std::to_string(materialIndex) + + " materialGuid=" + materialGuid.ToString() + + " name=\"" + materialAsset.Name + "\"" + + " baseColor=" + MetaCoreDebugVec3String(materialAsset.BaseColor) + + " metallic=" + std::to_string(materialAsset.Metallic) + + " roughness=" + std::to_string(materialAsset.Roughness) + ); if (assetEditingService->SaveMaterialAsset(materialGuid, materialAsset)) { - if (resolvedMaterial.has_value() && resolvedMaterial->GeneratedKind == "material") { - if (const auto modelDocument = assetEditingService->LoadModelAsset(resolvedMaterial->SourceAsset.Guid); - modelDocument.has_value()) { - MetaCoreApplyGeneratedMaterialPreviewToScene(editorContext, *modelDocument); - } - } else { - MetaCoreApplyMaterialPreviewToSceneUsers(editorContext, materialGuid, materialAsset); - } + MetaCoreDebugMaterialLog( + "Save material edit success materialGuid=" + materialGuid.ToString() + + " sceneRevision=" + std::to_string(editorContext.GetScene().GetRevision()) + ); + materialDrafts.erase(materialGuid); editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Material", "已保存材质资源修改"); } else { + MetaCoreDebugMaterialLog( + "Save material edit failed materialGuid=" + materialGuid.ToString() + ); editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Material", "材质修改保存失败"); } } @@ -4964,37 +5225,16 @@ void MetaCoreBuiltinAssetDatabaseService::LoadProjectDescriptor() { } bool MetaCoreBuiltinAssetDatabaseService::CreateProject(const std::filesystem::path& projectRoot, std::string_view projectName) { - if (projectRoot.empty()) { + if (!MetaCoreCreateProjectSkeleton(projectRoot, projectName)) { return false; } - std::error_code errorCode; - const std::filesystem::path normalizedRoot = std::filesystem::weakly_canonical(projectRoot.parent_path(), errorCode).empty() - ? projectRoot.lexically_normal() - : (std::filesystem::weakly_canonical(projectRoot.parent_path(), errorCode) / projectRoot.filename()).lexically_normal(); - - (void)std::filesystem::create_directories(normalizedRoot / "Assets"); - (void)std::filesystem::create_directories(normalizedRoot / "Scenes"); - (void)std::filesystem::create_directories(normalizedRoot / "Runtime"); - (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()) { - document.Name = std::string(projectName); - } - document.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; - - if (!MetaCoreWriteProjectFile(MetaCoreGetProjectFilePath(normalizedRoot), document)) { + const auto normalizedRoot = MetaCoreResolveProjectRootCandidate(projectRoot); + if (!normalizedRoot.has_value()) { return false; } - return OpenProject(normalizedRoot); + return OpenProject(*normalizedRoot); } bool MetaCoreBuiltinAssetDatabaseService::OpenProject(const std::filesystem::path& projectRoot) { @@ -5698,6 +5938,7 @@ public: [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.AssetEditing"; } void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + ModuleRegistry_ = &moduleRegistry; AssetDatabaseService_ = moduleRegistry.ResolveService(); PackageService_ = moduleRegistry.ResolveService(); ReflectionRegistry_ = moduleRegistry.ResolveService(); @@ -5705,6 +5946,7 @@ public: void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { (void)moduleRegistry; + ModuleRegistry_ = nullptr; AssetDatabaseService_.reset(); PackageService_.reset(); ReflectionRegistry_.reset(); @@ -5742,6 +5984,9 @@ public: ) const override; private: + void RescanHotReloadBaseline() const; + + MetaCoreEditorModuleRegistry* ModuleRegistry_ = nullptr; std::shared_ptr AssetDatabaseService_{}; std::shared_ptr PackageService_{}; std::shared_ptr ReflectionRegistry_{}; @@ -6027,6 +6272,16 @@ std::optional MetaCoreBuiltinAssetEditingService:: return resolved->Document.GeneratedTextureAssets[resolved->GeneratedIndex]; } +void MetaCoreBuiltinAssetEditingService::RescanHotReloadBaseline() const { + if (ModuleRegistry_ == nullptr) { + return; + } + if (const auto hotReloadService = ModuleRegistry_->ResolveService(); + hotReloadService != nullptr) { + hotReloadService->RescanBaseline(); + } +} + bool MetaCoreBuiltinAssetEditingService::SaveModelImportSettings( const MetaCoreAssetGuid& modelGuid, const MetaCoreModelImportSettingsDocument& importSettings @@ -6046,13 +6301,17 @@ bool MetaCoreBuiltinAssetEditingService::SaveModelImportSettings( } modelDocument->ImportSettings = importSettings; - return MetaCoreWriteImportedGltfAssetDocument( + const bool saved = MetaCoreWriteImportedGltfAssetDocument( *AssetDatabaseService_, *PackageService_, *ReflectionRegistry_, *assetRecord, *modelDocument ); + if (saved) { + RescanHotReloadBaseline(); + } + return saved; } bool MetaCoreBuiltinAssetEditingService::SaveMaterialAsset( @@ -6077,6 +6336,7 @@ bool MetaCoreBuiltinAssetEditingService::SaveMaterialAsset( metadata.SourceHash = MetaCoreHashFile(absolutePath).value_or(0); (void)MetaCoreWriteMetaJson(metaPath, metadata); } + RescanHotReloadBaseline(); return true; } } @@ -6103,6 +6363,7 @@ bool MetaCoreBuiltinAssetEditingService::SaveMaterialAsset( ); if (saved) { ModelCache_[resolved->AssetRecord.Guid] = editableDocument; + RescanHotReloadBaseline(); } return saved; } diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index dfa6e1f..49f4e2f 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -2896,6 +2896,17 @@ struct MetaCoreMaterialTextureBindingState { std::string LocateSubId{}; }; +struct MetaCoreMaterialEditorChange { + bool PreviewModified = false; + bool CommitModified = false; +}; + +[[nodiscard]] std::unordered_map& +MetaCoreGetMaterialEditorDrafts() { + static std::unordered_map drafts; + return drafts; +} + [[nodiscard]] std::vector MetaCoreCollectMaterialTextureChoices( MetaCoreIAssetDatabaseService& assetDatabaseService ) { @@ -3128,7 +3139,7 @@ void MetaCoreDrawMaterialPreviewCard(const MetaCoreMaterialAssetDocument& materi return changed; } -[[nodiscard]] bool MetaCoreDrawMaterialVisualEditor( +[[nodiscard]] MetaCoreMaterialEditorChange MetaCoreDrawMaterialVisualEditor( MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreMaterialAssetDocument& materialDocument, @@ -3136,7 +3147,22 @@ void MetaCoreDrawMaterialPreviewCard(const MetaCoreMaterialAssetDocument& materi const std::filesystem::path& displayPath, std::string_view idSuffix ) { - bool changed = false; + MetaCoreMaterialEditorChange change; + const auto markContinuousEdit = [&](bool changed) { + if (changed) { + change.PreviewModified = true; + } + if (ImGui::IsItemDeactivatedAfterEdit()) { + change.CommitModified = true; + } + }; + const auto markDiscreteEdit = [&](bool changed) { + if (changed) { + change.PreviewModified = true; + change.CommitModified = true; + } + }; + const std::string suffix(idSuffix); const auto textureChoices = MetaCoreCollectMaterialTextureChoices(assetDatabaseService); const auto assetEditingService = editorContext.GetModuleRegistry().ResolveService(); @@ -3166,10 +3192,11 @@ void MetaCoreDrawMaterialPreviewCard(const MetaCoreMaterialAssetDocument& materi std::array nameBuffer{}; std::snprintf(nameBuffer.data(), nameBuffer.size(), "%s", materialDocument.Name.c_str()); ImGui::SetNextItemWidth(-1.0F); - if (ImGui::InputText("名称", nameBuffer.data(), nameBuffer.size())) { + const bool nameChanged = ImGui::InputText("名称", nameBuffer.data(), nameBuffer.size()); + if (nameChanged) { materialDocument.Name = nameBuffer.data(); - changed = true; } + markContinuousEdit(nameChanged); const char* shaderItems[] = {"PBR 金属粗糙度"}; int shaderIndex = 0; @@ -3178,46 +3205,41 @@ void MetaCoreDrawMaterialPreviewCard(const MetaCoreMaterialAssetDocument& materi ImGui::EndDisabled(); float baseColor[3] = {materialDocument.BaseColor.r, materialDocument.BaseColor.g, materialDocument.BaseColor.b}; - if (ImGui::ColorEdit3("基础颜色", baseColor)) { + const bool baseColorChanged = ImGui::ColorEdit3("基础颜色", baseColor); + if (baseColorChanged) { materialDocument.BaseColor = {baseColor[0], baseColor[1], baseColor[2]}; - changed = true; - } - if (ImGui::SliderFloat("金属度", &materialDocument.Metallic, 0.0F, 1.0F, "%.3f")) { - changed = true; - } - if (ImGui::SliderFloat("粗糙度", &materialDocument.Roughness, 0.0F, 1.0F, "%.3f")) { - changed = true; } + markContinuousEdit(baseColorChanged); + markContinuousEdit(ImGui::SliderFloat("金属度", &materialDocument.Metallic, 0.0F, 1.0F, "%.3f")); + markContinuousEdit(ImGui::SliderFloat("粗糙度", &materialDocument.Roughness, 0.0F, 1.0F, "%.3f")); float emissiveColor[3] = {materialDocument.EmissiveColor.r, materialDocument.EmissiveColor.g, materialDocument.EmissiveColor.b}; - if (ImGui::ColorEdit3("发光颜色", emissiveColor)) { + const bool emissiveColorChanged = ImGui::ColorEdit3("发光颜色", emissiveColor); + if (emissiveColorChanged) { materialDocument.EmissiveColor = {emissiveColor[0], emissiveColor[1], emissiveColor[2]}; - changed = true; } + markContinuousEdit(emissiveColorChanged); } if (ImGui::CollapsingHeader("贴图", ImGuiTreeNodeFlags_DefaultOpen)) { - changed |= MetaCoreDrawMaterialTextureSlot(editorContext, assetDatabaseService, assetEditingService.get(), "基础颜色", "BaseColorTexture", materialDocument.BaseColorTexture, textureChoices); - changed |= MetaCoreDrawMaterialTextureSlot(editorContext, assetDatabaseService, assetEditingService.get(), "法线", "NormalTexture", materialDocument.NormalTexture, textureChoices); - changed |= MetaCoreDrawMaterialTextureSlot(editorContext, assetDatabaseService, assetEditingService.get(), "金属粗糙度", "MetallicRoughnessTexture", materialDocument.MetallicRoughnessTexture, textureChoices); - changed |= MetaCoreDrawMaterialTextureSlot(editorContext, assetDatabaseService, assetEditingService.get(), "遮蔽 AO", "AoTexture", materialDocument.AoTexture, textureChoices); - changed |= MetaCoreDrawMaterialTextureSlot(editorContext, assetDatabaseService, assetEditingService.get(), "发光", "EmissiveTexture", materialDocument.EmissiveTexture, textureChoices); + markDiscreteEdit(MetaCoreDrawMaterialTextureSlot(editorContext, assetDatabaseService, assetEditingService.get(), "基础颜色", "BaseColorTexture", materialDocument.BaseColorTexture, textureChoices)); + markDiscreteEdit(MetaCoreDrawMaterialTextureSlot(editorContext, assetDatabaseService, assetEditingService.get(), "法线", "NormalTexture", materialDocument.NormalTexture, textureChoices)); + markDiscreteEdit(MetaCoreDrawMaterialTextureSlot(editorContext, assetDatabaseService, assetEditingService.get(), "金属粗糙度", "MetallicRoughnessTexture", materialDocument.MetallicRoughnessTexture, textureChoices)); + markDiscreteEdit(MetaCoreDrawMaterialTextureSlot(editorContext, assetDatabaseService, assetEditingService.get(), "遮蔽 AO", "AoTexture", materialDocument.AoTexture, textureChoices)); + markDiscreteEdit(MetaCoreDrawMaterialTextureSlot(editorContext, assetDatabaseService, assetEditingService.get(), "发光", "EmissiveTexture", materialDocument.EmissiveTexture, textureChoices)); } if (ImGui::CollapsingHeader("透明与渲染选项", ImGuiTreeNodeFlags_DefaultOpen)) { const char* alphaItems[] = {"不透明", "镂空裁剪", "透明混合"}; int alphaIndex = static_cast(materialDocument.AlphaMode); - if (ImGui::Combo("透明模式", &alphaIndex, alphaItems, IM_ARRAYSIZE(alphaItems))) { + const bool alphaModeChanged = ImGui::Combo("透明模式", &alphaIndex, alphaItems, IM_ARRAYSIZE(alphaItems)); + if (alphaModeChanged) { materialDocument.AlphaMode = static_cast(std::clamp(alphaIndex, 0, 2)); - changed = true; } + markDiscreteEdit(alphaModeChanged); if (materialDocument.AlphaMode == MetaCoreMaterialAlphaMode::Mask) { - if (ImGui::SliderFloat("裁剪阈值", &materialDocument.AlphaCutoff, 0.0F, 1.0F, "%.3f")) { - changed = true; - } - } - if (ImGui::Checkbox("双面渲染", &materialDocument.DoubleSided)) { - changed = true; + markContinuousEdit(ImGui::SliderFloat("裁剪阈值", &materialDocument.AlphaCutoff, 0.0F, 1.0F, "%.3f")); } + markDiscreteEdit(ImGui::Checkbox("双面渲染", &materialDocument.DoubleSided)); } if (ImGui::CollapsingHeader("高级信息")) { @@ -3237,7 +3259,24 @@ void MetaCoreDrawMaterialPreviewCard(const MetaCoreMaterialAssetDocument& materi ImGui::PopStyleVar(); ImGui::PopID(); - return changed; + return change; +} + +void MetaCoreApplyGeneratedMaterialDraftPreviewToScene( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetEditingService& assetEditingService, + const MetaCoreResolvedGeneratedAssetRecord& resolvedMaterial, + const MetaCoreMaterialAssetDocument& materialAsset +) { + auto modelDocument = assetEditingService.LoadModelAsset(resolvedMaterial.SourceAsset.Guid); + if (!modelDocument.has_value() || + resolvedMaterial.GeneratedIndex >= modelDocument->GeneratedMaterialAssets.size()) { + return; + } + + modelDocument->GeneratedMaterialAssets[resolvedMaterial.GeneratedIndex] = materialAsset; + modelDocument->GeneratedMaterialAssets[resolvedMaterial.GeneratedIndex].AssetGuid = materialAsset.AssetGuid; + MetaCoreApplyMaterialPreviewToScene(editorContext, *modelDocument); } void DrawGeneratedMaterialDetails( @@ -3259,7 +3298,13 @@ void DrawGeneratedMaterialDetails( return; } - MetaCoreMaterialAssetDocument materialAsset = *loadedMaterial; + auto& materialDrafts = MetaCoreGetMaterialEditorDrafts(); + const auto draftIterator = materialDrafts.find(selectedMaterial.AssetGuid); + MetaCoreMaterialAssetDocument materialAsset = + draftIterator != materialDrafts.end() ? draftIterator->second : *loadedMaterial; + if (!materialAsset.AssetGuid.IsValid()) { + materialAsset.AssetGuid = selectedMaterial.AssetGuid; + } ImGui::Separator(); const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); if (assetDatabaseService == nullptr) { @@ -3276,7 +3321,7 @@ void DrawGeneratedMaterialDetails( DrawReferencedObjectList(editorContext, references, "SelectedMaterialRefs"); } - const bool modified = MetaCoreDrawMaterialVisualEditor( + const MetaCoreMaterialEditorChange materialChange = MetaCoreDrawMaterialVisualEditor( editorContext, *assetDatabaseService, materialAsset, @@ -3285,13 +3330,20 @@ void DrawGeneratedMaterialDetails( "GeneratedMaterial" ); - if (modified) { + if (materialChange.PreviewModified) { + materialDrafts[materialAsset.AssetGuid] = materialAsset; + MetaCoreApplyGeneratedMaterialDraftPreviewToScene( + editorContext, + *assetEditingService, + *resolvedMaterial, + materialAsset + ); + } + + if (materialChange.CommitModified) { if (assetEditingService->SaveMaterialAsset(materialAsset.AssetGuid, materialAsset)) { - if (const auto modelDocument = assetEditingService->LoadModelAsset(resolvedMaterial->SourceAsset.Guid); - modelDocument.has_value()) { - MetaCoreApplyMaterialPreviewToScene(editorContext, *modelDocument); - } - ImGui::TextColored(ImVec4(0.32F, 0.85F, 0.42F, 1.0F), "已自动保存材质资源"); + materialDrafts.erase(materialAsset.AssetGuid); + ImGui::TextColored(ImVec4(0.32F, 0.85F, 0.42F, 1.0F), "已保存材质资源"); } else { ImGui::TextColored(ImVec4(0.92F, 0.32F, 0.28F, 1.0F), "材质资源保存失败"); } @@ -4425,14 +4477,17 @@ void DrawMaterialAssetDetails( ImGui::TextColored(ImVec4(1.0F, 0.4F, 0.4F, 1.0F), "加载材质失败。"); return; } - MetaCoreMaterialAssetDocument materialDoc = *materialDocOpt; + auto& materialDrafts = MetaCoreGetMaterialEditorDrafts(); + const auto draftIterator = materialDrafts.find(selectedAsset.Guid); + MetaCoreMaterialAssetDocument materialDoc = + draftIterator != materialDrafts.end() ? draftIterator->second : *materialDocOpt; if (!materialDoc.AssetGuid.IsValid()) { materialDoc.AssetGuid = selectedAsset.Guid; } ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0F, 4.0F)); if (ImGui::BeginChild("MaterialInspector", ImVec2(0, 0), true, 0)) { - const bool changed = MetaCoreDrawMaterialVisualEditor( + const MetaCoreMaterialEditorChange materialChange = MetaCoreDrawMaterialVisualEditor( editorContext, assetDatabaseService, materialDoc, @@ -4440,10 +4495,14 @@ void DrawMaterialAssetDetails( assetRecord->RelativePath, "StandaloneMaterial" ); - if (changed) { + if (materialChange.PreviewModified) { + materialDrafts[selectedAsset.Guid] = materialDoc; + MetaCoreApplyStandaloneMaterialPreviewToScene(editorContext, assetDatabaseService, materialDoc); + } + if (materialChange.CommitModified) { if (assetEditingService->SaveMaterialAsset(selectedAsset.Guid, materialDoc)) { - MetaCoreApplyStandaloneMaterialPreviewToScene(editorContext, assetDatabaseService, materialDoc); - ImGui::TextColored(ImVec4(0.32F, 0.85F, 0.42F, 1.0F), "已自动保存材质资源"); + materialDrafts.erase(selectedAsset.Guid); + ImGui::TextColored(ImVec4(0.32F, 0.85F, 0.42F, 1.0F), "已保存材质资源"); } else { ImGui::TextColored(ImVec4(0.92F, 0.32F, 0.28F, 1.0F), "材质资源保存失败"); } diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index 2c9f3aa..b92c182 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 "MetaCoreRender/MetaCoreSceneRenderSync.h" #include "MetaCoreEditorCameraController.h" @@ -96,6 +97,50 @@ constexpr ImVec4 GInfernuxGhost{0.0F, 0.0F, 0.0F, 0.0F}; constexpr ImVec4 GInfernuxGhostHovered{1.0F, 1.0F, 1.0F, 0.06F}; constexpr ImVec4 GInfernuxGhostActive{1.0F, 1.0F, 1.0F, 0.10F}; +struct MetaCoreEditorStartupArguments { + std::optional ProjectRoot{}; + std::optional ScenePath{}; +}; + +[[nodiscard]] MetaCoreEditorStartupArguments MetaCoreParseEditorStartupArguments(int argc, char* argv[]) { + MetaCoreEditorStartupArguments arguments; + for (int index = 1; index < argc; ++index) { + if (argv[index] == nullptr) { + continue; + } + + const std::string value = argv[index]; + if (value == "--project") { + if (index + 1 < argc && argv[index + 1] != nullptr) { + arguments.ProjectRoot = std::filesystem::path(argv[++index]); + } + continue; + } + if (value == "--scene") { + if (index + 1 < argc && argv[index + 1] != nullptr) { + arguments.ScenePath = std::filesystem::path(argv[++index]); + } + continue; + } + + if (!value.empty() && value[0] == '-') { + continue; + } + if (!arguments.ScenePath.has_value()) { + arguments.ScenePath = std::filesystem::path(value); + } + } + return arguments; +} + +void MetaCoreSetProjectRootEnvironment(const std::filesystem::path& projectRoot) { +#if defined(_WIN32) + _putenv_s("METACORE_PROJECT_PATH", projectRoot.string().c_str()); +#else + setenv("METACORE_PROJECT_PATH", projectRoot.string().c_str(), 1); +#endif +} + void MetaCorePushFlatButtonStyle(const ImVec4& base) { ImGui::PushStyleColor(ImGuiCol_Button, base); ImGui::PushStyleColor( @@ -1132,6 +1177,17 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) { #endif } + const MetaCoreEditorStartupArguments startupArguments = MetaCoreParseEditorStartupArguments(argc, argv); + if (startupArguments.ProjectRoot.has_value()) { + const auto resolvedProjectRoot = MetaCoreResolveProjectRootCandidate(*startupArguments.ProjectRoot); + if (!resolvedProjectRoot.has_value()) { + MetaCoreTraceStartup("metacore.app: --project path is not a MetaCore project"); + return false; + } + MetaCoreSetProjectRootEnvironment(*resolvedProjectRoot); + MetaCoreTraceStartup(("metacore.app: --project=" + resolvedProjectRoot->string()).c_str()); + } + MetaCoreTraceStartup("metacore.app: initialize window"); if (!Window_.Initialize(1600, 900, "MetaCore Editor")) { MetaCoreTraceStartup("metacore.app: initialize window failed"); @@ -1188,8 +1244,8 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) { if (const auto scenePersistenceService = ModuleRegistry_.ResolveService(); scenePersistenceService != nullptr) { // 尝试从命令行参数直接加载场景(支持双击打开关联的场景文件) - if (argc > 1 && argv[1] != nullptr) { - const std::filesystem::path inputPath(argv[1]); + if (startupArguments.ScenePath.has_value()) { + const std::filesystem::path inputPath = *startupArguments.ScenePath; const auto assetDatabaseService = ModuleRegistry_.ResolveService(); if (assetDatabaseService != nullptr && assetDatabaseService->HasProject() && std::filesystem::is_regular_file(inputPath)) { const std::filesystem::path projectRoot = assetDatabaseService->GetProjectDescriptor().RootPath; diff --git a/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp b/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp index b32b46e..34aba13 100644 --- a/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp +++ b/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -75,12 +76,105 @@ namespace { return escaped; } +[[nodiscard]] std::filesystem::path MetaCoreNormalizeProjectRootForCreation( + const std::filesystem::path& projectRoot +) { + if (projectRoot.empty()) { + return {}; + } + + std::error_code errorCode; + std::filesystem::path absoluteRoot = projectRoot.is_absolute() + ? projectRoot + : std::filesystem::absolute(projectRoot, errorCode); + if (errorCode) { + absoluteRoot = projectRoot; + } + + const std::filesystem::path parentPath = absoluteRoot.parent_path(); + if (!parentPath.empty() && std::filesystem::exists(parentPath, errorCode)) { + errorCode.clear(); + const std::filesystem::path canonicalParent = std::filesystem::weakly_canonical(parentPath, errorCode); + if (!errorCode && !canonicalParent.empty()) { + return (canonicalParent / absoluteRoot.filename()).lexically_normal(); + } + } + + return absoluteRoot.lexically_normal(); +} + } // namespace std::filesystem::path MetaCoreGetProjectFilePath(const std::filesystem::path& projectRoot) { return projectRoot / "MetaCore.project.json"; } +std::optional MetaCoreResolveProjectRootCandidate( + const std::filesystem::path& candidatePath +) { + if (candidatePath.empty()) { + return std::nullopt; + } + + std::filesystem::path projectRoot = candidatePath; + if (projectRoot.filename() == "MetaCore.project.json") { + projectRoot = projectRoot.parent_path(); + } + + const std::filesystem::path projectFilePath = MetaCoreGetProjectFilePath(projectRoot); + if (!std::filesystem::exists(projectFilePath)) { + return std::nullopt; + } + + std::error_code errorCode; + const std::filesystem::path canonicalRoot = std::filesystem::weakly_canonical(projectRoot, errorCode); + if (!errorCode && !canonicalRoot.empty()) { + return canonicalRoot; + } + + return projectRoot.lexically_normal(); +} + +bool MetaCoreCreateProjectSkeleton( + const std::filesystem::path& projectRoot, + std::string_view projectName +) { + const std::filesystem::path normalizedRoot = MetaCoreNormalizeProjectRootForCreation(projectRoot); + if (normalizedRoot.empty()) { + return false; + } + + std::error_code errorCode; + const std::array directories = { + normalizedRoot / "Assets", + normalizedRoot / "Scenes", + normalizedRoot / "Runtime", + normalizedRoot / "Ui", + normalizedRoot / "Library", + normalizedRoot / "Build", + normalizedRoot / "Assets" / "Models", + normalizedRoot / "Assets" / "Materials", + normalizedRoot / "Assets" / "Textures", + normalizedRoot / "Assets" / "Prefabs" + }; + + for (const auto& directory : directories) { + errorCode.clear(); + std::filesystem::create_directories(directory, errorCode); + if (errorCode) { + return false; + } + } + + MetaCoreProjectFileDocument document; + if (!projectName.empty()) { + document.Name = std::string(projectName); + } + document.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; + + return MetaCoreWriteProjectFile(MetaCoreGetProjectFilePath(normalizedRoot), document); +} + std::optional MetaCoreReadProjectRootFromEnvironment() { #if defined(_MSC_VER) char* projectPathRaw = nullptr; diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreProject.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreProject.h index 8378202..3de1262 100644 --- a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreProject.h +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreProject.h @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace MetaCore { @@ -19,6 +20,15 @@ struct MetaCoreProjectFileDocument { [[nodiscard]] std::filesystem::path MetaCoreGetProjectFilePath(const std::filesystem::path& projectRoot); +[[nodiscard]] std::optional MetaCoreResolveProjectRootCandidate( + const std::filesystem::path& candidatePath +); + +[[nodiscard]] bool MetaCoreCreateProjectSkeleton( + const std::filesystem::path& projectRoot, + std::string_view projectName +); + [[nodiscard]] std::optional MetaCoreReadProjectRootFromEnvironment(); [[nodiscard]] std::optional MetaCoreDiscoverProjectRoot( diff --git a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp index 3625f69..1fcf14f 100644 --- a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp @@ -122,6 +122,45 @@ void MetaCoreFilamentDebugLog(const std::string& message) { std::cerr << "[MetaCoreDebug][Filament] " << message << std::endl; } +[[nodiscard]] bool MetaCoreMaterialDebugLogEnabled() { + const char* value = std::getenv("METACORE_DEBUG_MATERIAL"); + if (value == nullptr) { + return false; + } + return std::strcmp(value, "1") == 0 || + std::strcmp(value, "true") == 0 || + std::strcmp(value, "TRUE") == 0; +} + +void MetaCoreFilamentMaterialDebugLog(const std::string& message) { + if (MetaCoreMaterialDebugLogEnabled()) { + MetaCoreFilamentDebugLog(message); + } +} + +[[nodiscard]] std::string MetaCoreDebugVec3String(const glm::vec3& value) { + std::ostringstream stream; + stream << "(" << value.x << "," << value.y << "," << value.z << ")"; + return stream.str(); +} + +[[nodiscard]] std::string MetaCoreDebugGuidListString(const std::vector& guids) { + if (guids.empty()) { + return "[]"; + } + + std::ostringstream stream; + stream << "["; + for (std::size_t index = 0; index < guids.size(); ++index) { + if (index != 0) { + stream << ","; + } + stream << (guids[index].IsValid() ? guids[index].ToString() : std::string("")); + } + stream << "]"; + return stream.str(); +} + [[nodiscard]] bool MetaCorePreserveGltfBlendAlphaMode() { const char* preserveBlend = std::getenv("METACORE_GLTF_PRESERVE_BLEND_ALPHA"); if (preserveBlend == nullptr) { @@ -908,6 +947,7 @@ public: LastSyncSnapshot_ = snapshot; ObjectWorldMatrices_ = LastSyncSnapshot_.WorldMatrices; + const bool materialDebugLogging = MetaCoreMaterialDebugLogEnabled(); // 构建物体ID到网格快照的快速映射,方便后续查询可见性及材质属性 std::unordered_map renderableMap; @@ -972,6 +1012,40 @@ public: } } } + if (materialDebugLogging && + (!HasLoggedSnapshotDebug_ || + LastDebugSnapshotRevision_ != snapshot.SceneRevision || + LastDebugRenderableCount_ != snapshot.Renderables.size())) { + std::ostringstream summary; + summary + << "Sync snapshot frame=" << snapshot.FrameId + << " revision=" << snapshot.SceneRevision + << " renderables=" << snapshot.Renderables.size() + << " activeHosts=" << activeHostRootIds.size() + << " loadedAssets=" << LoadedAssets_.size() + << " mappings=" << ObjectToFilamentEntity_.size(); + MetaCoreFilamentMaterialDebugLog(summary.str()); + + for (const auto& renderable : snapshot.Renderables) { + std::ostringstream renderableStream; + renderableStream + << "Sync renderable objectId=" << renderable.ObjectId + << " name=\"" << renderable.Name << "\"" + << " hostRootId=" << renderable.HostRootId + << " host=\"" << renderable.HostRootName << "\"" + << " modelNode=" << renderable.ModelNodeIndex + << " materialGuids=" << MetaCoreDebugGuidListString(renderable.MaterialAssetGuids) + << " baseColor=" << MetaCoreDebugVec3String(renderable.BaseColor) + << " metallic=" << renderable.Metallic + << " roughness=" << renderable.Roughness + << " source=\"" << renderable.SourceModelPath << "\""; + MetaCoreFilamentMaterialDebugLog(renderableStream.str()); + } + + HasLoggedSnapshotDebug_ = true; + LastDebugSnapshotRevision_ = snapshot.SceneRevision; + LastDebugRenderableCount_ = snapshot.Renderables.size(); + } // 2. 检查所有已加载的资产,若其 Root ID 不在 activeHostRootIds 中,则说明已被删除,执行清理 for (auto it = LoadedAssets_.begin(); it != LoadedAssets_.end(); ) { @@ -1440,23 +1514,29 @@ public: } } - const auto setFloatParameterIfPresent = [](filament::MaterialInstance& materialInstance, const char* name, float value) { + const auto setFloatParameterIfPresent = [](filament::MaterialInstance& materialInstance, const char* name, float value) -> bool { const filament::Material* material = materialInstance.getMaterial(); if (material != nullptr && material->hasParameter(name)) { materialInstance.setParameter(name, value); + return true; } + return false; }; - const auto setFloat3ParameterIfPresent = [](filament::MaterialInstance& materialInstance, const char* name, filament::math::float3 value) { + const auto setFloat3ParameterIfPresent = [](filament::MaterialInstance& materialInstance, const char* name, filament::math::float3 value) -> bool { const filament::Material* material = materialInstance.getMaterial(); if (material != nullptr && material->hasParameter(name)) { materialInstance.setParameter(name, value); + return true; } + return false; }; - const auto setFloat4ParameterIfPresent = [](filament::MaterialInstance& materialInstance, const char* name, filament::math::float4 value) { + const auto setFloat4ParameterIfPresent = [](filament::MaterialInstance& materialInstance, const char* name, filament::math::float4 value) -> bool { const filament::Material* material = materialInstance.getMaterial(); if (material != nullptr && material->hasParameter(name)) { materialInstance.setParameter(name, value); + return true; } + return false; }; const auto setTextureParameterIfPresent = []( filament::MaterialInstance& materialInstance, @@ -1487,9 +1567,32 @@ public: for (const auto& renderable : snapshot.Renderables) { auto itEntity = ObjectToFilamentEntity_.find(renderable.ObjectId); if (itEntity == ObjectToFilamentEntity_.end()) { + if (materialDebugLogging) { + const std::string signature = "missing|" + std::to_string(snapshot.SceneRevision); + if (LastMaterialSyncDebugByObject_[renderable.ObjectId] != signature) { + LastMaterialSyncDebugByObject_[renderable.ObjectId] = signature; + MetaCoreFilamentMaterialDebugLog( + "Material sync skipped mapping missing objectId=" + std::to_string(renderable.ObjectId) + + " name=\"" + renderable.Name + "\"" + + " hostRootId=" + std::to_string(renderable.HostRootId) + + " materialGuids=" + MetaCoreDebugGuidListString(renderable.MaterialAssetGuids) + ); + } + } continue; } - const auto materialTargetCacheIterator = materialTargetCache.find(itEntity->second.second); + utils::Entity materialRootEntity = itEntity->second.second; + const utils::Entity originalMappedEntity = materialRootEntity; + const bool originalMappedEntityBelongsToAsset = + itEntity->second.first != nullptr && + EntityBelongsToAsset(itEntity->second.first, originalMappedEntity); + if (itEntity->second.first != nullptr && + renderable.ObjectId == renderable.HostRootId && + !originalMappedEntityBelongsToAsset) { + materialRootEntity = itEntity->second.first->getRoot(); + } + + const auto materialTargetCacheIterator = materialTargetCache.find(materialRootEntity); if (materialTargetCacheIterator == materialTargetCache.end()) { std::vector materialTargetEntities; if (itEntity->second.first != nullptr) { @@ -1501,18 +1604,53 @@ public: ).first; } CollectRenderableDescendantsFromChildMap( - itEntity->second.second, + materialRootEntity, childMapIterator->second, materialTargetEntities ); + if (materialTargetEntities.empty() && renderable.ObjectId == renderable.HostRootId) { + CollectAllRenderableAssetEntities(itEntity->second.first, materialTargetEntities); + if (materialDebugLogging && !materialTargetEntities.empty()) { + MetaCoreFilamentMaterialDebugLog( + "Material sync fallback all asset renderables objectId=" + std::to_string(renderable.ObjectId) + + " hostRootId=" + std::to_string(renderable.HostRootId) + + " count=" + std::to_string(materialTargetEntities.size()) + ); + } + } } else { - CollectRenderableDescendants(itEntity->second.first, itEntity->second.second, materialTargetEntities); + CollectRenderableDescendants(itEntity->second.first, materialRootEntity, materialTargetEntities); } - materialTargetCache.emplace(itEntity->second.second, std::move(materialTargetEntities)); + materialTargetCache.emplace(materialRootEntity, std::move(materialTargetEntities)); } - const std::vector& materialTargetEntities = materialTargetCache.find(itEntity->second.second)->second; + const std::vector& materialTargetEntities = materialTargetCache.find(materialRootEntity)->second; if (materialTargetEntities.empty()) { + if (materialDebugLogging) { + std::ostringstream signatureStream; + signatureStream + << "empty|" << snapshot.SceneRevision + << "|" << renderable.ObjectId + << "|" << materialRootEntity.getId() + << "|" << MetaCoreDebugGuidListString(renderable.MaterialAssetGuids) + << "|" << MetaCoreDebugVec3String(renderable.BaseColor) + << "|" << renderable.Metallic + << "|" << renderable.Roughness; + const std::string signature = signatureStream.str(); + if (LastMaterialSyncDebugByObject_[renderable.ObjectId] != signature) { + LastMaterialSyncDebugByObject_[renderable.ObjectId] = signature; + MetaCoreFilamentMaterialDebugLog( + "Material sync target empty objectId=" + std::to_string(renderable.ObjectId) + + " name=\"" + renderable.Name + "\"" + + " hostRootId=" + std::to_string(renderable.HostRootId) + + " mappedEntity=" + std::to_string(originalMappedEntity.getId()) + + " mappedBelongsToAsset=" + (originalMappedEntityBelongsToAsset ? std::string("true") : std::string("false")) + + " materialRootEntity=" + std::to_string(materialRootEntity.getId()) + + " materialGuids=" + MetaCoreDebugGuidListString(renderable.MaterialAssetGuids) + + " baseColor=" + MetaCoreDebugVec3String(renderable.BaseColor) + ); + } + } continue; } @@ -1542,6 +1680,14 @@ public: false ); + std::size_t primitiveCount = 0; + std::size_t materialInstanceCount = 0; + std::size_t baseColorWrites = 0; + std::size_t metallicWrites = 0; + std::size_t roughnessWrites = 0; + std::size_t emissiveWrites = 0; + std::size_t alphaCutoffWrites = 0; + std::size_t textureWrites = 0; for (utils::Entity materialTargetEntity : materialTargetEntities) { const auto instance = rm.getInstance(materialTargetEntity); if (!instance) { @@ -1551,18 +1697,26 @@ public: // 遍历所有 primitive,获取材质实例,并设置材质预览属性。 const size_t primCount = rm.getPrimitiveCount(instance); for (size_t primIndex = 0; primIndex < primCount; ++primIndex) { + ++primitiveCount; filament::MaterialInstance* matInst = rm.getMaterialInstanceAt(instance, primIndex); if (matInst) { + ++materialInstanceCount; filament::math::float4 baseColorVec{ renderable.BaseColor.x, renderable.BaseColor.y, renderable.BaseColor.z, 1.0f }; - setFloat4ParameterIfPresent(*matInst, "baseColorFactor", baseColorVec); - setFloatParameterIfPresent(*matInst, "metallicFactor", renderable.Metallic); - setFloatParameterIfPresent(*matInst, "roughnessFactor", renderable.Roughness); - setFloat3ParameterIfPresent( + if (setFloat4ParameterIfPresent(*matInst, "baseColorFactor", baseColorVec)) { + ++baseColorWrites; + } + if (setFloatParameterIfPresent(*matInst, "metallicFactor", renderable.Metallic)) { + ++metallicWrites; + } + if (setFloatParameterIfPresent(*matInst, "roughnessFactor", renderable.Roughness)) { + ++roughnessWrites; + } + if (setFloat3ParameterIfPresent( *matInst, "emissiveFactor", filament::math::float3{ @@ -1570,23 +1724,91 @@ public: renderable.EmissiveColor.y, renderable.EmissiveColor.z } - ); + )) { + ++emissiveWrites; + } if (renderable.AlphaMode == MetaCoreMeshAlphaMode::Mask || renderable.AlphaMode == MetaCoreMeshAlphaMode::Blend) { - setFloatParameterIfPresent(*matInst, "alphaCutoff", renderable.AlphaCutoff); - } - setTextureParameterIfPresent(*matInst, "baseColorMap", baseColorTexture, materialSampler); - setTextureParameterIfPresent(*matInst, "metallicRoughnessMap", metallicRoughnessTexture, materialSampler); - setTextureParameterIfPresent(*matInst, "normalMap", normalTexture, materialSampler); - setTextureParameterIfPresent(*matInst, "emissiveMap", emissiveTexture, materialSampler); - if (!setTextureParameterIfPresent(*matInst, "occlusionMap", aoTexture, materialSampler)) { - if (!setTextureParameterIfPresent(*matInst, "aoMap", aoTexture, materialSampler)) { - setTextureParameterIfPresent(*matInst, "ambientOcclusionMap", aoTexture, materialSampler); + if (setFloatParameterIfPresent(*matInst, "alphaCutoff", renderable.AlphaCutoff)) { + ++alphaCutoffWrites; } } + if (setTextureParameterIfPresent(*matInst, "baseColorMap", baseColorTexture, materialSampler)) { + ++textureWrites; + } + if (setTextureParameterIfPresent(*matInst, "metallicRoughnessMap", metallicRoughnessTexture, materialSampler)) { + ++textureWrites; + } + if (setTextureParameterIfPresent(*matInst, "normalMap", normalTexture, materialSampler)) { + ++textureWrites; + } + if (setTextureParameterIfPresent(*matInst, "emissiveMap", emissiveTexture, materialSampler)) { + ++textureWrites; + } + if (!setTextureParameterIfPresent(*matInst, "occlusionMap", aoTexture, materialSampler)) { + if (!setTextureParameterIfPresent(*matInst, "aoMap", aoTexture, materialSampler)) { + if (setTextureParameterIfPresent(*matInst, "ambientOcclusionMap", aoTexture, materialSampler)) { + ++textureWrites; + } + } else { + ++textureWrites; + } + } else { + ++textureWrites; + } } } } + + if (materialDebugLogging) { + std::ostringstream signatureStream; + signatureStream + << "sync|" << snapshot.SceneRevision + << "|" << renderable.ObjectId + << "|" << renderable.HostRootId + << "|" << materialRootEntity.getId() + << "|" << MetaCoreDebugGuidListString(renderable.MaterialAssetGuids) + << "|" << MetaCoreDebugVec3String(renderable.BaseColor) + << "|" << renderable.Metallic + << "|" << renderable.Roughness + << "|" << materialTargetEntities.size() + << "|" << primitiveCount + << "|" << materialInstanceCount + << "|" << baseColorWrites + << "|" << metallicWrites + << "|" << roughnessWrites + << "|" << textureWrites; + const std::string signature = signatureStream.str(); + if (LastMaterialSyncDebugByObject_[renderable.ObjectId] != signature) { + LastMaterialSyncDebugByObject_[renderable.ObjectId] = signature; + std::ostringstream stream; + stream + << "Material sync objectId=" << renderable.ObjectId + << " name=\"" << renderable.Name << "\"" + << " hostRootId=" << renderable.HostRootId + << " mappedEntity=" << originalMappedEntity.getId() + << " mappedBelongsToAsset=" << (originalMappedEntityBelongsToAsset ? "true" : "false") + << " materialRootEntity=" << materialRootEntity.getId() + << " targets=" << materialTargetEntities.size() + << " primitives=" << primitiveCount + << " matInstances=" << materialInstanceCount + << " writes={baseColor:" << baseColorWrites + << ", metallic:" << metallicWrites + << ", roughness:" << roughnessWrites + << ", emissive:" << emissiveWrites + << ", alphaCutoff:" << alphaCutoffWrites + << ", textures:" << textureWrites << "}" + << " materialGuids=" << MetaCoreDebugGuidListString(renderable.MaterialAssetGuids) + << " baseColor=" << MetaCoreDebugVec3String(renderable.BaseColor) + << " metallic=" << renderable.Metallic + << " roughness=" << renderable.Roughness + << " textures={baseColor:\"" << renderable.BaseColorTexturePath + << "\", mr:\"" << renderable.MetallicRoughnessTexturePath + << "\", normal:\"" << renderable.NormalTexturePath + << "\"}"; + MetaCoreFilamentMaterialDebugLog(stream.str()); + } + } } } @@ -1898,6 +2120,46 @@ public: } private: + bool EntityBelongsToAsset(filament::gltfio::FilamentAsset* asset, utils::Entity entity) const { + if (asset == nullptr || entity.isNull()) { + return false; + } + if (entity == asset->getRoot()) { + return true; + } + + const utils::Entity* entities = asset->getEntities(); + const size_t entityCount = asset->getEntityCount(); + for (size_t i = 0; i < entityCount; ++i) { + if (entities[i] == entity) { + return true; + } + } + return false; + } + + void CollectAllRenderableAssetEntities( + filament::gltfio::FilamentAsset* asset, + std::vector& outEntities + ) const { + if (asset == nullptr || Engine_ == nullptr) { + return; + } + + auto& rm = Engine_->getRenderableManager(); + const utils::Entity* entities = asset->getEntities(); + const size_t entityCount = asset->getEntityCount(); + for (size_t i = 0; i < entityCount; ++i) { + const utils::Entity entity = entities[i]; + if (entity.isNull() || !rm.getInstance(entity)) { + continue; + } + if (std::find(outEntities.begin(), outEntities.end(), entity) == outEntities.end()) { + outEntities.push_back(entity); + } + } + } + // 判断当前实体是否为过渡节点(即无名且无 Mesh 渲染组件的节点) bool IsTransitionNode(utils::Entity entity, filament::gltfio::FilamentAsset* asset) const { if (!entity) return false; @@ -2242,6 +2504,10 @@ private: MetaCoreSceneRenderSync RenderSync_{}; MetaCoreSceneRenderSyncSnapshot LastSyncSnapshot_{}; MetaCoreSceneView CurrentSceneView_{}; + bool HasLoggedSnapshotDebug_ = false; + std::uint64_t LastDebugSnapshotRevision_ = 0; + std::size_t LastDebugRenderableCount_ = 0; + std::unordered_map LastMaterialSyncDebugByObject_{}; std::unordered_map ObjectWorldMatrices_{}; std::unordered_map> ObjectToFilamentEntity_; std::unordered_map LoadedAssets_; diff --git a/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp b/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp index f1f8d77..ba3277f 100644 --- a/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp @@ -180,6 +180,7 @@ MetaCoreSceneRenderSyncSnapshot MetaCoreSceneRenderSync::BuildSnapshot(const Met hostRoot.GetId(), hostRoot.GetName(), hostRoot.HasComponent(), + meshRenderer.MaterialAssetGuids, meshRenderer.BaseColor, meshRenderer.Metallic, meshRenderer.Roughness, diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h index 4e65f3e..1a60a34 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h @@ -35,6 +35,7 @@ struct MetaCoreRenderSyncRenderable { MetaCoreId HostRootId = 0; std::string HostRootName{}; bool HostRootHasModelRootTag = false; + std::vector MaterialAssetGuids{}; glm::vec3 BaseColor{1.0F, 1.0F, 1.0F}; float Metallic = 0.0F; float Roughness = 1.0F; diff --git a/Source/MetaCoreScene/Private/MetaCoreScene.cpp b/Source/MetaCoreScene/Private/MetaCoreScene.cpp index b3d4697..c68a129 100644 --- a/Source/MetaCoreScene/Private/MetaCoreScene.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreScene.cpp @@ -474,9 +474,17 @@ MetaCoreScene MetaCoreCreateDefaultScene() { mainCamera.GetComponent().RotationEulerDegrees = glm::vec3(-15.0F, 0.0F, 0.0F); MetaCoreGameObject directionalLight = scene.CreateGameObject("Directional Light"); - directionalLight.AddComponent(); + MetaCoreLightComponent& keyLight = directionalLight.AddComponent(); + keyLight.Color = glm::vec3(1.0F, 0.96F, 0.9F); + keyLight.Intensity = 1.25F; directionalLight.GetComponent().RotationEulerDegrees = glm::vec3(-45.0F, 30.0F, 0.0F); + MetaCoreGameObject fillLight = scene.CreateGameObject("Fill Light"); + MetaCoreLightComponent& fillLightComponent = fillLight.AddComponent(); + fillLightComponent.Color = glm::vec3(0.72F, 0.82F, 1.0F); + fillLightComponent.Intensity = 0.18F; + fillLight.GetComponent().RotationEulerDegrees = glm::vec3(-20.0F, -135.0F, 0.0F); + return scene; } diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 6490ae6..7aabd79 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -76,6 +76,16 @@ void MetaCoreExpect(bool condition, const char* message) { } } +[[nodiscard]] std::size_t MetaCoreCountSceneLights(const MetaCore::MetaCoreScene& scene) { + std::size_t lightCount = 0; + for (const MetaCore::MetaCoreGameObject& object : scene.GetGameObjects()) { + if (object.HasComponent()) { + ++lightCount; + } + } + return lightCount; +} + [[nodiscard]] std::filesystem::path MetaCoreTestRepositoryRoot() { return std::filesystem::path(__FILE__).parent_path().parent_path(); } @@ -436,6 +446,54 @@ void MetaCoreTestProjectFileRoundTripAndDiscovery() { std::filesystem::remove_all(tempProjectRoot); } +void MetaCoreTestProjectSkeletonHelpers() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreProjectSkeletonHelpers"; + const std::filesystem::path createdProjectRoot = tempProjectRoot / "CreatedBySkeleton"; + std::filesystem::remove_all(tempProjectRoot); + + MetaCoreExpect( + MetaCore::MetaCoreCreateProjectSkeleton(createdProjectRoot, "CreatedBySkeleton"), + "项目骨架 helper 应能创建项目" + ); + MetaCoreExpect(std::filesystem::exists(createdProjectRoot / "MetaCore.project.json"), "项目骨架应包含项目文件"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets"), "项目骨架应包含 Assets"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Scenes"), "项目骨架应包含 Scenes"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Runtime"), "项目骨架应包含 Runtime"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Ui"), "项目骨架应包含 Ui"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Library"), "项目骨架应包含 Library"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Build"), "项目骨架应包含 Build"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Models"), "项目骨架应包含 Models"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Materials"), "项目骨架应包含 Materials"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Textures"), "项目骨架应包含 Textures"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Prefabs"), "项目骨架应包含 Prefabs"); + + const auto projectDocument = MetaCore::MetaCoreReadProjectFile(createdProjectRoot / "MetaCore.project.json"); + MetaCoreExpect(projectDocument.has_value(), "项目骨架项目文件应能读取"); + MetaCoreExpect(projectDocument->Name == "CreatedBySkeleton", "项目骨架项目名称应正确"); + MetaCoreExpect( + projectDocument->StartupScenePath == std::filesystem::path("Scenes") / "Main.mcscene.json", + "项目骨架默认启动场景路径应正确" + ); + + const auto resolvedFromRoot = MetaCore::MetaCoreResolveProjectRootCandidate(createdProjectRoot); + MetaCoreExpect(resolvedFromRoot.has_value(), "项目根路径应可解析"); + MetaCoreExpect( + std::filesystem::equivalent(*resolvedFromRoot, createdProjectRoot), + "项目根路径解析结果应正确" + ); + + const auto resolvedFromProjectFile = + MetaCore::MetaCoreResolveProjectRootCandidate(createdProjectRoot / "MetaCore.project.json"); + MetaCoreExpect(resolvedFromProjectFile.has_value(), "项目文件路径应可解析"); + MetaCoreExpect( + std::filesystem::equivalent(*resolvedFromProjectFile, createdProjectRoot), + "项目文件路径解析结果应正确" + ); + + std::filesystem::remove_all(tempProjectRoot); +} + void MetaCoreTestAssetDatabaseCreateAndOpenProject() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreCreateOpenProject"; @@ -454,6 +512,10 @@ void MetaCoreTestAssetDatabaseCreateAndOpenProject() { MetaCoreExpect(assetDatabase->CreateProject(createdProjectRoot, "CreatedProject"), "应能创建项目"); MetaCoreExpect(assetDatabase->HasProject(), "创建后应存在当前项目"); MetaCoreExpect(std::filesystem::exists(createdProjectRoot / "MetaCore.project.json"), "创建后应存在项目文件"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Models"), "创建后应存在模型目录"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Materials"), "创建后应存在材质目录"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Textures"), "创建后应存在贴图目录"); + MetaCoreExpect(std::filesystem::is_directory(createdProjectRoot / "Assets" / "Prefabs"), "创建后应存在 Prefabs 目录"); MetaCoreExpect(assetDatabase->GetProjectDescriptor().Name == "CreatedProject", "创建后项目名称应正确"); MetaCoreExpect(assetDatabase->GetProjectDescriptor().RootPath == createdProjectRoot, "创建后项目根路径应正确"); @@ -1547,6 +1609,7 @@ void MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson() { MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + const std::size_t defaultSceneObjectCount = scene.GetGameObjects().size(); MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; @@ -1596,7 +1659,7 @@ void MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson() { MetaCoreExpect(scene.GetGameObjects().empty(), "清空快照后场景应为空"); MetaCoreExpect(scenePersistenceService->LoadScene(editorContext, scenePath), "应能重新加载 JSON 场景"); - MetaCoreExpect(scene.GetGameObjects().size() == 2, "重新加载后对象数量应恢复"); + MetaCoreExpect(scene.GetGameObjects().size() == defaultSceneObjectCount, "重新加载后对象数量应恢复"); MetaCoreExpect(editorContext.GetSelectedObjectId() != 0, "重新加载后应恢复选中对象"); bool foundRenamedLight = false; @@ -2621,6 +2684,7 @@ void MetaCoreTestBootstrapStartupSceneCreation() { MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + const std::size_t defaultSceneObjectCount = scene.GetGameObjects().size(); MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; @@ -2650,7 +2714,7 @@ void MetaCoreTestBootstrapStartupSceneCreation() { MetaCoreExpect(std::filesystem::exists(tempProjectRoot / bootstrapScenePath), "应生成 Main.mcscene.json"); MetaCoreExpect(scenePersistenceService->LoadStartupScene(editorContext), "设置后应能加载 startup scene"); MetaCoreExpect(scenePersistenceService->GetCurrentScenePath() == bootstrapScenePath, "startup scene 路径应正确"); - MetaCoreExpect(scene.GetGameObjects().size() == 2, "bootstrap scene 应保存默认场景内容"); + MetaCoreExpect(scene.GetGameObjects().size() == defaultSceneObjectCount, "bootstrap scene 应保存默认场景内容"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); @@ -4482,7 +4546,7 @@ int main() { std::cout << "[RUN] MetaCoreCreateDefaultScene..." << std::endl; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); std::cout << "[RUN] MetaCoreExpect default objects count..." << std::endl; - MetaCoreExpect(scene.GetGameObjects().size() == 2, "默认场景应包含两个对象"); + MetaCoreExpect(scene.GetGameObjects().size() >= 3, "默认场景应包含相机和基础光照对象"); bool hasCamera = false; bool hasLight = false; @@ -4493,6 +4557,7 @@ int main() { MetaCoreExpect(hasCamera, "默认场景应包含摄像机"); MetaCoreExpect(hasLight, "默认场景应包含光源"); + MetaCoreExpect(MetaCoreCountSceneLights(scene) >= 2, "默认场景应包含主光和补光"); std::cout << "[RUN] MetaCoreDummyPanelProvider test..." << std::endl; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; @@ -4525,6 +4590,8 @@ int main() { MetaCoreTestLegacyBinarySceneStartupLoadCompatibility(); std::cout << "[RUN] MetaCoreTestProjectFileRoundTripAndDiscovery..." << std::endl; MetaCoreTestProjectFileRoundTripAndDiscovery(); + std::cout << "[RUN] MetaCoreTestProjectSkeletonHelpers..." << std::endl; + MetaCoreTestProjectSkeletonHelpers(); std::cout << "[RUN] MetaCoreTestAssetDatabaseCreateAndOpenProject..." << std::endl; MetaCoreTestAssetDatabaseCreateAndOpenProject(); std::cout << "[RUN] MetaCoreTestAssetDatabaseRenamePathUpdatesProjectDescriptor..." << std::endl;