fix: 修复多节点模型在场景树中删除子节点时的视口残留 Bug,并补充集成测试
This commit is contained in:
parent
77626017a3
commit
dab182cb96
@ -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.
@ -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{},
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
1089
docs/任务计划.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user