fix: 修复多节点模型在场景树中删除子节点时的视口残留 Bug,并补充集成测试

This commit is contained in:
ayuan9957 2026-05-22 11:02:37 +08:00
parent 77626017a3
commit dab182cb96
11 changed files with 2406 additions and 87 deletions

View File

@ -133,6 +133,18 @@ MetaCore::MetaCoreSceneView MetaCoreBuildPlayerSceneView() {
} // namespace
int main(int argc, char* argv[]) {
std::filesystem::path customProjectRoot{};
std::filesystem::path customScenePath{};
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--project") == 0 && i + 1 < argc) {
customProjectRoot = argv[i + 1];
++i;
} else if (std::strcmp(argv[i], "--scene") == 0 && i + 1 < argc) {
customScenePath = argv[i + 1];
++i;
}
}
if (argc > 0) {
std::filesystem::current_path(std::filesystem::path(argv[0]).parent_path());
}
@ -150,29 +162,39 @@ int main(int argc, char* argv[]) {
}
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
if (!viewportRenderer.Initialize(renderDevice, window)) {
if (!viewportRenderer.Initialize(renderDevice, window, false)) {
std::cerr << "MetaCorePlayer: viewport renderer initialize failed\n";
return 1;
}
const auto projectRoot = MetaCore::MetaCoreDiscoverProjectRoot();
if (!projectRoot.has_value()) {
std::cerr << "MetaCorePlayer: failed to discover project root\n";
return 1;
std::filesystem::path projectRoot;
if (!customProjectRoot.empty()) {
projectRoot = std::filesystem::absolute(customProjectRoot);
} else {
const auto discovered = MetaCore::MetaCoreDiscoverProjectRoot();
if (!discovered.has_value()) {
std::cerr << "MetaCorePlayer: failed to discover project root\n";
return 1;
}
projectRoot = *discovered;
}
const std::filesystem::path projectPath = MetaCore::MetaCoreGetProjectFilePath(*projectRoot);
viewportRenderer.SetProjectRootPath(*projectRoot);
const std::filesystem::path projectPath = MetaCore::MetaCoreGetProjectFilePath(projectRoot);
viewportRenderer.SetProjectRootPath(projectRoot);
MetaCore::MetaCoreTypeRegistry typeRegistry;
MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(typeRegistry);
const auto loadedRuntimeProjectDocument = MetaCore::MetaCoreReadRuntimeProjectDocument(
*projectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg",
projectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg",
typeRegistry
);
const auto runtimeProjectDocument = loadedRuntimeProjectDocument.value_or(MetaCoreBuildDefaultRuntimeProjectDocument());
std::optional<MetaCore::MetaCoreSceneDocument> startupSceneDocument;
if (!runtimeProjectDocument.StartupScenePath.empty()) {
startupSceneDocument = MetaCore::MetaCoreReadScenePackage(*projectRoot / runtimeProjectDocument.StartupScenePath);
if (!customScenePath.empty()) {
const auto absoluteScene = customScenePath.is_absolute() ? customScenePath : (projectRoot / customScenePath);
startupSceneDocument = MetaCore::MetaCoreReadScenePackage(absoluteScene);
}
if (!startupSceneDocument.has_value() && !runtimeProjectDocument.StartupScenePath.empty()) {
startupSceneDocument = MetaCore::MetaCoreReadScenePackage(projectRoot / runtimeProjectDocument.StartupScenePath);
}
if (!startupSceneDocument.has_value()) {
startupSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(projectPath);
@ -190,9 +212,9 @@ int main(int argc, char* argv[]) {
}
MetaCore::MetaCoreRuntimeDataDispatcher runtimeDataDispatcher(scene);
const auto sourcesPath = (*projectRoot / runtimeProjectDocument.DataSourcesPath).lexically_normal();
const auto bindingsPath = (*projectRoot / runtimeProjectDocument.BindingsPath).lexically_normal();
const auto diagnosticsPath = (*projectRoot / runtimeProjectDocument.DiagnosticsPath).lexically_normal();
const auto sourcesPath = (projectRoot / runtimeProjectDocument.DataSourcesPath).lexically_normal();
const auto bindingsPath = (projectRoot / runtimeProjectDocument.BindingsPath).lexically_normal();
const auto diagnosticsPath = (projectRoot / runtimeProjectDocument.DiagnosticsPath).lexically_normal();
const auto loadedSourcesDocument = MetaCore::MetaCoreReadRuntimeDataSourcesDocument(sourcesPath, typeRegistry);
const auto loadedBindingsDocument = MetaCore::MetaCoreReadRuntimeBindingsDocument(bindingsPath, typeRegistry);
const auto sourcesDocument = loadedSourcesDocument.value_or(MetaCoreBuildDefaultRuntimeDataSourcesDocument());
@ -279,6 +301,7 @@ int main(int argc, char* argv[]) {
}
}
viewportRenderer.RenderSceneToViewport(scene, MetaCoreBuildPlayerSceneView(), true);
viewportRenderer.RenderAll();
renderDevice.RenderFrame();
renderDevice.PresentFrame();
window.EndFrame();

Binary file not shown.

View File

@ -1792,6 +1792,14 @@ void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorCon
materialGuid = materialPayload->MaterialAssetGuid;
}
}
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(MetaCoreProjectAssetDragDropPayloadType);
payload != nullptr && payload->DataSize == sizeof(MetaCoreProjectAssetDragDropPayload)) {
const auto* assetPayload = static_cast<const MetaCoreProjectAssetDragDropPayload*>(payload->Data);
if (assetPayload != nullptr && std::strcmp(assetPayload->AssetType, "material") == 0 &&
MetaCoreApplyDraggedMaterialToSelectedObjects(editorContext, materialIndex, assetPayload->AssetGuid)) {
materialGuid = assetPayload->AssetGuid;
}
}
ImGui::EndDragDropTarget();
}
const std::string label = "Material Guid " + std::to_string(materialIndex);
@ -2794,7 +2802,8 @@ void MetaCoreWriteEmbeddedTextureRuntimePackages(
const std::string extension = path.extension().string();
if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga" ||
extension == ".ppm" || extension == ".pnm" || extension == ".pgm") {
extension == ".ppm" || extension == ".pnm" || extension == ".pgm" ||
extension == ".ktx" || extension == ".hdr") {
return "TextureImporter";
}
if (MetaCoreIsGltfModelPath(path)) {
@ -2841,7 +2850,8 @@ void MetaCoreWriteEmbeddedTextureRuntimePackages(
return "ui_document";
}
if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga" ||
extension == ".ppm" || extension == ".pnm" || extension == ".pgm") {
extension == ".ppm" || extension == ".pnm" || extension == ".pgm" ||
extension == ".ktx" || extension == ".hdr") {
return "texture";
}
if (extension == ".fbx" || extension == ".obj" || MetaCoreIsGltfModelPath(path)) {
@ -3194,6 +3204,7 @@ public:
[[nodiscard]] bool OpenProject(const std::filesystem::path& projectRoot) override;
[[nodiscard]] bool RenamePath(const std::filesystem::path& relativePath, const std::filesystem::path& newName) override;
[[nodiscard]] bool MovePath(const std::filesystem::path& relativePath, const std::filesystem::path& targetDirectory) override;
[[nodiscard]] bool DeletePath(const std::filesystem::path& relativePath) override;
[[nodiscard]] const std::vector<MetaCoreAssetRecord>& GetAssetRegistry() const override { return AssetRecords_; }
[[nodiscard]] std::optional<MetaCoreAssetRecord> FindAssetByGuid(const MetaCoreAssetGuid& assetGuid) const override {
@ -3321,6 +3332,10 @@ void MetaCoreBuiltinAssetDatabaseService::LoadProjectDescriptor() {
(void)std::filesystem::create_directories(Project_.UiPath);
(void)std::filesystem::create_directories(Project_.LibraryPath);
(void)std::filesystem::create_directories(Project_.BuildPath);
(void)std::filesystem::create_directories(Project_.AssetsPath / "Models");
(void)std::filesystem::create_directories(Project_.AssetsPath / "Materials");
(void)std::filesystem::create_directories(Project_.AssetsPath / "Textures");
(void)std::filesystem::create_directories(Project_.AssetsPath / "Prefabs");
// 首次打开项目时,自动初始化扫描全局资产 GUID 注册表 (对标 Unity 升级)
MetaCoreAssetRegistry::Get().Clear();
@ -3343,6 +3358,10 @@ bool MetaCoreBuiltinAssetDatabaseService::CreateProject(const std::filesystem::p
(void)std::filesystem::create_directories(normalizedRoot / "Ui");
(void)std::filesystem::create_directories(normalizedRoot / "Library");
(void)std::filesystem::create_directories(normalizedRoot / "Build");
(void)std::filesystem::create_directories(normalizedRoot / "Assets" / "Models");
(void)std::filesystem::create_directories(normalizedRoot / "Assets" / "Materials");
(void)std::filesystem::create_directories(normalizedRoot / "Assets" / "Textures");
(void)std::filesystem::create_directories(normalizedRoot / "Assets" / "Prefabs");
MetaCoreProjectFileDocument document;
if (!projectName.empty()) {
@ -3509,6 +3528,55 @@ bool MetaCoreBuiltinAssetDatabaseService::MovePath(
return Refresh();
}
bool MetaCoreBuiltinAssetDatabaseService::DeletePath(const std::filesystem::path& relativePath) {
if (!HasProject() || MetaCoreIsUnsafeRelativePath(relativePath) || relativePath.empty()) {
return false;
}
const std::filesystem::path normalizedSourcePath = relativePath.lexically_normal();
const std::filesystem::path absoluteSourcePath = Project_.RootPath / normalizedSourcePath;
if (!std::filesystem::exists(absoluteSourcePath)) {
return false;
}
std::error_code errorCode;
std::filesystem::remove_all(absoluteSourcePath, errorCode);
if (errorCode) {
return false;
}
const std::filesystem::path metaPath = Project_.RootPath / MetaCoreBuildMetaPath(normalizedSourcePath);
if (std::filesystem::exists(metaPath)) {
std::filesystem::remove_all(metaPath, errorCode);
}
const std::filesystem::path packagePath = Project_.RootPath / MetaCoreBuildPackagePathFromSource(normalizedSourcePath);
if (std::filesystem::exists(packagePath)) {
std::filesystem::remove_all(packagePath, errorCode);
}
auto isUnderDeleted = [&](const std::filesystem::path& path) {
if (path == normalizedSourcePath) {
return true;
}
const auto pathStr = path.lexically_normal().generic_string();
const auto sourceStr = normalizedSourcePath.generic_string() + "/";
return pathStr.rfind(sourceStr, 0) == 0;
};
if (isUnderDeleted(Project_.StartupScenePath)) {
Project_.StartupScenePath.clear();
}
Project_.ScenePaths.erase(
std::remove_if(Project_.ScenePaths.begin(), Project_.ScenePaths.end(), isUnderDeleted),
Project_.ScenePaths.end()
);
SaveProjectDescriptor();
return Refresh();
}
void MetaCoreBuiltinAssetDatabaseService::SaveProjectDescriptor() const {
if (!HasProject()) {
return;
@ -3558,9 +3626,13 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() {
}
MetaCoreAssetGuid sceneGuid{};
bool hasSceneMeta = false;
std::filesystem::path metaPath = MetaCoreBuildMetaPath(entry.path());
if (!std::filesystem::exists(metaPath)) {
metaPath = entry.path().string() + ".meta";
const auto legacyMetaPath = entry.path().string() + ".meta";
if (std::filesystem::exists(legacyMetaPath)) {
metaPath = legacyMetaPath;
}
}
if (std::filesystem::exists(metaPath)) {
std::ifstream metaFile(metaPath);
@ -3570,6 +3642,7 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() {
metaFile >> json;
if (json.contains("guid") && json["guid"].is_string()) {
sceneGuid = MetaCoreAssetGuid::Parse(json["guid"].get<std::string>()).value_or(MetaCoreAssetGuid{});
hasSceneMeta = true;
}
} catch (...) {}
}
@ -3579,6 +3652,20 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() {
sceneGuid = MetaCoreAssetGuid::Generate();
}
if (!hasSceneMeta) {
metaPath = MetaCoreBuildMetaPath(entry.path());
MetaCoreAssetMetadataDocument sceneMetadata;
sceneMetadata.AssetGuid = sceneGuid;
sceneMetadata.AssetType = "scene";
sceneMetadata.ImporterId = "SceneImporter";
sceneMetadata.SourcePath = entry.path().lexically_relative(Project_.RootPath);
sceneMetadata.PackagePath = sceneMetadata.SourcePath;
sceneMetadata.SourceHash = MetaCoreHashFile(entry.path()).value_or(0);
if (MetaCoreWriteMetaJson(metaPath, sceneMetadata)) {
hasSceneMeta = true;
}
}
const auto relativePath = entry.path().lexically_relative(Project_.RootPath);
const std::uint64_t fileHash = MetaCoreHashFile(entry.path()).value_or(0);
@ -3589,7 +3676,7 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() {
MetaCoreAssetStorageKind::SourceFile,
{},
relativePath,
std::filesystem::exists(metaPath) ? metaPath.lexically_relative(Project_.RootPath) : std::filesystem::path{},
hasSceneMeta ? metaPath.lexically_relative(Project_.RootPath) : std::filesystem::path{},
"SceneImporter",
fileHash
});
@ -3662,6 +3749,17 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() {
}
}
}
if (!hasMetadata) {
metadata.AssetGuid = MetaCoreAssetGuid::Generate();
metadata.AssetType = MetaCoreDetectAssetType(entry.path());
metadata.ImporterId = MetaCoreDetectImporterId(entry.path());
metadata.SourcePath = relativePath;
metadata.PackagePath = MetaCoreIsJsonSourceAssetPath(relativePath) ? relativePath : MetaCoreBuildPackagePathFromSource(relativePath);
metadata.SourceHash = MetaCoreHashFile(entry.path()).value_or(0);
if (MetaCoreWriteMetaJson(metaPath, metadata)) {
hasMetadata = true;
}
}
AssetRecords_.push_back(MetaCoreAssetRecord{
hasMetadata ? metadata.AssetGuid : MetaCoreAssetGuid{},

View File

@ -1,8 +1,10 @@
#include "MetaCoreBuiltinEditorModule.h"
#include <windows.h>
#include "MetaCoreEditor/MetaCoreEditorContext.h"
#include "MetaCoreEditor/MetaCoreEditorAssetTypes.h"
#include "MetaCoreEditor/MetaCoreEditorServices.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreEditorCameraController.h"
#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
@ -28,6 +30,8 @@
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <fstream>
#include <nlohmann/json.hpp>
namespace MetaCore {
@ -38,9 +42,21 @@ namespace MetaCore {
namespace {
[[nodiscard]] std::filesystem::path MetaCoreBuildMetaPath(const std::filesystem::path& sourcePath) {
return std::filesystem::path(sourcePath.string() + ".mcmeta");
}
void MetaCoreLaunchPlayer(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIScenePersistenceService& scenePersistenceService);
void MetaCorePollPlayerProcess(MetaCoreEditorContext& editorContext);
static std::string GeneratedResourceFilter_{};
static std::string GeneratedResourceKindFilter_{};
static std::array<char, 256> AssetFilterBuffer_{};
static std::filesystem::path PendingRenamePath_{};
static std::array<char, 256> RenameBuffer_{};
static std::array<char, 256> NewFolderBuffer_{};
static HANDLE PlayerProcessHandle_ = NULL;
static HANDLE PlayerStdoutRead_ = NULL;
[[nodiscard]] MetaCoreTypeRegistry MetaCoreBuildRuntimePanelTypeRegistry() {
MetaCoreTypeRegistry registry;
@ -104,7 +120,6 @@ struct MetaCoreUiDocumentSummary {
std::size_t ButtonCount = 0;
};
static std::filesystem::path PendingRenamePath_{};
static std::array<char, 128> FolderNameBuffer_{};
static std::array<char, 256> RenamePathBuffer_{};
@ -691,6 +706,14 @@ void MetaCoreHandleGlobalEditorShortcuts(MetaCoreEditorContext& editorContext) {
if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) {
editorContext.RequestRenameActiveObject();
}
if (ImGui::IsKeyPressed(ImGuiKey_F5, false)) {
const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIScenePersistenceService>();
const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
if (scenePersistenceService != nullptr && assetDatabaseService != nullptr && assetDatabaseService->HasProject()) {
MetaCoreLaunchPlayer(editorContext, *assetDatabaseService, *scenePersistenceService);
}
}
}
void MetaCoreApplyHierarchySelection(MetaCoreEditorContext& editorContext, MetaCoreId objectId, bool allowToggle) {
@ -1777,6 +1800,7 @@ public:
std::string GetProviderId() const override { return "MetaCoreDefaultMenuProvider"; }
void DrawMenuBar(MetaCoreEditorContext& editorContext) override {
MetaCorePollPlayerProcess(editorContext);
MetaCoreHandleGlobalEditorShortcuts(editorContext);
const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIScenePersistenceService>();
const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
@ -1804,6 +1828,9 @@ public:
if (ImGui::MenuItem("保存场景", "Ctrl+S", false, scenePersistenceService != nullptr)) {
MetaCoreHandleSaveScene(editorContext);
}
if (ImGui::MenuItem("一键保存并运行", "F5", false, scenePersistenceService != nullptr && assetDatabaseService != nullptr && assetDatabaseService->HasProject())) {
MetaCoreLaunchPlayer(editorContext, *assetDatabaseService, *scenePersistenceService);
}
if (ImGui::MenuItem("刷新 Asset 数据库", nullptr, false, assetDatabaseService != nullptr)) {
MetaCoreHandleReloadAssets(editorContext);
}
@ -3275,7 +3302,7 @@ void DrawModelAssetDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD
}
[[nodiscard]] bool MetaCoreCanDragProjectAsset(const MetaCoreAssetRecord& assetRecord) {
return assetRecord.Type == "model" || assetRecord.Type == "prefab";
return assetRecord.Type == "model" || assetRecord.Type == "prefab" || assetRecord.Type == "material" || assetRecord.Type == "texture";
}
void MetaCoreBeginProjectAssetDragDropSource(const MetaCoreAssetRecord& assetRecord) {
@ -3318,6 +3345,24 @@ void MetaCoreBeginProjectAssetDragDropSource(const MetaCoreAssetRecord& assetRec
editorContext.SelectOnly(*instantiatedObjectId);
return true;
}
if (assetType == "material") {
if (forcedParentId.has_value()) {
const MetaCoreId targetEntityId = *forcedParentId;
auto gameObject = editorContext.GetScene().FindGameObject(targetEntityId);
if (gameObject && gameObject.HasComponent<MetaCoreMeshRendererComponent>()) {
const MetaCoreEditorStateSnapshot beforeSnapshot = editorContext.CaptureStateSnapshot();
auto& meshRenderer = gameObject.GetComponent<MetaCoreMeshRendererComponent>();
if (meshRenderer.MaterialAssetGuids.empty()) {
meshRenderer.MaterialAssetGuids.resize(1);
}
meshRenderer.MaterialAssetGuids[0] = assetPayload->AssetGuid;
const MetaCoreEditorStateSnapshot afterSnapshot = editorContext.CaptureStateSnapshot();
(void)editorContext.CommitStateTransition("绑定材质槽位", beforeSnapshot, afterSnapshot, false);
return true;
}
}
return false;
}
if (assetType == "prefab") {
const auto instantiatedObjectId = MetaCoreInstantiatePrefab(editorContext, assetPayload->AssetGuid, forcedParentId);
if (!instantiatedObjectId.has_value()) {
@ -3331,6 +3376,59 @@ void MetaCoreBeginProjectAssetDragDropSource(const MetaCoreAssetRecord& assetRec
namespace {
void CreateNewMaterial(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService) {
const std::filesystem::path selectedDir = editorContext.GetSelectedProjectDirectory();
const std::filesystem::path absoluteDir = assetDatabaseService.GetProjectDescriptor().RootPath / selectedDir;
if (!std::filesystem::exists(absoluteDir)) {
return;
}
std::filesystem::path finalPath;
int index = 0;
while (true) {
std::string filename = "NewMaterial";
if (index > 0) {
filename += " " + std::to_string(index);
}
filename += ".mcmaterial.json";
finalPath = absoluteDir / filename;
if (!std::filesystem::exists(finalPath)) {
break;
}
index++;
}
MetaCoreMaterialAssetDocument materialDoc;
materialDoc.AssetGuid = MetaCoreAssetGuid::Generate();
materialDoc.Name = finalPath.stem().stem().string();
const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService<MetaCoreIReflectionRegistry>();
if (!reflectionRegistry) {
return;
}
if (MetaCoreSceneSerializer::SaveMaterialToJson(finalPath, materialDoc, reflectionRegistry->GetTypeRegistry())) {
const std::filesystem::path relativePath = finalPath.lexically_relative(assetDatabaseService.GetProjectDescriptor().RootPath);
const std::filesystem::path metaPath = assetDatabaseService.GetProjectDescriptor().RootPath / MetaCoreBuildMetaPath(relativePath);
nlohmann::json json;
json["guid"] = materialDoc.AssetGuid.ToString();
json["asset_type"] = "material";
json["importer_id"] = "MaterialImporter";
json["source_path"] = relativePath.generic_string();
json["package_path"] = relativePath.generic_string();
json["source_hash"] = MetaCoreHashFile(finalPath).value_or(0);
std::ofstream output(metaPath);
if (output.is_open()) {
output << json.dump(4);
output.close();
}
assetDatabaseService.Refresh();
}
}
void DrawPrefabDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIPackageService& packageService, MetaCoreIReflectionRegistry& reflectionRegistry) {
const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset();
const auto assetRecord = assetDatabaseService.FindAssetByGuid(selectedAsset.Guid);
@ -3381,11 +3479,253 @@ void DrawUiDocumentDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD
ImGui::PopStyleVar();
}
void MetaCoreLaunchPlayer(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIScenePersistenceService& scenePersistenceService) {
if (PlayerProcessHandle_ != NULL) {
TerminateProcess(PlayerProcessHandle_, 0);
CloseHandle(PlayerProcessHandle_);
PlayerProcessHandle_ = NULL;
if (PlayerStdoutRead_ != NULL) {
CloseHandle(PlayerStdoutRead_);
PlayerStdoutRead_ = NULL;
}
editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Info, "Editor", "正在重启 MetaCore Player 实例...");
}
(void)scenePersistenceService.SaveCurrentScene(editorContext);
const auto& project = assetDatabaseService.GetProjectDescriptor();
std::filesystem::path projectPath = project.RootPath;
std::filesystem::path scenePath = scenePersistenceService.GetCurrentScenePath();
HANDLE hStdoutWrite = NULL;
SECURITY_ATTRIBUTES saAttr;
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = TRUE;
saAttr.lpSecurityDescriptor = NULL;
if (!CreatePipe(&PlayerStdoutRead_, &hStdoutWrite, &saAttr, 0)) {
editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Error, "Editor", "创建 Player 日志重定向管道失败");
return;
}
SetHandleInformation(PlayerStdoutRead_, HANDLE_FLAG_INHERIT, 0);
char exePath[MAX_PATH];
GetModuleFileNameA(NULL, exePath, MAX_PATH);
std::filesystem::path currentExe(exePath);
std::filesystem::path playerExe = currentExe.parent_path() / "MetaCorePlayer.exe";
std::string cmdLine = "\"" + playerExe.string() + "\" --project \"" + projectPath.string() + "\"";
if (!scenePath.empty()) {
cmdLine += " --scene \"" + scenePath.string() + "\"";
}
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(STARTUPINFOA));
si.cb = sizeof(STARTUPINFOA);
si.hStdOutput = hStdoutWrite;
si.hStdError = hStdoutWrite;
si.dwFlags |= STARTF_USESTDHANDLES;
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
std::vector<char> cmdLineWritable(cmdLine.begin(), cmdLine.end());
cmdLineWritable.push_back('\0');
BOOL success = CreateProcessA(
NULL,
cmdLineWritable.data(),
NULL,
NULL,
TRUE,
CREATE_NO_WINDOW,
NULL,
NULL,
&si,
&pi
);
CloseHandle(hStdoutWrite);
if (!success) {
CloseHandle(PlayerStdoutRead_);
PlayerStdoutRead_ = NULL;
editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Error, "Editor", "启动 MetaCore Player 失败,请检查 MetaCorePlayer.exe 是否在同级目录");
return;
}
PlayerProcessHandle_ = pi.hProcess;
CloseHandle(pi.hThread);
editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Info, "Editor", "MetaCore Player 已启动:" + cmdLine);
}
void MetaCorePollPlayerProcess(MetaCoreEditorContext& editorContext) {
if (PlayerProcessHandle_ == NULL) {
return;
}
DWORD exitCode = 0;
if (GetExitCodeProcess(PlayerProcessHandle_, &exitCode)) {
if (exitCode != STILL_ACTIVE) {
editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Info, "Editor", "MetaCore Player 已退出,退出码:" + std::to_string(exitCode));
CloseHandle(PlayerProcessHandle_);
PlayerProcessHandle_ = NULL;
}
}
if (PlayerStdoutRead_ != NULL) {
DWORD bytesAvailable = 0;
if (PeekNamedPipe(PlayerStdoutRead_, NULL, 0, NULL, &bytesAvailable, NULL) && bytesAvailable > 0) {
std::vector<char> buffer(bytesAvailable + 1, '\0');
DWORD bytesRead = 0;
if (ReadFile(PlayerStdoutRead_, buffer.data(), bytesAvailable, &bytesRead, NULL) && bytesRead > 0) {
std::string logStr(buffer.data(), bytesRead);
std::size_t start = 0;
while (true) {
std::size_t pos = logStr.find('\n', start);
if (pos == std::string::npos) {
std::string line = logStr.substr(start);
if (!line.empty() && line != "\r") {
if (line.back() == '\r') line.pop_back();
editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Info, "Player", line);
}
break;
}
std::string line = logStr.substr(start, pos - start);
if (!line.empty() && line.back() == '\r') line.pop_back();
editorContext.GetLogService().AddEntry(MetaCoreLogLevel::Info, "Player", line);
start = pos + 1;
}
}
}
if (PlayerProcessHandle_ == NULL) {
CloseHandle(PlayerStdoutRead_);
PlayerStdoutRead_ = NULL;
}
}
}
void DrawMaterialAssetDetails(
MetaCoreEditorContext& editorContext,
MetaCoreIAssetDatabaseService& assetDatabaseService,
MetaCoreIReflectionRegistry& reflectionRegistry
) {
const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset();
const auto assetRecord = assetDatabaseService.FindAssetByGuid(selectedAsset.Guid);
if (!assetRecord.has_value()) return;
const std::filesystem::path absolutePath = assetDatabaseService.GetProjectDescriptor().RootPath / assetRecord->RelativePath;
auto materialDocOpt = MetaCoreSceneSerializer::LoadMaterialFromJson(absolutePath, reflectionRegistry.GetTypeRegistry());
if (!materialDocOpt.has_value()) {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "加载材质失败!");
return;
}
MetaCoreMaterialAssetDocument materialDoc = std::move(*materialDocOpt);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4));
if (ImGui::BeginChild("MaterialInspector", ImVec2(0, 0), true, 0)) {
ImGui::TextDisabled("MATERIAL ASSET");
ImGui::Separator();
ImGui::Spacing();
bool changed = false;
float color[3] = { materialDoc.BaseColor.r, materialDoc.BaseColor.g, materialDoc.BaseColor.b };
if (ImGui::ColorEdit3("基础颜色 (Base Color)", color)) {
materialDoc.BaseColor = glm::vec3(color[0], color[1], color[2]);
changed = true;
}
float metallic = materialDoc.Metallic;
if (ImGui::SliderFloat("金属度 (Metallic)", &metallic, 0.0f, 1.0f)) {
materialDoc.Metallic = metallic;
changed = true;
}
float roughness = materialDoc.Roughness;
if (ImGui::SliderFloat("粗糙度 (Roughness)", &roughness, 0.0f, 1.0f)) {
materialDoc.Roughness = roughness;
changed = true;
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Text("贴图绑定 (Textures)");
auto drawTextureCombo = [&](const char* label, MetaCoreAssetGuid& currentGuid) {
std::string previewName = "None";
if (currentGuid.IsValid()) {
const auto record = assetDatabaseService.FindAssetByGuid(currentGuid);
if (record.has_value()) {
previewName = record->RelativePath.filename().string();
} else {
previewName = "Missing (" + currentGuid.ToString() + ")";
}
}
if (ImGui::BeginCombo(label, previewName.c_str())) {
if (ImGui::Selectable("None", !currentGuid.IsValid())) {
if (currentGuid.IsValid()) {
currentGuid = MetaCoreAssetGuid{};
changed = true;
}
}
for (const auto& a : assetDatabaseService.GetAssetRegistry()) {
if (a.Type == "texture") {
const bool selected = (currentGuid == a.Guid);
const std::string name = a.RelativePath.filename().string();
if (ImGui::Selectable(name.c_str(), selected)) {
if (currentGuid != a.Guid) {
currentGuid = a.Guid;
changed = true;
}
}
}
}
ImGui::EndCombo();
}
};
drawTextureCombo("基础颜色贴图 (Base Color Texture)", materialDoc.BaseColorTexture);
drawTextureCombo("法线贴图 (Normal Texture)", materialDoc.NormalTexture);
drawTextureCombo("金属粗糙度贴图 (Metallic Roughness)", materialDoc.MetallicRoughnessTexture);
drawTextureCombo("遮蔽贴图 (Occlusion/Ao Texture)", materialDoc.AoTexture);
if (changed) {
if (MetaCoreSceneSerializer::SaveMaterialToJson(absolutePath, materialDoc, reflectionRegistry.GetTypeRegistry())) {
const std::filesystem::path metaPath = assetDatabaseService.GetProjectDescriptor().RootPath / MetaCoreBuildMetaPath(assetRecord->RelativePath);
const std::uint64_t newHash = MetaCoreHashFile(absolutePath).value_or(0);
std::error_code ec;
if (std::filesystem::exists(metaPath, ec)) {
try {
std::ifstream input(metaPath);
nlohmann::json json;
input >> json;
input.close();
json["source_hash"] = newHash;
std::ofstream output(metaPath);
output << json.dump(4);
output.close();
} catch (...) {}
}
assetDatabaseService.Refresh();
}
}
}
ImGui::EndChild();
ImGui::PopStyleVar();
}
void MetaCoreDrawAssetInspector(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIPackageService& packageService, MetaCoreIReflectionRegistry& reflectionRegistry) {
if (!editorContext.HasSelectedAsset()) return;
const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset();
if (selectedAsset.Type == "model") DrawModelAssetDetails(editorContext, assetDatabaseService);
else if (selectedAsset.Type == "prefab") DrawPrefabDetails(editorContext, assetDatabaseService, packageService, reflectionRegistry);
else if (selectedAsset.Type == "material") DrawMaterialAssetDetails(editorContext, assetDatabaseService, reflectionRegistry);
else if (selectedAsset.Type == "ui_document") DrawUiDocumentDetails(editorContext, assetDatabaseService, packageService, reflectionRegistry);
else {
ImGui::Separator();
@ -3443,6 +3783,7 @@ public:
const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIScenePersistenceService>();
if (!assetDatabaseService || !assetDatabaseService->HasProject()) return;
if (editorContext.GetSelectedProjectDirectory().empty()) editorContext.SetSelectedProjectDirectory("Assets");
bool openRenamePopup = false;
ImGui::TextDisabled("%s | %s", assetDatabaseService->GetProjectDescriptor().Name.c_str(), assetDatabaseService->GetProjectDescriptor().RootPath.string().c_str());
if (ImGui::Button("刷新")) { MetaCoreHandleReloadAssets(editorContext); assetDatabaseService->Refresh(); }
@ -3462,6 +3803,17 @@ public:
if (MatchesAssetFilter(d.filename().string()) && ImGui::Selectable(("[Dir] " + d.filename().string()).c_str(), selectedDirectory == d)) {
editorContext.SetSelectedProjectDirectory(d);
}
if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("重命名...")) {
PendingRenamePath_ = d;
std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", d.filename().string().c_str());
openRenamePopup = true;
}
if (ImGui::MenuItem("删除")) {
assetDatabaseService->DeletePath(d);
}
ImGui::EndPopup();
}
}
const MetaCoreAssetGuid selectedAssetGuid = editorContext.GetSelectedAsset().Guid;
for (const auto& a : assetDatabaseService->GetAssetsUnder(selectedDirectory)) {
@ -3484,6 +3836,17 @@ public:
}
}
MetaCoreBeginProjectAssetDragDropSource(a);
if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("重命名...")) {
PendingRenamePath_ = a.RelativePath;
std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", a.RelativePath.filename().string().c_str());
openRenamePopup = true;
}
if (ImGui::MenuItem("删除")) {
assetDatabaseService->DeletePath(a.RelativePath);
}
ImGui::EndPopup();
}
if (selected && a.Type == "model") {
const auto modelDocument = MetaCoreLoadModelAssetDocument(editorContext, a);
@ -3523,6 +3886,62 @@ public:
}
}
ImGui::Columns(1);
if (openRenamePopup) {
ImGui::OpenPopup("重命名资源");
}
if (ImGui::BeginPopupContextWindow("ProjectBlankContextMenu", ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("刷新")) {
MetaCoreHandleReloadAssets(editorContext);
assetDatabaseService->Refresh();
}
if (ImGui::MenuItem("新建文件夹")) {
std::snprintf(NewFolderBuffer_.data(), NewFolderBuffer_.size(), "NewFolder");
ImGui::OpenPopup("新建文件夹");
}
if (ImGui::MenuItem("新建材质")) {
CreateNewMaterial(editorContext, *assetDatabaseService);
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal("重命名资源", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Text("请输入新名称:");
ImGui::InputText("##NewName", RenameBuffer_.data(), RenameBuffer_.size());
if (ImGui::Button("确定", ImVec2(120, 0))) {
std::string newName(RenameBuffer_.data());
if (!newName.empty()) {
assetDatabaseService->RenamePath(PendingRenamePath_, newName);
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("取消", ImVec2(120, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal("新建文件夹", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Text("请输入文件夹名称:");
ImGui::InputText("##FolderName", NewFolderBuffer_.data(), NewFolderBuffer_.size());
if (ImGui::Button("确定", ImVec2(120, 0))) {
std::string folderName(NewFolderBuffer_.data());
if (!folderName.empty()) {
std::filesystem::path newDirPath = assetDatabaseService->GetProjectDescriptor().RootPath / selectedDirectory / folderName;
std::error_code ec;
std::filesystem::create_directories(newDirPath, ec);
assetDatabaseService->Refresh();
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("取消", ImVec2(120, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
};

View File

@ -140,6 +140,7 @@ public:
[[nodiscard]] virtual bool OpenProject(const std::filesystem::path& projectRoot) = 0;
[[nodiscard]] virtual bool RenamePath(const std::filesystem::path& relativePath, const std::filesystem::path& newName) = 0;
[[nodiscard]] virtual bool MovePath(const std::filesystem::path& relativePath, const std::filesystem::path& targetDirectory) = 0;
[[nodiscard]] virtual bool DeletePath(const std::filesystem::path& relativePath) = 0;
[[nodiscard]] virtual std::vector<std::filesystem::path> GetDirectoriesUnder(const std::filesystem::path& relativeDirectory) const = 0;
[[nodiscard]] virtual std::vector<MetaCoreAssetRecord> GetAssetsUnder(const std::filesystem::path& relativeDirectory) const = 0;
[[nodiscard]] virtual std::optional<MetaCoreAssetRecord> FindAssetByRelativePath(const std::filesystem::path& relativePath) const = 0;

View File

@ -39,7 +39,7 @@ void MetaCoreTrace(const char* message) {
}
} // namespace
bool MetaCoreEditorViewportRenderer::Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window) {
bool MetaCoreEditorViewportRenderer::Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window, bool offscreen) {
MetaCoreTrace("metacore.render: viewport init enter");
Shutdown();
@ -52,7 +52,7 @@ bool MetaCoreEditorViewportRenderer::Initialize(MetaCoreRenderDevice& renderDevi
}
*/
if (!FilamentSceneBridge_.Initialize(window)) {
if (!FilamentSceneBridge_.Initialize(window, offscreen)) {
MetaCoreTrace("metacore.render: viewport filament scene runtime bind failed");
return false;
}

View File

@ -65,7 +65,7 @@ public:
return RenderSync_.BuildSnapshot(scene);
}
bool Initialize(MetaCoreWindow& window) {
bool Initialize(MetaCoreWindow& window, bool offscreen = true) {
std::cout << "FilamentSceneBridge: Initializing..." << std::endl;
// 创建 Filament Engine (不共享上下文,避免闪退)
@ -98,6 +98,7 @@ public:
.castShadows(true)
.build(*Engine_, Light_);
Scene_->addEntity(Light_);
EntitiesInScene_.insert(Light_);
// 创建相机
utils::Entity cameraEntity = utils::EntityManager::get().create();
@ -108,40 +109,45 @@ public:
View_->setScene(Scene_);
View_->setCamera(Camera_);
// 创建 UI 视图
UIView_ = Engine_->createView();
// 创建 ImGuiHelper
ImGuiHelper_ = new MetaCoreImGuiHelper(Engine_, UIView_, "", ImGui::GetCurrentContext());
if (offscreen) {
// 创建 UI 视图
UIView_ = Engine_->createView();
// 创建 ImGuiHelper
ImGuiHelper_ = new MetaCoreImGuiHelper(Engine_, UIView_, "", ImGui::GetCurrentContext());
// 初始化离屏渲染纹理
auto [width, height] = window.GetFramebufferSize();
// 1. 创建 Filament 颜色纹理
FilamentTexture_ = filament::Texture::Builder()
.width(static_cast<uint32_t>(width))
.height(static_cast<uint32_t>(height))
.usage(filament::Texture::Usage::COLOR_ATTACHMENT | filament::Texture::Usage::SAMPLEABLE)
.format(filament::Texture::InternalFormat::RGBA8)
.build(*Engine_);
// 初始化离屏渲染纹理
auto [width, height] = window.GetFramebufferSize();
// 2. 创建深度纹理
DepthTexture_ = filament::Texture::Builder()
.width(static_cast<uint32_t>(width))
.height(static_cast<uint32_t>(height))
.usage(filament::Texture::Usage::DEPTH_ATTACHMENT)
.format(filament::Texture::InternalFormat::DEPTH24)
.build(*Engine_);
// 3. 创建 RenderTarget 并绑定
RenderTarget_ = filament::RenderTarget::Builder()
.texture(filament::RenderTarget::AttachmentPoint::COLOR, FilamentTexture_)
.texture(filament::RenderTarget::AttachmentPoint::DEPTH, DepthTexture_)
.build(*Engine_);
// 4. 设置到 View
View_->setRenderTarget(RenderTarget_);
View_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
// 1. 创建 Filament 颜色纹理
FilamentTexture_ = filament::Texture::Builder()
.width(static_cast<uint32_t>(width))
.height(static_cast<uint32_t>(height))
.usage(filament::Texture::Usage::COLOR_ATTACHMENT | filament::Texture::Usage::SAMPLEABLE)
.format(filament::Texture::InternalFormat::RGBA8)
.build(*Engine_);
// 2. 创建深度纹理
DepthTexture_ = filament::Texture::Builder()
.width(static_cast<uint32_t>(width))
.height(static_cast<uint32_t>(height))
.usage(filament::Texture::Usage::DEPTH_ATTACHMENT)
.format(filament::Texture::InternalFormat::DEPTH24)
.build(*Engine_);
// 3. 创建 RenderTarget 并绑定
RenderTarget_ = filament::RenderTarget::Builder()
.texture(filament::RenderTarget::AttachmentPoint::COLOR, FilamentTexture_)
.texture(filament::RenderTarget::AttachmentPoint::DEPTH, DepthTexture_)
.build(*Engine_);
// 4. 设置到 View
View_->setRenderTarget(RenderTarget_);
View_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
} else {
auto [width, height] = window.GetFramebufferSize();
View_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
}
// 确保开启后处理为了HDR
View_->setPostProcessingEnabled(true);
@ -151,7 +157,7 @@ public:
NameManager_ = new utils::NameComponentManager(utils::EntityManager::get());
AssetLoader_ = filament::gltfio::AssetLoader::create({ Engine_, MaterialProvider_, NameManager_ });
std::cout << "FilamentSceneBridge: Initialized with Offscreen Texture ID: " << GLTextureId_ << std::endl;
std::cout << "FilamentSceneBridge: Initialized with Offscreen=" << (offscreen ? "true" : "false") << std::endl;
return true;
}
@ -160,8 +166,13 @@ public:
std::cout << "FilamentSceneBridge: Shutting down..." << std::endl;
// 销毁所有加载的资产
std::cout << "[DEBUG] LoadedAssets_ size: " << LoadedAssets_.size() << std::endl;
for (auto& [id, asset] : LoadedAssets_) {
AssetLoader_->destroyAsset(asset);
std::cout << "[DEBUG] Destroying asset for ID: " << id << ", asset pointer: " << asset << std::endl;
if (asset) {
AssetLoader_->destroyAsset(asset);
}
std::cout << "[DEBUG] Destroyed asset for ID: " << id << std::endl;
}
LoadedAssets_.clear();
ObjectToFilamentEntity_.clear();
@ -170,13 +181,16 @@ public:
for (auto& [id, entity] : SceneLightEntities_) {
if (Scene_) {
Scene_->remove(entity);
EntitiesInScene_.erase(entity);
}
Engine_->destroy(entity);
utils::EntityManager::get().destroy(entity);
}
SceneLightEntities_.clear();
EntitiesInScene_.clear();
// 销毁 gltfio 资源
std::cout << "[DEBUG] Destroying gltfio resources..." << std::endl;
if (AssetLoader_) {
filament::gltfio::AssetLoader::destroy(&AssetLoader_);
AssetLoader_ = nullptr;
@ -204,10 +218,17 @@ public:
glDeleteTextures(1, &GLTextureId_);
GLTextureId_ = 0;
}
// 销毁 View
Engine_->destroy(View_);
if (UIView_) {
Engine_->destroy(UIView_);
UIView_ = nullptr;
}
if (ImGuiHelper_) {
delete ImGuiHelper_;
ImGuiHelper_ = nullptr;
}
// 销毁相机组件和实体
if (Camera_) {
utils::Entity cameraEntity = Camera_->getEntity();
@ -376,6 +397,75 @@ public:
}
}
// 1. 收集当前 snapshot 中所有存在的模型 Root ID
std::unordered_set<MetaCoreId> activeHostRootIds;
for (const auto& renderable : snapshot.Renderables) {
if (renderable.MeshSource == MetaCoreMeshSourceKind::Asset ||
(renderable.MeshSource == MetaCoreMeshSourceKind::Builtin && renderable.BuiltinMesh == MetaCoreBuiltinMeshType::Cube)) {
if (renderable.HostRootId != 0) {
activeHostRootIds.insert(renderable.HostRootId);
}
}
}
// 2. 检查所有已加载的资产,若其 Root ID 不在 activeHostRootIds 中,则说明已被删除,执行清理
for (auto it = LoadedAssets_.begin(); it != LoadedAssets_.end(); ) {
MetaCoreId hostRootId = it->first;
filament::gltfio::FilamentAsset* asset = it->second;
if (!activeHostRootIds.contains(hostRootId)) {
std::cout << "FilamentSceneBridge: Unloading deleted asset: " << hostRootId << std::endl;
// 从场景中移除该资产关联的所有实体
if (Scene_ && asset) {
const utils::Entity* entities = asset->getEntities();
size_t entityCount = asset->getEntityCount();
for (size_t i = 0; i < entityCount; ++i) {
Scene_->remove(entities[i]);
EntitiesInScene_.erase(entities[i]);
}
}
// 查找并清理对应的 pivotEntity
auto pivotIt = ObjectToFilamentEntity_.find(hostRootId);
if (pivotIt != ObjectToFilamentEntity_.end()) {
utils::Entity pivotEntity = pivotIt->second.second;
bool isPivotNode = (asset == nullptr || pivotEntity != asset->getRoot());
if (Scene_ && pivotEntity) {
Scene_->remove(pivotEntity);
EntitiesInScene_.erase(pivotEntity);
}
if (isPivotNode && pivotEntity) {
utils::EntityManager::get().destroy(pivotEntity);
}
}
// 销毁资产本身
if (asset) {
AssetLoader_->destroyAsset(asset);
}
// 从 ObjectToFilamentEntity_ 和 ObjectWorldMatrices_ 中清除所有指向该 asset 的映射
if (asset != nullptr) {
for (auto entIt = ObjectToFilamentEntity_.begin(); entIt != ObjectToFilamentEntity_.end(); ) {
if (entIt->second.first == asset) {
ObjectWorldMatrices_.erase(entIt->first);
entIt = ObjectToFilamentEntity_.erase(entIt);
} else {
++entIt;
}
}
}
// 确保顶级父 ID 的变换缓存也被清除
ObjectWorldMatrices_.erase(hostRootId);
// 从 LoadedAssets_ 中移除
it = LoadedAssets_.erase(it);
} else {
++it;
}
}
for (const auto& renderable : snapshot.Renderables) {
if (renderable.MeshSource != MetaCoreMeshSourceKind::Asset &&
renderable.MeshSource != MetaCoreMeshSourceKind::Builtin) {
@ -433,6 +523,13 @@ public:
// 添加到场景
Scene_->addEntities(asset->getEntities(), asset->getEntityCount());
{
const utils::Entity* entities = asset->getEntities();
size_t entityCount = asset->getEntityCount();
for (size_t i = 0; i < entityCount; ++i) {
EntitiesInScene_.insert(entities[i]);
}
}
LoadedAssets_[hostRootId] = asset;
@ -543,6 +640,7 @@ public:
ObjectToFilamentEntity_[hostRootId] = { asset, pivotEntity };
Scene_->addEntity(pivotEntity);
EntitiesInScene_.insert(pivotEntity);
// 更新初始位置
glm::mat4 initLocalMat{1.0f};
@ -553,11 +651,108 @@ public:
UpdateTransform(renderable.ObjectId, initLocalMat);
}
// 全量同步所有已映射对象的 Transform
// 1. 构建反向查找映射,方便反查 entity 对应的 objectId
std::unordered_map<utils::Entity, MetaCoreId, utils::Entity::Hasher> entityToObjectId;
for (const auto& [objectId, assetEntity] : ObjectToFilamentEntity_) {
auto itMat = snapshot.LocalMatrices.find(objectId);
if (itMat != snapshot.LocalMatrices.end()) {
UpdateTransform(objectId, itMat->second);
entityToObjectId[assetEntity.second] = objectId;
}
// 2. 控制所有已加载 glTF 资产内部所有实体(包括无直接映射的内部渲染实体)的可见性
auto& tm = Engine_->getTransformManager();
for (const auto& [hostRootId, asset] : LoadedAssets_) {
if (!asset) continue;
const utils::Entity* entities = asset->getEntities();
size_t entityCount = asset->getEntityCount();
for (size_t i = 0; i < entityCount; ++i) {
utils::Entity entity = entities[i];
if (!entity) continue;
bool shouldBeInScene = false;
auto it = entityToObjectId.find(entity);
if (it != entityToObjectId.end()) {
// 若是直接映射节点,其可见性由快照决定
shouldBeInScene = snapshot.WorldMatrices.contains(it->second);
} else {
// 若是没有直接映射关系的内部渲染/辅助实体,上溯其在 Filament 层级树中的父节点链,
// 找到最近的具有映射关系的祖先节点,并继承其可见性状态。
utils::Entity current = entity;
utils::Entity mappedAncestor;
while (current) {
auto instance = tm.getInstance(current);
if (!instance) break;
utils::Entity parent = tm.getParent(instance);
if (!parent) break;
if (entityToObjectId.contains(parent)) {
mappedAncestor = parent;
break;
}
current = parent;
}
if (mappedAncestor) {
shouldBeInScene = snapshot.WorldMatrices.contains(entityToObjectId[mappedAncestor]);
} else {
// 回退保护:如果没有找到,则与该资产对应的顶级根宿主保持状态一致
shouldBeInScene = snapshot.WorldMatrices.contains(hostRootId);
}
}
// 同步可见性状态到 Filament Scene
if (shouldBeInScene) {
if (Scene_ && !EntitiesInScene_.contains(entity)) {
Scene_->addEntity(entity);
EntitiesInScene_.insert(entity);
}
} else {
if (Scene_ && EntitiesInScene_.contains(entity)) {
Scene_->remove(entity);
EntitiesInScene_.erase(entity);
}
}
}
}
// 3. 控制 ObjectToFilamentEntity_ 中所有非资产实体(如 pivotEntity 辅助实体)的可见性
for (const auto& [objectId, assetEntity] : ObjectToFilamentEntity_) {
utils::Entity entity = assetEntity.second;
if (!entity) continue;
// 检查该实体是否为资产内部实体,如果不是(例如 pivotEntity则直接控制可见性
bool isAssetEntity = false;
if (assetEntity.first) {
const utils::Entity* entities = assetEntity.first->getEntities();
size_t entityCount = assetEntity.first->getEntityCount();
for (size_t i = 0; i < entityCount; ++i) {
if (entities[i] == entity) {
isAssetEntity = true;
break;
}
}
}
if (!isAssetEntity) {
bool shouldBeInScene = snapshot.WorldMatrices.contains(objectId);
if (shouldBeInScene) {
if (Scene_ && !EntitiesInScene_.contains(entity)) {
Scene_->addEntity(entity);
EntitiesInScene_.insert(entity);
}
} else {
if (Scene_ && EntitiesInScene_.contains(entity)) {
Scene_->remove(entity);
EntitiesInScene_.erase(entity);
}
}
}
}
// 4. 同步所有存活实体的本地和全局变换矩阵
for (const auto& [objectId, assetEntity] : ObjectToFilamentEntity_) {
if (snapshot.WorldMatrices.contains(objectId)) {
auto itMat = snapshot.LocalMatrices.find(objectId);
if (itMat != snapshot.LocalMatrices.end()) {
UpdateTransform(objectId, itMat->second);
}
}
}
}
@ -582,33 +777,44 @@ public:
filament::Camera::Fov::VERTICAL
);
}
void RenderAll() {
if (!Renderer_ || !SwapChain_ || !View_ || !UIView_ || !ImGuiHelper_) {
if (!Renderer_ || !SwapChain_ || !View_) {
return;
}
if (Renderer_->beginFrame(SwapChain_)) {
// 1. 渲染 3D 离屏视口 (输出到 RenderTarget)
filament::Renderer::ClearOptions options;
options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 深灰色
options.clear = true;
Renderer_->setClearOptions(options);
Renderer_->render(View_);
if (RenderTarget_) {
// 1. 渲染 3D 离屏视口 (输出到 RenderTarget)
filament::Renderer::ClearOptions options;
options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 深灰色
options.clear = true;
Renderer_->setClearOptions(options);
Renderer_->render(View_);
// 2. 准备并渲染 UI 视口 (输出到 SwapChain)
ImGuiIO& io = ImGui::GetIO();
ImGuiHelper_->setDisplaySize(io.DisplaySize.x, io.DisplaySize.y);
UIView_->setViewport({0, 0, static_cast<uint32_t>(io.DisplaySize.x), static_cast<uint32_t>(io.DisplaySize.y)});
options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 灰色
options.clear = true;
Renderer_->setClearOptions(options);
ImGuiHelper_->processImGuiCommands(ImGui::GetDrawData(), io);
Renderer_->render(UIView_);
if (UIView_ && ImGuiHelper_) {
// 2. 准备并渲染 UI 视口 (输出到 SwapChain)
ImGuiIO& io = ImGui::GetIO();
ImGuiHelper_->setDisplaySize(io.DisplaySize.x, io.DisplaySize.y);
UIView_->setViewport({0, 0, static_cast<uint32_t>(io.DisplaySize.x), static_cast<uint32_t>(io.DisplaySize.y)});
options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 灰色
options.clear = true;
Renderer_->setClearOptions(options);
ImGuiHelper_->processImGuiCommands(ImGui::GetDrawData(), io);
Renderer_->render(UIView_);
}
} else {
// 3. 直接上屏渲染
filament::Renderer::ClearOptions options;
options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 深灰色
options.clear = true;
Renderer_->setClearOptions(options);
Renderer_->render(View_);
}
Renderer_->endFrame();
@ -627,6 +833,11 @@ public:
void Resize(int width, int height) {
if (width <= 0 || height <= 0) return;
if (!RenderTarget_) {
View_->setViewport({0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)});
return;
}
// 【关键修复】:如果大小没有改变,不要重新创建纹理!
// 否则会导致上一帧传给 ImGui 的纹理指针在这一帧被提前销毁,引发 Use-after-free 崩溃。
if (FilamentTexture_ &&
@ -728,6 +939,12 @@ public:
return 0.0F;
}
bool VerifyEntityInSceneForTesting(MetaCoreId objectId) const {
auto it = ObjectToFilamentEntity_.find(objectId);
if (it == ObjectToFilamentEntity_.end()) return false;
return EntitiesInScene_.contains(it->second.second);
}
private:
static filament::math::float3 ToFilamentFloat3(const glm::vec3& value) {
return filament::math::float3{ value.x, value.y, value.z };
@ -788,6 +1005,7 @@ private:
.castShadows(true)
.build(*Engine_, lightEntity);
Scene_->addEntity(lightEntity);
EntitiesInScene_.insert(lightEntity);
lightIt = SceneLightEntities_.emplace(light.ObjectId, lightEntity).first;
}
@ -806,6 +1024,7 @@ private:
}
Scene_->remove(it->second);
EntitiesInScene_.erase(it->second);
Engine_->destroy(it->second);
utils::EntityManager::get().destroy(it->second);
it = SceneLightEntities_.erase(it);
@ -853,6 +1072,7 @@ private:
std::unordered_map<MetaCoreId, std::pair<filament::gltfio::FilamentAsset*, utils::Entity>> ObjectToFilamentEntity_;
std::unordered_map<MetaCoreId, filament::gltfio::FilamentAsset*> LoadedAssets_;
std::unordered_map<MetaCoreId, utils::Entity> SceneLightEntities_;
std::unordered_set<utils::Entity, utils::Entity::Hasher> EntitiesInScene_;
filament::View* UIView_ = nullptr;
MetaCoreImGuiHelper* ImGuiHelper_ = nullptr;
@ -873,8 +1093,8 @@ MetaCoreFilamentSceneBridge::MetaCoreFilamentSceneBridge()
MetaCoreFilamentSceneBridge::~MetaCoreFilamentSceneBridge() = default;
bool MetaCoreFilamentSceneBridge::Initialize(MetaCoreWindow& window) {
return Impl_->Initialize(window);
bool MetaCoreFilamentSceneBridge::Initialize(MetaCoreWindow& window, bool offscreen) {
return Impl_->Initialize(window, offscreen);
}
void MetaCoreFilamentSceneBridge::Shutdown() {
@ -938,4 +1158,8 @@ float MetaCoreFilamentSceneBridge::GetDefaultLightIntensityForTesting() const {
return Impl_->GetDefaultLightIntensityForTesting();
}
bool MetaCoreFilamentSceneBridge::VerifyEntityInSceneForTesting(MetaCoreId objectId) const {
return Impl_->VerifyEntityInSceneForTesting(objectId);
}
} // namespace MetaCore

View File

@ -15,7 +15,7 @@ class MetaCoreScene;
class MetaCoreEditorViewportRenderer {
public:
bool Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window);
bool Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window, bool offscreen = true);
void Shutdown();
void SetViewportRect(const MetaCoreViewportRect& viewportRect);
void RenderSceneToViewport(MetaCoreScene& scene, const MetaCoreSceneView& sceneView, bool useScenePrimaryCamera = false);

View File

@ -23,7 +23,7 @@ public:
MetaCoreFilamentSceneBridge();
~MetaCoreFilamentSceneBridge();
bool Initialize(MetaCoreWindow& window);
bool Initialize(MetaCoreWindow& window, bool offscreen = true);
void Shutdown();
void Resize(int width, int height);
void SetProjectRootPath(const std::filesystem::path& projectRootPath);
@ -42,6 +42,7 @@ public:
[[nodiscard]] bool VerifyTransformForTesting(MetaCoreId objectId, const glm::mat4& expectedMatrix) const;
[[nodiscard]] bool VerifyLightExistsForTesting(MetaCoreId objectId) const;
[[nodiscard]] float GetDefaultLightIntensityForTesting() const;
[[nodiscard]] bool VerifyEntityInSceneForTesting(MetaCoreId objectId) const;
private:
class MetaCoreFilamentSceneBridgeImpl;

1089
docs/任务计划.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,8 @@
#include "MetaCoreScene/MetaCoreScene.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreScene/MetaCoreTransformUtils.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include <nlohmann/json.hpp>
#include <cstdlib>
#include <cmath>
@ -1107,6 +1109,265 @@ void MetaCoreTestInstantiateMultiNodeModelAssetIntoScene() {
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport() {
const std::filesystem::path projectRoot = "d:/MetaCore/SandboxProject";
_putenv_s("METACORE_PROJECT_PATH", projectRoot.string().c_str());
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto assetDatabase = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetDatabaseService>();
const auto sceneEditingService = moduleRegistry.ResolveService<MetaCore::MetaCoreISceneEditingService>();
MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService");
MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService");
const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets/Models/box1.glb"));
MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的 box1.glb 模型资源");
const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt);
MetaCoreExpect(instantiatedRootId.has_value(), "应能将导入模型实例化到场景");
// 初始化窗口和 FilamentSceneBridge
MetaCore::MetaCoreWindow testWindow;
bool winOk = testWindow.Initialize(800, 600, "DeleteModelAssetTestWindow");
MetaCoreExpect(winOk, "测试窗口初始化应成功");
MetaCore::MetaCoreFilamentSceneBridge bridge;
bridge.SetProjectRootPath(projectRoot);
bool bridgeOk = bridge.Initialize(testWindow);
MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功");
// 同步场景,将模型加载到 FilamentSceneBridge 中
bridge.SyncScene(scene, false, false);
// 验证模型已被桥接器加载和映射
glm::mat4 modelWorldMatrix;
bool hasModel = bridge.TryGetObjectWorldMatrix(*instantiatedRootId, modelWorldMatrix);
MetaCoreExpect(hasModel, "同步场景后,桥接器中应包含已实例化模型的变换映射");
// 在场景中删除该模型 GameObject
const std::vector<MetaCore::MetaCoreId> deletedIds = scene.DeleteGameObjects({*instantiatedRootId});
MetaCoreExpect(!deletedIds.empty(), "删除模型对象应返回被删除的节点ID");
// 再次同步场景,此时应触发卸载
bridge.SyncScene(scene, false, false);
// 验证删除后,桥接器中是否还存在该模型的映射
bool hasModelAfterDelete = bridge.TryGetObjectWorldMatrix(*instantiatedRootId, modelWorldMatrix);
MetaCoreExpect(!hasModelAfterDelete, "删除模型后,桥接器映射中不应再包含该模型");
bridge.Shutdown();
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
}
void MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreDeleteModelSubChildProject";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Library");
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"DeleteModelSubChildProject\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
{
std::ofstream gltfFile(tempProjectRoot / "Assets" / "Assembly.gltf", std::ios::trunc);
gltfFile << "{\n"
<< " \"asset\": { \"version\": \"2.0\" },\n"
<< " \"scenes\": [{ \"nodes\": [0] }],\n"
<< " \"nodes\": [\n"
<< " { \"name\": \"AssemblyRoot\", \"children\": [1, 2] },\n"
<< " { \"name\": \"Valve_A\" },\n"
<< " { \"name\": \"Valve_B\" }\n"
<< " ]\n"
<< "}\n";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreWindow window;
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreEditorViewportRenderer viewportRenderer;
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreLogService logService;
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
MetaCore::MetaCoreEditorContext editorContext(
window,
renderDevice,
viewportRenderer,
scene,
logService,
moduleRegistry
);
const auto assetDatabase = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetDatabaseService>();
const auto sceneEditingService = moduleRegistry.ResolveService<MetaCore::MetaCoreISceneEditingService>();
const auto packageService = moduleRegistry.ResolveService<MetaCore::MetaCoreIPackageService>();
const auto reflectionRegistry = moduleRegistry.ResolveService<MetaCore::MetaCoreIReflectionRegistry>();
MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService");
MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService");
MetaCoreExpect(packageService != nullptr, "应解析到 PackageService");
MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry");
const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Assembly.gltf");
MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的多节点模型资源");
MetaCore::MetaCoreModelAssetDocument document;
document.AssetType = "model";
document.ImporterId = "GltfModelImporter";
document.SourcePath = std::filesystem::path("Assets") / "Assembly.gltf";
document.SourceHash = 1234;
document.ModelKind = MetaCore::MetaCoreModelAssetKind::Gltf;
document.SourceFormat = "gltf";
MetaCore::MetaCoreImportedGltfNodeDocument rootNode;
rootNode.Name = "AssemblyRoot";
rootNode.ParentIndex = -1;
rootNode.MeshIndex = -1;
document.Nodes.push_back(rootNode);
MetaCore::MetaCoreImportedGltfNodeDocument childNode;
childNode.Name = "Valve_A";
childNode.ParentIndex = 0;
childNode.MeshIndex = 0;
document.Nodes.push_back(childNode);
MetaCore::MetaCoreImportedGltfNodeDocument childNodeB;
childNodeB.Name = "Valve_B";
childNodeB.ParentIndex = 0;
childNodeB.MeshIndex = 0;
document.Nodes.push_back(childNodeB);
MetaCore::MetaCoreMeshAssetDocument meshAsset;
meshAsset.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate();
meshAsset.Name = "ValveMesh";
MetaCore::MetaCoreMeshSubMeshDocument firstSubMesh;
firstSubMesh.Name = "Primitive_0";
firstSubMesh.MaterialSlotIndex = 0;
firstSubMesh.FirstIndex = 0;
firstSubMesh.IndexCount = 3;
meshAsset.SubMeshes.push_back(firstSubMesh);
document.GeneratedMeshAssets.push_back(meshAsset);
const auto payload = MetaCore::MetaCoreSerializeToBytes(document, reflectionRegistry->GetTypeRegistry());
MetaCoreExpect(payload.has_value(), "多节点模型资源应能序列化");
MetaCore::MetaCorePackageDocument package;
package.Header.PackageType = MetaCore::MetaCorePackageType::Asset;
package.Header.PackageGuid = modelRecord->Guid;
package.Header.SourceHash = document.SourceHash;
package.NameTable.emplace_back("MetaCoreModelAssetDocument");
package.Exports.push_back(MetaCore::MetaCoreExportEntry{
modelRecord->Guid,
modelRecord->RelativePath.filename().string(),
MetaCore::MetaCoreMakeTypeId("MetaCoreModelAssetDocument"),
0,
static_cast<std::uint64_t>(payload->size())
});
package.PayloadSections.push_back(*payload);
MetaCoreExpect(
packageService->WritePackage(tempProjectRoot / modelRecord->PackagePath, std::move(package)),
"应能写入多节点模型资源包"
);
const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt);
MetaCoreExpect(instantiatedRootId.has_value(), "应能将多节点模型实例化到场景");
MetaCore::MetaCoreId childId = 0;
MetaCore::MetaCoreId childIdB = 0;
for (const auto& obj : scene.GetGameObjects()) {
if (obj.GetName() == "Valve_A") {
childId = obj.GetId();
} else if (obj.GetName() == "Valve_B") {
childIdB = obj.GetId();
}
}
MetaCoreExpect(childId != 0, "应能找到子节点 Valve_A");
MetaCoreExpect(childIdB != 0, "应能找到子节点 Valve_B");
// 初始化窗口和 FilamentSceneBridge
MetaCore::MetaCoreWindow testWindow;
bool winOk = testWindow.Initialize(800, 600, "DeleteModelSubChildTestWindow");
MetaCoreExpect(winOk, "测试窗口初始化应成功");
MetaCore::MetaCoreFilamentSceneBridge bridge;
bridge.SetProjectRootPath(tempProjectRoot);
bool bridgeOk = bridge.Initialize(testWindow);
MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功");
// 同步场景,将模型加载到 FilamentSceneBridge 中
bridge.SyncScene(scene, false, false);
// 验证子节点对应的实体是否已经在场景中渲染
bool isChildInScene = bridge.VerifyEntityInSceneForTesting(childId);
bool isChildBInScene = bridge.VerifyEntityInSceneForTesting(childIdB);
MetaCoreExpect(isChildInScene, "同步场景后,子节点 Valve_A 实体应在场景中被渲染");
MetaCoreExpect(isChildBInScene, "同步场景后,子节点 Valve_B 实体应在场景中被渲染");
// 在场景中删除子节点 Valve_A GameObject在 Command 中执行,为了后续 Undo
const bool deleted = editorContext.ExecuteSnapshotCommand("删除子节点 Valve_A", [&]() {
scene.DeleteGameObjects({childId});
return true;
});
MetaCoreExpect(deleted, "删除子节点 Valve_A 快照命令应成功");
// 再次同步场景
bridge.SyncScene(scene, false, false);
// 验证删除子节点 Valve_A 后,桥接器中该子节点对应的实体是否已被移出场景渲染
bool isChildInSceneAfterDelete = bridge.VerifyEntityInSceneForTesting(childId);
bool isChildBInSceneAfterDelete = bridge.VerifyEntityInSceneForTesting(childIdB);
MetaCoreExpect(!isChildInSceneAfterDelete, "删除子节点 Valve_A 后,其实体应被移出场景渲染");
MetaCoreExpect(isChildBInSceneAfterDelete, "删除子节点 Valve_A 后Valve_B 实体应依然保留在场景中渲染(没有卸载资产)");
// 撤销删除子节点
const bool undoSucceeded = editorContext.UndoCommand();
MetaCoreExpect(undoSucceeded, "撤销删除应成功");
// 再次同步场景
bridge.SyncScene(scene, false, false);
// 验证撤销后,该子节点实体是否已被重新恢复渲染
bool isChildInSceneAfterUndo = bridge.VerifyEntityInSceneForTesting(childId);
MetaCoreExpect(isChildInSceneAfterUndo, "撤销删除后,子节点 Valve_A 实体应重新被加回场景渲染");
bridge.Shutdown();
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
void MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreModelReimportStableProject";
@ -2485,21 +2746,27 @@ void MetaCoreTestDecoupledSnapshotSync() {
MetaCoreExpect(bridgeOk, "测试桥接器初始化应成功");
// 5. 在 scene == nullptr 的快照渲染模式下执行同步
std::cout << "[DEBUG] SyncScene starting..." << std::endl;
bridge.SyncScene(snapshot, nullptr, false, false);
std::cout << "[DEBUG] SyncScene completed." << std::endl;
// 6. 验证映射重建和变换同步
glm::mat4 cubeWorldMatrix;
bool hasCube = bridge.TryGetObjectWorldMatrix(cubeId, cubeWorldMatrix);
std::cout << "[DEBUG] hasCube: " << hasCube << std::endl;
MetaCoreExpect(hasCube, "无 scene 模式下重建映射后应包含 Cube 变换");
bool transOk = bridge.VerifyTransformForTesting(cubeId, customLocalMatrix);
std::cout << "[DEBUG] transOk: " << transOk << std::endl;
MetaCoreExpect(transOk, "快照中的自定义局部变换应正确同步到 Filament");
// 7. 验证 Enabled 状态为 false 的光源被过滤排除,且默认灯光起降机制生效 (自动亮起)
bool hasLight = bridge.VerifyLightExistsForTesting(lightId);
std::cout << "[DEBUG] hasLight: " << hasLight << std::endl;
MetaCoreExpect(!hasLight, "Enabled 为 false 的自定义光源不应被实例化");
float defaultLightIntensity = bridge.GetDefaultLightIntensityForTesting();
std::cout << "[DEBUG] defaultLightIntensity: " << defaultLightIntensity << std::endl;
MetaCoreExpect(defaultLightIntensity > 99000.0F, "无有效自定义光源时,默认灯光应起飞作为兜底");
// 8. 动态开启光源属性,验证默认灯光动态变暗
@ -2508,12 +2775,16 @@ void MetaCoreTestDecoupledSnapshotSync() {
light.Enabled = true;
}
}
std::cout << "[DEBUG] SyncScene 2 starting..." << std::endl;
bridge.SyncScene(snapshot, nullptr, false, false);
std::cout << "[DEBUG] SyncScene 2 completed." << std::endl;
bool hasLightNow = bridge.VerifyLightExistsForTesting(lightId);
std::cout << "[DEBUG] hasLightNow: " << hasLightNow << std::endl;
MetaCoreExpect(hasLightNow, "启用光源后自定义光源应被实例化");
float defaultLightIntensityNow = bridge.GetDefaultLightIntensityForTesting();
std::cout << "[DEBUG] defaultLightIntensityNow: " << defaultLightIntensityNow << std::endl;
MetaCoreExpect(defaultLightIntensityNow < 1.0F, "启用自定义光源时,默认灯光强度应自动降为 0");
// 9. 再次禁用光源,验证自定义光源被销毁并且默认灯光复亮
@ -2522,19 +2793,205 @@ void MetaCoreTestDecoupledSnapshotSync() {
light.Enabled = false;
}
}
std::cout << "[DEBUG] SyncScene 3 starting..." << std::endl;
bridge.SyncScene(snapshot, nullptr, false, false);
std::cout << "[DEBUG] SyncScene 3 completed." << std::endl;
bool hasLightFinal = bridge.VerifyLightExistsForTesting(lightId);
std::cout << "[DEBUG] hasLightFinal: " << hasLightFinal << std::endl;
MetaCoreExpect(!hasLightFinal, "再次禁用光源后自定义光源实体应被正确销毁移出");
float defaultLightIntensityFinal = bridge.GetDefaultLightIntensityForTesting();
std::cout << "[DEBUG] defaultLightIntensityFinal: " << defaultLightIntensityFinal << std::endl;
MetaCoreExpect(defaultLightIntensityFinal > 99000.0F, "再次无激活光源时,默认灯光应重新亮起兜底");
// 9.5 模拟删除网格对象,验证其在 FilamentSceneBridge 中被成功卸载
std::cout << "[DEBUG] Deleting mesh object..." << std::endl;
for (auto it = snapshot.Renderables.begin(); it != snapshot.Renderables.end(); ) {
if (it->ObjectId == cubeId) {
it = snapshot.Renderables.erase(it);
} else {
++it;
}
}
std::cout << "[DEBUG] SyncScene 4 starting..." << std::endl;
bridge.SyncScene(snapshot, nullptr, false, false);
std::cout << "[DEBUG] SyncScene 4 completed." << std::endl;
glm::mat4 deletedWorldMatrix;
bool hasCubeAfterDelete = bridge.TryGetObjectWorldMatrix(cubeId, deletedWorldMatrix);
std::cout << "[DEBUG] hasCubeAfterDelete: " << hasCubeAfterDelete << std::endl;
MetaCoreExpect(!hasCubeAfterDelete, "删除网格对象后,其在桥接器映射中不应再存在");
// 10. 资源清理
std::cout << "[DEBUG] bridge.Shutdown starting..." << std::endl;
bridge.Shutdown();
std::cout << "[DEBUG] bridge.Shutdown completed." << std::endl;
window.Shutdown();
}
void MetaCoreTestP3ProductivityStage() {
const std::filesystem::path tempProjectRoot =
std::filesystem::temp_directory_path() / "MetaCoreTestP3Project";
std::filesystem::remove_all(tempProjectRoot);
std::filesystem::create_directories(tempProjectRoot / "Assets");
std::filesystem::create_directories(tempProjectRoot / "Scenes");
std::filesystem::create_directories(tempProjectRoot / "Library");
// 1. 写入默认的 MetaCore.project.json
{
std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc);
projectFile << "{\n"
<< " \"name\": \"P3Project\",\n"
<< " \"version\": \"0.1.0\",\n"
<< " \"scenes\": [],\n"
<< " \"startup_scene\": \"\"\n"
<< "}\n";
}
_putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str());
MetaCore::MetaCoreEditorModuleRegistry moduleRegistry;
auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule();
coreServicesModule->Startup(moduleRegistry);
const auto assetDatabase = moduleRegistry.ResolveService<MetaCore::MetaCoreIAssetDatabaseService>();
MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService");
MetaCoreExpect(assetDatabase->HasProject(), "应成功打开项目");
// 2. 测试资产自动导入与默认目录 (P3.2)
// 写入一个没有 .mcmeta 的 .ktx 贴图文件
const std::filesystem::path texturePath = tempProjectRoot / "Assets" / "test_texture.ktx";
{
std::ofstream textureFile(texturePath, std::ios::trunc);
textureFile << "dummy-ktx-data";
}
assetDatabase->Refresh();
// 验证是否在 Assets 下自动生成了 test_texture.ktx.mcmeta 物理元文件
const std::filesystem::path metaPath = tempProjectRoot / "Assets" / "test_texture.ktx.mcmeta";
MetaCoreExpect(std::filesystem::exists(metaPath), "Refresh 后应生成 test_texture.ktx.mcmeta 物理元文件");
// 读取该 .mcmeta 文件内容,校验结构
std::ifstream metaFile(metaPath);
nlohmann::json metaJson;
metaFile >> metaJson;
metaFile.close();
MetaCoreExpect(metaJson.contains("guid"), "元文件应包含 guid");
MetaCoreExpect(metaJson.contains("asset_type") && metaJson["asset_type"] == "texture", "元文件 asset_type 应为 texture");
MetaCoreExpect(metaJson.contains("importer_id") && metaJson["importer_id"] == "TextureImporter", "元文件 importer_id 应为 TextureImporter");
const std::string textureGuidStr = metaJson["guid"].get<std::string>();
MetaCore::MetaCoreAssetGuid textureGuid = MetaCore::MetaCoreAssetGuid::Parse(textureGuidStr).value_or(MetaCore::MetaCoreAssetGuid{});
MetaCoreExpect(textureGuid.IsValid(), "自动分配的材质 GUID 应为有效 GUID");
// 3. 测试资产重命名与删除 (P3.1, P3.4)
// 重命名该贴图文件
MetaCoreExpect(assetDatabase->RenamePath(std::filesystem::path("Assets") / "test_texture.ktx", "renamed_texture.ktx"), "应能重命名贴图文件");
const std::filesystem::path renamedTexturePath = tempProjectRoot / "Assets" / "renamed_texture.ktx";
const std::filesystem::path renamedMetaPath = tempProjectRoot / "Assets" / "renamed_texture.ktx.mcmeta";
MetaCoreExpect(std::filesystem::exists(renamedTexturePath), "重命名后新贴图文件应存在");
MetaCoreExpect(!std::filesystem::exists(texturePath), "重命名后旧贴图文件不应存在");
MetaCoreExpect(std::filesystem::exists(renamedMetaPath), "重命名后新 mcmeta 文件应存在");
MetaCoreExpect(!std::filesystem::exists(metaPath), "重命名后旧 mcmeta 文件不应存在");
// 删除重命名后的贴图文件
MetaCoreExpect(assetDatabase->DeletePath(std::filesystem::path("Assets") / "renamed_texture.ktx"), "应能删除贴图文件");
MetaCoreExpect(!std::filesystem::exists(renamedTexturePath), "删除后贴图文件应不存在");
MetaCoreExpect(!std::filesystem::exists(renamedMetaPath), "删除后 mcmeta 文件应不存在");
// 4. 测试材质创建、加载、编辑保存及物理 Hash 更新 (P3.4)
// 保存一个初始材质
const std::filesystem::path materialPath = tempProjectRoot / "Assets" / "test_material.mcmaterial.json";
MetaCore::MetaCoreMaterialAssetDocument materialDoc;
materialDoc.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate();
materialDoc.Name = "test_material";
materialDoc.BaseColor = glm::vec3(1.0F, 0.0F, 0.0F);
materialDoc.Metallic = 0.5F;
materialDoc.Roughness = 0.8F;
MetaCore::MetaCoreTypeRegistry registry;
MetaCoreRegisterFoundationGeneratedTypes(registry);
MetaCoreRegisterSceneGeneratedTypes(registry);
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(materialPath, materialDoc, registry),
"应能保存材质文件"
);
// 自动刷新生成 .mcmeta 物理元文件
assetDatabase->Refresh();
const std::filesystem::path materialMetaPath = tempProjectRoot / "Assets" / "test_material.mcmaterial.json.mcmeta";
MetaCoreExpect(std::filesystem::exists(materialMetaPath), "Refresh 后应生成材质的 mcmeta 元文件");
// 编辑材质属性并重新保存
materialDoc.BaseColor = glm::vec3(0.0F, 1.0F, 0.0F);
materialDoc.Metallic = 0.9F;
materialDoc.Roughness = 0.1F;
MetaCoreExpect(
MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(materialPath, materialDoc, registry),
"应能重新保存修改后的材质文件"
);
// 模拟材质编辑器的物理 Hash 保存更新逻辑
std::uint64_t initialHash = 0;
{
std::ifstream input(materialMetaPath);
nlohmann::json json;
input >> json;
input.close();
if (json.contains("source_hash")) {
initialHash = json["source_hash"].get<std::uint64_t>();
}
}
// 更新 Hash
const std::uint64_t newHash = MetaCore::MetaCoreHashFile(materialPath).value_or(0);
{
std::ifstream input(materialMetaPath);
nlohmann::json json;
input >> json;
input.close();
json["source_hash"] = newHash;
std::ofstream output(materialMetaPath);
output << json.dump(4);
output.close();
}
// 读回以校验
std::uint64_t savedHash = 0;
{
std::ifstream input(materialMetaPath);
nlohmann::json json;
input >> json;
input.close();
if (json.contains("source_hash")) {
savedHash = json["source_hash"].get<std::uint64_t>();
}
}
MetaCoreExpect(savedHash == newHash, "保存并更新后,材质 mcmeta 物理哈希值应被更新且一致");
// 5. 测试拖拽绑定到 Mesh 槽位 (P3.3)
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject gameObject = scene.CreateGameObject("MeshObject");
auto& meshRenderer = gameObject.AddComponent<MetaCore::MetaCoreMeshRendererComponent>();
// 模拟槽位绑定
if (meshRenderer.MaterialAssetGuids.empty()) {
meshRenderer.MaterialAssetGuids.resize(1);
}
meshRenderer.MaterialAssetGuids[0] = materialDoc.AssetGuid;
MetaCoreExpect(meshRenderer.MaterialAssetGuids[0] == materialDoc.AssetGuid, "拖拽绑定后槽位 0 材质 Guid 应正确");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
_putenv_s("METACORE_PROJECT_PATH", "");
std::filesystem::remove_all(tempProjectRoot);
}
} // namespace
int main() {
@ -2598,6 +3055,10 @@ int main() {
MetaCoreTestInstantiateImportedModelAssetIntoScene();
std::cout << "[RUN] MetaCoreTestInstantiateMultiNodeModelAssetIntoScene..." << std::endl;
MetaCoreTestInstantiateMultiNodeModelAssetIntoScene();
std::cout << "[RUN] MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport..." << std::endl;
MetaCoreTestDeleteModelAssetCorrectlyClearsFromViewport();
std::cout << "[RUN] MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport..." << std::endl;
MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport();
std::cout << "[RUN] MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable..." << std::endl;
MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable();
std::cout << "[RUN] MetaCoreTestBootstrapStartupSceneCreation..." << std::endl;
@ -2654,6 +3115,9 @@ int main() {
std::cout << "[RUN] MetaCoreTestDecoupledSnapshotSync..." << std::endl;
MetaCoreTestDecoupledSnapshotSync();
std::cout << "[RUN] MetaCoreTestP3ProductivityStage..." << std::endl;
MetaCoreTestP3ProductivityStage();
std::cout << "MetaCoreSmokeTests passed\n";
return 0;
}