diff --git a/CMakeLists.txt b/CMakeLists.txt index 123f791..8c4b1d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -362,6 +362,7 @@ target_link_libraries(MetaCoreEditorApp ) metacore_stage_panda3d_runtime(MetaCoreEditorApp) +metacore_stage_simplepbr_runtime(MetaCoreEditorApp) add_executable(MetaCorePlayer Apps/MetaCorePlayer/main.cpp @@ -378,6 +379,7 @@ target_link_libraries(MetaCorePlayer ) metacore_stage_panda3d_runtime(MetaCorePlayer) +metacore_stage_simplepbr_runtime(MetaCorePlayer) if(METACORE_BUILD_TESTS) enable_testing() @@ -393,5 +395,6 @@ if(METACORE_BUILD_TESTS) ) metacore_stage_panda3d_runtime(MetaCoreSmokeTests) + metacore_stage_simplepbr_runtime(MetaCoreSmokeTests) add_test(NAME MetaCoreSmokeTests COMMAND MetaCoreSmokeTests) endif() diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index d3842b9..7abc1a9 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +66,326 @@ void MetaCoreTrackComponentInspectorEdit(MetaCoreEditorContext& editorContext, c MetaCoreNearlyEqual(lhs.z, rhs.z); } +void MetaCoreCopyStringToBuffer(const std::string& value, char* buffer, std::size_t bufferSize) { + if (buffer == nullptr || bufferSize == 0) { + return; + } + std::snprintf(buffer, bufferSize, "%s", value.c_str()); +} + +[[nodiscard]] bool MetaCoreEditAssetGuidField( + const char* label, + MetaCoreAssetGuid& assetGuid, + const char* emptyHint = "" +) { + std::array buffer{}; + const std::string currentValue = assetGuid.IsValid() ? assetGuid.ToString() : std::string{}; + MetaCoreCopyStringToBuffer(currentValue, buffer.data(), buffer.size()); + + const bool changed = ImGui::InputText(label, buffer.data(), buffer.size()); + if (!changed) { + return false; + } + + const std::string editedValue = buffer.data(); + if (editedValue.empty()) { + assetGuid = MetaCoreAssetGuid{}; + return true; + } + + const auto parsedGuid = MetaCoreAssetGuid::Parse(editedValue); + if (parsedGuid.has_value()) { + assetGuid = *parsedGuid; + return true; + } + + ImGui::SameLine(); + ImGui::TextDisabled("%s", emptyHint); + return false; +} + +template +[[nodiscard]] std::optional MetaCoreReadTypedPackagePayload( + const MetaCorePackageDocument& package, + const MetaCoreTypeRegistry& registry, + std::string_view expectedTypeName +); + +struct MetaCoreResolvedGeneratedMaterialDocument { + MetaCoreAssetRecord AssetRecord{}; + MetaCoreImportedGltfAssetDocument Document{}; + std::size_t MaterialIndex = 0; +}; + +struct MetaCoreGeneratedAssetChoice { + MetaCoreAssetGuid AssetGuid{}; + std::string Label{}; +}; + +template +[[nodiscard]] std::vector MetaCoreCollectGeneratedAssetChoices( + MetaCoreEditorContext& editorContext, + TLabelBuilder&& labelBuilder +) { + std::vector choices; + + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + const auto packageService = editorContext.GetModuleRegistry().ResolveService(); + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + return choices; + } + + for (const MetaCoreAssetRecord& record : assetDatabaseService->GetAssetRegistry()) { + if (record.Type != "model" || record.StorageKind != MetaCoreAssetStorageKind::SourcePackage) { + continue; + } + + const std::filesystem::path relativePackagePath = + !record.PackagePath.empty() ? record.PackagePath : record.RelativePath; + const auto package = packageService->ReadPackage(assetDatabaseService->GetProjectDescriptor().RootPath / relativePackagePath); + if (!package.has_value()) { + continue; + } + + const auto importedDocument = MetaCoreReadTypedPackagePayload( + *package, + reflectionRegistry->GetTypeRegistry(), + "MetaCoreImportedGltfAssetDocument" + ); + if (!importedDocument.has_value()) { + continue; + } + + const std::vector* generatedAssets = nullptr; + if constexpr (std::is_same_v) { + generatedAssets = &importedDocument->GeneratedMeshAssets; + } else if constexpr (std::is_same_v) { + generatedAssets = &importedDocument->GeneratedMaterialAssets; + } else if constexpr (std::is_same_v) { + generatedAssets = &importedDocument->GeneratedTextureAssets; + } + + for (const TGeneratedAsset& asset : *generatedAssets) { + MetaCoreGeneratedAssetChoice choice; + choice.AssetGuid = asset.AssetGuid; + choice.Label = labelBuilder(record, asset); + choices.push_back(std::move(choice)); + } + } + + std::sort(choices.begin(), choices.end(), [](const MetaCoreGeneratedAssetChoice& lhs, const MetaCoreGeneratedAssetChoice& rhs) { + return lhs.Label < rhs.Label; + }); + return choices; +} + +[[nodiscard]] std::vector MetaCoreCollectMeshAssetChoices(MetaCoreEditorContext& editorContext) { + return MetaCoreCollectGeneratedAssetChoices( + editorContext, + [](const MetaCoreAssetRecord& record, const MetaCoreMeshAssetDocument& asset) { + return record.RelativePath.filename().string() + " / Mesh / " + asset.Name; + } + ); +} + +[[nodiscard]] std::vector MetaCoreCollectMaterialAssetChoices(MetaCoreEditorContext& editorContext) { + return MetaCoreCollectGeneratedAssetChoices( + editorContext, + [](const MetaCoreAssetRecord& record, const MetaCoreMaterialAssetDocument& asset) { + return record.RelativePath.filename().string() + " / Material / " + asset.Name; + } + ); +} + +[[nodiscard]] std::vector MetaCoreCollectTextureAssetChoices(MetaCoreEditorContext& editorContext) { + return MetaCoreCollectGeneratedAssetChoices( + editorContext, + [](const MetaCoreAssetRecord& record, const MetaCoreTextureAssetDocument& asset) { + return record.RelativePath.filename().string() + " / Texture / " + asset.Name; + } + ); +} + +[[nodiscard]] bool MetaCoreWriteImportedGltfAssetDocument( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetRecord& assetRecord, + const MetaCoreImportedGltfAssetDocument& document +) { + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + const auto packageService = editorContext.GetModuleRegistry().ResolveService(); + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + return false; + } + + const std::filesystem::path relativePackagePath = + !assetRecord.PackagePath.empty() ? assetRecord.PackagePath : assetRecord.RelativePath; + const std::filesystem::path absolutePackagePath = assetDatabaseService->GetProjectDescriptor().RootPath / relativePackagePath; + const auto payload = MetaCoreSerializeToBytes(document, reflectionRegistry->GetTypeRegistry()); + if (!payload.has_value()) { + return false; + } + + const auto existingPackage = packageService->ReadPackage(absolutePackagePath); + const MetaCorePackageType packageType = + existingPackage.has_value() ? existingPackage->Header.PackageType : MetaCorePackageType::Asset; + MetaCorePackageDocument package; + package.Header.PackageType = packageType; + package.Header.PackageGuid = assetRecord.Guid; + package.Header.SourceHash = document.SourceHash; + package.NameTable.emplace_back("MetaCoreImportedGltfAssetDocument"); + package.Exports.push_back(MetaCoreExportEntry{ + assetRecord.Guid, + relativePackagePath.filename().string(), + MetaCoreMakeTypeId("MetaCoreImportedGltfAssetDocument"), + 0, + static_cast(payload->size()) + }); + package.PayloadSections.push_back(*payload); + return packageService->WritePackage(absolutePackagePath, std::move(package)); +} + +[[nodiscard]] std::optional MetaCoreResolveGeneratedMaterialDocument( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& materialGuid +) { + if (!materialGuid.IsValid()) { + return std::nullopt; + } + + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + const auto packageService = editorContext.GetModuleRegistry().ResolveService(); + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + return std::nullopt; + } + + for (const MetaCoreAssetRecord& record : assetDatabaseService->GetAssetRegistry()) { + if (record.Type != "model" || record.StorageKind != MetaCoreAssetStorageKind::SourcePackage) { + continue; + } + + const std::filesystem::path relativePackagePath = + !record.PackagePath.empty() ? record.PackagePath : record.RelativePath; + const auto package = packageService->ReadPackage(assetDatabaseService->GetProjectDescriptor().RootPath / relativePackagePath); + if (!package.has_value()) { + continue; + } + + const auto importedDocument = MetaCoreReadTypedPackagePayload( + *package, + reflectionRegistry->GetTypeRegistry(), + "MetaCoreImportedGltfAssetDocument" + ); + if (!importedDocument.has_value()) { + continue; + } + + for (std::size_t index = 0; index < importedDocument->GeneratedMaterialAssets.size(); ++index) { + if (importedDocument->GeneratedMaterialAssets[index].AssetGuid == materialGuid) { + return MetaCoreResolvedGeneratedMaterialDocument{ + record, + *importedDocument, + index + }; + } + } + } + + return std::nullopt; +} + +void MetaCoreApplyGeneratedMaterialPreviewToScene( + MetaCoreEditorContext& editorContext, + const MetaCoreImportedGltfAssetDocument& document +) { + std::unordered_map texturePaths; + for (const MetaCoreTextureAssetDocument& textureAsset : document.GeneratedTextureAssets) { + if (textureAsset.AssetGuid.IsValid()) { + texturePaths[textureAsset.AssetGuid] = textureAsset.SourcePath.generic_string(); + } + } + + for (MetaCoreGameObject& sceneObject : editorContext.GetScene().GetGameObjects()) { + if (!sceneObject.MeshRenderer.has_value()) { + continue; + } + + for (const MetaCoreAssetGuid& materialGuid : sceneObject.MeshRenderer->MaterialAssetGuids) { + const auto materialIterator = std::find_if( + document.GeneratedMaterialAssets.begin(), + document.GeneratedMaterialAssets.end(), + [&](const MetaCoreMaterialAssetDocument& materialAsset) { + return materialAsset.AssetGuid == materialGuid; + } + ); + if (materialIterator == document.GeneratedMaterialAssets.end()) { + continue; + } + + sceneObject.MeshRenderer->BaseColor = materialIterator->BaseColor; + sceneObject.MeshRenderer->Metallic = materialIterator->Metallic; + sceneObject.MeshRenderer->Roughness = materialIterator->Roughness; + sceneObject.MeshRenderer->DoubleSided = materialIterator->DoubleSided; + sceneObject.MeshRenderer->EmissiveColor = materialIterator->EmissiveColor; + sceneObject.MeshRenderer->AlphaCutoff = materialIterator->AlphaCutoff; + sceneObject.MeshRenderer->AlphaMode = + materialIterator->AlphaMode == MetaCoreMaterialAlphaMode::Mask + ? MetaCoreMeshAlphaMode::Mask + : (materialIterator->AlphaMode == MetaCoreMaterialAlphaMode::Blend + ? MetaCoreMeshAlphaMode::Blend + : MetaCoreMeshAlphaMode::Opaque); + sceneObject.MeshRenderer->BaseColorTexturePath = + materialIterator->BaseColorTexture.IsValid() && texturePaths.contains(materialIterator->BaseColorTexture) + ? texturePaths[materialIterator->BaseColorTexture] + : std::string{}; + sceneObject.MeshRenderer->MetallicRoughnessTexturePath = + materialIterator->MetallicRoughnessTexture.IsValid() && texturePaths.contains(materialIterator->MetallicRoughnessTexture) + ? texturePaths[materialIterator->MetallicRoughnessTexture] + : std::string{}; + sceneObject.MeshRenderer->NormalTexturePath = + materialIterator->NormalTexture.IsValid() && texturePaths.contains(materialIterator->NormalTexture) + ? texturePaths[materialIterator->NormalTexture] + : std::string{}; + sceneObject.MeshRenderer->EmissiveTexturePath = + materialIterator->EmissiveTexture.IsValid() && texturePaths.contains(materialIterator->EmissiveTexture) + ? texturePaths[materialIterator->EmissiveTexture] + : std::string{}; + sceneObject.MeshRenderer->AoTexturePath = + materialIterator->AoTexture.IsValid() && texturePaths.contains(materialIterator->AoTexture) + ? texturePaths[materialIterator->AoTexture] + : std::string{}; + break; + } + } +} + +[[nodiscard]] bool MetaCoreDrawGeneratedAssetCombo( + const char* label, + const std::vector& choices, + MetaCoreAssetGuid& assetGuid +) { + std::vector itemPointers; + itemPointers.reserve(choices.size() + 1); + itemPointers.push_back(""); + + int selectedIndex = 0; + for (std::size_t index = 0; index < choices.size(); ++index) { + itemPointers.push_back(choices[index].Label.c_str()); + if (choices[index].AssetGuid == assetGuid) { + selectedIndex = static_cast(index + 1); + } + } + + if (!ImGui::Combo(label, &selectedIndex, itemPointers.data(), static_cast(itemPointers.size()))) { + return false; + } + + assetGuid = selectedIndex == 0 ? MetaCoreAssetGuid{} : choices[static_cast(selectedIndex - 1)].AssetGuid; + return true; +} + template void MetaCoreForEachSelectedGameObject(MetaCoreEditorContext& editorContext, TAccessor&& accessor); @@ -376,6 +697,7 @@ void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorCon return; } + MetaCoreMeshRendererComponent& meshRenderer = *gameObject.MeshRenderer; const MetaCoreGameObject* prefabObject = nullptr; if (const auto prefabDocument = MetaCoreLoadPrefabDocumentForInstance(editorContext, gameObject); prefabDocument.has_value() && gameObject.PrefabInstance.has_value()) { @@ -387,6 +709,186 @@ void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorCon ImGui::TextDisabled("Multi-edit %zu objects", editorContext.GetSelectedObjectIds().size()); } + int meshSourceIndex = static_cast(meshRenderer.MeshSource); + const char* meshSourceItems[] = {"Builtin", "Asset"}; + if (ImGui::Combo("Mesh Source", &meshSourceIndex, meshSourceItems, IM_ARRAYSIZE(meshSourceItems))) { + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); + const MetaCoreMeshSourceKind meshSource = static_cast(meshSourceIndex); + MetaCoreApplyValueToSelectedObjects( + editorContext, + [](MetaCoreGameObject& selectedObject) -> MetaCoreMeshSourceKind* { + return selectedObject.MeshRenderer.has_value() ? &selectedObject.MeshRenderer->MeshSource : nullptr; + }, + meshSource + ); + } + + if (meshRenderer.MeshSource == MetaCoreMeshSourceKind::Builtin) { + int builtinMeshIndex = static_cast(meshRenderer.BuiltinMesh); + const char* builtinMeshItems[] = {"Cube"}; + if (ImGui::Combo("Builtin Mesh", &builtinMeshIndex, builtinMeshItems, IM_ARRAYSIZE(builtinMeshItems))) { + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); + const MetaCoreBuiltinMeshType builtinMesh = static_cast(builtinMeshIndex); + MetaCoreApplyValueToSelectedObjects( + editorContext, + [](MetaCoreGameObject& selectedObject) -> MetaCoreBuiltinMeshType* { + return selectedObject.MeshRenderer.has_value() ? &selectedObject.MeshRenderer->BuiltinMesh : nullptr; + }, + builtinMesh + ); + } + } else { + const auto meshChoices = MetaCoreCollectMeshAssetChoices(editorContext); + MetaCoreAssetGuid meshAssetGuid = meshRenderer.MeshAssetGuid; + if (MetaCoreDrawGeneratedAssetCombo("Mesh Asset", meshChoices, meshAssetGuid)) { + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); + MetaCoreApplyValueToSelectedObjects( + editorContext, + [&](MetaCoreGameObject& selectedObject) -> MetaCoreAssetGuid* { + return selectedObject.MeshRenderer.has_value() ? &selectedObject.MeshRenderer->MeshAssetGuid : nullptr; + }, + meshAssetGuid + ); + } + if (MetaCoreEditAssetGuidField("Mesh Asset Guid", meshAssetGuid)) { + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); + MetaCoreApplyValueToSelectedObjects( + editorContext, + [&](MetaCoreGameObject& selectedObject) -> MetaCoreAssetGuid* { + return selectedObject.MeshRenderer.has_value() ? &selectedObject.MeshRenderer->MeshAssetGuid : nullptr; + }, + meshAssetGuid + ); + } + + int materialSlotCount = static_cast(meshRenderer.MaterialAssetGuids.size()); + if (ImGui::InputInt("Material Slot Count", &materialSlotCount)) { + materialSlotCount = std::max(materialSlotCount, 0); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); + MetaCoreForEachSelectedGameObject(editorContext, [&](MetaCoreGameObject& selectedObject) { + if (!selectedObject.MeshRenderer.has_value()) { + return; + } + selectedObject.MeshRenderer->MaterialAssetGuids.resize(static_cast(materialSlotCount)); + }); + } + + const auto materialChoices = MetaCoreCollectMaterialAssetChoices(editorContext); + for (std::size_t materialIndex = 0; materialIndex < meshRenderer.MaterialAssetGuids.size(); ++materialIndex) { + MetaCoreAssetGuid materialGuid = meshRenderer.MaterialAssetGuids[materialIndex]; + const std::string comboLabel = "Material Asset " + std::to_string(materialIndex); + if (MetaCoreDrawGeneratedAssetCombo(comboLabel.c_str(), materialChoices, materialGuid)) { + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); + MetaCoreForEachSelectedGameObject(editorContext, [&](MetaCoreGameObject& selectedObject) { + if (!selectedObject.MeshRenderer.has_value()) { + return; + } + if (selectedObject.MeshRenderer->MaterialAssetGuids.size() <= materialIndex) { + selectedObject.MeshRenderer->MaterialAssetGuids.resize(materialIndex + 1); + } + selectedObject.MeshRenderer->MaterialAssetGuids[materialIndex] = materialGuid; + }); + } + const std::string label = "Material Guid " + std::to_string(materialIndex); + if (MetaCoreEditAssetGuidField(label.c_str(), materialGuid)) { + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); + MetaCoreForEachSelectedGameObject(editorContext, [&](MetaCoreGameObject& selectedObject) { + if (!selectedObject.MeshRenderer.has_value()) { + return; + } + if (selectedObject.MeshRenderer->MaterialAssetGuids.size() <= materialIndex) { + selectedObject.MeshRenderer->MaterialAssetGuids.resize(materialIndex + 1); + } + selectedObject.MeshRenderer->MaterialAssetGuids[materialIndex] = materialGuid; + }); + } + + if (materialGuid.IsValid()) { + const std::string treeLabel = "编辑材质槽 " + std::to_string(materialIndex) + "##MaterialInspector_" + std::to_string(materialIndex); + if (ImGui::TreeNode(treeLabel.c_str())) { + const auto resolvedMaterial = MetaCoreResolveGeneratedMaterialDocument(editorContext, materialGuid); + if (!resolvedMaterial.has_value()) { + ImGui::TextDisabled("未找到对应材质资源"); + } else { + auto editableDocument = resolvedMaterial->Document; + auto& materialAsset = editableDocument.GeneratedMaterialAssets[resolvedMaterial->MaterialIndex]; + bool modified = false; + + ImGui::TextDisabled("材质: %s", materialAsset.Name.c_str()); + float baseColor[3] = {materialAsset.BaseColor.x, materialAsset.BaseColor.y, materialAsset.BaseColor.z}; + if (ImGui::ColorEdit3(("Base Color##Inline" + std::to_string(materialIndex)).c_str(), baseColor)) { + materialAsset.BaseColor = {baseColor[0], baseColor[1], baseColor[2]}; + modified = true; + } + if (ImGui::SliderFloat(("Metallic##Inline" + std::to_string(materialIndex)).c_str(), &materialAsset.Metallic, 0.0F, 1.0F)) { + modified = true; + } + if (ImGui::SliderFloat(("Roughness##Inline" + std::to_string(materialIndex)).c_str(), &materialAsset.Roughness, 0.0F, 1.0F)) { + modified = true; + } + if (ImGui::Checkbox(("Double Sided##Inline" + std::to_string(materialIndex)).c_str(), &materialAsset.DoubleSided)) { + modified = true; + } + + const char* alphaModeItems[] = {"Opaque", "Mask", "Blend"}; + int alphaModeIndex = static_cast(materialAsset.AlphaMode); + if (ImGui::Combo(("Alpha Mode##Inline" + std::to_string(materialIndex)).c_str(), &alphaModeIndex, alphaModeItems, IM_ARRAYSIZE(alphaModeItems))) { + materialAsset.AlphaMode = static_cast(alphaModeIndex); + modified = true; + } + if (materialAsset.AlphaMode == MetaCoreMaterialAlphaMode::Mask) { + if (ImGui::SliderFloat(("Alpha Cutoff##Inline" + std::to_string(materialIndex)).c_str(), &materialAsset.AlphaCutoff, 0.0F, 1.0F)) { + modified = true; + } + } + + const auto textureChoices = MetaCoreCollectTextureAssetChoices(editorContext); + MetaCoreAssetGuid baseColorTexture = materialAsset.BaseColorTexture; + if (MetaCoreDrawGeneratedAssetCombo(("Base Color Texture##Inline" + std::to_string(materialIndex)).c_str(), textureChoices, baseColorTexture)) { + materialAsset.BaseColorTexture = baseColorTexture; + modified = true; + } + MetaCoreAssetGuid metalRoughTexture = materialAsset.MetallicRoughnessTexture; + if (MetaCoreDrawGeneratedAssetCombo(("MetalRough Texture##Inline" + std::to_string(materialIndex)).c_str(), textureChoices, metalRoughTexture)) { + materialAsset.MetallicRoughnessTexture = metalRoughTexture; + modified = true; + } + MetaCoreAssetGuid normalTexture = materialAsset.NormalTexture; + if (MetaCoreDrawGeneratedAssetCombo(("Normal Texture##Inline" + std::to_string(materialIndex)).c_str(), textureChoices, normalTexture)) { + materialAsset.NormalTexture = normalTexture; + modified = true; + } + float emissiveColor[3] = {materialAsset.EmissiveColor.x, materialAsset.EmissiveColor.y, materialAsset.EmissiveColor.z}; + if (ImGui::ColorEdit3(("Emissive Color##Inline" + std::to_string(materialIndex)).c_str(), emissiveColor)) { + materialAsset.EmissiveColor = {emissiveColor[0], emissiveColor[1], emissiveColor[2]}; + modified = true; + } + MetaCoreAssetGuid emissiveTexture = materialAsset.EmissiveTexture; + if (MetaCoreDrawGeneratedAssetCombo(("Emissive Texture##Inline" + std::to_string(materialIndex)).c_str(), textureChoices, emissiveTexture)) { + materialAsset.EmissiveTexture = emissiveTexture; + modified = true; + } + MetaCoreAssetGuid aoTexture = materialAsset.AoTexture; + if (MetaCoreDrawGeneratedAssetCombo(("AO Texture##Inline" + std::to_string(materialIndex)).c_str(), textureChoices, aoTexture)) { + materialAsset.AoTexture = aoTexture; + modified = true; + } + + if (modified) { + if (MetaCoreWriteImportedGltfAssetDocument(editorContext, resolvedMaterial->AssetRecord, editableDocument)) { + MetaCoreApplyGeneratedMaterialPreviewToScene(editorContext, editableDocument); + ImGui::TextColored(ImVec4(0.32F, 0.85F, 0.42F, 1.0F), "已保存材质修改"); + } else { + ImGui::TextColored(ImVec4(0.92F, 0.32F, 0.28F, 1.0F), "材质修改保存失败"); + } + } + } + ImGui::TreePop(); + } + } + } + } + const auto sharedColor = MetaCoreGetSharedSelectedValue( editorContext, [](MetaCoreGameObject& selectedObject) -> std::optional { @@ -939,7 +1441,7 @@ bool MetaCoreDeserializeComponentValue( [[nodiscard]] bool MetaCoreIsPackagePath(const std::filesystem::path& path) { const std::string extension = path.extension().string(); - return extension == ".mcscene" || extension == ".mcprefab" || extension == ".mcasset" || extension == ".mcmeta"; + return extension == ".mcscene" || extension == ".mcprefab" || extension == ".mcasset" || extension == ".mcui" || extension == ".mcmeta"; } [[nodiscard]] bool MetaCoreIsScenePath(const std::filesystem::path& path) { @@ -962,12 +1464,20 @@ bool MetaCoreDeserializeComponentValue( return std::filesystem::path(sourcePath.string() + ".mcasset"); } +[[nodiscard]] bool MetaCoreIsGltfModelPath(const std::filesystem::path& path) { + const std::string extension = path.extension().string(); + return extension == ".gltf" || extension == ".glb"; +} + [[nodiscard]] std::string MetaCoreDetectImporterId(const std::filesystem::path& path) { const std::string extension = path.extension().string(); if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga") { return "TextureImporter"; } - if (extension == ".fbx" || extension == ".obj" || extension == ".gltf") { + if (MetaCoreIsGltfModelPath(path)) { + return "GltfModelImporter"; + } + if (extension == ".fbx" || extension == ".obj") { return "ModelImporter"; } if (extension == ".wav" || extension == ".ogg" || extension == ".mp3") { @@ -990,10 +1500,13 @@ bool MetaCoreDeserializeComponentValue( if (extension == ".mcprefab") { return "prefab"; } + if (extension == ".mcui") { + return "ui_document"; + } if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".tga") { return "texture"; } - if (extension == ".fbx" || extension == ".obj" || extension == ".gltf") { + if (extension == ".fbx" || extension == ".obj" || MetaCoreIsGltfModelPath(path)) { return "model"; } if (extension == ".wav" || extension == ".ogg" || extension == ".mp3") { @@ -1008,6 +1521,130 @@ bool MetaCoreDeserializeComponentValue( return "asset"; } +[[nodiscard]] MetaCoreImportedGltfAssetDocument MetaCoreBuildImportedGltfAssetDocument( + const std::filesystem::path& absoluteSourcePath, + const std::filesystem::path& relativeSourcePath, + std::uint64_t sourceHash +) { + MetaCoreImportedGltfAssetDocument document; + document.AssetType = "model"; + document.ImporterId = "GltfModelImporter"; + document.SourcePath = relativeSourcePath; + document.SourceHash = sourceHash; + document.SourceFormat = absoluteSourcePath.extension() == ".glb" ? "glb" : "gltf"; + + MetaCoreImportedGltfNodeDocument rootNode; + rootNode.Name = absoluteSourcePath.stem().string(); + document.Nodes.push_back(std::move(rootNode)); + + if (absoluteSourcePath.extension() == ".gltf") { + const auto sourceText = MetaCoreReadTextFile(absoluteSourcePath); + if (sourceText.has_value()) { + const std::regex meshNamePattern("\"meshes\"\\s*:\\s*\\[([\\s\\S]*?)\\]", std::regex::icase); + const std::regex materialNamePattern("\"materials\"\\s*:\\s*\\[([\\s\\S]*?)\\]", std::regex::icase); + const std::regex imageUriPattern("\"uri\"\\s*:\\s*\"([^\"]+)\""); + const std::regex namePattern("\"name\"\\s*:\\s*\"([^\"]+)\""); + std::smatch match; + + if (std::regex_search(*sourceText, match, meshNamePattern)) { + const std::string body = match[1].str(); + for (std::sregex_iterator it(body.begin(), body.end(), namePattern), end; it != end; ++it) { + MetaCoreImportedGltfMeshDocument meshDocument; + meshDocument.Name = (*it)[1].str(); + meshDocument.PrimitiveCount = 1; + meshDocument.MaterialSlots.push_back(static_cast(document.Materials.size())); + document.Meshes.push_back(std::move(meshDocument)); + } + } + if (std::regex_search(*sourceText, match, materialNamePattern)) { + const std::string body = match[1].str(); + for (std::sregex_iterator it(body.begin(), body.end(), namePattern), end; it != end; ++it) { + MetaCoreImportedGltfMaterialDocument materialDocument; + materialDocument.Name = (*it)[1].str(); + document.Materials.push_back(std::move(materialDocument)); + } + } + for (std::sregex_iterator it(sourceText->begin(), sourceText->end(), imageUriPattern), end; it != end; ++it) { + MetaCoreImportedGltfTextureDocument textureDocument; + textureDocument.SourcePath = std::filesystem::path((*it)[1].str()).lexically_normal(); + textureDocument.UsageHint = "Unknown"; + const std::int32_t textureIndex = static_cast(document.Textures.size()); + document.Textures.push_back(std::move(textureDocument)); + for (MetaCoreImportedGltfMaterialDocument& material : document.Materials) { + material.TextureIndices.push_back(textureIndex); + } + } + } + } + + if (document.Meshes.empty()) { + MetaCoreImportedGltfMeshDocument meshDocument; + meshDocument.Name = absoluteSourcePath.stem().string(); + meshDocument.PrimitiveCount = 1; + document.Meshes.push_back(std::move(meshDocument)); + } + + if (document.Materials.empty()) { + MetaCoreImportedGltfMaterialDocument materialDocument; + materialDocument.Name = absoluteSourcePath.stem().string() + "_Material"; + document.Materials.push_back(std::move(materialDocument)); + } + + for (const MetaCoreImportedGltfTextureDocument& importedTexture : document.Textures) { + MetaCoreTextureAssetDocument textureAsset; + textureAsset.AssetGuid = MetaCoreAssetGuid::Generate(); + textureAsset.Name = importedTexture.SourcePath.stem().string(); + textureAsset.SourcePath = importedTexture.SourcePath; + textureAsset.UsageHint = importedTexture.UsageHint; + document.GeneratedTextureAssets.push_back(std::move(textureAsset)); + } + + for (const MetaCoreImportedGltfMaterialDocument& importedMaterial : document.Materials) { + MetaCoreMaterialAssetDocument materialAsset; + materialAsset.AssetGuid = MetaCoreAssetGuid::Generate(); + materialAsset.Name = importedMaterial.Name.empty() ? absoluteSourcePath.stem().string() + "_Material" : importedMaterial.Name; + materialAsset.DoubleSided = importedMaterial.DoubleSided; + if (importedMaterial.AlphaMode == "Mask") { + materialAsset.AlphaMode = MetaCoreMaterialAlphaMode::Mask; + } else if (importedMaterial.AlphaMode == "Blend") { + materialAsset.AlphaMode = MetaCoreMaterialAlphaMode::Blend; + } + if (!importedMaterial.TextureIndices.empty()) { + const std::int32_t textureIndex = importedMaterial.TextureIndices.front(); + if (textureIndex >= 0 && static_cast(textureIndex) < document.GeneratedTextureAssets.size()) { + materialAsset.BaseColorTexture = document.GeneratedTextureAssets[static_cast(textureIndex)].AssetGuid; + } + } + document.GeneratedMaterialAssets.push_back(std::move(materialAsset)); + } + + for (const MetaCoreImportedGltfMeshDocument& importedMesh : document.Meshes) { + MetaCoreMeshAssetDocument meshAsset; + meshAsset.AssetGuid = MetaCoreAssetGuid::Generate(); + meshAsset.Name = importedMesh.Name.empty() ? absoluteSourcePath.stem().string() : importedMesh.Name; + meshAsset.VertexCount = 0; + meshAsset.IndexCount = 0; + for (std::int32_t materialSlot : importedMesh.MaterialSlots) { + MetaCoreMeshSubMeshDocument subMesh; + subMesh.Name = importedMesh.Name.empty() ? "SubMesh" : importedMesh.Name + "_SubMesh"; + subMesh.MaterialSlotIndex = materialSlot; + meshAsset.SubMeshes.push_back(std::move(subMesh)); + } + if (meshAsset.SubMeshes.empty()) { + meshAsset.SubMeshes.push_back(MetaCoreMeshSubMeshDocument{"DefaultSubMesh", 0}); + } + document.GeneratedMeshAssets.push_back(std::move(meshAsset)); + } + + if (document.Nodes.size() == 1 && + document.Nodes.front().MeshIndex < 0 && + !document.GeneratedMeshAssets.empty()) { + document.Nodes.front().MeshIndex = 0; + } + + return document; +} + [[nodiscard]] MetaCorePackageDocument MetaCoreBuildTypedPackage( MetaCorePackageType packageType, const MetaCoreAssetGuid& assetGuid, @@ -1275,7 +1912,10 @@ void MetaCoreBuiltinAssetDatabaseService::LoadProjectDescriptor() { Project_.RootPath = *projectRoot; Project_.AssetsPath = Project_.RootPath / "Assets"; Project_.ScenesPath = Project_.RootPath / "Scenes"; + Project_.RuntimePath = Project_.RootPath / "Runtime"; + Project_.UiPath = Project_.RootPath / "Ui"; Project_.LibraryPath = Project_.RootPath / "Library"; + Project_.BuildPath = Project_.RootPath / "Build"; Project_.Name = "MetaCoreProject"; Project_.Version = "0.1.0"; @@ -1290,12 +1930,27 @@ void MetaCoreBuiltinAssetDatabaseService::LoadProjectDescriptor() { if (const auto startupScene = MetaCoreFindJsonStringValue(*projectDocument, "startup_scene"); startupScene.has_value()) { Project_.StartupScenePath = std::filesystem::path(*startupScene).lexically_normal(); } + if (const auto runtimeDir = MetaCoreFindJsonStringValue(*projectDocument, "runtime_directory"); runtimeDir.has_value()) { + Project_.RuntimePath = (Project_.RootPath / std::filesystem::path(*runtimeDir)).lexically_normal(); + } + if (const auto uiDir = MetaCoreFindJsonStringValue(*projectDocument, "ui_directory"); uiDir.has_value()) { + Project_.UiPath = (Project_.RootPath / std::filesystem::path(*uiDir)).lexically_normal(); + } + if (const auto buildDir = MetaCoreFindJsonStringValue(*projectDocument, "build_directory"); buildDir.has_value()) { + Project_.BuildPath = (Project_.RootPath / std::filesystem::path(*buildDir)).lexically_normal(); + } for (const std::string& scenePath : MetaCoreFindJsonStringArray(*projectDocument, "scenes")) { Project_.ScenePaths.push_back(std::filesystem::path(scenePath).lexically_normal()); } } RefreshScenePathsFromDisk(); + (void)std::filesystem::create_directories(Project_.AssetsPath); + (void)std::filesystem::create_directories(Project_.ScenesPath); + (void)std::filesystem::create_directories(Project_.RuntimePath); + (void)std::filesystem::create_directories(Project_.UiPath); + (void)std::filesystem::create_directories(Project_.LibraryPath); + (void)std::filesystem::create_directories(Project_.BuildPath); } void MetaCoreBuiltinAssetDatabaseService::SaveProjectDescriptor() const { @@ -1311,6 +1966,9 @@ void MetaCoreBuiltinAssetDatabaseService::SaveProjectDescriptor() const { output << "{\n"; output << " \"name\": \"" << MetaCoreEscapeJsonString(Project_.Name) << "\",\n"; output << " \"version\": \"" << MetaCoreEscapeJsonString(Project_.Version) << "\",\n"; + output << " \"runtime_directory\": \"" << MetaCoreEscapeJsonString(MetaCorePathToPortableString(Project_.RuntimePath.lexically_relative(Project_.RootPath))) << "\",\n"; + output << " \"ui_directory\": \"" << MetaCoreEscapeJsonString(MetaCorePathToPortableString(Project_.UiPath.lexically_relative(Project_.RootPath))) << "\",\n"; + output << " \"build_directory\": \"" << MetaCoreEscapeJsonString(MetaCorePathToPortableString(Project_.BuildPath.lexically_relative(Project_.RootPath))) << "\",\n"; output << " \"startup_scene\": \"" << MetaCoreEscapeJsonString(MetaCorePathToPortableString(Project_.StartupScenePath)) << "\",\n"; output << " \"scenes\": [\n"; for (std::size_t index = 0; index < Project_.ScenePaths.size(); ++index) { @@ -1383,7 +2041,7 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { } const std::filesystem::path relativePath = entry.path().lexically_relative(Project_.RootPath); - if (entry.path().extension() == ".mcasset" || entry.path().extension() == ".mcprefab") { + if (entry.path().extension() == ".mcasset" || entry.path().extension() == ".mcprefab" || entry.path().extension() == ".mcui") { const auto package = PackageService_->ReadPackage(entry.path()); if (!package.has_value()) { continue; @@ -1795,13 +2453,22 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste } if (needsPackageWrite) { - MetaCoreImportedAssetDocument importedAsset; - importedAsset.AssetType = metadata.AssetType; - importedAsset.ImporterId = metadata.ImporterId; - importedAsset.SourcePath = metadata.SourcePath; - importedAsset.SourceHash = metadata.SourceHash; + std::optional> importedAssetBytes; + std::string importedAssetTypeName = "MetaCoreImportedAssetDocument"; + if (metadata.ImporterId == "GltfModelImporter") { + MetaCoreImportedGltfAssetDocument importedAsset = + MetaCoreBuildImportedGltfAssetDocument(absoluteSourcePath, metadata.SourcePath, metadata.SourceHash); + importedAssetBytes = MetaCoreSerializeToBytes(importedAsset, registry); + importedAssetTypeName = "MetaCoreImportedGltfAssetDocument"; + } else { + MetaCoreImportedAssetDocument importedAsset; + importedAsset.AssetType = metadata.AssetType; + importedAsset.ImporterId = metadata.ImporterId; + importedAsset.SourcePath = metadata.SourcePath; + importedAsset.SourceHash = metadata.SourceHash; + importedAssetBytes = MetaCoreSerializeToBytes(importedAsset, registry); + } - const auto importedAssetBytes = MetaCoreSerializeToBytes(importedAsset, registry); const auto metadataBytes = MetaCoreSerializeToBytes(metadata, registry); if (!importedAssetBytes.has_value() || !metadataBytes.has_value()) { return false; @@ -1811,7 +2478,7 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste MetaCorePackageType::Asset, metadata.AssetGuid, absoluteSourcePath.filename().string(), - "MetaCoreImportedAssetDocument", + importedAssetTypeName, metadata.SourceHash, *importedAssetBytes ); @@ -2111,18 +2778,41 @@ public: class MetaCoreBuiltinSceneEditingService final : public MetaCoreISceneEditingService { public: [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.SceneEditing"; } + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + AssetDatabaseService_ = moduleRegistry.ResolveService(); + PackageService_ = moduleRegistry.ResolveService(); + ReflectionRegistry_ = moduleRegistry.ResolveService(); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + AssetDatabaseService_.reset(); + PackageService_.reset(); + ReflectionRegistry_.reset(); + } + [[nodiscard]] std::optional CreateGameObject( MetaCoreEditorContext& editorContext, const std::string& objectName, bool withMeshRenderer, std::optional forcedParentId ) override; + [[nodiscard]] std::optional InstantiateModelAsset( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& assetGuid, + std::optional forcedParentId + ) override; [[nodiscard]] bool RenameGameObject( MetaCoreEditorContext& editorContext, MetaCoreId objectId, const std::string& name ) override; [[nodiscard]] bool ReparentSelection(MetaCoreEditorContext& editorContext, MetaCoreId newParentId) override; + +private: + std::shared_ptr AssetDatabaseService_{}; + std::shared_ptr PackageService_{}; + std::shared_ptr ReflectionRegistry_{}; }; class MetaCoreBuiltinPrefabService final : public MetaCoreIPrefabService { @@ -2798,6 +3488,198 @@ std::optional MetaCoreBuiltinSceneEditingService::CreateGameObject( return createdObjectId; } +std::optional MetaCoreBuiltinSceneEditingService::InstantiateModelAsset( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& assetGuid, + std::optional forcedParentId +) { + if (AssetDatabaseService_ == nullptr || + PackageService_ == nullptr || + ReflectionRegistry_ == nullptr || + !AssetDatabaseService_->HasProject()) { + return std::nullopt; + } + + const auto assetRecord = AssetDatabaseService_->FindAssetByGuid(assetGuid); + if (!assetRecord.has_value() || assetRecord->Type != "model") { + return std::nullopt; + } + + const std::filesystem::path relativePackagePath = + !assetRecord->PackagePath.empty() ? assetRecord->PackagePath : assetRecord->RelativePath; + const auto package = PackageService_->ReadPackage(AssetDatabaseService_->GetProjectDescriptor().RootPath / relativePackagePath); + if (!package.has_value()) { + return std::nullopt; + } + + const auto importedDocument = MetaCoreReadTypedPayload( + *package, + ReflectionRegistry_->GetTypeRegistry(), + "MetaCoreImportedGltfAssetDocument" + ); + if (!importedDocument.has_value() || importedDocument->Nodes.empty()) { + return std::nullopt; + } + + MetaCoreId instantiatedRootId = 0; + const bool instantiated = editorContext.ExecuteSnapshotCommand("实例化模型资源", [&]() { + std::vector createdIds; + createdIds.reserve(importedDocument->Nodes.size()); + const MetaCoreId externalParentId = forcedParentId.value_or(editorContext.GetActiveObjectId()); + + for (std::size_t nodeIndex = 0; nodeIndex < importedDocument->Nodes.size(); ++nodeIndex) { + const MetaCoreImportedGltfNodeDocument& sourceNode = importedDocument->Nodes[nodeIndex]; + const std::int32_t resolvedMeshIndex = + sourceNode.MeshIndex >= 0 + ? sourceNode.MeshIndex + : ((importedDocument->Nodes.size() == 1 && !importedDocument->GeneratedMeshAssets.empty()) ? 0 : -1); + MetaCoreId parentId = 0; + if (sourceNode.ParentIndex >= 0 && static_cast(sourceNode.ParentIndex) < createdIds.size()) { + parentId = createdIds[static_cast(sourceNode.ParentIndex)]; + } else if (nodeIndex == 0 && externalParentId != 0 && editorContext.GetScene().FindGameObject(externalParentId) != nullptr) { + parentId = externalParentId; + } + + const std::string objectName = sourceNode.Name.empty() + ? assetRecord->RelativePath.stem().string() + : sourceNode.Name; + MetaCoreGameObject& object = editorContext.GetScene().CreateGameObject(objectName, parentId); + + if (resolvedMeshIndex >= 0 && + static_cast(resolvedMeshIndex) < importedDocument->GeneratedMeshAssets.size()) { + object.MeshRenderer = MetaCoreMeshRendererComponent{}; + object.MeshRenderer->MeshSource = MetaCoreMeshSourceKind::Asset; + object.MeshRenderer->MeshAssetGuid = + importedDocument->GeneratedMeshAssets[static_cast(resolvedMeshIndex)].AssetGuid; + object.MeshRenderer->SourceModelPath = + (!assetRecord->SourcePath.empty() ? assetRecord->SourcePath : assetRecord->RelativePath).generic_string(); + + if (static_cast(resolvedMeshIndex) < importedDocument->Meshes.size()) { + const auto& importedMesh = importedDocument->Meshes[static_cast(resolvedMeshIndex)]; + for (const std::int32_t materialSlot : importedMesh.MaterialSlots) { + if (materialSlot >= 0 && + static_cast(materialSlot) < importedDocument->GeneratedMaterialAssets.size()) { + object.MeshRenderer->MaterialAssetGuids.push_back( + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].AssetGuid + ); + object.MeshRenderer->BaseColor = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].BaseColor; + object.MeshRenderer->DoubleSided = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].DoubleSided; + object.MeshRenderer->Metallic = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].Metallic; + object.MeshRenderer->Roughness = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].Roughness; + object.MeshRenderer->AlphaCutoff = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].AlphaCutoff; + object.MeshRenderer->AlphaMode = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].AlphaMode == + MetaCoreMaterialAlphaMode::Mask + ? MetaCoreMeshAlphaMode::Mask + : (importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].AlphaMode == + MetaCoreMaterialAlphaMode::Blend + ? MetaCoreMeshAlphaMode::Blend + : MetaCoreMeshAlphaMode::Opaque); + object.MeshRenderer->EmissiveColor = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].EmissiveColor; + const MetaCoreAssetGuid textureGuid = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].BaseColorTexture; + if (textureGuid.IsValid()) { + const auto textureIterator = std::find_if( + importedDocument->GeneratedTextureAssets.begin(), + importedDocument->GeneratedTextureAssets.end(), + [&](const MetaCoreTextureAssetDocument& textureAsset) { + return textureAsset.AssetGuid == textureGuid; + } + ); + if (textureIterator != importedDocument->GeneratedTextureAssets.end()) { + object.MeshRenderer->BaseColorTexturePath = textureIterator->SourcePath.generic_string(); + } + } + const MetaCoreAssetGuid metallicRoughnessTextureGuid = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].MetallicRoughnessTexture; + if (metallicRoughnessTextureGuid.IsValid()) { + const auto textureIterator = std::find_if( + importedDocument->GeneratedTextureAssets.begin(), + importedDocument->GeneratedTextureAssets.end(), + [&](const MetaCoreTextureAssetDocument& textureAsset) { + return textureAsset.AssetGuid == metallicRoughnessTextureGuid; + } + ); + if (textureIterator != importedDocument->GeneratedTextureAssets.end()) { + object.MeshRenderer->MetallicRoughnessTexturePath = textureIterator->SourcePath.generic_string(); + } + } + const MetaCoreAssetGuid normalTextureGuid = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].NormalTexture; + if (normalTextureGuid.IsValid()) { + const auto textureIterator = std::find_if( + importedDocument->GeneratedTextureAssets.begin(), + importedDocument->GeneratedTextureAssets.end(), + [&](const MetaCoreTextureAssetDocument& textureAsset) { + return textureAsset.AssetGuid == normalTextureGuid; + } + ); + if (textureIterator != importedDocument->GeneratedTextureAssets.end()) { + object.MeshRenderer->NormalTexturePath = textureIterator->SourcePath.generic_string(); + } + } + const MetaCoreAssetGuid emissiveTextureGuid = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].EmissiveTexture; + if (emissiveTextureGuid.IsValid()) { + const auto textureIterator = std::find_if( + importedDocument->GeneratedTextureAssets.begin(), + importedDocument->GeneratedTextureAssets.end(), + [&](const MetaCoreTextureAssetDocument& textureAsset) { + return textureAsset.AssetGuid == emissiveTextureGuid; + } + ); + if (textureIterator != importedDocument->GeneratedTextureAssets.end()) { + object.MeshRenderer->EmissiveTexturePath = textureIterator->SourcePath.generic_string(); + } + } + const MetaCoreAssetGuid aoTextureGuid = + importedDocument->GeneratedMaterialAssets[static_cast(materialSlot)].AoTexture; + if (aoTextureGuid.IsValid()) { + const auto textureIterator = std::find_if( + importedDocument->GeneratedTextureAssets.begin(), + importedDocument->GeneratedTextureAssets.end(), + [&](const MetaCoreTextureAssetDocument& textureAsset) { + return textureAsset.AssetGuid == aoTextureGuid; + } + ); + if (textureIterator != importedDocument->GeneratedTextureAssets.end()) { + object.MeshRenderer->AoTexturePath = textureIterator->SourcePath.generic_string(); + } + } + } + } + } + } + + createdIds.push_back(object.Id); + if (nodeIndex == 0) { + instantiatedRootId = object.Id; + } + } + + if (instantiatedRootId == 0) { + return false; + } + + editorContext.SelectOnly(instantiatedRootId); + editorContext.SetSelectionAnchorId(instantiatedRootId); + return true; + }); + + if (!instantiated || instantiatedRootId == 0) { + return std::nullopt; + } + + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "已实例化模型资源"); + return instantiatedRootId; +} + bool MetaCoreBuiltinSceneEditingService::RenameGameObject( MetaCoreEditorContext& editorContext, MetaCoreId objectId, diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index 2f687ff..1854f16 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -13,6 +13,7 @@ #include +#include #include #include #include @@ -20,7 +21,9 @@ #include #include #include +#include #include +#include #include namespace MetaCore { @@ -145,6 +148,18 @@ namespace { return prefabService->InstantiatePrefab(editorContext, prefabAssetGuid, forcedParentId); } +[[nodiscard]] std::optional MetaCoreInstantiateModelAsset( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& modelAssetGuid, + std::optional forcedParentId +) { + const auto sceneEditingService = editorContext.GetModuleRegistry().ResolveService(); + if (sceneEditingService == nullptr) { + return std::nullopt; + } + return sceneEditingService->InstantiateModelAsset(editorContext, modelAssetGuid, forcedParentId); +} + [[nodiscard]] bool MetaCoreApplySelectedPrefabInstance(MetaCoreEditorContext& editorContext) { const auto prefabService = editorContext.GetModuleRegistry().ResolveService(); return prefabService != nullptr && prefabService->ApplySelectedPrefabInstance(editorContext); @@ -325,6 +340,431 @@ template return value; } +[[nodiscard]] bool MetaCoreWriteImportedGltfAssetDocument( + MetaCoreIAssetDatabaseService& assetDatabaseService, + MetaCoreIPackageService& packageService, + MetaCoreIReflectionRegistry& reflectionRegistry, + const MetaCoreAssetRecord& assetRecord, + const MetaCoreImportedGltfAssetDocument& document, + MetaCorePackageType packageType +) { + const auto payload = MetaCoreSerializeToBytes(document, reflectionRegistry.GetTypeRegistry()); + if (!payload.has_value()) { + return false; + } + + const std::filesystem::path relativePackagePath = + !assetRecord.PackagePath.empty() ? assetRecord.PackagePath : assetRecord.RelativePath; + MetaCorePackageDocument package = MetaCoreBuildTypedPackage( + packageType, + assetRecord.Guid, + relativePackagePath.filename().string(), + "MetaCoreImportedGltfAssetDocument", + document.SourceHash, + *payload + ); + return packageService.WritePackage( + assetDatabaseService.GetProjectDescriptor().RootPath / relativePackagePath, + std::move(package) + ); +} + +template +[[nodiscard]] bool MetaCoreWriteTypedAssetDocument( + MetaCoreIAssetDatabaseService& assetDatabaseService, + MetaCoreIPackageService& packageService, + MetaCoreIReflectionRegistry& reflectionRegistry, + const MetaCoreAssetRecord& assetRecord, + const TDocument& document, + std::string_view typeName, + MetaCorePackageType packageType = MetaCorePackageType::Asset +) { + const auto payload = MetaCoreSerializeToBytes(document, reflectionRegistry.GetTypeRegistry()); + if (!payload.has_value()) { + return false; + } + + const std::filesystem::path relativePackagePath = + !assetRecord.PackagePath.empty() ? assetRecord.PackagePath : assetRecord.RelativePath; + MetaCorePackageDocument package = MetaCoreBuildTypedPackage( + packageType, + assetRecord.Guid, + relativePackagePath.filename().string(), + typeName, + 0, + *payload + ); + return packageService.WritePackage( + assetDatabaseService.GetProjectDescriptor().RootPath / relativePackagePath, + std::move(package) + ); +} + +[[nodiscard]] std::filesystem::path MetaCoreBuildUniqueUiDocumentPath( + const MetaCoreProjectDescriptor& projectDescriptor, + const std::filesystem::path& relativeDirectory +) { + const std::filesystem::path baseDirectory = + relativeDirectory.empty() || relativeDirectory.string().rfind("Assets", 0) != 0 + ? std::filesystem::path("Assets") / "Ui" + : relativeDirectory; + + std::filesystem::path candidate = baseDirectory / "UiDocument.mcui"; + std::size_t suffix = 1; + while (std::filesystem::exists(projectDescriptor.RootPath / candidate)) { + candidate = baseDirectory / ("UiDocument" + std::to_string(suffix++) + ".mcui"); + } + return candidate.lexically_normal(); +} + +[[nodiscard]] bool MetaCoreCreateUiDocumentAsset( + MetaCoreEditorContext& editorContext, + const std::filesystem::path& relativeDirectory, + std::filesystem::path& createdRelativePath, + MetaCoreAssetGuid& createdAssetGuid +) { + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + const auto packageService = editorContext.GetModuleRegistry().ResolveService(); + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + return false; + } + + createdRelativePath = MetaCoreBuildUniqueUiDocumentPath(assetDatabaseService->GetProjectDescriptor(), relativeDirectory); + (void)std::filesystem::create_directories((assetDatabaseService->GetProjectDescriptor().RootPath / createdRelativePath).parent_path()); + createdAssetGuid = MetaCoreAssetGuid::Generate(); + + MetaCoreUiDocument document; + document.Name = createdRelativePath.stem().string(); + + const auto payload = MetaCoreSerializeToBytes(document, reflectionRegistry->GetTypeRegistry()); + if (!payload.has_value()) { + return false; + } + + MetaCorePackageDocument package = MetaCoreBuildTypedPackage( + MetaCorePackageType::Asset, + createdAssetGuid, + createdRelativePath.filename().string(), + "MetaCoreUiDocument", + 0, + *payload + ); + + if (!packageService->WritePackage(assetDatabaseService->GetProjectDescriptor().RootPath / createdRelativePath, std::move(package))) { + return false; + } + + return assetDatabaseService->Refresh(); +} + +[[nodiscard]] std::string MetaCoreMakeUniqueUiNodeId(const MetaCoreUiDocument& document, std::string_view prefix) { + std::string base = std::string(prefix); + if (base.empty()) { + base = "node"; + } + + std::size_t suffix = 1; + std::string candidate = base; + const auto exists = [&](const std::string& id) { + return std::any_of(document.Nodes.begin(), document.Nodes.end(), [&](const MetaCoreUiNodeDocument& node) { + return node.Id == id; + }); + }; + + while (exists(candidate)) { + candidate = base + "." + std::to_string(suffix++); + } + return candidate; +} + +[[nodiscard]] MetaCoreUiNodeDocument* MetaCoreFindUiNode(MetaCoreUiDocument& document, const std::string& nodeId) { + const auto iterator = std::find_if(document.Nodes.begin(), document.Nodes.end(), [&](MetaCoreUiNodeDocument& node) { + return node.Id == nodeId; + }); + return iterator == document.Nodes.end() ? nullptr : &(*iterator); +} + +[[nodiscard]] const MetaCoreUiNodeDocument* MetaCoreFindUiNode(const MetaCoreUiDocument& document, const std::string& nodeId) { + const auto iterator = std::find_if(document.Nodes.begin(), document.Nodes.end(), [&](const MetaCoreUiNodeDocument& node) { + return node.Id == nodeId; + }); + return iterator == document.Nodes.end() ? nullptr : &(*iterator); +} + +[[nodiscard]] bool MetaCoreUiNodeIsDescendant( + const MetaCoreUiDocument& document, + const std::string& nodeId, + const std::string& candidateDescendantId +) { + const MetaCoreUiNodeDocument* node = MetaCoreFindUiNode(document, nodeId); + if (node == nullptr) { + return false; + } + + for (const std::string& childId : node->Children) { + if (childId == candidateDescendantId || MetaCoreUiNodeIsDescendant(document, childId, candidateDescendantId)) { + return true; + } + } + return false; +} + +void MetaCoreCollectUiNodeDescendants( + const MetaCoreUiDocument& document, + const std::string& rootNodeId, + std::unordered_set& nodeIds +) { + if (!nodeIds.insert(rootNodeId).second) { + return; + } + + const MetaCoreUiNodeDocument* node = MetaCoreFindUiNode(document, rootNodeId); + if (node == nullptr) { + return; + } + + for (const std::string& childId : node->Children) { + MetaCoreCollectUiNodeDescendants(document, childId, nodeIds); + } +} + +[[nodiscard]] bool MetaCoreReparentUiNode( + MetaCoreUiDocument& document, + const std::string& nodeId, + const std::string& newParentId +) { + MetaCoreUiNodeDocument* node = MetaCoreFindUiNode(document, nodeId); + if (node == nullptr) { + return false; + } + if (nodeId == newParentId) { + return false; + } + if (!newParentId.empty() && MetaCoreUiNodeIsDescendant(document, nodeId, newParentId)) { + return false; + } + + if (node->ParentId.empty()) { + document.RootNodeIds.erase( + std::remove(document.RootNodeIds.begin(), document.RootNodeIds.end(), nodeId), + document.RootNodeIds.end() + ); + } else if (MetaCoreUiNodeDocument* oldParent = MetaCoreFindUiNode(document, node->ParentId); oldParent != nullptr) { + oldParent->Children.erase( + std::remove(oldParent->Children.begin(), oldParent->Children.end(), nodeId), + oldParent->Children.end() + ); + } + + node->ParentId = newParentId; + if (newParentId.empty()) { + document.RootNodeIds.push_back(nodeId); + } else if (MetaCoreUiNodeDocument* newParent = MetaCoreFindUiNode(document, newParentId); newParent != nullptr) { + newParent->Children.push_back(nodeId); + } else { + node->ParentId.clear(); + document.RootNodeIds.push_back(nodeId); + return false; + } + + return true; +} + +void MetaCoreApplyMaterialPreviewToScene( + MetaCoreEditorContext& editorContext, + const MetaCoreImportedGltfAssetDocument& document +) { + std::unordered_map materialBaseColors; + std::unordered_map texturePaths; + std::unordered_map doubleSidedFlags; + std::unordered_map metallicValues; + std::unordered_map roughnessValues; + std::unordered_map emissiveColors; + std::unordered_map alphaModes; + std::unordered_map alphaCutoffs; + for (const MetaCoreTextureAssetDocument& textureAsset : document.GeneratedTextureAssets) { + if (textureAsset.AssetGuid.IsValid()) { + texturePaths[textureAsset.AssetGuid] = textureAsset.SourcePath.generic_string(); + } + } + for (const MetaCoreMaterialAssetDocument& materialAsset : document.GeneratedMaterialAssets) { + if (materialAsset.AssetGuid.IsValid()) { + materialBaseColors[materialAsset.AssetGuid] = materialAsset.BaseColor; + doubleSidedFlags[materialAsset.AssetGuid] = materialAsset.DoubleSided; + metallicValues[materialAsset.AssetGuid] = materialAsset.Metallic; + roughnessValues[materialAsset.AssetGuid] = materialAsset.Roughness; + emissiveColors[materialAsset.AssetGuid] = materialAsset.EmissiveColor; + alphaModes[materialAsset.AssetGuid] = materialAsset.AlphaMode; + alphaCutoffs[materialAsset.AssetGuid] = materialAsset.AlphaCutoff; + } + } + + if (materialBaseColors.empty()) { + return; + } + + for (MetaCoreGameObject& gameObject : editorContext.GetScene().GetGameObjects()) { + if (!gameObject.MeshRenderer.has_value()) { + continue; + } + + for (const MetaCoreAssetGuid& materialGuid : gameObject.MeshRenderer->MaterialAssetGuids) { + const auto iterator = materialBaseColors.find(materialGuid); + if (iterator != materialBaseColors.end()) { + gameObject.MeshRenderer->BaseColor = iterator->second; + const auto materialDocumentIterator = std::find_if( + document.GeneratedMaterialAssets.begin(), + document.GeneratedMaterialAssets.end(), + [&](const MetaCoreMaterialAssetDocument& materialAsset) { + return materialAsset.AssetGuid == materialGuid; + } + ); + if (materialDocumentIterator != document.GeneratedMaterialAssets.end() && + materialDocumentIterator->BaseColorTexture.IsValid()) { + const auto textureIterator = texturePaths.find(materialDocumentIterator->BaseColorTexture); + gameObject.MeshRenderer->BaseColorTexturePath = + textureIterator != texturePaths.end() ? textureIterator->second : std::string{}; + } else { + gameObject.MeshRenderer->BaseColorTexturePath.clear(); + } + if (materialDocumentIterator != document.GeneratedMaterialAssets.end() && + materialDocumentIterator->MetallicRoughnessTexture.IsValid()) { + const auto textureIterator = texturePaths.find(materialDocumentIterator->MetallicRoughnessTexture); + gameObject.MeshRenderer->MetallicRoughnessTexturePath = + textureIterator != texturePaths.end() ? textureIterator->second : std::string{}; + } else { + gameObject.MeshRenderer->MetallicRoughnessTexturePath.clear(); + } + const auto doubleSidedIterator = doubleSidedFlags.find(materialGuid); + gameObject.MeshRenderer->DoubleSided = + doubleSidedIterator != doubleSidedFlags.end() ? doubleSidedIterator->second : true; + const auto metallicIterator = metallicValues.find(materialGuid); + gameObject.MeshRenderer->Metallic = + metallicIterator != metallicValues.end() ? metallicIterator->second : 0.0F; + const auto roughnessIterator = roughnessValues.find(materialGuid); + gameObject.MeshRenderer->Roughness = + roughnessIterator != roughnessValues.end() ? roughnessIterator->second : 1.0F; + const auto emissiveColorIterator = emissiveColors.find(materialGuid); + gameObject.MeshRenderer->EmissiveColor = + emissiveColorIterator != emissiveColors.end() ? emissiveColorIterator->second : glm::vec3(0.0F, 0.0F, 0.0F); + const auto alphaModeIterator = alphaModes.find(materialGuid); + gameObject.MeshRenderer->AlphaMode = + alphaModeIterator != alphaModes.end() + ? (alphaModeIterator->second == MetaCoreMaterialAlphaMode::Mask + ? MetaCoreMeshAlphaMode::Mask + : (alphaModeIterator->second == MetaCoreMaterialAlphaMode::Blend + ? MetaCoreMeshAlphaMode::Blend + : MetaCoreMeshAlphaMode::Opaque)) + : MetaCoreMeshAlphaMode::Opaque; + const auto alphaCutoffIterator = alphaCutoffs.find(materialGuid); + gameObject.MeshRenderer->AlphaCutoff = + alphaCutoffIterator != alphaCutoffs.end() ? alphaCutoffIterator->second : 0.5F; + if (materialDocumentIterator != document.GeneratedMaterialAssets.end() && + materialDocumentIterator->NormalTexture.IsValid()) { + const auto textureIterator = texturePaths.find(materialDocumentIterator->NormalTexture); + gameObject.MeshRenderer->NormalTexturePath = + textureIterator != texturePaths.end() ? textureIterator->second : std::string{}; + } else { + gameObject.MeshRenderer->NormalTexturePath.clear(); + } + if (materialDocumentIterator != document.GeneratedMaterialAssets.end() && + materialDocumentIterator->EmissiveTexture.IsValid()) { + const auto textureIterator = texturePaths.find(materialDocumentIterator->EmissiveTexture); + gameObject.MeshRenderer->EmissiveTexturePath = + textureIterator != texturePaths.end() ? textureIterator->second : std::string{}; + } else { + gameObject.MeshRenderer->EmissiveTexturePath.clear(); + } + if (materialDocumentIterator != document.GeneratedMaterialAssets.end() && + materialDocumentIterator->AoTexture.IsValid()) { + const auto textureIterator = texturePaths.find(materialDocumentIterator->AoTexture); + gameObject.MeshRenderer->AoTexturePath = + textureIterator != texturePaths.end() ? textureIterator->second : std::string{}; + } else { + gameObject.MeshRenderer->AoTexturePath.clear(); + } + break; + } + } + } +} + +struct MetaCoreGeneratedAssetChoice { + MetaCoreAssetGuid AssetGuid{}; + std::string Label{}; +}; + +[[nodiscard]] std::vector MetaCoreCollectTextureAssetChoices(MetaCoreEditorContext& editorContext) { + std::vector choices; + + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + const auto packageService = editorContext.GetModuleRegistry().ResolveService(); + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + return choices; + } + + for (const MetaCoreAssetRecord& record : assetDatabaseService->GetAssetRegistry()) { + if (record.Type != "model" || record.StorageKind != MetaCoreAssetStorageKind::SourcePackage) { + continue; + } + + const std::filesystem::path relativePackagePath = + !record.PackagePath.empty() ? record.PackagePath : record.RelativePath; + const auto package = packageService->ReadPackage(assetDatabaseService->GetProjectDescriptor().RootPath / relativePackagePath); + if (!package.has_value()) { + continue; + } + + const auto importedDocument = MetaCoreReadTypedPackagePayload( + *package, + reflectionRegistry->GetTypeRegistry(), + "MetaCoreImportedGltfAssetDocument" + ); + if (!importedDocument.has_value()) { + continue; + } + + for (const MetaCoreTextureAssetDocument& textureAsset : importedDocument->GeneratedTextureAssets) { + choices.push_back(MetaCoreGeneratedAssetChoice{ + textureAsset.AssetGuid, + record.RelativePath.filename().string() + " / Texture / " + textureAsset.Name + }); + } + } + + std::sort(choices.begin(), choices.end(), [](const MetaCoreGeneratedAssetChoice& lhs, const MetaCoreGeneratedAssetChoice& rhs) { + return lhs.Label < rhs.Label; + }); + return choices; +} + +[[nodiscard]] bool MetaCoreDrawGeneratedAssetCombo( + const char* label, + const std::vector& choices, + MetaCoreAssetGuid& assetGuid +) { + std::vector itemPointers; + itemPointers.reserve(choices.size() + 1); + itemPointers.push_back(""); + + int selectedIndex = 0; + for (std::size_t index = 0; index < choices.size(); ++index) { + itemPointers.push_back(choices[index].Label.c_str()); + if (choices[index].AssetGuid == assetGuid) { + selectedIndex = static_cast(index + 1); + } + } + + if (!ImGui::Combo(label, &selectedIndex, itemPointers.data(), static_cast(itemPointers.size()))) { + return false; + } + + assetGuid = selectedIndex == 0 ? MetaCoreAssetGuid{} : choices[static_cast(selectedIndex - 1)].AssetGuid; + return true; +} + [[nodiscard]] std::optional MetaCoreLoadPrefabDocumentForInstance( MetaCoreEditorContext& editorContext, const MetaCoreGameObject& gameObject @@ -1185,6 +1625,8 @@ public: void DrawPanel(MetaCoreEditorContext& editorContext) override { const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); + const auto packageService = editorContext.GetModuleRegistry().ResolveService(); + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) { ImGui::TextUnformatted("当前没有可用项目。"); return; @@ -1200,6 +1642,26 @@ public: if (ImGui::Button("刷新##ProjectAssets")) { MetaCoreHandleReloadAssets(editorContext); } + ImGui::SameLine(); + if (ImGui::Button("新建UI文档##ProjectAssets")) { + std::filesystem::path createdRelativePath; + MetaCoreAssetGuid createdAssetGuid{}; + if (MetaCoreCreateUiDocumentAsset(editorContext, SelectedDirectory_, createdRelativePath, createdAssetGuid)) { + SelectedDirectory_ = createdRelativePath.parent_path(); + SelectedAssetGuid_ = createdAssetGuid; + SelectedAssetPath_ = createdRelativePath; + SelectedAssetType_ = "ui_document"; + SelectedAssetStorageKind_ = MetaCoreAssetStorageKind::SourcePackage; + SelectedGeneratedResourceGuid_ = MetaCoreAssetGuid{}; + SelectedGeneratedResourceKind_.clear(); + GeneratedResourceFilter_.clear(); + GeneratedResourceKindFilter_.clear(); + SelectedUiNodeId_.clear(); + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "UI", "已创建 UI 文档"); + } else { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "UI", "创建 UI 文档失败"); + } + } ImGui::Columns(2, "ProjectColumns", true); if (ImGui::IsWindowAppearing()) { @@ -1227,16 +1689,29 @@ public: const bool isCurrentScene = scenePersistenceService != nullptr && scenePersistenceService->GetCurrentScenePath() == record.RelativePath; + const bool isSelectedAsset = SelectedAssetGuid_ == record.Guid; if (isCurrentScene) { assetLabel += scenePersistenceService->IsSceneDirty() ? " *" : " (Open)"; } - if (ImGui::Selectable(assetLabel.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::Selectable(assetLabel.c_str(), isSelectedAsset, ImGuiSelectableFlags_AllowDoubleClick)) { + if (SelectedAssetGuid_ != record.Guid) { + SelectedGeneratedResourceGuid_ = MetaCoreAssetGuid{}; + SelectedGeneratedResourceKind_.clear(); + GeneratedResourceFilter_.clear(); + SelectedUiNodeId_.clear(); + } + SelectedAssetGuid_ = record.Guid; + SelectedAssetPath_ = record.RelativePath; + SelectedAssetType_ = record.Type; + SelectedAssetStorageKind_ = record.StorageKind; if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && record.StorageKind == MetaCoreAssetStorageKind::SourcePackage) { if (record.Type == "scene" && scenePersistenceService != nullptr) { (void)scenePersistenceService->LoadScene(editorContext, record.RelativePath); + } else if (record.Type == "model") { + (void)MetaCoreInstantiateModelAsset(editorContext, record.Guid, std::nullopt); } else if (record.Type == "prefab") { (void)MetaCoreInstantiatePrefab(editorContext, record.Guid, std::nullopt); } @@ -1244,10 +1719,955 @@ public: } } + ImGui::Separator(); + if (!SelectedAssetGuid_.IsValid()) { + SelectedGeneratedResourceGuid_ = MetaCoreAssetGuid{}; + SelectedGeneratedResourceKind_.clear(); + ImGui::TextDisabled("选择资源以查看可用操作。"); + } else { + ImGui::Text("当前资源: %s", SelectedAssetPath_.generic_string().c_str()); + ImGui::TextDisabled("类型: %s", SelectedAssetType_.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled("| 存储: %s", MetaCoreAssetStorageLabel(SelectedAssetStorageKind_)); + + if (SelectedGeneratedResourceGuid_.IsValid()) { + ImGui::Separator(); + ImGui::Text("当前子资源入口"); + ImGui::SameLine(); + ImGui::TextDisabled("[%s]", SelectedGeneratedResourceKind_.empty() ? "unknown" : SelectedGeneratedResourceKind_.c_str()); + if (packageService != nullptr && reflectionRegistry != nullptr && SelectedAssetType_ == "model") { + const auto relativePackagePath = + assetDatabaseService->FindAssetByGuid(SelectedAssetGuid_)->PackagePath.empty() + ? assetDatabaseService->FindAssetByGuid(SelectedAssetGuid_)->RelativePath + : assetDatabaseService->FindAssetByGuid(SelectedAssetGuid_)->PackagePath; + const auto selectedPackage = packageService->ReadPackage( + assetDatabaseService->GetProjectDescriptor().RootPath / relativePackagePath + ); + if (selectedPackage.has_value()) { + const auto selectedDocument = MetaCoreReadTypedPackagePayload( + *selectedPackage, + reflectionRegistry->GetTypeRegistry(), + "MetaCoreImportedGltfAssetDocument" + ); + if (selectedDocument.has_value()) { + std::string selectedName = ""; + int usageCount = 0; + std::vector siblingResourceGuids; + std::size_t selectedSiblingIndex = 0; + bool selectedSiblingFound = false; + if (SelectedGeneratedResourceKind_ == "mesh") { + siblingResourceGuids.reserve(selectedDocument->GeneratedMeshAssets.size()); + const auto iterator = std::find_if( + selectedDocument->GeneratedMeshAssets.begin(), + selectedDocument->GeneratedMeshAssets.end(), + [&](const MetaCoreMeshAssetDocument& meshAsset) { + return meshAsset.AssetGuid == SelectedGeneratedResourceGuid_; + } + ); + if (iterator != selectedDocument->GeneratedMeshAssets.end()) { + selectedName = iterator->Name; + } + for (const MetaCoreMeshAssetDocument& meshAsset : selectedDocument->GeneratedMeshAssets) { + siblingResourceGuids.push_back(meshAsset.AssetGuid); + if (meshAsset.AssetGuid == SelectedGeneratedResourceGuid_) { + selectedSiblingIndex = siblingResourceGuids.size() - 1; + selectedSiblingFound = true; + } + } + usageCount = static_cast(FindSceneObjectsUsingMesh(editorContext, SelectedGeneratedResourceGuid_).size()); + } else if (SelectedGeneratedResourceKind_ == "material") { + siblingResourceGuids.reserve(selectedDocument->GeneratedMaterialAssets.size()); + const auto iterator = std::find_if( + selectedDocument->GeneratedMaterialAssets.begin(), + selectedDocument->GeneratedMaterialAssets.end(), + [&](const MetaCoreMaterialAssetDocument& materialAsset) { + return materialAsset.AssetGuid == SelectedGeneratedResourceGuid_; + } + ); + if (iterator != selectedDocument->GeneratedMaterialAssets.end()) { + selectedName = iterator->Name; + } + for (const MetaCoreMaterialAssetDocument& materialAsset : selectedDocument->GeneratedMaterialAssets) { + siblingResourceGuids.push_back(materialAsset.AssetGuid); + if (materialAsset.AssetGuid == SelectedGeneratedResourceGuid_) { + selectedSiblingIndex = siblingResourceGuids.size() - 1; + selectedSiblingFound = true; + } + } + usageCount = static_cast(FindSceneObjectsUsingMaterial(editorContext, SelectedGeneratedResourceGuid_).size()); + } else if (SelectedGeneratedResourceKind_ == "texture") { + siblingResourceGuids.reserve(selectedDocument->GeneratedTextureAssets.size()); + const auto iterator = std::find_if( + selectedDocument->GeneratedTextureAssets.begin(), + selectedDocument->GeneratedTextureAssets.end(), + [&](const MetaCoreTextureAssetDocument& textureAsset) { + return textureAsset.AssetGuid == SelectedGeneratedResourceGuid_; + } + ); + if (iterator != selectedDocument->GeneratedTextureAssets.end()) { + selectedName = iterator->Name; + } + for (const MetaCoreTextureAssetDocument& textureAsset : selectedDocument->GeneratedTextureAssets) { + siblingResourceGuids.push_back(textureAsset.AssetGuid); + if (textureAsset.AssetGuid == SelectedGeneratedResourceGuid_) { + selectedSiblingIndex = siblingResourceGuids.size() - 1; + selectedSiblingFound = true; + } + } + usageCount = static_cast(FindSceneObjectsUsingTexture(editorContext, *selectedDocument, SelectedGeneratedResourceGuid_).size()); + } + + const char* resourceTag = "[RES]"; + if (SelectedGeneratedResourceKind_ == "mesh") { + resourceTag = "[Mesh]"; + } else if (SelectedGeneratedResourceKind_ == "material") { + resourceTag = "[Mat]"; + } else if (SelectedGeneratedResourceKind_ == "texture") { + resourceTag = "[Tex]"; + } + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.45F, 0.72F, 0.96F, 1.0F), "%s", resourceTag); + ImGui::SameLine(); + ImGui::Text("名称: %s", selectedName.c_str()); + ImGui::TextWrapped( + "虚拟路径: %s", + BuildGeneratedResourceVirtualPath( + SelectedAssetPath_, + SelectedGeneratedResourceKind_, + selectedName + ).c_str() + ); + ImGui::TextWrapped("来源: %s", SelectedAssetPath_.generic_string().c_str()); + ImGui::TextDisabled("引用对象: %d | Guid: %s", usageCount, SelectedGeneratedResourceGuid_.ToString().c_str()); + if (selectedSiblingFound && siblingResourceGuids.size() > 1) { + if (ImGui::Button("上一个同类资源")) { + const std::size_t previousIndex = + selectedSiblingIndex == 0 ? siblingResourceGuids.size() - 1 : selectedSiblingIndex - 1; + SelectedGeneratedResourceGuid_ = siblingResourceGuids[previousIndex]; + } + ImGui::SameLine(); + if (ImGui::Button("下一个同类资源")) { + const std::size_t nextIndex = (selectedSiblingIndex + 1) % siblingResourceGuids.size(); + SelectedGeneratedResourceGuid_ = siblingResourceGuids[nextIndex]; + } + } + ImGui::Separator(); + ImGui::TextDisabled("默认动作"); + if (SelectedGeneratedResourceKind_ == "mesh") { + if (ImGui::Button("实例化所属模型##GeneratedResourceContext")) { + (void)MetaCoreInstantiateModelAsset(editorContext, SelectedAssetGuid_, std::nullopt); + } + } else if (SelectedGeneratedResourceKind_ == "material") { + const auto referencedObjects = FindSceneObjectsUsingMaterial(editorContext, SelectedGeneratedResourceGuid_); + if (!referencedObjects.empty() && ImGui::Button("选择使用该材质的对象##GeneratedResourceContext")) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + } else if (SelectedGeneratedResourceKind_ == "texture") { + const auto referencedObjects = FindSceneObjectsUsingTexture(editorContext, *selectedDocument, SelectedGeneratedResourceGuid_); + if (!referencedObjects.empty() && ImGui::Button("选择使用该贴图的对象##GeneratedResourceContext")) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + } + } + } + } + if (ImGui::Button("返回主资源")) { + SelectedGeneratedResourceGuid_ = MetaCoreAssetGuid{}; + SelectedGeneratedResourceKind_.clear(); + } + ImGui::SameLine(); + if (ImGui::Button("清空筛选##GeneratedResourceContext")) { + GeneratedResourceFilter_.clear(); + GeneratedResourceKindFilter_.clear(); + } + } + + if (SelectedAssetStorageKind_ == MetaCoreAssetStorageKind::SourcePackage && SelectedAssetType_ == "model") { + if (ImGui::Button("实例化模型到场景")) { + (void)MetaCoreInstantiateModelAsset(editorContext, SelectedAssetGuid_, std::nullopt); + } + } else if (SelectedAssetStorageKind_ == MetaCoreAssetStorageKind::SourcePackage && SelectedAssetType_ == "ui_document") { + ImGui::TextDisabled("UI 文档资源可在下方层级树与 Inspector 中编辑。"); + } else if (SelectedAssetStorageKind_ == MetaCoreAssetStorageKind::SourcePackage && SelectedAssetType_ == "prefab") { + if (ImGui::Button("实例化 Prefab 到场景")) { + (void)MetaCoreInstantiatePrefab(editorContext, SelectedAssetGuid_, std::nullopt); + } + } else if (SelectedAssetStorageKind_ == MetaCoreAssetStorageKind::SourcePackage && SelectedAssetType_ == "scene") { + if (scenePersistenceService != nullptr && ImGui::Button("打开场景")) { + (void)scenePersistenceService->LoadScene(editorContext, SelectedAssetPath_); + } + } else { + ImGui::TextDisabled("当前资源暂未提供快捷操作。"); + } + + if (SelectedAssetStorageKind_ == MetaCoreAssetStorageKind::SourcePackage && + SelectedAssetType_ == "model" && + packageService != nullptr && + reflectionRegistry != nullptr) { + DrawModelAssetDetails( + editorContext, + *assetDatabaseService, + *packageService, + *reflectionRegistry + ); + } else if (SelectedAssetStorageKind_ == MetaCoreAssetStorageKind::SourcePackage && + SelectedAssetType_ == "ui_document" && + packageService != nullptr && + reflectionRegistry != nullptr) { + DrawUiDocumentDetails( + *assetDatabaseService, + *packageService, + *reflectionRegistry + ); + } + } + ImGui::Columns(1); } private: + struct MetaCoreGeneratedResourceListEntry { + MetaCoreAssetGuid AssetGuid{}; + std::string Kind{}; + std::string Name{}; + std::string SecondaryText{}; + }; + + [[nodiscard]] std::vector BuildGeneratedResourceListEntries( + const MetaCoreImportedGltfAssetDocument& document + ) const { + std::vector entries; + entries.reserve( + document.GeneratedMeshAssets.size() + + document.GeneratedMaterialAssets.size() + + document.GeneratedTextureAssets.size() + ); + + for (const MetaCoreMeshAssetDocument& meshAsset : document.GeneratedMeshAssets) { + entries.push_back(MetaCoreGeneratedResourceListEntry{ + meshAsset.AssetGuid, + "mesh", + meshAsset.Name, + "Mesh" + }); + } + for (const MetaCoreMaterialAssetDocument& materialAsset : document.GeneratedMaterialAssets) { + entries.push_back(MetaCoreGeneratedResourceListEntry{ + materialAsset.AssetGuid, + "material", + materialAsset.Name, + "Material" + }); + } + for (const MetaCoreTextureAssetDocument& textureAsset : document.GeneratedTextureAssets) { + entries.push_back(MetaCoreGeneratedResourceListEntry{ + textureAsset.AssetGuid, + "texture", + textureAsset.Name, + "Texture" + }); + } + + return entries; + } + + [[nodiscard]] bool MatchesGeneratedResourceKindFilter(std::string_view kind) const { + return GeneratedResourceKindFilter_.empty() || GeneratedResourceKindFilter_ == kind; + } + + [[nodiscard]] std::string BuildGeneratedResourceVirtualPath( + const std::filesystem::path& assetPath, + std::string_view kind, + std::string_view resourceName + ) const { + std::string kindSegment = "Resource"; + if (kind == "mesh") { + kindSegment = "Mesh"; + } else if (kind == "material") { + kindSegment = "Material"; + } else if (kind == "texture") { + kindSegment = "Texture"; + } + + return assetPath.generic_string() + "::" + kindSegment + "/" + std::string(resourceName); + } + + [[nodiscard]] bool MatchesGeneratedResourceFilter(const std::string& label) const { + if (GeneratedResourceFilter_.empty()) { + return true; + } + + std::string haystack = label; + std::string needle = GeneratedResourceFilter_; + std::transform(haystack.begin(), haystack.end(), haystack.begin(), [](unsigned char value) { + return static_cast(std::tolower(value)); + }); + std::transform(needle.begin(), needle.end(), needle.begin(), [](unsigned char value) { + return static_cast(std::tolower(value)); + }); + return haystack.find(needle) != std::string::npos; + } + + [[nodiscard]] std::vector FindSceneObjectsUsingMaterial( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& materialGuid + ) const { + std::vector objectIds; + if (!materialGuid.IsValid()) { + return objectIds; + } + + for (const MetaCoreGameObject& gameObject : editorContext.GetScene().GetGameObjects()) { + if (!gameObject.MeshRenderer.has_value()) { + continue; + } + + if (std::find( + gameObject.MeshRenderer->MaterialAssetGuids.begin(), + gameObject.MeshRenderer->MaterialAssetGuids.end(), + materialGuid + ) != gameObject.MeshRenderer->MaterialAssetGuids.end()) { + objectIds.push_back(gameObject.Id); + } + } + return objectIds; + } + + [[nodiscard]] std::vector FindSceneObjectsUsingMesh( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& meshGuid + ) const { + std::vector objectIds; + if (!meshGuid.IsValid()) { + return objectIds; + } + + for (const MetaCoreGameObject& gameObject : editorContext.GetScene().GetGameObjects()) { + if (!gameObject.MeshRenderer.has_value()) { + continue; + } + + if (gameObject.MeshRenderer->MeshSource == MetaCoreMeshSourceKind::Asset && + gameObject.MeshRenderer->MeshAssetGuid == meshGuid) { + objectIds.push_back(gameObject.Id); + } + } + return objectIds; + } + + [[nodiscard]] std::vector FindSceneObjectsUsingTexture( + MetaCoreEditorContext& editorContext, + const MetaCoreImportedGltfAssetDocument& document, + const MetaCoreAssetGuid& textureGuid + ) const { + std::vector objectIds; + if (!textureGuid.IsValid()) { + return objectIds; + } + + std::unordered_set materialGuidsUsingTexture; + for (const auto& materialAsset : document.GeneratedMaterialAssets) { + if (materialAsset.BaseColorTexture == textureGuid || + materialAsset.MetallicRoughnessTexture == textureGuid || + materialAsset.NormalTexture == textureGuid || + materialAsset.EmissiveTexture == textureGuid || + materialAsset.AoTexture == textureGuid) { + materialGuidsUsingTexture.insert(materialAsset.AssetGuid); + } + } + + if (materialGuidsUsingTexture.empty()) { + return objectIds; + } + + for (const MetaCoreGameObject& gameObject : editorContext.GetScene().GetGameObjects()) { + if (!gameObject.MeshRenderer.has_value()) { + continue; + } + + for (const MetaCoreAssetGuid& materialGuid : gameObject.MeshRenderer->MaterialAssetGuids) { + if (materialGuidsUsingTexture.contains(materialGuid)) { + objectIds.push_back(gameObject.Id); + break; + } + } + } + return objectIds; + } + + void DrawReferencedObjectList( + MetaCoreEditorContext& editorContext, + const std::vector& objectIds, + std::string_view idSuffix + ) const { + if (objectIds.empty()) { + return; + } + + if (!ImGui::TreeNode(("引用对象列表##" + std::string(idSuffix)).c_str())) { + return; + } + + for (std::size_t index = 0; index < objectIds.size(); ++index) { + const MetaCoreId objectId = objectIds[index]; + const MetaCoreGameObject* gameObject = editorContext.GetScene().FindGameObject(objectId); + const std::string objectLabel = + (gameObject != nullptr ? gameObject->Name : std::string("")) + + "##ReferencedObject_" + std::string(idSuffix) + "_" + std::to_string(index); + if (ImGui::Selectable(objectLabel.c_str(), false)) { + editorContext.SetSelection({objectId}, objectId); + } + } + + ImGui::TreePop(); + } + + void DrawModelAssetDetails( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService, + MetaCoreIPackageService& packageService, + MetaCoreIReflectionRegistry& reflectionRegistry + ) { + const auto assetRecord = assetDatabaseService.FindAssetByGuid(SelectedAssetGuid_); + if (!assetRecord.has_value()) { + return; + } + + const std::filesystem::path relativePackagePath = + !assetRecord->PackagePath.empty() ? assetRecord->PackagePath : assetRecord->RelativePath; + const auto package = packageService.ReadPackage(assetDatabaseService.GetProjectDescriptor().RootPath / relativePackagePath); + if (!package.has_value()) { + ImGui::Separator(); + ImGui::TextDisabled("无法读取模型资源包。"); + return; + } + + const auto importedDocument = MetaCoreReadTypedPackagePayload( + *package, + reflectionRegistry.GetTypeRegistry(), + "MetaCoreImportedGltfAssetDocument" + ); + if (!importedDocument.has_value()) { + ImGui::Separator(); + ImGui::TextDisabled("当前模型资源包尚无可显示的导入详情。"); + return; + } + + ImGui::Separator(); + if (ImGui::CollapsingHeader("模型资源详情", ImGuiTreeNodeFlags_DefaultOpen)) { + MetaCoreImportedGltfAssetDocument editableDocument = *importedDocument; + bool modified = false; + const auto textureChoices = MetaCoreCollectTextureAssetChoices(editorContext); + char filterBuffer[128]{}; + std::snprintf(filterBuffer, sizeof(filterBuffer), "%s", GeneratedResourceFilter_.c_str()); + ImGui::Text("源格式: %s", importedDocument->SourceFormat.c_str()); + ImGui::Text("节点: %d | Mesh: %d | 材质: %d | 贴图: %d", + static_cast(importedDocument->Nodes.size()), + static_cast(importedDocument->Meshes.size()), + static_cast(importedDocument->Materials.size()), + static_cast(importedDocument->Textures.size())); + if (ImGui::InputText("子资源筛选", filterBuffer, sizeof(filterBuffer))) { + GeneratedResourceFilter_ = filterBuffer; + } + ImGui::SameLine(); + const char* kindFilterItems[] = {"全部", "Mesh", "Material", "Texture"}; + int kindFilterIndex = 0; + if (GeneratedResourceKindFilter_ == "mesh") { + kindFilterIndex = 1; + } else if (GeneratedResourceKindFilter_ == "material") { + kindFilterIndex = 2; + } else if (GeneratedResourceKindFilter_ == "texture") { + kindFilterIndex = 3; + } + if (ImGui::Combo("类型筛选", &kindFilterIndex, kindFilterItems, IM_ARRAYSIZE(kindFilterItems))) { + switch (kindFilterIndex) { + case 1: + GeneratedResourceKindFilter_ = "mesh"; + break; + case 2: + GeneratedResourceKindFilter_ = "material"; + break; + case 3: + GeneratedResourceKindFilter_ = "texture"; + break; + default: + GeneratedResourceKindFilter_.clear(); + break; + } + } + + if (SelectedGeneratedResourceGuid_.IsValid()) { + ImGui::Separator(); + if (SelectedGeneratedResourceKind_ == "mesh") { + const auto meshIterator = std::find_if( + editableDocument.GeneratedMeshAssets.begin(), + editableDocument.GeneratedMeshAssets.end(), + [&](const MetaCoreMeshAssetDocument& meshAsset) { + return meshAsset.AssetGuid == SelectedGeneratedResourceGuid_; + } + ); + if (meshIterator != editableDocument.GeneratedMeshAssets.end()) { + const auto referencedObjects = FindSceneObjectsUsingMesh(editorContext, meshIterator->AssetGuid); + ImGui::Text("当前子资源: [Mesh] %s", meshIterator->Name.c_str()); + ImGui::TextDisabled("引用对象: %d | Guid: %s", + static_cast(referencedObjects.size()), + meshIterator->AssetGuid.ToString().c_str()); + if (!referencedObjects.empty() && ImGui::Button("选择使用该 Mesh 的对象##SelectedMeshContext")) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + if (!referencedObjects.empty()) { + ImGui::SameLine(); + } + if (ImGui::Button("实例化所属模型##SelectedMeshContext")) { + (void)MetaCoreInstantiateModelAsset(editorContext, SelectedAssetGuid_, std::nullopt); + } + ImGui::SameLine(); + if (ImGui::Button("清除当前子资源##SelectedMeshContext")) { + SelectedGeneratedResourceGuid_ = MetaCoreAssetGuid{}; + SelectedGeneratedResourceKind_.clear(); + } + } + } else if (SelectedGeneratedResourceKind_ == "material") { + const auto materialIterator = std::find_if( + editableDocument.GeneratedMaterialAssets.begin(), + editableDocument.GeneratedMaterialAssets.end(), + [&](const MetaCoreMaterialAssetDocument& materialAsset) { + return materialAsset.AssetGuid == SelectedGeneratedResourceGuid_; + } + ); + if (materialIterator != editableDocument.GeneratedMaterialAssets.end()) { + const auto referencedObjects = FindSceneObjectsUsingMaterial(editorContext, materialIterator->AssetGuid); + ImGui::Text("当前子资源: [Material] %s", materialIterator->Name.c_str()); + ImGui::TextDisabled("引用对象: %d | Guid: %s", + static_cast(referencedObjects.size()), + materialIterator->AssetGuid.ToString().c_str()); + if (!referencedObjects.empty() && ImGui::Button("选择使用该材质的对象##SelectedMaterialContext")) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + ImGui::SameLine(); + if (ImGui::Button("清除当前子资源##SelectedMaterialContext")) { + SelectedGeneratedResourceGuid_ = MetaCoreAssetGuid{}; + SelectedGeneratedResourceKind_.clear(); + } + } + } else if (SelectedGeneratedResourceKind_ == "texture") { + const auto textureIterator = std::find_if( + editableDocument.GeneratedTextureAssets.begin(), + editableDocument.GeneratedTextureAssets.end(), + [&](const MetaCoreTextureAssetDocument& textureAsset) { + return textureAsset.AssetGuid == SelectedGeneratedResourceGuid_; + } + ); + if (textureIterator != editableDocument.GeneratedTextureAssets.end()) { + const auto referencedObjects = FindSceneObjectsUsingTexture(editorContext, editableDocument, textureIterator->AssetGuid); + ImGui::Text("当前子资源: [Texture] %s", textureIterator->Name.c_str()); + ImGui::TextDisabled("引用对象: %d | Guid: %s", + static_cast(referencedObjects.size()), + textureIterator->AssetGuid.ToString().c_str()); + if (!referencedObjects.empty() && ImGui::Button("选择使用该贴图的对象##SelectedTextureContext")) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + ImGui::SameLine(); + if (ImGui::Button("清除当前子资源##SelectedTextureContext")) { + SelectedGeneratedResourceGuid_ = MetaCoreAssetGuid{}; + SelectedGeneratedResourceKind_.clear(); + } + } + } + } + + if (ImGui::TreeNode("子资源列表")) { + const auto generatedEntries = BuildGeneratedResourceListEntries(editableDocument); + int visibleEntryCount = 0; + int visibleMeshCount = 0; + int visibleMaterialCount = 0; + int visibleTextureCount = 0; + for (const auto& entry : generatedEntries) { + if (MatchesGeneratedResourceFilter(entry.Name) && MatchesGeneratedResourceKindFilter(entry.Kind)) { + ++visibleEntryCount; + if (entry.Kind == "mesh") { + ++visibleMeshCount; + } else if (entry.Kind == "material") { + ++visibleMaterialCount; + } else if (entry.Kind == "texture") { + ++visibleTextureCount; + } + } + } + ImGui::TextDisabled("显示 %d / %d 个子资源", visibleEntryCount, static_cast(generatedEntries.size())); + ImGui::TextDisabled( + "Mesh %d | Material %d | Texture %d", + visibleMeshCount, + visibleMaterialCount, + visibleTextureCount + ); + for (std::size_t entryIndex = 0; entryIndex < generatedEntries.size(); ++entryIndex) { + const auto& entry = generatedEntries[entryIndex]; + if (!MatchesGeneratedResourceFilter(entry.Name) || !MatchesGeneratedResourceKindFilter(entry.Kind)) { + continue; + } + + const bool isSelected = + SelectedGeneratedResourceKind_ == entry.Kind && + SelectedGeneratedResourceGuid_ == entry.AssetGuid; + int usageCount = 0; + if (entry.Kind == "mesh") { + usageCount = static_cast(FindSceneObjectsUsingMesh(editorContext, entry.AssetGuid).size()); + } else if (entry.Kind == "material") { + usageCount = static_cast(FindSceneObjectsUsingMaterial(editorContext, entry.AssetGuid).size()); + } else if (entry.Kind == "texture") { + usageCount = static_cast(FindSceneObjectsUsingTexture(editorContext, editableDocument, entry.AssetGuid).size()); + } + const std::string label = "[" + entry.SecondaryText + "] " + entry.Name + + " (" + std::to_string(usageCount) + ")" + + "##GeneratedResourceList_" + std::to_string(entryIndex); + if (ImGui::Selectable(label.c_str(), isSelected)) { + SelectedGeneratedResourceKind_ = entry.Kind; + SelectedGeneratedResourceGuid_ = entry.AssetGuid; + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + if (entry.Kind == "mesh") { + const auto referencedObjects = FindSceneObjectsUsingMesh(editorContext, entry.AssetGuid); + if (!referencedObjects.empty()) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } else { + (void)MetaCoreInstantiateModelAsset(editorContext, SelectedAssetGuid_, std::nullopt); + } + } else if (entry.Kind == "material") { + const auto referencedObjects = FindSceneObjectsUsingMaterial(editorContext, entry.AssetGuid); + if (!referencedObjects.empty()) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + } else if (entry.Kind == "texture") { + const auto referencedObjects = FindSceneObjectsUsingTexture(editorContext, editableDocument, entry.AssetGuid); + if (!referencedObjects.empty()) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + } + } + } + } + ImGui::TreePop(); + } + + if (ImGui::TreeNode("节点")) { + for (std::size_t index = 0; index < importedDocument->Nodes.size(); ++index) { + const auto& node = importedDocument->Nodes[index]; + ImGui::BulletText( + "%s (Parent=%d, Mesh=%d)", + node.Name.empty() ? "" : node.Name.c_str(), + node.ParentIndex, + node.MeshIndex + ); + } + ImGui::TreePop(); + } + + if (ImGui::TreeNode("生成 Mesh 资源")) { + for (std::size_t meshIndex = 0; meshIndex < importedDocument->GeneratedMeshAssets.size(); ++meshIndex) { + const auto& meshAsset = importedDocument->GeneratedMeshAssets[meshIndex]; + if (!MatchesGeneratedResourceFilter(meshAsset.Name) || !MatchesGeneratedResourceKindFilter("mesh")) { + continue; + } + const bool isSelectedGeneratedMesh = + SelectedGeneratedResourceKind_ == "mesh" && + SelectedGeneratedResourceGuid_ == meshAsset.AssetGuid; + if (ImGui::Selectable( + (meshAsset.Name + "##GeneratedMeshSelect_" + std::to_string(meshIndex)).c_str(), + isSelectedGeneratedMesh)) { + SelectedGeneratedResourceKind_ = "mesh"; + SelectedGeneratedResourceGuid_ = meshAsset.AssetGuid; + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + const auto referencedObjects = FindSceneObjectsUsingMesh(editorContext, meshAsset.AssetGuid); + if (!referencedObjects.empty()) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } else { + (void)MetaCoreInstantiateModelAsset(editorContext, SelectedAssetGuid_, std::nullopt); + } + } + } + ImGui::TextDisabled( + "SubMeshes=%d | Guid=%s", + static_cast(meshAsset.SubMeshes.size()), + meshAsset.AssetGuid.ToString().c_str() + ); + } + ImGui::TreePop(); + } + + if (ImGui::TreeNode("生成材质资源")) { + for (std::size_t materialIndex = 0; materialIndex < editableDocument.GeneratedMaterialAssets.size(); ++materialIndex) { + auto& materialAsset = editableDocument.GeneratedMaterialAssets[materialIndex]; + if (!MatchesGeneratedResourceFilter(materialAsset.Name) || !MatchesGeneratedResourceKindFilter("material")) { + continue; + } + const bool isSelectedGeneratedMaterial = + SelectedGeneratedResourceKind_ == "material" && + SelectedGeneratedResourceGuid_ == materialAsset.AssetGuid; + if (ImGui::Selectable( + ("选中##GeneratedMaterialSelect_" + std::to_string(materialIndex)).c_str(), + isSelectedGeneratedMaterial, + 0, + ImVec2(48.0F, 0.0F))) { + SelectedGeneratedResourceKind_ = "material"; + SelectedGeneratedResourceGuid_ = materialAsset.AssetGuid; + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + const auto referencedObjects = FindSceneObjectsUsingMaterial(editorContext, materialAsset.AssetGuid); + if (!referencedObjects.empty()) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + } + } + ImGui::SameLine(); + const std::string treeLabel = materialAsset.Name + "##MaterialAsset_" + std::to_string(materialIndex); + if (!ImGui::TreeNode(treeLabel.c_str())) { + continue; + } + + ImGui::TextDisabled("Guid: %s", materialAsset.AssetGuid.ToString().c_str()); + const char* shaderItems[] = {"PbrMetalRough", "UnlitColor", "UnlitTexture"}; + int shaderIndex = static_cast(materialAsset.ShaderModel); + if (ImGui::Combo(("Shader##" + std::to_string(materialIndex)).c_str(), &shaderIndex, shaderItems, IM_ARRAYSIZE(shaderItems))) { + materialAsset.ShaderModel = static_cast(shaderIndex); + modified = true; + } + + float baseColor[3] = {materialAsset.BaseColor.x, materialAsset.BaseColor.y, materialAsset.BaseColor.z}; + if (ImGui::ColorEdit3(("Base Color##" + std::to_string(materialIndex)).c_str(), baseColor)) { + materialAsset.BaseColor = {baseColor[0], baseColor[1], baseColor[2]}; + modified = true; + } + + if (ImGui::SliderFloat(("Metallic##" + std::to_string(materialIndex)).c_str(), &materialAsset.Metallic, 0.0F, 1.0F)) { + modified = true; + } + if (ImGui::SliderFloat(("Roughness##" + std::to_string(materialIndex)).c_str(), &materialAsset.Roughness, 0.0F, 1.0F)) { + modified = true; + } + if (ImGui::Checkbox(("Double Sided##" + std::to_string(materialIndex)).c_str(), &materialAsset.DoubleSided)) { + modified = true; + } + + const char* alphaModeItems[] = {"Opaque", "Mask", "Blend"}; + int alphaModeIndex = static_cast(materialAsset.AlphaMode); + if (ImGui::Combo(("Alpha Mode##" + std::to_string(materialIndex)).c_str(), &alphaModeIndex, alphaModeItems, IM_ARRAYSIZE(alphaModeItems))) { + materialAsset.AlphaMode = static_cast(alphaModeIndex); + modified = true; + } + if (materialAsset.AlphaMode == MetaCoreMaterialAlphaMode::Mask) { + if (ImGui::SliderFloat(("Alpha Cutoff##" + std::to_string(materialIndex)).c_str(), &materialAsset.AlphaCutoff, 0.0F, 1.0F)) { + modified = true; + } + } + + MetaCoreAssetGuid baseColorTexture = materialAsset.BaseColorTexture; + if (MetaCoreDrawGeneratedAssetCombo(("Base Color Texture##" + std::to_string(materialIndex)).c_str(), textureChoices, baseColorTexture)) { + materialAsset.BaseColorTexture = baseColorTexture; + modified = true; + } + ImGui::TextDisabled("BaseColorTexture Guid: %s", + materialAsset.BaseColorTexture.IsValid() ? materialAsset.BaseColorTexture.ToString().c_str() : ""); + + MetaCoreAssetGuid metallicRoughnessTexture = materialAsset.MetallicRoughnessTexture; + if (MetaCoreDrawGeneratedAssetCombo(("MetalRough Texture##" + std::to_string(materialIndex)).c_str(), textureChoices, metallicRoughnessTexture)) { + materialAsset.MetallicRoughnessTexture = metallicRoughnessTexture; + modified = true; + } + ImGui::TextDisabled("MetalRoughTexture Guid: %s", + materialAsset.MetallicRoughnessTexture.IsValid() ? materialAsset.MetallicRoughnessTexture.ToString().c_str() : ""); + + MetaCoreAssetGuid normalTexture = materialAsset.NormalTexture; + if (MetaCoreDrawGeneratedAssetCombo(("Normal Texture##" + std::to_string(materialIndex)).c_str(), textureChoices, normalTexture)) { + materialAsset.NormalTexture = normalTexture; + modified = true; + } + ImGui::TextDisabled("NormalTexture Guid: %s", + materialAsset.NormalTexture.IsValid() ? materialAsset.NormalTexture.ToString().c_str() : ""); + + float emissiveColor[3] = { + materialAsset.EmissiveColor.x, + materialAsset.EmissiveColor.y, + materialAsset.EmissiveColor.z + }; + if (ImGui::ColorEdit3(("Emissive Color##" + std::to_string(materialIndex)).c_str(), emissiveColor)) { + materialAsset.EmissiveColor = {emissiveColor[0], emissiveColor[1], emissiveColor[2]}; + modified = true; + } + + MetaCoreAssetGuid emissiveTexture = materialAsset.EmissiveTexture; + if (MetaCoreDrawGeneratedAssetCombo(("Emissive Texture##" + std::to_string(materialIndex)).c_str(), textureChoices, emissiveTexture)) { + materialAsset.EmissiveTexture = emissiveTexture; + modified = true; + } + ImGui::TextDisabled("EmissiveTexture Guid: %s", + materialAsset.EmissiveTexture.IsValid() ? materialAsset.EmissiveTexture.ToString().c_str() : ""); + + MetaCoreAssetGuid aoTexture = materialAsset.AoTexture; + if (MetaCoreDrawGeneratedAssetCombo(("AO Texture##" + std::to_string(materialIndex)).c_str(), textureChoices, aoTexture)) { + materialAsset.AoTexture = aoTexture; + modified = true; + } + ImGui::TextDisabled("AoTexture Guid: %s", + materialAsset.AoTexture.IsValid() ? materialAsset.AoTexture.ToString().c_str() : ""); + ImGui::TreePop(); + } + ImGui::TreePop(); + } + + if (ImGui::TreeNode("生成贴图资源")) { + for (std::size_t textureIndex = 0; textureIndex < importedDocument->GeneratedTextureAssets.size(); ++textureIndex) { + const auto& textureAsset = importedDocument->GeneratedTextureAssets[textureIndex]; + if ((!MatchesGeneratedResourceFilter(textureAsset.Name) && + !MatchesGeneratedResourceFilter(textureAsset.SourcePath.generic_string())) || + !MatchesGeneratedResourceKindFilter("texture")) { + continue; + } + const bool isSelectedGeneratedTexture = + SelectedGeneratedResourceKind_ == "texture" && + SelectedGeneratedResourceGuid_ == textureAsset.AssetGuid; + if (ImGui::Selectable( + (textureAsset.Name + "##GeneratedTextureSelect_" + std::to_string(textureIndex)).c_str(), + isSelectedGeneratedTexture)) { + SelectedGeneratedResourceKind_ = "texture"; + SelectedGeneratedResourceGuid_ = textureAsset.AssetGuid; + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + const auto referencedObjects = FindSceneObjectsUsingTexture(editorContext, editableDocument, textureAsset.AssetGuid); + if (!referencedObjects.empty()) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + } + } + ImGui::TextDisabled( + "Source=%s | Guid=%s", + textureAsset.SourcePath.generic_string().c_str(), + textureAsset.AssetGuid.ToString().c_str() + ); + } + ImGui::TreePop(); + } + + if (SelectedGeneratedResourceGuid_.IsValid()) { + ImGui::Separator(); + if (SelectedGeneratedResourceKind_ == "mesh") { + const auto meshIterator = std::find_if( + editableDocument.GeneratedMeshAssets.begin(), + editableDocument.GeneratedMeshAssets.end(), + [&](const MetaCoreMeshAssetDocument& meshAsset) { + return meshAsset.AssetGuid == SelectedGeneratedResourceGuid_; + } + ); + if (meshIterator != editableDocument.GeneratedMeshAssets.end()) { + const auto referencedObjects = FindSceneObjectsUsingMesh(editorContext, meshIterator->AssetGuid); + ImGui::Text("选中 Mesh: %s", meshIterator->Name.c_str()); + ImGui::TextWrapped("所属模型包: %s", SelectedAssetPath_.generic_string().c_str()); + ImGui::TextDisabled("Guid: %s", meshIterator->AssetGuid.ToString().c_str()); + ImGui::TextDisabled( + "Vertex=%d | Index=%d | SubMeshes=%d", + meshIterator->VertexCount, + meshIterator->IndexCount, + static_cast(meshIterator->SubMeshes.size()) + ); + for (std::size_t subMeshIndex = 0; subMeshIndex < meshIterator->SubMeshes.size(); ++subMeshIndex) { + const auto& subMesh = meshIterator->SubMeshes[subMeshIndex]; + ImGui::BulletText( + "SubMesh %d: %s | MaterialSlot=%d", + static_cast(subMeshIndex), + subMesh.Name.c_str(), + subMesh.MaterialSlotIndex + ); + } + ImGui::TextDisabled("场景引用对象: %d", static_cast(referencedObjects.size())); + if (!referencedObjects.empty() && ImGui::Button("选择使用该 Mesh 的对象")) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + if (!referencedObjects.empty()) { + ImGui::SameLine(); + } + if (ImGui::Button("实例化该 Mesh 所属模型")) { + (void)MetaCoreInstantiateModelAsset(editorContext, SelectedAssetGuid_, std::nullopt); + } + DrawReferencedObjectList(editorContext, referencedObjects, "MeshDetail"); + } + } else if (SelectedGeneratedResourceKind_ == "material") { + const auto materialIterator = std::find_if( + editableDocument.GeneratedMaterialAssets.begin(), + editableDocument.GeneratedMaterialAssets.end(), + [&](const MetaCoreMaterialAssetDocument& materialAsset) { + return materialAsset.AssetGuid == SelectedGeneratedResourceGuid_; + } + ); + if (materialIterator != editableDocument.GeneratedMaterialAssets.end()) { + const auto referencedObjects = FindSceneObjectsUsingMaterial(editorContext, materialIterator->AssetGuid); + ImGui::Text("选中材质: %s", materialIterator->Name.c_str()); + ImGui::TextWrapped("所属模型包: %s", SelectedAssetPath_.generic_string().c_str()); + ImGui::TextDisabled("Guid: %s", materialIterator->AssetGuid.ToString().c_str()); + ImGui::TextDisabled("Shader: %d | AlphaMode: %d", static_cast(materialIterator->ShaderModel), static_cast(materialIterator->AlphaMode)); + ImGui::TextDisabled( + "BaseColor: %.2f %.2f %.2f | Metallic: %.2f | Roughness: %.2f", + materialIterator->BaseColor.x, + materialIterator->BaseColor.y, + materialIterator->BaseColor.z, + materialIterator->Metallic, + materialIterator->Roughness + ); + ImGui::TextDisabled("场景引用对象: %d", static_cast(referencedObjects.size())); + if (!referencedObjects.empty() && ImGui::Button("选择使用该材质的对象")) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + ImGui::SameLine(); + if (ImGui::Button("清除材质子资源选择")) { + SelectedGeneratedResourceGuid_ = MetaCoreAssetGuid{}; + SelectedGeneratedResourceKind_.clear(); + } + DrawReferencedObjectList(editorContext, referencedObjects, "MaterialDetail"); + } + } else if (SelectedGeneratedResourceKind_ == "texture") { + const auto textureIterator = std::find_if( + editableDocument.GeneratedTextureAssets.begin(), + editableDocument.GeneratedTextureAssets.end(), + [&](const MetaCoreTextureAssetDocument& textureAsset) { + return textureAsset.AssetGuid == SelectedGeneratedResourceGuid_; + } + ); + if (textureIterator != editableDocument.GeneratedTextureAssets.end()) { + const auto referencedObjects = FindSceneObjectsUsingTexture(editorContext, editableDocument, textureIterator->AssetGuid); + ImGui::Text("选中贴图: %s", textureIterator->Name.c_str()); + ImGui::TextWrapped("所属模型包: %s", SelectedAssetPath_.generic_string().c_str()); + ImGui::TextDisabled("Guid: %s", textureIterator->AssetGuid.ToString().c_str()); + ImGui::TextDisabled("Usage: %s", textureIterator->UsageHint.c_str()); + ImGui::TextWrapped("Source: %s", textureIterator->SourcePath.generic_string().c_str()); + ImGui::TextDisabled("场景引用对象: %d", static_cast(referencedObjects.size())); + if (!referencedObjects.empty() && ImGui::Button("选择使用该贴图的对象")) { + editorContext.SetSelection(referencedObjects, referencedObjects.front()); + } + ImGui::SameLine(); + if (ImGui::Button("清除贴图子资源选择")) { + SelectedGeneratedResourceGuid_ = MetaCoreAssetGuid{}; + SelectedGeneratedResourceKind_.clear(); + } + DrawReferencedObjectList(editorContext, referencedObjects, "TextureDetail"); + } + } + } + + if (modified) { + if (MetaCoreWriteImportedGltfAssetDocument( + assetDatabaseService, + packageService, + reflectionRegistry, + *assetRecord, + editableDocument, + package->Header.PackageType + )) { + MetaCoreApplyMaterialPreviewToScene(editorContext, editableDocument); + ImGui::TextColored(ImVec4(0.32F, 0.85F, 0.42F, 1.0F), "已保存材质修改"); + } else { + ImGui::TextColored(ImVec4(0.92F, 0.32F, 0.28F, 1.0F), "材质修改保存失败"); + } + } + } + } + void DrawDirectoryTree( MetaCoreIAssetDatabaseService& assetDatabaseService, const std::filesystem::path& relativeDirectory, @@ -1274,7 +2694,579 @@ private: } } + [[nodiscard]] bool AddUiNode( + MetaCoreUiDocument& document, + MetaCoreUiNodeType nodeType, + const std::string& parentId + ) { + const char* typePrefix = "panel"; + if (nodeType == MetaCoreUiNodeType::Text) { + typePrefix = "text"; + } else if (nodeType == MetaCoreUiNodeType::Image) { + typePrefix = "image"; + } else if (nodeType == MetaCoreUiNodeType::Button) { + typePrefix = "button"; + } + + MetaCoreUiNodeDocument node; + node.Id = MetaCoreMakeUniqueUiNodeId(document, typePrefix); + node.Name = node.Id; + node.Type = nodeType; + node.ParentId = parentId; + node.Visible = true; + node.Style.HorizontalAlignment = MetaCoreUiHorizontalAlignment::Left; + node.Style.VerticalAlignment = MetaCoreUiVerticalAlignment::Top; + if (nodeType == MetaCoreUiNodeType::Text) { + node.Text = "Text"; + } else if (nodeType == MetaCoreUiNodeType::Button) { + node.Text = "Button"; + node.Interactable = true; + node.Style.BackgroundColor = glm::vec3(0.12F, 0.42F, 0.82F); + } + + if (parentId.empty()) { + document.RootNodeIds.push_back(node.Id); + } else { + MetaCoreUiNodeDocument* parentNode = MetaCoreFindUiNode(document, parentId); + if (parentNode == nullptr) { + return false; + } + parentNode->Children.push_back(node.Id); + } + + document.Nodes.push_back(std::move(node)); + SelectedUiNodeId_ = document.Nodes.back().Id; + return true; + } + + struct MetaCoreUiImageChoice { + MetaCoreAssetGuid AssetGuid{}; + std::string Label{}; + }; + + struct MetaCoreUiPreviewHitEntry { + std::string NodeId{}; + ImVec2 Min{}; + ImVec2 Max{}; + }; + + [[nodiscard]] std::vector BuildUiImageChoices( + MetaCoreIAssetDatabaseService& assetDatabaseService + ) const { + std::vector choices; + for (const MetaCoreAssetRecord& record : assetDatabaseService.GetAssetRegistry()) { + if (record.Type != "texture" || !record.Guid.IsValid()) { + continue; + } + choices.push_back(MetaCoreUiImageChoice{ + record.Guid, + record.RelativePath.filename().string() + "##" + record.Guid.ToString() + }); + } + return choices; + } + + bool DrawUiImageAssetSelector( + const char* label, + MetaCoreAssetGuid& assetGuid, + const std::vector& choices + ) const { + int selectedIndex = 0; + for (std::size_t index = 0; index < choices.size(); ++index) { + if (choices[index].AssetGuid == assetGuid) { + selectedIndex = static_cast(index) + 1; + break; + } + } + + std::vector labels; + labels.reserve(choices.size() + 1); + labels.push_back(""); + for (const MetaCoreUiImageChoice& choice : choices) { + labels.push_back(choice.Label.c_str()); + } + + const bool changed = ImGui::Combo(label, &selectedIndex, labels.data(), static_cast(labels.size())); + if (changed) { + assetGuid = selectedIndex == 0 ? MetaCoreAssetGuid{} : choices[static_cast(selectedIndex - 1)].AssetGuid; + } + return changed; + } + + [[nodiscard]] ImU32 ToImGuiColor(const glm::vec3& color, float alpha = 1.0F) const { + return ImGui::ColorConvertFloat4ToU32(ImVec4(color.x, color.y, color.z, alpha)); + } + + [[nodiscard]] ImVec2 ComputeUiPreviewPoint( + const MetaCoreUiRectTransformDocument& rectTransform, + const ImVec2& parentMin, + const ImVec2& parentMax, + bool useMaxAnchor + ) const { + const ImVec2 parentSize(parentMax.x - parentMin.x, parentMax.y - parentMin.y); + const ImVec2 anchor( + useMaxAnchor ? rectTransform.AnchorMax.x : rectTransform.AnchorMin.x, + useMaxAnchor ? rectTransform.AnchorMax.y : rectTransform.AnchorMin.y + ); + return ImVec2( + parentMin.x + parentSize.x * anchor.x, + parentMin.y + parentSize.y * anchor.y + ); + } + + void DrawUiPreviewNode( + const MetaCoreUiDocument& document, + const std::string& nodeId, + ImDrawList* drawList, + const ImVec2& parentMin, + const ImVec2& parentMax, + std::vector& hitEntries + ) const { + const MetaCoreUiNodeDocument* node = MetaCoreFindUiNode(document, nodeId); + if (node == nullptr || !node->Visible) { + return; + } + + const ImVec2 parentSize(parentMax.x - parentMin.x, parentMax.y - parentMin.y); + const ImVec2 anchorMin = ComputeUiPreviewPoint(node->RectTransform, parentMin, parentMax, false); + const ImVec2 anchorMax = ComputeUiPreviewPoint(node->RectTransform, parentMin, parentMax, true); + ImVec2 nodeMin{}; + ImVec2 nodeMax{}; + + if (std::abs(node->RectTransform.AnchorMin.x - node->RectTransform.AnchorMax.x) < 0.0001F && + std::abs(node->RectTransform.AnchorMin.y - node->RectTransform.AnchorMax.y) < 0.0001F) { + const ImVec2 size(node->RectTransform.Size.x, node->RectTransform.Size.y); + const ImVec2 pivot(node->RectTransform.Pivot.x, node->RectTransform.Pivot.y); + nodeMin = ImVec2( + anchorMin.x + node->RectTransform.Position.x - size.x * pivot.x, + anchorMin.y + node->RectTransform.Position.y - size.y * pivot.y + ); + nodeMax = ImVec2(nodeMin.x + size.x, nodeMin.y + size.y); + } else { + nodeMin = ImVec2( + anchorMin.x + node->RectTransform.Position.x, + anchorMin.y + node->RectTransform.Position.y + ); + nodeMax = ImVec2( + anchorMax.x + node->RectTransform.Position.x + node->RectTransform.Size.x, + anchorMax.y + node->RectTransform.Position.y + node->RectTransform.Size.y + ); + } + + nodeMin.x = std::clamp(nodeMin.x, parentMin.x, parentMax.x); + nodeMin.y = std::clamp(nodeMin.y, parentMin.y, parentMax.y); + nodeMax.x = std::clamp(nodeMax.x, parentMin.x, parentMax.x); + nodeMax.y = std::clamp(nodeMax.y, parentMin.y, parentMax.y); + if (nodeMax.x <= nodeMin.x || nodeMax.y <= nodeMin.y) { + nodeMax = ImVec2(nodeMin.x + std::max(4.0F, parentSize.x * 0.05F), nodeMin.y + std::max(4.0F, parentSize.y * 0.05F)); + } + + glm::vec3 fillColor = node->Style.BackgroundColor; + if (node->Type == MetaCoreUiNodeType::Text) { + fillColor = glm::vec3(0.10F, 0.10F, 0.10F); + } else if (node->Type == MetaCoreUiNodeType::Image) { + fillColor = node->Style.TintColor; + } else if (node->Type == MetaCoreUiNodeType::Button) { + fillColor = node->Style.BackgroundColor; + } + + drawList->AddRectFilled(nodeMin, nodeMax, ToImGuiColor(fillColor, node->Type == MetaCoreUiNodeType::Text ? 0.18F : 0.35F), 4.0F); + drawList->AddRect(nodeMin, nodeMax, ToImGuiColor(glm::vec3(0.65F, 0.72F, 0.82F), 0.90F), 4.0F, 0, 1.5F); + + std::string label = node->Name; + if (node->Type == MetaCoreUiNodeType::Text || node->Type == MetaCoreUiNodeType::Button) { + if (!node->Text.empty()) { + label += ": " + node->Text; + } + } else if (node->Type == MetaCoreUiNodeType::Image) { + label += node->Style.ImageAssetGuid.IsValid() ? " [Image]" : " [No Image]"; + } + drawList->AddText(ImVec2(nodeMin.x + 6.0F, nodeMin.y + 6.0F), ToImGuiColor(node->Style.TextColor), label.c_str()); + + hitEntries.push_back(MetaCoreUiPreviewHitEntry{node->Id, nodeMin, nodeMax}); + for (const std::string& childId : node->Children) { + DrawUiPreviewNode(document, childId, drawList, nodeMin, nodeMax, hitEntries); + } + } + + bool DrawUiDocumentPreview(const MetaCoreUiDocument& document) { + ImGui::Separator(); + ImGui::Text("UI 预览"); + const ImVec2 available = ImGui::GetContentRegionAvail(); + const float previewHeight = std::max(260.0F, available.y * 0.55F); + const ImVec2 canvasSize(std::max(200.0F, available.x), previewHeight); + const ImVec2 canvasMin = ImGui::GetCursorScreenPos(); + const ImVec2 canvasMax(canvasMin.x + canvasSize.x, canvasMin.y + canvasSize.y); + + ImGui::InvisibleButton("MetaCoreUiPreviewCanvas", canvasSize); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(canvasMin, canvasMax, IM_COL32(24, 28, 36, 255), 6.0F); + drawList->AddRect(canvasMin, canvasMax, IM_COL32(86, 94, 112, 255), 6.0F, 0, 1.5F); + + const float referenceWidth = static_cast(std::max(1, document.ReferenceWidth)); + const float referenceHeight = static_cast(std::max(1, document.ReferenceHeight)); + const float scale = std::min((canvasSize.x - 24.0F) / referenceWidth, (canvasSize.y - 24.0F) / referenceHeight); + const ImVec2 contentSize(referenceWidth * scale, referenceHeight * scale); + const ImVec2 contentMin( + canvasMin.x + (canvasSize.x - contentSize.x) * 0.5F, + canvasMin.y + (canvasSize.y - contentSize.y) * 0.5F + ); + const ImVec2 contentMax(contentMin.x + contentSize.x, contentMin.y + contentSize.y); + + drawList->AddRectFilled(contentMin, contentMax, IM_COL32(40, 44, 54, 255), 4.0F); + drawList->AddRect(contentMin, contentMax, IM_COL32(110, 120, 142, 255), 4.0F, 0, 1.0F); + + std::vector hitEntries; + for (const std::string& rootNodeId : document.RootNodeIds) { + DrawUiPreviewNode(document, rootNodeId, drawList, contentMin, contentMax, hitEntries); + } + + for (const MetaCoreUiPreviewHitEntry& entry : hitEntries) { + if (entry.NodeId == SelectedUiNodeId_) { + drawList->AddRect(entry.Min, entry.Max, IM_COL32(255, 215, 96, 255), 4.0F, 0, 2.5F); + } + } + + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + const ImVec2 mouse = ImGui::GetIO().MousePos; + for (auto iterator = hitEntries.rbegin(); iterator != hitEntries.rend(); ++iterator) { + if (mouse.x >= iterator->Min.x && mouse.x <= iterator->Max.x && + mouse.y >= iterator->Min.y && mouse.y <= iterator->Max.y) { + SelectedUiNodeId_ = iterator->NodeId; + return true; + } + } + } + return false; + } + + bool DeleteUiNode(MetaCoreUiDocument& document, const std::string& nodeId) { + if (nodeId.empty()) { + return false; + } + + std::unordered_set idsToDelete; + MetaCoreCollectUiNodeDescendants(document, nodeId, idsToDelete); + if (idsToDelete.empty()) { + return false; + } + + for (MetaCoreUiNodeDocument& node : document.Nodes) { + node.Children.erase( + std::remove_if(node.Children.begin(), node.Children.end(), [&](const std::string& childId) { + return idsToDelete.contains(childId); + }), + node.Children.end() + ); + } + document.RootNodeIds.erase( + std::remove_if(document.RootNodeIds.begin(), document.RootNodeIds.end(), [&](const std::string& rootId) { + return idsToDelete.contains(rootId); + }), + document.RootNodeIds.end() + ); + document.Nodes.erase( + std::remove_if(document.Nodes.begin(), document.Nodes.end(), [&](const MetaCoreUiNodeDocument& node) { + return idsToDelete.contains(node.Id); + }), + document.Nodes.end() + ); + SelectedUiNodeId_.clear(); + return true; + } + + bool DrawUiNodeTree(MetaCoreUiDocument& document, const std::string& nodeId) { + const MetaCoreUiNodeDocument* node = MetaCoreFindUiNode(document, nodeId); + if (node == nullptr) { + return false; + } + + bool modified = false; + const bool isSelected = SelectedUiNodeId_ == nodeId; + const ImGuiTreeNodeFlags flags = + ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_SpanFullWidth | + (node->Children.empty() ? ImGuiTreeNodeFlags_Leaf : 0) | + (isSelected ? ImGuiTreeNodeFlags_Selected : 0); + const std::string label = node->Name + "##UiNode_" + node->Id; + const bool opened = ImGui::TreeNodeEx(label.c_str(), flags); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + SelectedUiNodeId_ = node->Id; + } + + if (ImGui::BeginDragDropSource()) { + ImGui::SetDragDropPayload("MetaCoreUiNode", node->Id.c_str(), node->Id.size() + 1); + ImGui::Text("移动节点: %s", node->Name.c_str()); + ImGui::EndDragDropSource(); + } + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("MetaCoreUiNode")) { + const char* draggedNodeId = static_cast(payload->Data); + if (draggedNodeId != nullptr) { + modified = MetaCoreReparentUiNode(document, draggedNodeId, node->Id) || modified; + SelectedUiNodeId_ = draggedNodeId; + } + } + ImGui::EndDragDropTarget(); + } + + if (opened) { + for (const std::string& childId : node->Children) { + modified = DrawUiNodeTree(document, childId) || modified; + } + ImGui::TreePop(); + } + return modified; + } + + bool DrawUiNodeInspector( + MetaCoreUiDocument& document, + MetaCoreIAssetDatabaseService& assetDatabaseService + ) { + MetaCoreUiNodeDocument* node = MetaCoreFindUiNode(document, SelectedUiNodeId_); + if (node == nullptr) { + ImGui::TextDisabled("选择 UI 节点以编辑属性。"); + return false; + } + + bool modified = false; + char nameBuffer[256]{}; + std::snprintf(nameBuffer, sizeof(nameBuffer), "%s", node->Name.c_str()); + if (ImGui::InputText("名称", nameBuffer, sizeof(nameBuffer))) { + node->Name = nameBuffer; + modified = true; + } + + ImGui::TextDisabled("Id: %s", node->Id.c_str()); + ImGui::Checkbox("Visible", &node->Visible); + modified = modified || ImGui::IsItemDeactivatedAfterEdit(); + + const char* typeLabel = "Panel"; + if (node->Type == MetaCoreUiNodeType::Text) { + typeLabel = "Text"; + } else if (node->Type == MetaCoreUiNodeType::Image) { + typeLabel = "Image"; + } else if (node->Type == MetaCoreUiNodeType::Button) { + typeLabel = "Button"; + } + ImGui::TextDisabled("类型: %s", typeLabel); + + float position[2] = {node->RectTransform.Position.x, node->RectTransform.Position.y}; + if (ImGui::InputFloat2("位置", position)) { + node->RectTransform.Position.x = position[0]; + node->RectTransform.Position.y = position[1]; + modified = true; + } + float size[2] = {node->RectTransform.Size.x, node->RectTransform.Size.y}; + if (ImGui::InputFloat2("尺寸", size)) { + node->RectTransform.Size.x = size[0]; + node->RectTransform.Size.y = size[1]; + modified = true; + } + float anchorMin[2] = {node->RectTransform.AnchorMin.x, node->RectTransform.AnchorMin.y}; + if (ImGui::InputFloat2("Anchor Min", anchorMin)) { + node->RectTransform.AnchorMin.x = anchorMin[0]; + node->RectTransform.AnchorMin.y = anchorMin[1]; + modified = true; + } + float anchorMax[2] = {node->RectTransform.AnchorMax.x, node->RectTransform.AnchorMax.y}; + if (ImGui::InputFloat2("Anchor Max", anchorMax)) { + node->RectTransform.AnchorMax.x = anchorMax[0]; + node->RectTransform.AnchorMax.y = anchorMax[1]; + modified = true; + } + + float backgroundColor[3] = {node->Style.BackgroundColor.x, node->Style.BackgroundColor.y, node->Style.BackgroundColor.z}; + if (ImGui::ColorEdit3("背景色", backgroundColor)) { + node->Style.BackgroundColor = glm::vec3(backgroundColor[0], backgroundColor[1], backgroundColor[2]); + modified = true; + } + + if (node->Type == MetaCoreUiNodeType::Text || node->Type == MetaCoreUiNodeType::Button) { + char textBuffer[256]{}; + std::snprintf(textBuffer, sizeof(textBuffer), "%s", node->Text.c_str()); + if (ImGui::InputText("文本", textBuffer, sizeof(textBuffer))) { + node->Text = textBuffer; + modified = true; + } + + float textColor[3] = {node->Style.TextColor.x, node->Style.TextColor.y, node->Style.TextColor.z}; + if (ImGui::ColorEdit3("文本颜色", textColor)) { + node->Style.TextColor = glm::vec3(textColor[0], textColor[1], textColor[2]); + modified = true; + } + if (ImGui::InputFloat("字体大小", &node->Style.FontSize)) { + modified = true; + } + } + + if (node->Type == MetaCoreUiNodeType::Image || node->Type == MetaCoreUiNodeType::Button) { + float tintColor[3] = {node->Style.TintColor.x, node->Style.TintColor.y, node->Style.TintColor.z}; + if (ImGui::ColorEdit3("Tint", tintColor)) { + node->Style.TintColor = glm::vec3(tintColor[0], tintColor[1], tintColor[2]); + modified = true; + } + const auto imageChoices = BuildUiImageChoices(assetDatabaseService); + modified = DrawUiImageAssetSelector("图片资源", node->Style.ImageAssetGuid, imageChoices) || modified; + if (ImGui::Checkbox("保持宽高比", &node->Style.PreserveAspect)) { + modified = true; + } + } + + if (node->Type == MetaCoreUiNodeType::Button) { + if (ImGui::Checkbox("Interactable", &node->Interactable)) { + modified = true; + } + } + + if (!node->ParentId.empty()) { + ImGui::TextDisabled("父节点: %s", node->ParentId.c_str()); + if (ImGui::Button("提升为根节点")) { + modified = MetaCoreReparentUiNode(document, node->Id, "") || modified; + } + } else { + ImGui::TextDisabled("父节点: "); + } + + return modified; + } + + void DrawUiDocumentDetails( + MetaCoreIAssetDatabaseService& assetDatabaseService, + MetaCoreIPackageService& packageService, + MetaCoreIReflectionRegistry& reflectionRegistry + ) { + const auto assetRecord = assetDatabaseService.FindAssetByGuid(SelectedAssetGuid_); + if (!assetRecord.has_value()) { + return; + } + + const std::filesystem::path relativePackagePath = + assetRecord->PackagePath.empty() ? assetRecord->RelativePath : assetRecord->PackagePath; + const auto package = packageService.ReadPackage(assetDatabaseService.GetProjectDescriptor().RootPath / relativePackagePath); + if (!package.has_value()) { + ImGui::TextColored(ImVec4(0.92F, 0.32F, 0.28F, 1.0F), "UI 文档读取失败"); + return; + } + + auto document = MetaCoreReadTypedPackagePayload( + *package, + reflectionRegistry.GetTypeRegistry(), + "MetaCoreUiDocument" + ); + if (!document.has_value()) { + ImGui::TextColored(ImVec4(0.92F, 0.32F, 0.28F, 1.0F), "UI 文档反序列化失败"); + return; + } + + bool modified = false; + ImGui::Separator(); + ImGui::Text("UI 文档"); + + char documentNameBuffer[256]{}; + std::snprintf(documentNameBuffer, sizeof(documentNameBuffer), "%s", document->Name.c_str()); + if (ImGui::InputText("UI 名称", documentNameBuffer, sizeof(documentNameBuffer))) { + document->Name = documentNameBuffer; + modified = true; + } + if (ImGui::InputInt("参考宽度", &document->ReferenceWidth)) { + modified = true; + } + if (ImGui::InputInt("参考高度", &document->ReferenceHeight)) { + modified = true; + } + + if (SelectedUiNodeId_.empty() || MetaCoreFindUiNode(*document, SelectedUiNodeId_) == nullptr) { + if (!document->RootNodeIds.empty()) { + SelectedUiNodeId_ = document->RootNodeIds.front(); + } else if (!document->Nodes.empty()) { + SelectedUiNodeId_ = document->Nodes.front().Id; + } + } + + ImGui::Separator(); + ImGui::Columns(2, "UiDocumentColumns", true); + ImGui::Text("层级树"); + ImGui::TextDisabled("拖拽节点可重排父子级"); + ImGui::Button("根层##UiRootDropTarget", ImVec2(-1.0F, 0.0F)); + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("MetaCoreUiNode")) { + const char* draggedNodeId = static_cast(payload->Data); + if (draggedNodeId != nullptr) { + modified = MetaCoreReparentUiNode(*document, draggedNodeId, "") || modified; + SelectedUiNodeId_ = draggedNodeId; + } + } + ImGui::EndDragDropTarget(); + } + if (ImGui::Button("添加根Panel")) { + modified = AddUiNode(*document, MetaCoreUiNodeType::Panel, "") || modified; + } + ImGui::SameLine(); + if (ImGui::Button("添加根Text")) { + modified = AddUiNode(*document, MetaCoreUiNodeType::Text, "") || modified; + } + ImGui::SameLine(); + if (ImGui::Button("添加根Image")) { + modified = AddUiNode(*document, MetaCoreUiNodeType::Image, "") || modified; + } + ImGui::SameLine(); + if (ImGui::Button("添加根Button")) { + modified = AddUiNode(*document, MetaCoreUiNodeType::Button, "") || modified; + } + for (const std::string& rootNodeId : document->RootNodeIds) { + modified = DrawUiNodeTree(*document, rootNodeId) || modified; + } + + ImGui::NextColumn(); + ImGui::Text("节点 Inspector"); + if (!SelectedUiNodeId_.empty()) { + if (ImGui::Button("添加子Panel")) { + modified = AddUiNode(*document, MetaCoreUiNodeType::Panel, SelectedUiNodeId_) || modified; + } + ImGui::SameLine(); + if (ImGui::Button("添加子Text")) { + modified = AddUiNode(*document, MetaCoreUiNodeType::Text, SelectedUiNodeId_) || modified; + } + ImGui::SameLine(); + if (ImGui::Button("添加子Image")) { + modified = AddUiNode(*document, MetaCoreUiNodeType::Image, SelectedUiNodeId_) || modified; + } + ImGui::SameLine(); + if (ImGui::Button("删除节点")) { + modified = DeleteUiNode(*document, SelectedUiNodeId_) || modified; + } + } + modified = DrawUiNodeInspector(*document, assetDatabaseService) || modified; + ImGui::Columns(1); + modified = DrawUiDocumentPreview(*document) || modified; + + if (modified) { + if (MetaCoreWriteTypedAssetDocument( + assetDatabaseService, + packageService, + reflectionRegistry, + *assetRecord, + *document, + "MetaCoreUiDocument" + )) { + ImGui::TextColored(ImVec4(0.32F, 0.85F, 0.42F, 1.0F), "已保存 UI 文档修改"); + } else { + ImGui::TextColored(ImVec4(0.92F, 0.32F, 0.28F, 1.0F), "UI 文档保存失败"); + } + } + } + std::filesystem::path SelectedDirectory_{}; + MetaCoreAssetGuid SelectedAssetGuid_{}; + std::filesystem::path SelectedAssetPath_{}; + std::string SelectedAssetType_{}; + MetaCoreAssetStorageKind SelectedAssetStorageKind_ = MetaCoreAssetStorageKind::SourcePackage; + MetaCoreAssetGuid SelectedGeneratedResourceGuid_{}; + std::string SelectedGeneratedResourceKind_{}; + std::string GeneratedResourceFilter_{}; + std::string GeneratedResourceKindFilter_{}; + std::string SelectedUiNodeId_{}; }; class MetaCoreConsolePanelProvider final : public MetaCoreIEditorPanelProvider { diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index 7c307c2..4b762e2 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); @@ -60,6 +61,288 @@ constexpr bool GMetaCoreEnableImGuizmo = true; constexpr bool GMetaCoreEnableImGuizmo = true; #endif +bool MetaCoreProjectWorldPointToViewport( + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState, + const glm::vec3& worldPoint, + ImVec2& screenPoint +) { + if (viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) { + return false; + } + + const glm::mat4 viewMatrix = glm::lookAt( + sceneView.CameraPosition, + sceneView.CameraTarget, + sceneView.CameraUp + ); + const glm::mat4 projectionMatrix = glm::perspective( + glm::radians(sceneView.VerticalFieldOfViewDegrees), + viewportState.Width / viewportState.Height, + 0.05F, + 500.0F + ); + + const glm::vec4 clipPosition = projectionMatrix * viewMatrix * glm::vec4(worldPoint, 1.0F); + if (clipPosition.w <= 0.0001F) { + return false; + } + + const glm::vec3 ndc = glm::vec3(clipPosition) / clipPosition.w; + if (ndc.z < -1.0F || ndc.z > 1.0F) { + return false; + } + + screenPoint.x = viewportState.Left + ((ndc.x + 1.0F) * 0.5F) * viewportState.Width; + screenPoint.y = viewportState.Top + ((1.0F - ndc.y) * 0.5F) * viewportState.Height; + return true; +} + +void MetaCoreDrawWorldOriginOverlay( + const MetaCoreEditorContext& editorContext, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState +) { + if (!editorContext.GetShowWorldOrigin()) { + return; + } + + ImVec2 originScreenPoint{}; + if (!MetaCoreProjectWorldPointToViewport(sceneView, viewportState, glm::vec3(0.0F, 0.0F, 0.0F), originScreenPoint)) { + return; + } + + ImDrawList* drawList = ImGui::GetForegroundDrawList(); + const ImU32 xColor = IM_COL32(220, 80, 80, 220); + const ImU32 yColor = IM_COL32(120, 210, 100, 220); + const ImU32 zColor = IM_COL32(90, 140, 235, 220); + drawList->AddCircleFilled(originScreenPoint, 3.5F, IM_COL32(255, 255, 255, 220)); + drawList->AddLine(originScreenPoint, ImVec2(originScreenPoint.x + 16.0F, originScreenPoint.y), xColor, 2.0F); + drawList->AddLine(originScreenPoint, ImVec2(originScreenPoint.x, originScreenPoint.y - 16.0F), yColor, 2.0F); + drawList->AddLine(originScreenPoint, ImVec2(originScreenPoint.x - 12.0F, originScreenPoint.y + 12.0F), zColor, 2.0F); + drawList->AddText(ImVec2(originScreenPoint.x + 18.0F, originScreenPoint.y - 8.0F), xColor, "X"); + drawList->AddText(ImVec2(originScreenPoint.x - 4.0F, originScreenPoint.y - 30.0F), yColor, "Y"); + drawList->AddText(ImVec2(originScreenPoint.x - 24.0F, originScreenPoint.y + 10.0F), zColor, "Z"); +} + +void MetaCoreDrawViewportGridOverlay( + const MetaCoreEditorContext& editorContext, + const MetaCoreSceneViewportState& viewportState +) { + if (!editorContext.GetShowViewportGrid()) { + return; + } + if (viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) { + return; + } + + ImDrawList* drawList = ImGui::GetForegroundDrawList(); + const ImVec2 topLeft(viewportState.Left, viewportState.Top); + const ImVec2 bottomRight(viewportState.Left + viewportState.Width, viewportState.Top + viewportState.Height); + const ImVec2 center((topLeft.x + bottomRight.x) * 0.5F, (topLeft.y + bottomRight.y) * 0.5F); + + const float minorSpacing = 32.0F; + const int verticalMinorCount = static_cast(viewportState.Width / minorSpacing) + 2; + const int horizontalMinorCount = static_cast(viewportState.Height / minorSpacing) + 2; + const ImU32 minorColor = IM_COL32(110, 120, 135, 40); + const ImU32 majorColor = IM_COL32(150, 165, 185, 70); + const ImU32 axisXColor = IM_COL32(220, 80, 80, 95); + const ImU32 axisYColor = IM_COL32(120, 210, 100, 95); + + for (int lineIndex = -verticalMinorCount; lineIndex <= verticalMinorCount; ++lineIndex) { + const float x = center.x + static_cast(lineIndex) * minorSpacing; + if (x < topLeft.x || x > bottomRight.x) { + continue; + } + const bool isMajor = (lineIndex % 4) == 0; + drawList->AddLine( + ImVec2(x, topLeft.y), + ImVec2(x, bottomRight.y), + lineIndex == 0 ? axisXColor : (isMajor ? majorColor : minorColor), + lineIndex == 0 ? 1.8F : (isMajor ? 1.2F : 1.0F) + ); + } + + for (int lineIndex = -horizontalMinorCount; lineIndex <= horizontalMinorCount; ++lineIndex) { + const float y = center.y + static_cast(lineIndex) * minorSpacing; + if (y < topLeft.y || y > bottomRight.y) { + continue; + } + const bool isMajor = (lineIndex % 4) == 0; + drawList->AddLine( + ImVec2(topLeft.x, y), + ImVec2(bottomRight.x, y), + lineIndex == 0 ? axisYColor : (isMajor ? majorColor : minorColor), + lineIndex == 0 ? 1.8F : (isMajor ? 1.2F : 1.0F) + ); + } +} + +void MetaCoreDrawSelectionBoundsOverlay( + const MetaCoreEditorContext& editorContext, + const MetaCoreScene& scene, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState +) { + const auto& selectedObjectIds = editorContext.GetSelectedObjectIds(); + if (selectedObjectIds.empty()) { + return; + } + + ImDrawList* drawList = ImGui::GetForegroundDrawList(); + const ImU32 boxColor = IM_COL32(255, 204, 64, 220); + const ImU32 activeBoxColor = IM_COL32(255, 235, 120, 255); + const ImU32 fillColor = IM_COL32(255, 204, 64, 24); + bool hasGroupBounds = false; + ImVec2 groupMin(FLT_MAX, FLT_MAX); + ImVec2 groupMax(-FLT_MAX, -FLT_MAX); + + for (const MetaCoreId selectedObjectId : selectedObjectIds) { + const MetaCoreGameObject* selectedObject = scene.FindGameObject(selectedObjectId); + if (selectedObject == nullptr) { + continue; + } + + const glm::vec3 halfExtents( + std::max(std::abs(selectedObject->Transform.Scale.x), 0.5F) * 0.5F, + std::max(std::abs(selectedObject->Transform.Scale.y), 0.5F) * 0.5F, + std::max(std::abs(selectedObject->Transform.Scale.z), 0.5F) * 0.5F + ); + const glm::vec3 center = selectedObject->Transform.Position; + + const std::array corners = { + center + glm::vec3(-halfExtents.x, -halfExtents.y, -halfExtents.z), + center + glm::vec3( halfExtents.x, -halfExtents.y, -halfExtents.z), + center + glm::vec3(-halfExtents.x, halfExtents.y, -halfExtents.z), + center + glm::vec3( halfExtents.x, halfExtents.y, -halfExtents.z), + center + glm::vec3(-halfExtents.x, -halfExtents.y, halfExtents.z), + center + glm::vec3( halfExtents.x, -halfExtents.y, halfExtents.z), + center + glm::vec3(-halfExtents.x, halfExtents.y, halfExtents.z), + center + glm::vec3( halfExtents.x, halfExtents.y, halfExtents.z) + }; + + bool anyPointVisible = false; + ImVec2 minPoint(FLT_MAX, FLT_MAX); + ImVec2 maxPoint(-FLT_MAX, -FLT_MAX); + for (const glm::vec3& corner : corners) { + ImVec2 screenPoint{}; + if (!MetaCoreProjectWorldPointToViewport(sceneView, viewportState, corner, screenPoint)) { + continue; + } + anyPointVisible = true; + minPoint.x = std::min(minPoint.x, screenPoint.x); + minPoint.y = std::min(minPoint.y, screenPoint.y); + maxPoint.x = std::max(maxPoint.x, screenPoint.x); + maxPoint.y = std::max(maxPoint.y, screenPoint.y); + } + + if (!anyPointVisible) { + continue; + } + + hasGroupBounds = true; + groupMin.x = std::min(groupMin.x, minPoint.x); + groupMin.y = std::min(groupMin.y, minPoint.y); + groupMax.x = std::max(groupMax.x, maxPoint.x); + groupMax.y = std::max(groupMax.y, maxPoint.y); + + const bool isActive = selectedObjectId == editorContext.GetActiveObjectId(); + const ImU32 strokeColor = isActive ? activeBoxColor : boxColor; + drawList->AddRectFilled(minPoint, maxPoint, fillColor, 2.0F); + drawList->AddRect(minPoint, maxPoint, strokeColor, 2.0F, 0, isActive ? 2.0F : 1.5F); + drawList->AddText(ImVec2(minPoint.x, minPoint.y - 18.0F), strokeColor, selectedObject->Name.c_str()); + } + + if (selectedObjectIds.size() > 1 && hasGroupBounds) { + const ImU32 groupStrokeColor = IM_COL32(255, 245, 180, 220); + const ImU32 groupFillColor = IM_COL32(255, 245, 180, 10); + drawList->AddRectFilled(groupMin, groupMax, groupFillColor, 4.0F); + drawList->AddRect(groupMin, groupMax, groupStrokeColor, 4.0F, 0, 2.0F); + const std::string groupLabel = "Selection x" + std::to_string(selectedObjectIds.size()); + drawList->AddText(ImVec2(groupMin.x, groupMin.y - 34.0F), groupStrokeColor, groupLabel.c_str()); + } +} + +void MetaCoreDrawSelectionHierarchyOverlay( + const MetaCoreEditorContext& editorContext, + const MetaCoreScene& scene, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState +) { + const auto& selectedObjectIds = editorContext.GetSelectedObjectIds(); + if (selectedObjectIds.size() < 2) { + return; + } + + std::unordered_set selectedSet(selectedObjectIds.begin(), selectedObjectIds.end()); + ImDrawList* drawList = ImGui::GetForegroundDrawList(); + const ImU32 lineColor = IM_COL32(255, 230, 160, 140); + + for (const MetaCoreId selectedObjectId : selectedObjectIds) { + const MetaCoreGameObject* childObject = scene.FindGameObject(selectedObjectId); + if (childObject == nullptr || childObject->ParentId == 0 || !selectedSet.contains(childObject->ParentId)) { + continue; + } + + const MetaCoreGameObject* parentObject = scene.FindGameObject(childObject->ParentId); + if (parentObject == nullptr) { + continue; + } + + ImVec2 childPoint{}; + ImVec2 parentPoint{}; + if (!MetaCoreProjectWorldPointToViewport(sceneView, viewportState, childObject->Transform.Position, childPoint) || + !MetaCoreProjectWorldPointToViewport(sceneView, viewportState, parentObject->Transform.Position, parentPoint)) { + continue; + } + + drawList->AddLine(parentPoint, childPoint, lineColor, 1.5F); + drawList->AddCircleFilled(parentPoint, 2.5F, lineColor); + drawList->AddCircleFilled(childPoint, 2.5F, lineColor); + } +} + +void MetaCoreDrawSelectionGroupCenterOverlay( + const MetaCoreEditorContext& editorContext, + const MetaCoreScene& scene, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState +) { + const auto& selectedObjectIds = editorContext.GetSelectedObjectIds(); + if (selectedObjectIds.size() < 2) { + return; + } + + glm::vec3 center(0.0F); + int validObjectCount = 0; + for (const MetaCoreId selectedObjectId : selectedObjectIds) { + const MetaCoreGameObject* selectedObject = scene.FindGameObject(selectedObjectId); + if (selectedObject == nullptr) { + continue; + } + center += selectedObject->Transform.Position; + ++validObjectCount; + } + + if (validObjectCount == 0) { + return; + } + center /= static_cast(validObjectCount); + + ImVec2 centerPoint{}; + if (!MetaCoreProjectWorldPointToViewport(sceneView, viewportState, center, centerPoint)) { + return; + } + + ImDrawList* drawList = ImGui::GetForegroundDrawList(); + const ImU32 centerColor = IM_COL32(255, 245, 180, 230); + drawList->AddCircle(centerPoint, 10.0F, centerColor, 24, 2.0F); + drawList->AddLine(ImVec2(centerPoint.x - 14.0F, centerPoint.y), ImVec2(centerPoint.x + 14.0F, centerPoint.y), centerColor, 1.5F); + drawList->AddLine(ImVec2(centerPoint.x, centerPoint.y - 14.0F), ImVec2(centerPoint.x, centerPoint.y + 14.0F), centerColor, 1.5F); + const std::string label = "Group Center"; + drawList->AddText(ImVec2(centerPoint.x + 12.0F, centerPoint.y + 12.0F), centerColor, label.c_str()); +} + void MetaCoreApplyEditorStyle() { ImGuiStyle& style = ImGui::GetStyle(); ImGui::StyleColorsDark(); @@ -563,6 +846,12 @@ void MetaCoreEditorApp::DrawEditorFrame() { } + MetaCoreDrawViewportGridOverlay(*EditorContext_, viewportState); + MetaCoreDrawWorldOriginOverlay(*EditorContext_, sceneView, viewportState); + MetaCoreDrawSelectionBoundsOverlay(*EditorContext_, Scene_, sceneView, viewportState); + MetaCoreDrawSelectionGroupCenterOverlay(*EditorContext_, Scene_, sceneView, viewportState); + MetaCoreDrawSelectionHierarchyOverlay(*EditorContext_, Scene_, sceneView, viewportState); + SceneInteractionService_.HandleGizmoEndUse(*EditorContext_, gizmoUsing); SceneInteractionService_.DrawViewportToolbar(*EditorContext_); } else { diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.cpp index 4d5fd79..3109515 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.cpp @@ -66,6 +66,11 @@ void MetaCoreEditorCameraController::FocusGameObject(const MetaCoreGameObject& g Distance_ = std::max(3.5F, scaleRadius * 4.0F + 2.5F); } +void MetaCoreEditorCameraController::FocusBounds(const glm::vec3& center, float radius) { + Target_ = center; + Distance_ = std::max(4.0F, radius * 3.5F + 3.0F); +} + void MetaCoreEditorCameraController::ApplySceneView(const MetaCoreSceneView& sceneView) { Target_ = sceneView.CameraTarget; FieldOfViewDegrees_ = sceneView.VerticalFieldOfViewDegrees; diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.h b/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.h index 2b45dae..42351d3 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.h +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.h @@ -14,6 +14,7 @@ class MetaCoreEditorCameraController { public: void Update(const MetaCoreSceneViewportState& viewportState, const MetaCoreInput& input); void FocusGameObject(const MetaCoreGameObject& gameObject); + void FocusBounds(const glm::vec3& center, float radius); [[nodiscard]] MetaCoreSceneView BuildSceneView() const; [[nodiscard]] glm::vec3 GetCameraPosition() const; void ApplySceneView(const MetaCoreSceneView& sceneView); diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp index 83c2adb..169ce1f 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp @@ -336,6 +336,10 @@ MetaCoreGizmoOperation MetaCoreEditorContext::GetGizmoOperation() const { return void MetaCoreEditorContext::SetGizmoOperation(MetaCoreGizmoOperation operation) { GizmoOperation_ = operation; } MetaCoreGizmoMode MetaCoreEditorContext::GetGizmoMode() const { return GizmoMode_; } void MetaCoreEditorContext::SetGizmoMode(MetaCoreGizmoMode mode) { GizmoMode_ = mode; } +bool MetaCoreEditorContext::GetShowViewportGrid() const { return ShowViewportGrid_; } +void MetaCoreEditorContext::SetShowViewportGrid(bool show) { ShowViewportGrid_ = show; } +bool MetaCoreEditorContext::GetShowWorldOrigin() const { return ShowWorldOrigin_; } +void MetaCoreEditorContext::SetShowWorldOrigin(bool show) { ShowWorldOrigin_ = show; } MetaCoreReparentTransformRule MetaCoreEditorContext::GetReparentTransformRule() const { return ReparentTransformRule_; } void MetaCoreEditorContext::SetReparentTransformRule(MetaCoreReparentTransformRule rule) { ReparentTransformRule_ = rule; } diff --git a/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp index cd9e102..398cd3b 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp @@ -87,6 +87,24 @@ ImGuizmo::OPERATION MetaCoreToImGuizmoOperation(MetaCoreGizmoOperation operation } } +const char* MetaCoreGizmoOperationLabel(MetaCoreGizmoOperation operation) { + switch (operation) { + case MetaCoreGizmoOperation::None: return "View"; + case MetaCoreGizmoOperation::Translate: return "Move"; + case MetaCoreGizmoOperation::Rotate: return "Rotate"; + case MetaCoreGizmoOperation::Scale: return "Scale"; + default: return "Unknown"; + } +} + +const char* MetaCoreGizmoModeLabel(MetaCoreGizmoMode mode) { + switch (mode) { + case MetaCoreGizmoMode::Local: return "Local"; + case MetaCoreGizmoMode::Global: return "Global"; + default: return "Unknown"; + } +} + } // namespace void MetaCoreSceneInteractionService::ResetFrameState() { @@ -260,6 +278,101 @@ void MetaCoreSceneInteractionService::HandleGizmoEndUse(MetaCoreEditorContext& e GizmoWasUsing_ = gizmoUsing; } +void MetaCoreSceneInteractionService::FocusSelectedObject(MetaCoreEditorContext& editorContext) const { + const auto& selectedObjectIds = editorContext.GetSelectedObjectIds(); + if (selectedObjectIds.empty()) { + return; + } + + if (selectedObjectIds.size() == 1) { + MetaCoreGameObject* selectedObject = editorContext.GetSelectedGameObject(); + if (selectedObject != nullptr) { + editorContext.GetCameraController().FocusGameObject(*selectedObject); + } + return; + } + + bool initialized = false; + glm::vec3 boundsMin(0.0F); + glm::vec3 boundsMax(0.0F); + for (const MetaCoreId selectedObjectId : selectedObjectIds) { + const MetaCoreGameObject* selectedObject = editorContext.GetScene().FindGameObject(selectedObjectId); + if (selectedObject == nullptr) { + continue; + } + + const float extent = std::max({ + std::abs(selectedObject->Transform.Scale.x), + std::abs(selectedObject->Transform.Scale.y), + std::abs(selectedObject->Transform.Scale.z), + 0.5F + }); + const glm::vec3 objectMin = selectedObject->Transform.Position - glm::vec3(extent); + const glm::vec3 objectMax = selectedObject->Transform.Position + glm::vec3(extent); + + if (!initialized) { + boundsMin = objectMin; + boundsMax = objectMax; + initialized = true; + continue; + } + + boundsMin = glm::min(boundsMin, objectMin); + boundsMax = glm::max(boundsMax, objectMax); + } + + if (!initialized) { + return; + } + + const glm::vec3 center = (boundsMin + boundsMax) * 0.5F; + const glm::vec3 size = boundsMax - boundsMin; + const float radius = std::max({size.x, size.y, size.z, 1.0F}) * 0.5F; + editorContext.GetCameraController().FocusBounds(center, radius); +} + +void MetaCoreSceneInteractionService::FocusVisibleScene(MetaCoreEditorContext& editorContext) const { + const auto& gameObjects = editorContext.GetScene().GetGameObjects(); + bool initialized = false; + glm::vec3 boundsMin(0.0F); + glm::vec3 boundsMax(0.0F); + + for (const MetaCoreGameObject& gameObject : gameObjects) { + if (gameObject.MeshRenderer.has_value() && !gameObject.MeshRenderer->Visible) { + continue; + } + + const float extent = std::max({ + std::abs(gameObject.Transform.Scale.x), + std::abs(gameObject.Transform.Scale.y), + std::abs(gameObject.Transform.Scale.z), + 0.5F + }); + const glm::vec3 objectMin = gameObject.Transform.Position - glm::vec3(extent); + const glm::vec3 objectMax = gameObject.Transform.Position + glm::vec3(extent); + + if (!initialized) { + boundsMin = objectMin; + boundsMax = objectMax; + initialized = true; + continue; + } + + boundsMin = glm::min(boundsMin, objectMin); + boundsMax = glm::max(boundsMax, objectMax); + } + + if (!initialized) { + editorContext.GetCameraController().FocusBounds(glm::vec3(0.0F, 0.5F, 0.0F), 2.0F); + return; + } + + const glm::vec3 center = (boundsMin + boundsMax) * 0.5F; + const glm::vec3 size = boundsMax - boundsMin; + const float radius = std::max({size.x, size.y, size.z, 1.0F}) * 0.5F; + editorContext.GetCameraController().FocusBounds(center, radius); +} + void MetaCoreSceneInteractionService::HandleGizmoManipulation(MetaCoreEditorContext& editorContext) { if (editorContext.GetGizmoOperation() == MetaCoreGizmoOperation::None) { return; @@ -390,6 +503,61 @@ void MetaCoreSceneInteractionService::DrawViewportToolbar(MetaCoreEditorContext& if (drawToolbarToggle("Global", editorContext.GetGizmoMode() == MetaCoreGizmoMode::Global)) { editorContext.SetGizmoMode(MetaCoreGizmoMode::Global); } + + ImGui::SameLine(0, 12); + const bool hasSelection = editorContext.GetSelectedGameObject() != nullptr; + if (!hasSelection) { + ImGui::BeginDisabled(); + } + if (drawToolbarToggle(" F ", false)) { + FocusSelectedObject(editorContext); + } + if (!hasSelection) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0, 2); + if (drawToolbarToggle(" A ", false)) { + FocusVisibleScene(editorContext); + } + + ImGui::SameLine(0, 12); + bool showGrid = editorContext.GetShowViewportGrid(); + if (drawToolbarToggle(" G ", showGrid)) { + editorContext.SetShowViewportGrid(!showGrid); + } + ImGui::SameLine(); + bool showOrigin = editorContext.GetShowWorldOrigin(); + if (drawToolbarToggle(" O ", showOrigin)) { + editorContext.SetShowWorldOrigin(!showOrigin); + } + + ImGui::SameLine(0, 12); + ImGui::TextDisabled( + "Tool: %s | Mode: %s | Grid: %s | Origin: %s | [Q/W/E/R] [Z] [F] [A] [G] [O]", + MetaCoreGizmoOperationLabel(editorContext.GetGizmoOperation()), + MetaCoreGizmoModeLabel(editorContext.GetGizmoMode()), + editorContext.GetShowViewportGrid() ? "On" : "Off", + editorContext.GetShowWorldOrigin() ? "On" : "Off" + ); + + ImGui::SameLine(0, 12); + const auto& selectedObjectIds = editorContext.GetSelectedObjectIds(); + if (selectedObjectIds.empty()) { + ImGui::TextDisabled("Selection: None"); + } else if (selectedObjectIds.size() == 1) { + const MetaCoreGameObject* activeObject = editorContext.GetSelectedGameObject(); + ImGui::TextDisabled( + "Selection: %s", + activeObject != nullptr ? activeObject->Name.c_str() : "" + ); + } else { + const MetaCoreGameObject* activeObject = editorContext.GetSelectedGameObject(); + ImGui::TextDisabled( + "Selection: %zu objects | Active: %s", + selectedObjectIds.size(), + activeObject != nullptr ? activeObject->Name.c_str() : "" + ); + } } ImGui::End(); ImGui::PopStyleColor(); @@ -420,6 +588,22 @@ void MetaCoreSceneInteractionService::HandleShortcuts(MetaCoreEditorContext& edi editorContext.SetGizmoMode(MetaCoreGizmoMode::Local); } } + + if (ImGui::IsKeyPressed(ImGuiKey_F) && editorContext.GetSelectedGameObject() != nullptr) { + FocusSelectedObject(editorContext); + } + + if (ImGui::IsKeyPressed(ImGuiKey_A)) { + FocusVisibleScene(editorContext); + } + + if (ImGui::IsKeyPressed(ImGuiKey_G)) { + editorContext.SetShowViewportGrid(!editorContext.GetShowViewportGrid()); + } + + if (ImGui::IsKeyPressed(ImGuiKey_O)) { + editorContext.SetShowWorldOrigin(!editorContext.GetShowWorldOrigin()); + } } } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h index 7713f62..03287e8 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h @@ -5,12 +5,52 @@ #include "MetaCoreScene/MetaCoreSceneDocument.h" #include "MetaCoreScene/MetaCoreGameObject.h" +#include + #include #include #include namespace MetaCore { +MC_ENUM() +enum class MetaCoreMaterialShaderModel { + PbrMetalRough = 0, + UnlitColor, + UnlitTexture +}; + +MC_ENUM() +enum class MetaCoreMaterialAlphaMode { + Opaque = 0, + Mask, + Blend +}; + +MC_ENUM() +enum class MetaCoreUiNodeType { + Panel = 0, + Text, + Image, + Button +}; + +MC_ENUM() +enum class MetaCoreUiHorizontalAlignment { + Left = 0, + Center, + Right, + Stretch +}; + +MC_ENUM() +enum class MetaCoreUiVerticalAlignment { + Top = 0, + Center, + Bottom, + Stretch +}; + MC_STRUCT() struct MetaCoreAssetMetadataDocument { MC_GENERATED_BODY() @@ -51,6 +91,201 @@ struct MetaCoreImportedAssetDocument { std::uint64_t SourceHash = 0; }; +MC_STRUCT() +struct MetaCoreImportedGltfNodeDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Name{}; + + MC_PROPERTY() + std::int32_t ParentIndex = -1; + + MC_PROPERTY() + std::int32_t MeshIndex = -1; +}; + +MC_STRUCT() +struct MetaCoreTextureAssetDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid AssetGuid{}; + + MC_PROPERTY() + std::string Name{}; + + MC_PROPERTY() + std::filesystem::path SourcePath{}; + + MC_PROPERTY() + std::string UsageHint{}; +}; + +MC_STRUCT() +struct MetaCoreMaterialAssetDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid AssetGuid{}; + + MC_PROPERTY() + std::string Name{}; + + MC_PROPERTY() + MetaCoreMaterialShaderModel ShaderModel = MetaCoreMaterialShaderModel::PbrMetalRough; + + MC_PROPERTY() + glm::vec3 BaseColor{1.0F, 1.0F, 1.0F}; + + MC_PROPERTY() + MetaCoreAssetGuid BaseColorTexture{}; + + MC_PROPERTY() + MetaCoreAssetGuid NormalTexture{}; + + MC_PROPERTY() + float Metallic = 0.0F; + + MC_PROPERTY() + float Roughness = 1.0F; + + MC_PROPERTY() + MetaCoreAssetGuid MetallicRoughnessTexture{}; + + MC_PROPERTY() + MetaCoreAssetGuid AoTexture{}; + + MC_PROPERTY() + glm::vec3 EmissiveColor{0.0F, 0.0F, 0.0F}; + + MC_PROPERTY() + MetaCoreAssetGuid EmissiveTexture{}; + + MC_PROPERTY() + MetaCoreMaterialAlphaMode AlphaMode = MetaCoreMaterialAlphaMode::Opaque; + + MC_PROPERTY() + float AlphaCutoff = 0.5F; + + MC_PROPERTY() + bool DoubleSided = false; +}; + +MC_STRUCT() +struct MetaCoreMeshSubMeshDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Name{}; + + MC_PROPERTY() + std::int32_t MaterialSlotIndex = -1; +}; + +MC_STRUCT() +struct MetaCoreMeshAssetDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid AssetGuid{}; + + MC_PROPERTY() + std::string Name{}; + + MC_PROPERTY() + std::int32_t VertexCount = 0; + + MC_PROPERTY() + std::int32_t IndexCount = 0; + + MC_PROPERTY() + std::vector SubMeshes{}; +}; + +MC_STRUCT() +struct MetaCoreImportedGltfMeshDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Name{}; + + MC_PROPERTY() + std::int32_t PrimitiveCount = 0; + + MC_PROPERTY() + std::vector MaterialSlots{}; +}; + +MC_STRUCT() +struct MetaCoreImportedGltfMaterialDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Name{}; + + MC_PROPERTY() + bool DoubleSided = false; + + MC_PROPERTY() + std::string AlphaMode{"Opaque"}; + + MC_PROPERTY() + std::vector TextureIndices{}; +}; + +MC_STRUCT() +struct MetaCoreImportedGltfTextureDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::filesystem::path SourcePath{}; + + MC_PROPERTY() + std::string UsageHint{}; +}; + +MC_STRUCT() +struct MetaCoreImportedGltfAssetDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string AssetType{}; + + MC_PROPERTY() + std::string ImporterId{}; + + MC_PROPERTY() + std::filesystem::path SourcePath{}; + + MC_PROPERTY() + std::uint64_t SourceHash = 0; + + MC_PROPERTY() + std::string SourceFormat{}; + + MC_PROPERTY() + std::vector Meshes{}; + + MC_PROPERTY() + std::vector Materials{}; + + MC_PROPERTY() + std::vector Textures{}; + + MC_PROPERTY() + std::vector GeneratedMeshAssets{}; + + MC_PROPERTY() + std::vector GeneratedMaterialAssets{}; + + MC_PROPERTY() + std::vector GeneratedTextureAssets{}; + + MC_PROPERTY() + std::vector Nodes{}; +}; + MC_STRUCT() struct MetaCorePrefabDocument { MC_GENERATED_BODY() @@ -90,4 +325,111 @@ struct MetaCoreCookManifestDocument { std::vector Entries{}; }; +MC_STRUCT() +struct MetaCoreUiRectTransformDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + glm::vec3 AnchorMin{0.0F, 0.0F, 0.0F}; + + MC_PROPERTY() + glm::vec3 AnchorMax{1.0F, 1.0F, 0.0F}; + + MC_PROPERTY() + glm::vec3 Pivot{0.5F, 0.5F, 0.0F}; + + MC_PROPERTY() + glm::vec3 Position{0.0F, 0.0F, 0.0F}; + + MC_PROPERTY() + glm::vec3 Size{100.0F, 100.0F, 0.0F}; +}; + +MC_STRUCT() +struct MetaCoreUiStyleDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + glm::vec3 BackgroundColor{0.15F, 0.15F, 0.15F}; + + MC_PROPERTY() + glm::vec3 TextColor{1.0F, 1.0F, 1.0F}; + + MC_PROPERTY() + glm::vec3 TintColor{1.0F, 1.0F, 1.0F}; + + MC_PROPERTY() + float FontSize = 16.0F; + + MC_PROPERTY() + glm::vec3 Padding{8.0F, 8.0F, 0.0F}; + + MC_PROPERTY() + MetaCoreUiHorizontalAlignment HorizontalAlignment = MetaCoreUiHorizontalAlignment::Left; + + MC_PROPERTY() + MetaCoreUiVerticalAlignment VerticalAlignment = MetaCoreUiVerticalAlignment::Top; + + MC_PROPERTY() + MetaCoreAssetGuid ImageAssetGuid{}; + + MC_PROPERTY() + bool PreserveAspect = false; +}; + +MC_STRUCT() +struct MetaCoreUiNodeDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Id{}; + + MC_PROPERTY() + std::string Name{}; + + MC_PROPERTY() + MetaCoreUiNodeType Type = MetaCoreUiNodeType::Panel; + + MC_PROPERTY() + std::string ParentId{}; + + MC_PROPERTY() + std::vector Children{}; + + MC_PROPERTY() + bool Visible = true; + + MC_PROPERTY() + MetaCoreUiRectTransformDocument RectTransform{}; + + MC_PROPERTY() + MetaCoreUiStyleDocument Style{}; + + MC_PROPERTY() + std::string Text{}; + + MC_PROPERTY() + bool Interactable = false; +}; + +MC_STRUCT() +struct MetaCoreUiDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string Name{}; + + MC_PROPERTY() + std::int32_t ReferenceWidth = 1920; + + MC_PROPERTY() + std::int32_t ReferenceHeight = 1080; + + MC_PROPERTY() + std::vector RootNodeIds{}; + + MC_PROPERTY() + std::vector Nodes{}; +}; + } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h index 9d313b7..43ed052 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h @@ -106,6 +106,10 @@ public: void SetGizmoOperation(MetaCoreGizmoOperation operation); [[nodiscard]] MetaCoreGizmoMode GetGizmoMode() const; void SetGizmoMode(MetaCoreGizmoMode mode); + [[nodiscard]] bool GetShowViewportGrid() const; + void SetShowViewportGrid(bool show); + [[nodiscard]] bool GetShowWorldOrigin() const; + void SetShowWorldOrigin(bool show); [[nodiscard]] MetaCoreReparentTransformRule GetReparentTransformRule() const; void SetReparentTransformRule(MetaCoreReparentTransformRule rule); [[nodiscard]] MetaCoreEditorCommandService& GetCommandService(); @@ -153,6 +157,8 @@ private: MetaCoreId SelectionAnchorId_ = 0; MetaCoreGizmoOperation GizmoOperation_ = MetaCoreGizmoOperation::Translate; MetaCoreGizmoMode GizmoMode_ = MetaCoreGizmoMode::Local; + bool ShowViewportGrid_ = true; + bool ShowWorldOrigin_ = true; MetaCoreReparentTransformRule ReparentTransformRule_ = MetaCoreReparentTransformRule::KeepWorld; MetaCoreEditorCommandService CommandService_{}; MetaCoreId PendingRenameObjectId_ = 0; diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index 5d5ed81..6ad93c6 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -80,7 +80,10 @@ struct MetaCoreProjectDescriptor { std::filesystem::path RootPath{}; std::filesystem::path AssetsPath{}; std::filesystem::path ScenesPath{}; + std::filesystem::path RuntimePath{}; + std::filesystem::path UiPath{}; std::filesystem::path LibraryPath{}; + std::filesystem::path BuildPath{}; std::filesystem::path StartupScenePath{}; std::vector ScenePaths{}; }; @@ -192,6 +195,12 @@ public: std::optional forcedParentId ) = 0; + [[nodiscard]] virtual std::optional InstantiateModelAsset( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& assetGuid, + std::optional forcedParentId + ) = 0; + [[nodiscard]] virtual bool RenameGameObject( MetaCoreEditorContext& editorContext, MetaCoreId objectId, diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h index b82f8d6..a7a61cd 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h @@ -26,6 +26,8 @@ public: void HandleGizmoEndUse(MetaCoreEditorContext& editorContext, bool gizmoUsing); // Unity-style modular interactive features + void FocusSelectedObject(MetaCoreEditorContext& editorContext) const; + void FocusVisibleScene(MetaCoreEditorContext& editorContext) const; void HandleGizmoManipulation(MetaCoreEditorContext& editorContext); void DrawViewportToolbar(MetaCoreEditorContext& editorContext); void HandleShortcuts(MetaCoreEditorContext& editorContext); diff --git a/Source/MetaCoreRender/Private/MetaCorePandaSceneBridge.cpp b/Source/MetaCoreRender/Private/MetaCorePandaSceneBridge.cpp index fa0a1a4..88874b8 100644 --- a/Source/MetaCoreRender/Private/MetaCorePandaSceneBridge.cpp +++ b/Source/MetaCoreRender/Private/MetaCorePandaSceneBridge.cpp @@ -15,11 +15,16 @@ #include "geomVertexData.h" #include "geomVertexFormat.h" #include "geomVertexWriter.h" +#include "internalName.h" #include "lineSegs.h" #include "loader.h" #include "nodePath.h" #include "perspectiveLens.h" #include "pandaNode.h" +#include "shader.h" +#include "texture.h" +#include "texturePool.h" +#include "transparencyAttrib.h" #include "windowFramework.h" #define GLM_ENABLE_EXPERIMENTAL @@ -28,6 +33,7 @@ #include #include +#include #include #include #include @@ -238,6 +244,430 @@ NodePath MetaCoreCreateUnitCubeNode(const NodePath& parentNode) { return parentNode.attach_new_node(geomNode); } +[[nodiscard]] std::filesystem::path MetaCoreResolveProjectRootFromEnvironment() { + char* rawProjectPath = nullptr; + std::size_t valueLength = 0; + if (_dupenv_s(&rawProjectPath, &valueLength, "METACORE_PROJECT_PATH") != 0 || rawProjectPath == nullptr || valueLength == 0) { + if (rawProjectPath != nullptr) { + std::free(rawProjectPath); + } + return {}; + } + + const std::filesystem::path projectRoot(rawProjectPath); + std::free(rawProjectPath); + return projectRoot; +} + +[[nodiscard]] NodePath MetaCoreTryLoadModelNode( + WindowFramework& windowFramework, + const NodePath& parentNode, + const std::filesystem::path& projectRoot, + const std::string& relativeSourcePath +) { + if (projectRoot.empty() || relativeSourcePath.empty()) { + return NodePath(); + } + + const std::filesystem::path absoluteSourcePath = projectRoot / std::filesystem::path(relativeSourcePath); + if (!std::filesystem::exists(absoluteSourcePath)) { + MetaCoreTrace(("load_model: source file missing: " + absoluteSourcePath.string()).c_str()); + return NodePath(); + } + + const Filename pandaPath = Filename::from_os_specific(absoluteSourcePath.string()); + NodePath loadedNode = windowFramework.load_model(parentNode, pandaPath); + if (loadedNode.is_empty()) { + MetaCoreTrace(("load_model: Panda3D failed to load: " + absoluteSourcePath.string()).c_str()); + return NodePath(); + } + MetaCoreTrace(("load_model: success: " + absoluteSourcePath.string()).c_str()); + return loadedNode; +} + +[[nodiscard]] PT(Shader) MetaCoreLoadSimplePbrShader() { + static PT(Shader) cachedShader; + static bool attemptedLoad = false; + if (attemptedLoad) { + return cachedShader; + } + + attemptedLoad = true; + const std::filesystem::path shaderRoot = std::filesystem::current_path() / "simplepbr" / "shaders"; + const std::filesystem::path vertexPath = shaderRoot / "simplepbr.vert"; + const std::filesystem::path fragmentPath = shaderRoot / "simplepbr.frag"; + if (!std::filesystem::exists(vertexPath) || !std::filesystem::exists(fragmentPath)) { + MetaCoreTrace("simplepbr: shader files missing in runtime directory"); + return nullptr; + } + + cachedShader = Shader::load( + Shader::SL_GLSL, + Filename::from_os_specific(vertexPath.string()), + Filename::from_os_specific(fragmentPath.string()) + ); + if (cachedShader == nullptr) { + MetaCoreTrace("simplepbr: failed to load GLSL shader"); + } else { + MetaCoreTrace("simplepbr: shader loaded"); + } + return cachedShader; +} + +[[nodiscard]] PT(Texture) MetaCoreCreateSolidTexture( + const std::string& textureName, + unsigned char r, + unsigned char g, + unsigned char b, + unsigned char a +) { + PT(Texture) texture = new Texture(textureName); + texture->setup_2d_texture(1, 1, Texture::T_unsigned_byte, Texture::F_rgba8); + PTA_uchar image = texture->modify_ram_image(); + if (image.size() >= 4) { + image[0] = r; + image[1] = g; + image[2] = b; + image[3] = a; + } + texture->set_minfilter(SamplerState::FT_nearest); + texture->set_magfilter(SamplerState::FT_nearest); + texture->set_wrap_u(SamplerState::WM_repeat); + texture->set_wrap_v(SamplerState::WM_repeat); + return texture; +} + +[[nodiscard]] PT(Texture) MetaCoreCreateSolidCubeMap( + const std::string& textureName, + unsigned char r, + unsigned char g, + unsigned char b, + unsigned char a +) { + PT(Texture) texture = new Texture(textureName); + texture->setup_cube_map(1, Texture::T_unsigned_byte, Texture::F_rgba8); + PTA_uchar image = texture->modify_ram_image(); + for (size_t index = 0; index + 3 < image.size(); index += 4) { + image[index + 0] = r; + image[index + 1] = g; + image[index + 2] = b; + image[index + 3] = a; + } + texture->set_minfilter(SamplerState::FT_nearest); + texture->set_magfilter(SamplerState::FT_nearest); + texture->set_wrap_u(SamplerState::WM_clamp); + texture->set_wrap_v(SamplerState::WM_clamp); + texture->set_wrap_w(SamplerState::WM_clamp); + return texture; +} + +[[nodiscard]] PT(Texture) MetaCoreGetDefaultBaseColorTexture() { + static PT(Texture) texture = MetaCoreCreateSolidTexture("MetaCoreDefaultBaseColor", 255, 255, 255, 255); + return texture; +} + +[[nodiscard]] PT(Texture) MetaCoreGetDefaultMetalRoughnessTexture() { + static PT(Texture) texture = MetaCoreCreateSolidTexture("MetaCoreDefaultMetalRoughness", 255, 255, 255, 255); + return texture; +} + +[[nodiscard]] PT(Texture) MetaCoreGetDefaultEmissionTexture() { + static PT(Texture) texture = MetaCoreCreateSolidTexture("MetaCoreDefaultEmission", 0, 0, 0, 255); + return texture; +} + +[[nodiscard]] PT(Texture) MetaCoreGetDefaultAoTexture() { + static PT(Texture) texture = MetaCoreCreateSolidTexture("MetaCoreDefaultAo", 255, 255, 255, 255); + return texture; +} + +[[nodiscard]] PT(Texture) MetaCoreGetDefaultNormalTexture() { + static PT(Texture) texture = MetaCoreCreateSolidTexture("MetaCoreDefaultNormal", 128, 128, 255, 255); + return texture; +} + +[[nodiscard]] PT(Texture) MetaCoreLoadBrdfLutTexture() { + static PT(Texture) cachedTexture; + static bool attemptedLoad = false; + if (attemptedLoad) { + return cachedTexture; + } + + attemptedLoad = true; + const std::filesystem::path texturePath = std::filesystem::current_path() / "simplepbr" / "textures" / "brdf_lut.txo"; + if (!std::filesystem::exists(texturePath)) { + MetaCoreTrace("simplepbr: brdf_lut.txo missing in runtime directory"); + cachedTexture = MetaCoreCreateSolidTexture("MetaCoreFallbackBrdfLut", 255, 255, 255, 255); + return cachedTexture; + } + + cachedTexture = TexturePool::load_texture(Filename::from_os_specific(texturePath.string())); + if (cachedTexture == nullptr) { + MetaCoreTrace("simplepbr: failed to load brdf_lut.txo"); + cachedTexture = MetaCoreCreateSolidTexture("MetaCoreFallbackBrdfLut", 255, 255, 255, 255); + } + return cachedTexture; +} + +[[nodiscard]] PT(Texture) MetaCoreGetDefaultFilteredEnvMap() { + static PT(Texture) texture = MetaCoreCreateSolidCubeMap("MetaCoreDefaultFilteredEnvMap", 0, 0, 0, 255); + return texture; +} + +[[nodiscard]] PT(Texture) MetaCoreResolveBaseColorTexture( + const std::filesystem::path& projectRoot, + const std::string& relativeTexturePath +) { + if (projectRoot.empty() || relativeTexturePath.empty()) { + return MetaCoreGetDefaultBaseColorTexture(); + } + + const std::filesystem::path absoluteTexturePath = projectRoot / std::filesystem::path(relativeTexturePath); + if (!std::filesystem::exists(absoluteTexturePath)) { + return MetaCoreGetDefaultBaseColorTexture(); + } + + PT(Texture) texture = TexturePool::load_texture(Filename::from_os_specific(absoluteTexturePath.string())); + if (texture == nullptr) { + MetaCoreTrace(("load_texture: Panda3D failed to load: " + absoluteTexturePath.string()).c_str()); + return MetaCoreGetDefaultBaseColorTexture(); + } + + return texture; +} + +[[nodiscard]] PT(Texture) MetaCoreResolveTextureWithFallback( + const std::filesystem::path& projectRoot, + const std::string& relativeTexturePath, + const PT(Texture)& fallbackTexture +) { + if (projectRoot.empty() || relativeTexturePath.empty()) { + return fallbackTexture; + } + + const std::filesystem::path absoluteTexturePath = projectRoot / std::filesystem::path(relativeTexturePath); + if (!std::filesystem::exists(absoluteTexturePath)) { + return fallbackTexture; + } + + PT(Texture) texture = TexturePool::load_texture(Filename::from_os_specific(absoluteTexturePath.string())); + if (texture == nullptr) { + MetaCoreTrace(("load_texture: Panda3D failed to load: " + absoluteTexturePath.string()).c_str()); + return fallbackTexture; + } + + return texture; +} + +void MetaCoreApplyBaseColorTexture( + NodePath& meshNode, + const std::filesystem::path& projectRoot, + const std::string& relativeTexturePath +) { + if (meshNode.is_empty()) { + return; + } + + PT(Texture) texture = MetaCoreResolveBaseColorTexture(projectRoot, relativeTexturePath); + if (texture == nullptr) { + meshNode.set_texture_off(1); + return; + } + + meshNode.set_texture(texture, 1); + meshNode.set_shader_input(InternalName::make("p3d_TextureBaseColor"), texture); +} + +void MetaCoreApplyMetallicRoughnessTexture( + NodePath& meshNode, + const std::filesystem::path& projectRoot, + const std::string& relativeTexturePath +) { + if (meshNode.is_empty()) { + return; + } + + PT(Texture) texture = MetaCoreResolveTextureWithFallback( + projectRoot, + relativeTexturePath, + MetaCoreGetDefaultMetalRoughnessTexture() + ); + if (texture == nullptr) { + return; + } + + meshNode.set_shader_input(InternalName::make("p3d_TextureMetalRoughness"), texture); +} + +void MetaCoreApplyNormalTexture( + NodePath& meshNode, + const std::filesystem::path& projectRoot, + const std::string& relativeTexturePath +) { + if (meshNode.is_empty()) { + return; + } + + PT(Texture) texture = MetaCoreResolveTextureWithFallback( + projectRoot, + relativeTexturePath, + MetaCoreGetDefaultNormalTexture() + ); + if (texture == nullptr) { + return; + } + + meshNode.set_shader_input(InternalName::make("p3d_TextureNormal"), texture); +} + +void MetaCoreApplyEmissionTexture( + NodePath& meshNode, + const std::filesystem::path& projectRoot, + const std::string& relativeTexturePath +) { + if (meshNode.is_empty()) { + return; + } + + PT(Texture) texture = MetaCoreResolveTextureWithFallback( + projectRoot, + relativeTexturePath, + MetaCoreGetDefaultEmissionTexture() + ); + if (texture == nullptr) { + return; + } + + meshNode.set_shader_input(InternalName::make("p3d_TextureEmission"), texture); +} + +void MetaCoreApplyAoTexture( + NodePath& meshNode, + const std::filesystem::path& projectRoot, + const std::string& relativeTexturePath +) { + if (meshNode.is_empty()) { + return; + } + + PT(Texture) texture = MetaCoreResolveTextureWithFallback( + projectRoot, + relativeTexturePath, + MetaCoreGetDefaultAoTexture() + ); + if (texture == nullptr) { + return; + } + + meshNode.set_shader_input(InternalName::make("p3d_TextureOcclusion"), texture); +} + +void MetaCoreApplySimplePbrPreview(NodePath& meshNode) { + if (meshNode.is_empty()) { + return; + } + + PT(Shader) simplePbrShader = MetaCoreLoadSimplePbrShader(); + if (simplePbrShader == nullptr) { + meshNode.clear_shader(); + return; + } + + meshNode.set_shader(simplePbrShader, 1); +} + +void MetaCoreApplySimplePbrMaterialInputs( + NodePath& meshNode, + const MetaCoreMeshRendererComponent& meshRenderer +) { + if (meshNode.is_empty()) { + return; + } + + meshNode.set_shader_input( + InternalName::make("p3d_Material.baseColor"), + LVecBase4( + meshRenderer.BaseColor.r, + meshRenderer.BaseColor.g, + meshRenderer.BaseColor.b, + 1.0F + ) + ); + meshNode.set_shader_input( + InternalName::make("p3d_Material.roughness"), + LVecBase4(meshRenderer.Roughness, 0.0F, 0.0F, 0.0F) + ); + meshNode.set_shader_input( + InternalName::make("p3d_Material.metallic"), + LVecBase4(meshRenderer.Metallic, 0.0F, 0.0F, 0.0F) + ); + meshNode.set_shader_input( + InternalName::make("p3d_Material.emission"), + LVecBase4( + meshRenderer.EmissiveColor.r, + meshRenderer.EmissiveColor.g, + meshRenderer.EmissiveColor.b, + 1.0F + ) + ); + meshNode.set_shader_input( + InternalName::make("p3d_TextureMetalRoughness"), + MetaCoreGetDefaultMetalRoughnessTexture() + ); + meshNode.set_shader_input( + InternalName::make("p3d_TextureNormal"), + MetaCoreGetDefaultNormalTexture() + ); + meshNode.set_shader_input( + InternalName::make("p3d_TextureEmission"), + MetaCoreGetDefaultEmissionTexture() + ); + meshNode.set_shader_input( + InternalName::make("p3d_TextureOcclusion"), + MetaCoreGetDefaultAoTexture() + ); + meshNode.set_shader_input( + InternalName::make("brdf_lut"), + MetaCoreLoadBrdfLutTexture() + ); + meshNode.set_shader_input( + InternalName::make("filtered_env_map"), + MetaCoreGetDefaultFilteredEnvMap() + ); + meshNode.set_shader_input( + InternalName::make("max_reflection_lod"), + LVecBase4(0.0F, 0.0F, 0.0F, 0.0F) + ); + meshNode.set_shader_input( + InternalName::make("metacore_AlphaCutoff"), + LVecBase4(meshRenderer.AlphaCutoff, 0.0F, 0.0F, 0.0F) + ); + meshNode.set_shader_input( + InternalName::make("metacore_AlphaMode"), + LVecBase4(static_cast(meshRenderer.AlphaMode), 0.0F, 0.0F, 0.0F) + ); +} + +void MetaCoreApplySimplePbrViewInputs( + NodePath& meshNode, + const NodePath& sceneRootNode, + const NodePath& cameraNode +) { + if (meshNode.is_empty()) { + return; + } + + LPoint3f cameraWorldPosition(0.0F, 0.0F, 0.0F); + if (!cameraNode.is_empty()) { + cameraWorldPosition = cameraNode.get_pos(sceneRootNode); + } + + meshNode.set_shader_input( + InternalName::make("camera_world_position"), + LVecBase3f(cameraWorldPosition.get_x(), cameraWorldPosition.get_y(), cameraWorldPosition.get_z()) + ); +} + glm::mat4 MetaCoreConvertPandaMatrixToGlm(const LMatrix4f& matrix) { glm::mat4 result(1.0F); for (int col = 0; col < 4; ++col) { @@ -256,11 +686,16 @@ public: NodePath RootNode{}; NodePath MeshNode{}; NodePath LightNode{}; + MetaCoreMeshSourceKind MeshSource = MetaCoreMeshSourceKind::Builtin; + MetaCoreBuiltinMeshType BuiltinMesh = MetaCoreBuiltinMeshType::Cube; + MetaCoreAssetGuid MeshAssetGuid{}; + std::string SourceModelPath{}; }; MetaCoreRenderDevice* RenderDevice = nullptr; NodePath GridNode{}; NodePath AmbientLightNode{}; + std::filesystem::path ProjectRootPath{}; MetaCoreId SelectedObjectId = 0; std::unordered_map ObjectStates{}; }; @@ -283,6 +718,7 @@ bool MetaCorePandaSceneBridge::Initialize(MetaCoreRenderDevice& renderDevice) { } Impl_->RenderDevice = &renderDevice; + Impl_->ProjectRootPath = MetaCoreResolveProjectRootFromEnvironment(); Impl_->GridNode = MetaCoreCreateGridNode(*sceneRootHandle); Impl_->GridNode.set_light_off(1); @@ -320,6 +756,7 @@ void MetaCorePandaSceneBridge::Shutdown() { } Impl_->RenderDevice = nullptr; + Impl_->ProjectRootPath.clear(); Impl_->SelectedObjectId = 0; } @@ -361,22 +798,93 @@ void MetaCorePandaSceneBridge::SyncScene(const MetaCoreScene& scene) { } if (gameObject.MeshRenderer.has_value()) { - if (objectState.MeshNode.is_empty()) { - objectState.MeshNode = MetaCoreCreateUnitCubeNode(objectState.RootNode); + const bool requiresMeshRebuild = + objectState.MeshNode.is_empty() || + objectState.MeshSource != gameObject.MeshRenderer->MeshSource || + (gameObject.MeshRenderer->MeshSource == MetaCoreMeshSourceKind::Builtin && + objectState.BuiltinMesh != gameObject.MeshRenderer->BuiltinMesh) || + (gameObject.MeshRenderer->MeshSource == MetaCoreMeshSourceKind::Asset && + (objectState.MeshAssetGuid != gameObject.MeshRenderer->MeshAssetGuid || + objectState.SourceModelPath != gameObject.MeshRenderer->SourceModelPath)); + + if (requiresMeshRebuild && !objectState.MeshNode.is_empty()) { + objectState.MeshNode.remove_node(); + objectState.MeshNode = NodePath(); + } + + if (requiresMeshRebuild) { + if (gameObject.MeshRenderer->MeshSource == MetaCoreMeshSourceKind::Builtin) { + objectState.MeshNode = MetaCoreCreateUnitCubeNode(objectState.RootNode); + } else { + objectState.MeshNode = MetaCoreTryLoadModelNode( + *windowFrameworkHandle, + objectState.RootNode, + Impl_->ProjectRootPath, + gameObject.MeshRenderer->SourceModelPath + ); + if (objectState.MeshNode.is_empty()) { + objectState.MeshNode = MetaCoreCreateUnitCubeNode(objectState.RootNode); + } + } + objectState.MeshSource = gameObject.MeshRenderer->MeshSource; + objectState.BuiltinMesh = gameObject.MeshRenderer->BuiltinMesh; + objectState.MeshAssetGuid = gameObject.MeshRenderer->MeshAssetGuid; + objectState.SourceModelPath = gameObject.MeshRenderer->SourceModelPath; } if (!objectState.MeshNode.is_empty()) { - objectState.MeshNode.set_texture_off(1); objectState.MeshNode.set_material_off(1); - objectState.MeshNode.set_shader_off(1); objectState.MeshNode.set_light_off(1); - objectState.MeshNode.set_two_sided(true); + objectState.MeshNode.set_two_sided(gameObject.MeshRenderer->DoubleSided); objectState.MeshNode.set_color( gameObject.MeshRenderer->BaseColor.r, gameObject.MeshRenderer->BaseColor.g, gameObject.MeshRenderer->BaseColor.b, 1.0F ); + MetaCoreApplyBaseColorTexture( + objectState.MeshNode, + Impl_->ProjectRootPath, + gameObject.MeshRenderer->BaseColorTexturePath + ); + MetaCoreApplyMetallicRoughnessTexture( + objectState.MeshNode, + Impl_->ProjectRootPath, + gameObject.MeshRenderer->MetallicRoughnessTexturePath + ); + MetaCoreApplyNormalTexture( + objectState.MeshNode, + Impl_->ProjectRootPath, + gameObject.MeshRenderer->NormalTexturePath + ); + MetaCoreApplyEmissionTexture( + objectState.MeshNode, + Impl_->ProjectRootPath, + gameObject.MeshRenderer->EmissiveTexturePath + ); + MetaCoreApplyAoTexture( + objectState.MeshNode, + Impl_->ProjectRootPath, + gameObject.MeshRenderer->AoTexturePath + ); + if (gameObject.MeshRenderer->MeshSource == MetaCoreMeshSourceKind::Asset) { + MetaCoreApplySimplePbrPreview(objectState.MeshNode); + MetaCoreApplySimplePbrMaterialInputs(objectState.MeshNode, *gameObject.MeshRenderer); + auto* editorCameraHandle = static_cast(Impl_->RenderDevice->GetNativeEditorCameraHandle()); + if (editorCameraHandle != nullptr) { + MetaCoreApplySimplePbrViewInputs(objectState.MeshNode, *sceneRootHandle, *editorCameraHandle); + } + } else { + objectState.MeshNode.set_shader_off(1); + } + + if (gameObject.MeshRenderer->AlphaMode == MetaCoreMeshAlphaMode::Blend) { + objectState.MeshNode.set_transparency(TransparencyAttrib::M_alpha); + } else if (gameObject.MeshRenderer->AlphaMode == MetaCoreMeshAlphaMode::Mask) { + objectState.MeshNode.set_transparency(TransparencyAttrib::M_binary); + } else { + objectState.MeshNode.clear_transparency(); + } if (gameObject.MeshRenderer->Visible) { objectState.MeshNode.show(); diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h index d060d7f..1e7e26f 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h @@ -1,11 +1,13 @@ #pragma once +#include "MetaCoreFoundation/MetaCoreAssetGuid.h" #include "MetaCoreFoundation/MetaCoreReflection.h" #include #include #include +#include namespace MetaCore { @@ -15,6 +17,19 @@ enum class MetaCoreBuiltinMeshType { Cube }; +MC_ENUM() +enum class MetaCoreMeshSourceKind { + Builtin = 0, + Asset +}; + +MC_ENUM() +enum class MetaCoreMeshAlphaMode { + Opaque = 0, + Mask, + Blend +}; + MC_STRUCT() // 表示 Unity 风格对象的变换组件。 struct MetaCoreTransformComponent { @@ -48,9 +63,39 @@ MC_STRUCT() struct MetaCoreMeshRendererComponent { MC_GENERATED_BODY() + MC_PROPERTY() + MetaCoreMeshSourceKind MeshSource = MetaCoreMeshSourceKind::Builtin; MC_PROPERTY() MetaCoreBuiltinMeshType BuiltinMesh = MetaCoreBuiltinMeshType::Cube; MC_PROPERTY() + MetaCoreAssetGuid MeshAssetGuid{}; + MC_PROPERTY() + std::vector MaterialAssetGuids{}; + MC_PROPERTY() + std::string SourceModelPath{}; + MC_PROPERTY() + std::string BaseColorTexturePath{}; + MC_PROPERTY() + std::string MetallicRoughnessTexturePath{}; + MC_PROPERTY() + std::string NormalTexturePath{}; + MC_PROPERTY() + std::string EmissiveTexturePath{}; + MC_PROPERTY() + std::string AoTexturePath{}; + MC_PROPERTY() + bool DoubleSided = true; + MC_PROPERTY() + float Metallic = 0.0F; + MC_PROPERTY() + float Roughness = 1.0F; + MC_PROPERTY() + MetaCoreMeshAlphaMode AlphaMode = MetaCoreMeshAlphaMode::Opaque; + MC_PROPERTY() + float AlphaCutoff = 0.5F; + MC_PROPERTY() + glm::vec3 EmissiveColor{0.0F, 0.0F, 0.0F}; + MC_PROPERTY() glm::vec3 BaseColor{0.75F, 0.78F, 0.84F}; MC_PROPERTY() bool Visible = true; diff --git a/TestProject/MetaCore.project.json b/TestProject/MetaCore.project.json index 1eef1c7..04e64ae 100644 --- a/TestProject/MetaCore.project.json +++ b/TestProject/MetaCore.project.json @@ -1,8 +1,11 @@ { "name": "MetaCoreTest", + "version": "0.1.0", + "runtime_directory": "Runtime", + "ui_directory": "Ui", + "build_directory": "Build", "scenes": [ "Scenes/Main.mcscene" ], - "startup_scene": "Scenes/Main.mcscene", - "version": "0.1.0" + "startup_scene": "Scenes/Main.mcscene" } diff --git a/cmake/MetaCorePanda3D.cmake b/cmake/MetaCorePanda3D.cmake index f2b64d9..d74779a 100644 --- a/cmake/MetaCorePanda3D.cmake +++ b/cmake/MetaCorePanda3D.cmake @@ -112,3 +112,27 @@ function(metacore_stage_panda3d_runtime target_name) ) endif() endfunction() + +function(metacore_stage_simplepbr_runtime target_name) + if(NOT TARGET ${target_name}) + message(FATAL_ERROR "Target ${target_name} does not exist.") + endif() + + set(_metacore_simplepbr_root "${CMAKE_SOURCE_DIR}/third_party/simplepbr-shaders") + + if(EXISTS "${_metacore_simplepbr_root}/shaders") + add_custom_command(TARGET ${target_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${_metacore_simplepbr_root}/shaders" + "$/simplepbr/shaders" + ) + endif() + + if(EXISTS "${_metacore_simplepbr_root}/textures") + add_custom_command(TARGET ${target_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${_metacore_simplepbr_root}/textures" + "$/simplepbr/textures" + ) + endif() +endfunction() diff --git a/docs/designs/metacore-gltf-glb-importer-design.md b/docs/designs/metacore-gltf-glb-importer-design.md new file mode 100644 index 0000000..11076b7 --- /dev/null +++ b/docs/designs/metacore-gltf-glb-importer-design.md @@ -0,0 +1,396 @@ +# MetaCore glTF/glb 首批导入器设计 + +生成时间:2026-03-28 +状态:草案 +范围:M2 资源与模型导入循环 + +## 目的 + +这份文档用于明确 MetaCore 第一阶段首批生产级模型导入器为什么优先选择 `glTF/.glb`,以及这条导入链应该如何设计。 + +它要解决的问题不是“能不能识别扩展名”,而是: + +- `glTF/.glb` 导入后要产出哪些 MetaCore 资产 +- 模型节点层级如何进入场景或资源系统 +- 材质和贴图如何抽取 +- 重导入如何保持资源身份稳定 +- 第一阶段哪些能力必须做,哪些应后置 + +## 结论先说 + +MetaCore 第一阶段首批真正应做成生产级闭环的模型导入格式,应明确为: + +**P0:`glTF/.glb`** + +不是因为它覆盖一切,而是因为它最适合作为: + +- 第一阶段材质系统的起点 +- 第一阶段模型导入工作流的基线 +- 第一阶段数字孪生项目资产导入的主格式 + +`FBX` 很重要,但不应该先拿来卡死整个导入架构。 + +## 为什么 `glTF/.glb` 应该是第一优先级 + +### 1. 更适合现代 PBR 工作流 + +第一阶段 MetaCore 已经确定: + +- 材质层先走基础 PBR +- shader 后端先接 `panda3d-simplepbr` + +在这个前提下,`glTF/.glb` 的材质语义更接近第一阶段目标: + +- BaseColor +- Metallic +- Roughness +- Normal +- Emissive +- DoubleSided +- AlphaMode + +这比先做 `FBX` 的材质归一化成本更低,也更稳。 + +### 2. 对引擎资源化更友好 + +MetaCore 第一阶段不是只想“显示模型”,而是要把模型转成: + +- Mesh Asset +- Material Asset +- Texture Asset + +`glTF/.glb` 更适合做这种规范化导入起点。 + +### 3. 更适合做第一条闭环 + +第一阶段的目标不是“支持格式列表尽量长”,而是先让至少一种格式形成真正可用闭环: + +```text +导入 + -> 生成正式资源 + -> 放进场景 + -> 保存 + -> 重新打开 + -> Player 正确渲染 +``` + +`glTF/.glb` 是最适合先把这条链打通的格式。 + +## 第一阶段范围 + +### 必须完成 + +- `.glb` 导入 +- `.gltf + 外部贴图/二进制` 导入 +- Mesh 解析 +- 节点层级解析 +- Material 解析 +- Texture 引用抽取 +- 生成 MetaCore Mesh / Material / Texture 资产 +- 生成导入元数据 +- 支持重新导入 + +### 可以后置 + +- 动画 +- 蒙皮 +- Morph Targets +- 相机导入 +- 灯光导入 +- 扩展插件全量支持 +- 非标准材质扩展的完整支持 + +第一阶段要非常克制,只先把静态模型和基础材质工作流做硬。 + +## 导入器总链路 + +```mermaid +flowchart LR + A[".glb / .gltf"] --> B["glTF 导入器"] + B --> C["导入结果文档"] + C --> D["Mesh Assets"] + C --> E["Material Assets"] + C --> F["Texture Assets"] + C --> G["Model/Node 描述"] + D --> H["Asset Database"] + E --> H + F --> H + G --> H +``` + +## 第一阶段推荐输出资产 + +导入一个 `glTF/.glb` 文件后,第一阶段至少应输出: + +- 一个导入元数据文档 +- 零个或多个 `Mesh Asset` +- 零个或多个 `Material Asset` +- 零个或多个 `Texture Asset` +- 一个模型节点描述文档或模型资源文档 + +### 为什么要有“模型节点描述文档” + +因为静态模型导入不只是生成网格和材质,还要保留: + +- 原始节点层级 +- 节点名称 +- 节点局部变换 +- 节点到 Mesh 的关系 + +如果没有这层,后面把模型拖进场景时就只能变成一个平面资源引用,无法还原源模型结构。 + +## 第一阶段推荐的导入输出结构 + +### 1. MetaCoreImportedAssetDocument + +继续作为导入来源的基本记录,表达: + +- 源文件路径 +- 导入器 ID +- 源文件 Hash +- 资产类型 + +### 2. MetaCoreGltfModelImportDocument + +建议新增一份导入结果文档,专门记录: + +- 源模型名称 +- 生成的 Mesh Asset 列表 +- 生成的 Material Asset 列表 +- 生成的 Texture Asset 列表 +- 节点层级描述 + +这份文档的作用是: + +- 为重导入提供稳定参照 +- 为调试提供可读结构 +- 为“拖入场景时生成对象树”提供输入 + +### 3. Mesh Asset + +每个 glTF mesh primitive 或合并策略后的 mesh 生成正式 `Mesh Asset`。 + +### 4. Material Asset + +每个 glTF material 生成正式 `Material Asset`。 + +### 5. Texture Asset + +对被材质引用的贴图生成正式 `Texture Asset`。 + +## 节点层级映射规则 + +这是第一阶段必须明确的设计点。 + +### 规则 1:保留源节点层级 + +导入后应保留: + +- 节点名称 +- 父子关系 +- 局部变换 + +因为工业场景模型常常天然依赖节点组织来表达: + +- 设备结构 +- 部件结构 +- 装配关系 + +### 规则 2:节点和 Mesh 资源分离 + +节点不是网格本身。 + +建议做法是: + +- 节点文档保存树结构 +- 节点引用 `Mesh Asset` +- 节点再引用默认材质槽资源 + +### 规则 3:拖入场景时由模型节点文档实例化对象树 + +也就是说: + +- 源文件导入阶段不直接生成 Scene 对象 +- 只有当用户将模型资源放入场景时,才根据节点描述生成 `GameObject` 树 + +这样资源与场景的边界更清楚。 + +## 材质抽取规则 + +第一阶段应按 glTF 的标准 PBR metal-rough 语义归一化。 + +### 应抽取的字段 + +- `baseColorFactor` +- `baseColorTexture` +- `metallicFactor` +- `roughnessFactor` +- `metallicRoughnessTexture` +- `normalTexture` +- `emissiveFactor` +- `emissiveTexture` +- `occlusionTexture` +- `alphaMode` +- `alphaCutoff` +- `doubleSided` + +### 第一阶段允许简化的点 + +- 不追求所有扩展都支持 +- 不追求材质表现完全等价 +- 对复杂扩展给出 warning 即可 + +重点是输出统一、稳定的 MetaCore 材质资源。 + +## 贴图抽取规则 + +第一阶段应支持: + +- 嵌入式贴图 +- 外部文件贴图引用 + +导入后应生成正式 `Texture Asset`,而不是只保留源贴图路径字符串。 + +### 建议记录的信息 + +- 来源 URI 或 bufferView +- 目标纹理资源 GUID +- 使用场景 +- 原始色彩空间推断结果 + +### 色彩空间建议 + +第一阶段至少做最基本区分: + +- BaseColor / Emissive:sRGB +- Normal / MetallicRoughness / AO:Linear + +## 重导入策略 + +这块必须第一阶段就定清楚。 + +### 目标 + +重导入后尽量保持: + +- Mesh 资源 GUID 稳定 +- Material 资源 GUID 稳定 +- Texture 资源 GUID 稳定 + +否则场景和 prefab 的引用会断。 + +### 推荐策略 + +第一阶段建议基于“稳定导出键”进行匹配,例如: + +- 源文件 GUID +- 节点路径 +- mesh primitive 索引 +- material 名称或导出索引 +- texture 名称或导出索引 + +生成一套稳定导入键,再映射到已有资源 GUID。 + +### 第一阶段允许的限制 + +如果源文件结构变化过大,第一阶段允许: + +- 资源重建 +- 给出明确 warning + +但默认路径应尽量保住已有 GUID。 + +## 失败模式与诊断 + +第一阶段至少要覆盖这些失败模式: + +- 文件损坏或不是合法 glTF +- 外部贴图丢失 +- 顶点数据不完整 +- 节点引用丢失 +- 材质参数无法识别 +- 重导入时键冲突 + +对于这些问题,至少应在: + +- Import Console +- Asset 日志 +- 导入结果摘要 + +中给出可定位提示。 + +## 第一阶段 Inspector / Project 工作流影响 + +导入这条线做完后,编辑器应至少能: + +- 在 Project 面板看到导入得到的模型相关资产 +- 区分源文件与生成资源 +- 查看模型资源包含多少 Mesh / Material / Texture +- 将模型资源拖入场景 +- 在 Inspector 查看材质槽与 Mesh 引用 + +否则导入链就还没有真正进入生产工作流。 + +## 推荐实现顺序 + +建议按下面顺序推进: + +1. 选定 glTF 解析库和接入方式 +2. 建立 `glTF 导入器` 骨架 +3. 先打通 `.glb` 单文件导入 +4. 打通 `.gltf + 外部资源` 导入 +5. 生成 Mesh / Material / Texture 正式资源 +6. 生成模型节点描述文档 +7. 支持拖入场景生成对象树 +8. 支持重导入 +9. 补错误诊断与测试 + +## 测试建议 + +第一阶段至少要有下面几类样例: + +- 单网格、单材质 +- 多 SubMesh、多材质 +- 带法线贴图 +- 带 emissive +- 带透明模式 +- 带外部贴图引用 +- 节点层级多层嵌套 + +### 必测项 + +- 导入成功 +- 资产数量正确 +- 材质参数映射正确 +- 节点层级正确 +- 重新导入后 GUID 尽量保持稳定 +- 场景引用后保存/重开仍然有效 +- Player 渲染结果基本正确 + +## 与其他文档的关系 + +这份文档是下面几份文档的具体化: + +- [metacore-engine-foundation-priority.md](D:/MetaCore/docs/designs/metacore-engine-foundation-priority.md) +- [metacore-m1-m2-detailed-task-breakdown.md](D:/MetaCore/docs/designs/metacore-m1-m2-detailed-task-breakdown.md) +- [metacore-material-import-integration-design.md](D:/MetaCore/docs/designs/metacore-material-import-integration-design.md) +- [metacore-material-meshrenderer-data-structure-design.md](D:/MetaCore/docs/designs/metacore-material-meshrenderer-data-structure-design.md) + +它负责把“首批格式”和“首批导入器”真正定成一个可执行方向。 + +## 最终建议 + +第一阶段不要追求“支持所有模型格式”,而要追求: + +**把 `glTF/.glb` 做成真正的生产级基线导入格式。** + +只要这条线做实,MetaCore 就会第一次真正拥有: + +- 模型导入 +- 材质抽取 +- 层级保留 +- 资源生成 +- 场景实例化 +- 打包运行 + +这一整套能支撑工业数字孪生项目开发的基础内容工作流。 diff --git a/docs/designs/metacore-m1-m2-detailed-task-breakdown.md b/docs/designs/metacore-m1-m2-detailed-task-breakdown.md new file mode 100644 index 0000000..0cb9f22 --- /dev/null +++ b/docs/designs/metacore-m1-m2-detailed-task-breakdown.md @@ -0,0 +1,428 @@ +# MetaCore M1-M2 详细任务分解 + +生成时间:2026-03-28 +状态:草案 +范围:M1 引擎壳与项目循环,M2 资源与模型导入循环 + +## 目的 + +这份文档把当前最应该推进的两个里程碑拆成真正可执行的任务单。 + +它不是概念清单,而是用于直接指导后续开发排序、拆工和验收的。 + +当前原则: + +- 先把引擎基础工作流做扎实 +- 不再让 RuntimeData 继续主导第一阶段节奏 +- 所有任务都要服务于“像 Unity / UE 一样完成工业项目开发”的最小生产力闭环 + +## 当前代码基础判断 + +结合现有仓库,MetaCore 已经具备一些可以复用的基础: + +- 项目描述文件读取基础 +- 资源数据库与 package 基础能力 +- 场景保存/加载二进制方向 +- 基础层级编辑能力 +- 基础 Inspector +- 基础 prefab 能力 +- 独立 Player + +但当前真正缺的是: + +- 把项目系统做成稳定入口 +- 把导入流程做成真正可用的资源工作流 +- 把模型导入从“识别格式”推进到“可用于场景” +- 把 Project 面板、资源引用、重新导入和 package 行为串成闭环 + +所以 M1-M2 的目标不是“继续补零散能力”,而是把这条主链打通: + +```text +打开项目 + -> 浏览资源 + -> 导入模型 + -> 模型变成可引用资产 + -> 放入场景 + -> 保存并重开仍然有效 +``` + +## M1:引擎壳与项目循环 + +### M1 目标 + +把 MetaCore 做成一个稳定的“项目入口”,而不只是一个能打开窗口的编辑器。 + +### M1 完成标准 + +- 能稳定定位和打开项目 +- 项目描述文件能被一致读取和保存 +- 启动场景机制稳定 +- Editor 和 Player 都能围绕项目根目录工作 +- 新老路径兼容行为清晰,不再依赖临时 fallback 才能运行 + +--- + +## M1 工作包 + +### M1-A 项目定位与启动逻辑统一 + +#### 目标 + +统一 Editor、Player、工具对项目根目录的解析逻辑。 + +#### 现状 + +当前代码里已经存在多处项目路径解析和 fallback 逻辑,但它们分散在不同入口。 + +#### 任务 + +- 抽出统一的项目路径解析帮助层 +- 统一 `MetaCore.project.json` 定位逻辑 +- 统一 startup scene 解析逻辑 +- 统一 Runtime 目录定位逻辑 +- 明确命令行传项目路径与默认样板项目路径的优先级 + +#### 交付物 + +- 一个公共项目路径解析入口 +- Editor/Player/工具共用同一套项目根解析规则 + +#### 验收 + +- 从不同当前工作目录启动时仍能打开同一个项目 +- 样板项目不依赖“碰巧的相对路径”才能运行 + +### M1-B 项目描述文件能力补齐 + +#### 目标 + +把 `MetaCore.project.json` 做成真正的项目描述文件,而不是只存一个 startup scene。 + +#### 建议字段 + +当前至少明确以下字段: + +- `name` +- `version` +- `startup_scene` +- `scenes` +- `runtime_config` 或转向固定 runtime 目录策略 +- 未来可预留 `asset_roots`、`ui_roots` + +#### 任务 + +- 梳理项目描述文件的最小字段集 +- 统一读写逻辑 +- 明确旧字段兼容策略 +- 对非法项目描述给出明确错误 + +#### 验收 + +- 坏项目文件能报错 +- 缺字段时行为一致 +- 保存后再次打开结果一致 + +### M1-C 启动场景工作流稳定化 + +#### 目标 + +确保项目和启动场景关系稳定。 + +#### 任务 + +- 明确 startup scene 的设置入口 +- 明确 startup scene 保存后项目描述同步规则 +- 明确启动场景缺失时的行为 +- Player 和 Editor 对 startup scene 的解释保持一致 + +#### 验收 + +- 设置 startup scene 后,Editor/Player 行为一致 +- 场景改名或移动后,引用更新策略清晰 + +### M1-D 样板项目 bootstrap 规则 + +#### 目标 + +样板项目成为可持续维护的基线项目,而不是演示残留物。 + +#### 任务 + +- 固定样板项目目录结构 +- 固定 `Scenes / Assets / Runtime / Library` 的职责 +- 明确自动生成内容与手工维护内容边界 +- 把当前样板工程转成规范项目 + +#### 验收 + +- 新人能直接理解样板项目结构 +- 工具生成文件不会污染错误目录 + +### M1-E 项目级测试 + +#### 目标 + +给项目循环加回归保护。 + +#### 必测项 + +- 项目描述读取 +- startup scene 读取 +- startup scene 缺失处理 +- 项目根目录解析 +- Player 项目启动链 + +--- + +## M2:资源与模型导入循环 + +### M2 目标 + +把 MetaCore 变成一个真正围绕资源工作的工具,而不是只围绕内建对象工作的编辑器。 + +### M2 完成标准 + +- 导入的资源进入 Asset Database +- 模型资源成为可追踪资产 +- Project 面板能看见并定位这些资源 +- 导入资源能被放进场景并在保存后重开 +- 重新导入和元数据行为稳定 + +--- + +## M2 工作包 + +### M2-A 导入器策略收敛 + +#### 目标 + +明确第一阶段首批导入策略,不再模糊支持。 + +#### 建议首批格式 + +- P0:`glTF/.glb` +- P1:`FBX` +- P2:`OBJ` + +#### 理由 + +- `glTF/.glb` 更适合作为第一生产级导入格式 +- `FBX` 很重要,但复杂度高,不应该先把整条链卡死在 FBX 上 +- `OBJ` 可作为简化补充,不应该主导设计 + +#### 任务 + +- 明确导入器接口的能力边界 +- 区分“识别格式”和“完整导入能力” +- 明确哪些格式只是注册,哪些格式是真生产支持 + +#### 验收 + +- 文档和代码里对格式支持口径一致 + +### M2-B 模型资源数据模型 + +#### 目标 + +给模型资源一个稳定的数据表示,不要只把原始文件名记进数据库。 + +#### 最低要求 + +- 资源 GUID +- 资源类型 +- 原始源文件路径 +- package 路径 +- 导入器 id +- 源文件 hash +- 模型节点层级信息的承载位置 + +#### 任务 + +- 明确“模型资产”和“导入源文件”的关系 +- 明确 package 里到底存什么 +- 明确导入后场景引用的是什么对象 + +#### 验收 + +- 一个模型导入后有明确资产身份 +- 场景引用模型时不依赖源文件路径硬绑 + +### M2-C 模型导入最小闭环 + +#### 目标 + +把至少一种格式做成真的可用,而不是仅识别扩展名。 + +#### 推荐第一闭环 + +优先 `glTF/.glb` + +#### 闭环要求 + +- 选择模型文件导入 +- 生成资产记录 +- 生成 package +- Project 面板里可见 +- 放进场景后可渲染 +- 保存场景后重开仍然存在 + +#### 验收 + +- 至少一个 `glTF/.glb` 样例能完整跑通上述闭环 + +### M2-D 层级保留与归一化 + +#### 目标 + +导入模型时不要只保一个“单网格结果”,要为数字孪生常见设备结构保留层级。 + +#### 任务 + +- 保留模型节点层级 +- 明确坐标系转换 +- 明确缩放归一化 +- 明确材质槽映射 +- 明确空节点是否保留 + +#### 验收 + +- 导入后层级结构与源模型基本一致 +- 不会出现方向、缩放完全错误的基础问题 + +### M2-E 重新导入策略 + +#### 目标 + +资源改动后可刷新,而不是删了重来。 + +#### 任务 + +- 基于源 hash 检测变化 +- 明确重新导入触发方式 +- 明确重新导入后 package 更新规则 +- 明确重新导入对场景引用的影响 + +#### 验收 + +- 修改源模型后可稳定刷新 +- 场景里的引用不被无故打断 + +### M2-F Project 面板与资源浏览增强 + +#### 目标 + +让资源工作流真正可用。 + +#### 必须支持 + +- 浏览项目资源 +- 区分 source / meta / package +- 定位资源 +- 显示资源类型 +- 打开场景资产 +- 未来可扩展到双击实例化模型 + +#### 任务 + +- 补齐资源显示信息 +- 强化 scene / prefab / asset 的区分 +- 明确从资源到场景的最小放置流程 + +#### 验收 + +- 用户能在 Project 面板里明确找到导入的模型资产 + +### M2-G 导入错误与诊断 + +#### 目标 + +导入失败要可定位。 + +#### 必须可见的错误类型 + +- 文件不存在 +- 格式不支持 +- package 写入失败 +- 元数据损坏 +- 重新导入失败 + +#### 验收 + +- 失败不会静默 +- Console 有明确错误类别和信息 + +### M2-H M2 测试矩阵 + +#### 目标 + +给资源与导入闭环加回归保护。 + +#### 必测项 + +- 项目导入一个模型 +- 生成 metadata +- 生成 package +- Asset Database 记录稳定 +- Project 面板可见 +- 场景引用导入资源 +- 保存并重开仍然有效 +- 重新导入不打断引用 + +--- + +## 推荐开发顺序 + +M1-M2 的推荐顺序如下: + +```text +M1-A 项目定位与启动逻辑统一 + -> M1-B 项目描述文件能力补齐 + -> M1-C 启动场景工作流稳定化 + -> M1-D 样板项目 bootstrap 规则 + -> M1-E 项目级测试 + -> M2-A 导入器策略收敛 + -> M2-B 模型资源数据模型 + -> M2-C 模型导入最小闭环 + -> M2-D 层级保留与归一化 + -> M2-F Project 面板与资源浏览增强 + -> M2-E 重新导入策略 + -> M2-G 导入错误与诊断 + -> M2-H M2 测试矩阵 +``` + +## 当前代码库下的优先实现建议 + +基于现有仓库状态,我建议最先动这几条: + +### 第一批 + +- 项目路径与项目描述统一 +- startup scene 行为统一 +- 导入器支持口径收敛 +- `glTF/.glb` 作为第一优先格式定下来 + +### 第二批 + +- 模型资产数据结构 +- 最小模型导入闭环 +- Project 面板识别模型资产 + +### 第三批 + +- 重新导入 +- 导入错误诊断 +- 资源引用稳定性 + +## Definition of Done + +M1-M2 完成时,MetaCore 至少要做到: + +- 能稳定打开项目 +- 能稳定打开 startup scene +- 能导入第一种正式支持的模型格式 +- 能在 Project 面板里看到模型资产 +- 能把模型资产放进场景 +- 场景保存、重开后模型仍然有效 +- 打包链路不会因为导入资产而断掉 + +这才说明 MetaCore 开始真正具备“像 Unity/UE 一样开发项目”的最小引擎基础能力。 diff --git a/docs/designs/metacore-material-import-integration-design.md b/docs/designs/metacore-material-import-integration-design.md new file mode 100644 index 0000000..4c3339c --- /dev/null +++ b/docs/designs/metacore-material-import-integration-design.md @@ -0,0 +1,415 @@ +# MetaCore 材质系统与模型导入衔接设计 + +生成时间:2026-03-28 +状态:草案 +范围:M2 资源与模型导入循环、M3 场景编辑、材质与光照工作流 + +## 目的 + +这份文档用于解决一个关键衔接问题: + +- 模型导入后,材质信息如何进入 MetaCore +- MetaCore 内部材质资源应该长什么样 +- 场景对象如何引用材质 +- 第一阶段如何接入 `panda3d-simplepbr` + +如果这条链不先设计清楚,后面模型导入、Inspector 编辑、场景保存、Player 渲染都会各做各的,最终一定返工。 + +## 结论先说 + +MetaCore 第一阶段应采用下面这条链路: + +**源模型文件 -> 导入文档 -> 网格资源 / 材质资源 -> 场景对象 MeshRenderer -> simplepbr 参数映射 -> Player 渲染** + +这里必须明确两件事: + +1. 模型导入负责“生成 MetaCore 自己的资源” +2. 渲染后端只负责“消费 MetaCore 材质资源” + +不能让模型文件直接决定运行时材质结构,也不能让 `simplepbr` 反向决定 MetaCore 材质资源长什么样。 + +## 当前代码库现状 + +从当前仓库看,已经具备下面的基础: + +- 资产元数据与资产数据库骨架 +- 导入服务接口与 cook 接口 +- 包格式与二进制读写能力 +- `MeshRenderer` 组件基础 +- `MeshRenderer.BaseColor` +- `MeshRenderer.Visible` + +但当前还缺: + +- 正式的材质资源类型 +- 正式的网格资源类型 +- 模型导入后的材质槽映射定义 +- 场景对象对材质资源的引用方式 +- 材质 Inspector 与资源编辑流程 +- 材质到 `simplepbr` 的统一映射层 + +因此当前最合理的做法不是直接继续加渲染效果,而是先把资源和组件关系定清楚。 + +## 第一阶段总链路 + +```mermaid +flowchart LR + A["源模型文件 (.glb/.gltf/.fbx)"] --> B["导入器"] + B --> C["导入结果文档"] + C --> D["网格资源 (.mcasset/.mcmesh)"] + C --> E["材质资源 (.mcasset/.mcmat)"] + D --> F["场景对象 MeshRenderer"] + E --> F + F --> G["MetaCore 渲染抽象层"] + G --> H["simplepbr 参数映射"] + H --> I["Player 渲染输出"] +``` + +## 第一阶段资源分层 + +### 1. 源资源 + +这是项目中由用户导入的原始文件: + +- `.glb` +- `.gltf` +- `.fbx` +- 后续 `.obj` + +这些文件是输入,不是 MetaCore 运行时直接消费的最终资源。 + +### 2. 导入结果文档 + +导入器解析源文件后,生成一份导入结果文档,用于表达: + +- 导入得到几个网格资源 +- 导入得到几个材质资源 +- 每个网格默认使用哪个材质槽 +- 原始节点层级如何映射 +- 原始命名如何保留 + +这份文档属于导入中间层,主要为导入器、重导入和调试服务。 + +### 3. MetaCore 正式资源 + +第一阶段至少应该落两类正式资源: + +- `Mesh Asset` +- `Material Asset` + +后续再扩: + +- `Texture Asset` +- `Model Asset` +- `Material Instance Asset` + +## 第一阶段推荐资源模型 + +### Mesh Asset + +`Mesh Asset` 第一阶段最少应包含: + +- 资源 GUID +- Mesh 名称 +- 顶点数据 +- 索引数据 +- 法线 +- UV0 +- 切线 +- 包围盒 +- SubMesh 列表 +- 每个 SubMesh 对应的默认材质槽索引 + +如果一个模型含多个材质槽,应该通过 `SubMesh -> MaterialSlotIndex` 关系表达,而不是把整个模型强行压成一个材质。 + +### Material Asset + +`Material Asset` 第一阶段最少应包含: + +- 资源 GUID +- 材质名称 +- 材质类型 +- BaseColor +- BaseColorTexture +- NormalTexture +- Metallic +- Roughness +- MetallicRoughnessTexture +- EmissiveColor +- EmissiveTexture +- AoTexture +- AlphaMode +- AlphaCutoff +- DoubleSided + +第一阶段不要求复杂 shader graph,但必须保证这个资源结构已经是 MetaCore 自己的,不是 `simplepbr` 参数字典。 + +### Texture Asset + +如果当前阶段不想单独做完整纹理资源编辑器,也至少要有稳定纹理资源记录,用于: + +- BaseColorTexture +- NormalTexture +- MetallicRoughnessTexture +- EmissiveTexture +- AoTexture + +第一阶段可以先把纹理当成辅助资源,由材质资源引用。 + +## 模型导入后的材质映射规则 + +这是第一阶段最重要的设计点之一。 + +### 规则 1:导入器保留源模型的材质槽 + +例如一个源模型含 3 个材质槽,导入后应保留: + +- `SubMesh 0 -> Material Slot 0` +- `SubMesh 1 -> Material Slot 1` +- `SubMesh 2 -> Material Slot 2` + +不能在第一阶段就把它们合并掉。 + +### 规则 2:每个源材质生成一个 MetaCore Material Asset + +也就是说,导入器不只是读材质参数,而是正式创建: + +- `Valve_Body.mcmat` +- `Valve_Handle.mcmat` + +类似这样的材质资源。 + +这样后面 Inspector、资源浏览、复用和重导入才有稳定对象可操作。 + +### 规则 3:导入器负责参数归一化 + +第一阶段推荐优先按 `glTF` 语义归一化: + +- BaseColor +- BaseColorTexture +- Metallic +- Roughness +- MetallicRoughnessTexture +- NormalTexture +- Emissive +- DoubleSided +- AlphaMode + +对于 `FBX` 等不完全一致的格式,第一阶段允许做保守映射,但输出必须仍然变成同一套 MetaCore 材质结构。 + +### 规则 4:重导入默认保留资源身份 + +重导入时应尽量保持: + +- 材质资源 GUID 不变 +- 网格资源 GUID 不变 + +否则场景对象和 prefab 的引用会被打断。 + +## 第一阶段推荐的组件模型 + +当前 `MeshRenderer` 只保存: + +- `BuiltinMesh` +- `BaseColor` +- `Visible` + +这不足以支撑真实模型和材质工作流。 + +第一阶段应逐步演进为: + +- `MeshAssetGuid` +- `MaterialAssetGuids` +- `Visible` +- 必要时保留调试型覆盖参数 + +### 推荐结构 + +第一阶段建议 `MeshRenderer` 具备: + +- 一个 `MeshAssetGuid` +- 一个 `std::vector` 用于材质槽引用 +- 一个 `Visible` + +如果 `SubMesh` 数量和 `MaterialAssetGuids` 数量不一致,应在 Editor 和 Cook 阶段报错或警告。 + +### 不建议的做法 + +第一阶段不要继续把材质核心参数直接塞在 `MeshRenderer` 组件里。 + +`BaseColor` 作为调试或临时覆盖参数还能容忍,但长期必须迁回正式材质资源。 + +## Inspector 工作流建议 + +第一阶段材质编辑工作流应分成两层: + +### 1. 对象级 Inspector + +用于编辑: + +- 当前对象引用哪个 Mesh +- 当前对象每个材质槽引用哪个 Material +- 是否可见 + +这一层主要做“资源指派”。 + +### 2. 材质资源 Inspector + +用于编辑: + +- BaseColor +- 贴图槽 +- Metallic / Roughness +- Emissive +- AlphaMode +- DoubleSided + +这一层主要做“资源参数编辑”。 + +不要把“对象引用”和“材质内容”全部混在同一个对象 Inspector 里。 + +## 与 simplepbr 的衔接方式 + +第一阶段 `simplepbr` 应只出现在渲染映射层。 + +也就是: + +- `MetaCoreMaterialAsset` + -> `MetaCoreRenderMaterialParams` + -> Panda3D / `simplepbr` + +### 第一阶段最小映射 + +应至少支持: + +- BaseColor +- BaseColorTexture +- NormalTexture +- Metallic +- Roughness +- EmissiveColor +- EmissiveTexture +- DoubleSided + +### 明确不要做 + +第一阶段不要把 `simplepbr` 的内部参数名、调用方式、配置细节直接暴露到: + +- 材质资源格式 +- Editor Inspector +- 场景组件结构 + +否则后面替换渲染后端时,整条链都要重写。 + +## 首批格式建议 + +### P0:glTF / .glb + +这是第一阶段最应该优先打通的格式。 + +原因: + +- 现代 +- 对 PBR 语义支持较清晰 +- 与材质参数映射天然更顺 +- 适合作为数字孪生模型导入起点 + +### P1:FBX + +可以作为第二优先级。 + +原因: + +- 现实项目里经常遇到 +- 但材质与层级语义更杂 +- 更容易出现导入归一化问题 + +### P2:OBJ + +只建议作为补充格式,不建议第一阶段主打。 + +原因: + +- 表达能力弱 +- 对现代 PBR 工作流支撑差 + +## 保存与打包建议 + +第一阶段推荐: + +- 项目内保留源文件 +- 导入后生成材质/网格正式资源 +- Scene 保存时只引用资源 GUID +- 打包时由 cook 收集依赖资源 + +不要让 Scene 直接内嵌整份材质内容。 + +这对后面: + +- 资源复用 +- 场景共享 +- 材质统一替换 +- 打包依赖分析 + +都更健康。 + +## 错误与诊断要求 + +第一阶段至少要能发现这些问题: + +- 模型导入缺失材质槽 +- 材质引用的纹理丢失 +- 场景对象引用了不存在的 Mesh +- 材质槽数量与 SubMesh 数量不匹配 +- 材质参数无法映射到当前后端 + +这些问题应至少在: + +- 导入阶段 +- Inspector +- Cook 阶段 + +给出明确提示。 + +## 推荐实现顺序 + +建议按下面顺序推进: + +1. 定义 `Material Asset` 资源结构 +2. 定义 `Mesh Asset` 资源结构 +3. 扩展 `MeshRenderer`,让它引用资源 GUID +4. 先打通 `glTF/.glb -> Mesh/Material Asset` +5. 打通对象级 Mesh/Material 指派 +6. 打通材质资源 Inspector +7. 建立 `Material -> simplepbr` 映射层 +8. 验证 Editor 与 Player 一致性 +9. 再考虑 `FBX` 与重导入增强 + +## 与第一阶段主线的关系 + +这份设计属于: + +- `M2` 资源与模型导入循环 +- `M3` 场景编辑、材质与光照工作流 + +它的意义是把这两段主线真正接起来。 + +如果没有这份衔接设计,M2 和 M3 会变成两个独立子系统: + +- M2 只能“导进来” +- M3 只能“手工调颜色” + +这不是一个真正可用的引擎工作流。 + +## 最终建议 + +第一阶段最合理的策略不是: + +- 直接做完整 URP +- 也不是继续把材质逻辑散落在 `MeshRenderer` + +而是: + +**先建立 MetaCore 自己的 Mesh/Material 资源体系,再把模型导入和 `simplepbr` 渲染后端挂到这套资源体系两端。** + +这是让 MetaCore 真正形成“模型导入 -> 材质编辑 -> 场景搭建 -> Player 渲染”闭环的关键一步。 diff --git a/docs/designs/metacore-material-meshrenderer-data-structure-design.md b/docs/designs/metacore-material-meshrenderer-data-structure-design.md new file mode 100644 index 0000000..509ac51 --- /dev/null +++ b/docs/designs/metacore-material-meshrenderer-data-structure-design.md @@ -0,0 +1,479 @@ +# MetaCore 材质资源与 MeshRenderer 数据结构设计 + +生成时间:2026-03-28 +状态:草案 +范围:M2 资源与模型导入循环、M3 场景编辑、材质与光照工作流 + +## 目的 + +这份文档用于把后续会真正落到代码里的几类核心结构定清楚: + +- `Material Asset` 应该有哪些字段 +- `Mesh Asset` 应该有哪些字段 +- `MeshRenderer` 应该如何从当前简化结构演进 +- 这些结构如何参与场景保存、资源引用和打包 + +这一步的目标不是讨论渲染效果,而是先锁定数据模型。 + +因为对引擎来说,数据结构一旦定错,后面: + +- 模型导入 +- Inspector +- Scene 保存 +- Prefab +- Cook +- Player 渲染 + +都会一起返工。 + +## 当前代码现状 + +当前 `MeshRenderer` 组件大致是: + +- `BuiltinMesh` +- `BaseColor` +- `Visible` + +这适合原型验证,但不足以支撑真实模型与材质工作流。 + +当前仓库里还没有正式的: + +- `Material Asset Document` +- `Mesh Asset Document` +- `Texture Asset Document` +- `MeshRenderer` 的资源引用结构 + +因此第一阶段必须先完成“从原型组件到正式资源引用组件”的演进设计。 + +## 设计目标 + +第一阶段数据结构要同时满足下面几个要求: + +- 能支撑 `glTF/.glb` 首批导入 +- 能支撑简单模型和多材质槽模型 +- 能在 Scene 中稳定保存引用 +- 能被 Editor Inspector 编辑 +- 能被 Cook 和 Package 依赖分析 +- 能接入 `simplepbr` +- 不锁死未来类 URP 的演进空间 + +## 资源与组件的关系 + +```mermaid +flowchart TD + A["Mesh Asset"] --> C["MeshRenderer Component"] + B["Material Asset (0..N)"] --> C + C --> D["Scene Document"] + A --> E["Cook / Package"] + B --> E + D --> F["Player Runtime"] + E --> F +``` + +含义是: + +- `MeshRenderer` 不再直接保存完整材质内容 +- `MeshRenderer` 只保存对资源的引用 +- 资源本体由资产系统管理 + +## 第一阶段 Material Asset 结构 + +第一阶段建议定义正式的 `MetaCoreMaterialAssetDocument`。 + +### 核心字段 + +- `AssetGuid` +- `Name` +- `ShaderModel` +- `BaseColor` +- `BaseColorTexture` +- `NormalTexture` +- `Metallic` +- `Roughness` +- `MetallicRoughnessTexture` +- `AoTexture` +- `EmissiveColor` +- `EmissiveTexture` +- `AlphaMode` +- `AlphaCutoff` +- `DoubleSided` + +### 字段说明 + +#### AssetGuid + +材质资源的稳定身份。 + +这是 Scene、Prefab、Cook、重导入能够持续引用它的基础。 + +#### Name + +材质资源名,主要用于: + +- Project 面板显示 +- Inspector 显示 +- 调试与错误提示 + +#### ShaderModel + +第一阶段建议不要设计成任意 shader 名字字符串,而是明确一个有限枚举,例如: + +- `PbrMetalRough` +- `UnlitColor` +- `UnlitTexture` + +第一阶段主打 `PbrMetalRough`。 + +这样做的好处是: + +- 数据更稳定 +- Inspector 更清楚 +- 后续再映射到不同 render backend 更容易 + +#### BaseColor + +基础颜色乘子。 + +#### BaseColorTexture + +对纹理资源的引用。建议类型为: + +- `std::optional` + +不要在材质里直接存文件路径。 + +#### NormalTexture + +法线贴图资源引用。 + +#### Metallic / Roughness + +金属度与粗糙度标量参数。 + +#### MetallicRoughnessTexture + +金属粗糙度复合图资源引用。 + +#### AoTexture + +AO 贴图资源引用。 + +#### EmissiveColor / EmissiveTexture + +自发光颜色和自发光贴图引用。 + +#### AlphaMode + +第一阶段建议枚举化,例如: + +- `Opaque` +- `Mask` +- `Blend` + +#### AlphaCutoff + +用于 `Mask` 模式。 + +#### DoubleSided + +双面渲染开关。 + +### 第一阶段不建议直接放进去的内容 + +这些不要在第一阶段过早塞进正式材质结构: + +- 任意 shader 参数字典 +- 自定义 render feature 列表 +- 任意 pass 配置 +- 大而全的 keyword 系统 + +这些都会把第一阶段数据结构做得又重又不稳。 + +## 第一阶段 Mesh Asset 结构 + +第一阶段建议定义正式的 `MetaCoreMeshAssetDocument`。 + +### 核心字段 + +- `AssetGuid` +- `Name` +- `BoundsMin` +- `BoundsMax` +- `VertexCount` +- `IndexCount` +- `SubMeshes` + +如果当前阶段还不想把大块几何数据直接暴露在文档结构里,也可以让文档描述元信息,而实际几何 buffer 落在 package payload。 + +### SubMesh 结构 + +第一阶段建议有明确的 `MetaCoreSubMeshDescriptor`: + +- `Name` +- `IndexOffset` +- `IndexCount` +- `MaterialSlotIndex` + +这个结构非常关键,因为它把: + +- 一个网格中的多个几何段 +- 和材质槽位的关系 + +固定了下来。 + +### 顶点流建议 + +第一阶段至少支持: + +- Position +- Normal +- UV0 +- Tangent + +如果导入格式缺少某些数据,可以在导入阶段: + +- 自动补全 +- 或给出警告 + +但不要让运行时数据结构含糊不清。 + +## 第一阶段 Texture Asset 结构 + +即使暂时不做完整纹理编辑器,也建议正式定义 `MetaCoreTextureAssetDocument`,至少有: + +- `AssetGuid` +- `Name` +- `Width` +- `Height` +- `Format` +- `ColorSpace` +- `Usage` + +其中 `Usage` 第一阶段建议枚举化,例如: + +- `BaseColor` +- `Normal` +- `LinearMask` +- `Emissive` +- `Ao` + +这有利于: + +- 导入校验 +- Inspector 提示 +- simplepbr 映射 + +## MeshRenderer 组件演进设计 + +这是最关键的组件演进点。 + +### 当前结构 + +- `BuiltinMesh` +- `BaseColor` +- `Visible` + +### 第一阶段目标结构 + +建议演进为: + +- `MeshSourceKind` +- `BuiltinMesh` +- `MeshAssetGuid` +- `MaterialAssetGuids` +- `Visible` + +### 推荐字段解释 + +#### MeshSourceKind + +建议新增一个枚举,明确当前对象使用的是: + +- `Builtin` +- `Asset` + +这样可以兼容当前已有的内置立方体、球体等原型对象,也可以平滑接入正式模型资源。 + +#### BuiltinMesh + +保留现有字段,但只在 `MeshSourceKind == Builtin` 时生效。 + +这有利于: + +- 继续保留最小原型能力 +- 不阻塞后面正式资源化 + +#### MeshAssetGuid + +当 `MeshSourceKind == Asset` 时,引用正式网格资源。 + +建议类型: + +- `std::optional` + +#### MaterialAssetGuids + +一个数组,长度 ideally 对应 `SubMesh` 的材质槽数量。 + +建议类型: + +- `std::vector` + +第一阶段如果导入模型只有一个材质,也仍建议用数组,而不是单个字段。 + +因为多材质槽模型在工业场景里非常常见。 + +#### Visible + +保留。 + +### 第一阶段允许保留的兼容字段 + +为了兼容已有原型代码,第一阶段可以短期保留: + +- `BaseColor` + +但需要明确它的定位只能是: + +- 调试覆盖色 +- RuntimeData 的临时驱动入口 + +它不应继续作为正式材质系统的核心字段。 + +也就是说,后续优先级应该是: + +- 资源材质主导 +- `BaseColor` 仅作为可选 override + +## Scene 保存策略 + +Scene 中不应内嵌完整的网格和材质内容。 + +Scene 对 `MeshRenderer` 的保存应以“引用”为主: + +- `MeshSourceKind` +- `BuiltinMesh` +- `MeshAssetGuid` +- `MaterialAssetGuids` +- `Visible` + +这样好处是: + +- 场景更轻 +- 资源可复用 +- 材质统一修改后能反映到多个对象 +- Cook 更容易做依赖分析 + +## Prefab 保存策略 + +Prefab 与 Scene 一样,建议保存资源引用,而不是内嵌资源内容。 + +这样设备、阀门、管段、告警灯等 prefab 才能真正复用: + +- 同一 Mesh +- 同一 Material +- 不同 Transform +- 不同业务逻辑组件 + +## Cook 与 Package 策略 + +Cook 阶段应能够从 Scene / Prefab 中追踪依赖: + +- MeshRenderer -> MeshAssetGuid +- MeshRenderer -> MaterialAssetGuids +- MaterialAsset -> TextureAsset 引用 + +这样最终打包时,才能把真正运行所需资源闭包带全。 + +## Inspector 建议 + +### 对象 Inspector + +第一阶段最少应支持: + +- 切换 Builtin / Asset Mesh +- 选择 Mesh Asset +- 查看材质槽数量 +- 为每个材质槽选择 Material Asset +- `Visible` + +### 材质资源 Inspector + +第一阶段最少应支持: + +- BaseColor +- BaseColorTexture +- NormalTexture +- Metallic +- Roughness +- EmissiveColor +- EmissiveTexture +- AlphaMode +- DoubleSided + +### 不建议的做法 + +不要让对象 Inspector 同时承担: + +- 对象引用管理 +- 材质参数完整编辑 + +这会让工作流非常乱。 + +## 与 RuntimeData 的关系 + +材质资源化之后,`RuntimeData` 仍然可以继续驱动: + +- `MeshRenderer.Visible` +- 材质 override 颜色 +- 灯光参数 + +但不建议第一阶段直接让 RuntimeData 去改材质资源本体。 + +更合理的做法是: + +- Runtime 改对象实例级显示状态 +- Asset 仍然由资源系统管理 + +## 推荐实现顺序 + +建议按下面顺序推进: + +1. 定义 `Material Asset Document` +2. 定义 `Mesh Asset Document` +3. 定义 `Texture Asset Document` +4. 扩展 `MeshRenderer` 数据结构 +5. 更新 Scene / Prefab 序列化 +6. 更新 Inspector +7. 再接模型导入与材质映射 +8. 最后接 `simplepbr` 参数映射 + +## 第一阶段 Definition of Done + +当下面这些都成立时,可以认为这套数据结构设计已经真正落地: + +- Scene 中的 `MeshRenderer` 不再只依赖 `BuiltinMesh` +- 导入模型能够生成正式 Mesh/Material 资源 +- 对象可以引用 Mesh Asset 和多个 Material Asset +- Inspector 能指派和编辑材质 +- Scene / Prefab 保存的是资源引用 +- Cook 能正确追踪 Mesh/Material/Texture 依赖 +- Player 能基于这些资源正确渲染 + +## 最终建议 + +第一阶段最关键的不是“效果做多炫”,而是先把结构做对。 + +所以最合理的方向是: + +**把 Mesh、Material、Texture 正式变成 MetaCore 资源,把 MeshRenderer 正式变成资源引用组件,再让导入、Inspector、Cook 和 simplepbr 全部围绕这套结构协作。** + +这一步一旦定稳,后续: + +- 模型导入 +- 材质编辑 +- 场景保存 +- 打包运行 +- RuntimeData 显示驱动 + +都会顺很多。 diff --git a/docs/designs/metacore-material-render-pipeline-selection.md b/docs/designs/metacore-material-render-pipeline-selection.md new file mode 100644 index 0000000..8b44cde --- /dev/null +++ b/docs/designs/metacore-material-render-pipeline-selection.md @@ -0,0 +1,225 @@ +# MetaCore 材质与渲染管线选型说明 + +生成时间:2026-03-28 +状态:草案 +范围:M3 场景编辑、材质与光照工作流 + +## 目的 + +这份文档用于明确 MetaCore 第一阶段材质与渲染管线的选型策略。 + +目标不是一步到位做出完整的 Unity URP 等价物,而是: + +- 第一阶段先把基础 PBR 材质工作流跑通 +- 让材质、灯光、模型和场景搭建形成稳定闭环 +- 在不锁死未来架构的前提下,借力现有可行方案 + +## 结论先说 + +**第一阶段使用 `panda3d-simplepbr` 作为基础 PBR Shader 实现,是正确且务实的选择。** + +但这必须满足一个前提: + +**MetaCore 自己定义材质系统,`simplepbr` 只是第一版渲染后端实现,不是最终渲染架构。** + +## 为什么这个选型合理 + +对于第一阶段来说,你当前最需要的不是一整套宏大的渲染管线,而是: + +- 模型导入后能有像样的材质表现 +- 场景中灯光表现稳定 +- 编辑器和运行时对材质的理解尽量一致 +- 工程团队能尽快把材质工作流做起来 + +`panda3d-simplepbr` 非常适合作为这个阶段的起点,因为它解决的是: + +- 基础 PBR 材质 +- 基础灯光 +- 贴图和阴影的第一版工程问题 + +这正好对应 MetaCore 当前的 M3 目标。 + +## 为什么它不能作为最终方案 + +MetaCore 的长期目标不是“接一个现成 shader 包就结束”,而是: + +- 拥有自己的材质资源模型 +- 拥有自己的渲染配置抽象 +- 逐步演进为类 Unity URP 的可控渲染管线 + +因此必须把两层分清楚: + +### 第一层:MetaCore 材质系统 + +由 MetaCore 自己定义: + +- 材质资源结构 +- 参数命名与参数槽位 +- 贴图槽定义 +- 默认材质类型 +- 材质序列化与资源管理 + +### 第二层:第一版 shader / render backend + +当前先借助: + +- `panda3d-simplepbr` + +以后可以逐步替换为: + +- MetaCore 自己的 render pipeline +- 更像 Unity URP 的 pass / feature / renderer 组织 + +## 第一阶段推荐落地方式 + +### 1. 先定义 MetaCore 自己的材质资源模型 + +不要先把材质资源直接设计成 `simplepbr` 参数表。 + +第一阶段就应当有 MetaCore 自己的材质抽象层。 + +最低应包含: + +- `MaterialType` +- `BaseColor` +- `BaseColorTexture` +- `NormalTexture` +- `Metallic` +- `Roughness` +- `MetallicRoughnessTexture` +- `AoTexture` +- `EmissiveColor` +- `EmissiveTexture` +- `DoubleSided` +- `AlphaMode` + +这层定义的是“MetaCore 材质是什么”,不是“simplepbr 怎么用”。 + +### 2. simplepbr 只做第一版 shader 映射 + +也就是: + +- MetaCore 材质参数 + -> 映射到 Panda3D / simplepbr 需要的参数 + +这样后面如果你替换渲染后端: + +- 资源不需要全改 +- 编辑器也不需要推翻 + +### 3. 优先保证编辑器和运行时一致性 + +第一阶段不要追求特效广度,先保证: + +- 编辑器里看到的材质结果 +- Player 里看到的材质结果 + +尽量一致。 + +对工业项目来说,这比“先上很多高级效果”更重要。 + +## 第一阶段材质系统必须支持的能力 + +### P0 + +- 基础 PBR 材质资源 +- BaseColor +- BaseColorTexture +- NormalTexture +- Metallic / Roughness +- Emissive +- 材质资源保存与加载 +- 材质复用 +- 材质在场景对象上的指派 + +### P1 + +- AO 贴图 +- Alpha 模式 +- 双面材质 +- 基础材质实例工作流 + +### P2 + +- 更高级的渲染 feature +- 后处理 +- 特效扩展 +- 更完整的 shadow / transparency 组织 + +## 第一阶段光照建议 + +基于当前阶段,先把这几个做稳: + +- Directional Light +- Point Light +- Spot Light +- 基础阴影 +- 基础环境光 / IBL + +重点不是参数数量,而是: + +- 稳定 +- 一致 +- 可编辑 +- 可序列化 + +## 与 M3 的关系 + +这个选型属于: + +**M3 场景编辑、材质与光照工作流** + +它的作用是让 MetaCore 在这一阶段具备真正的材质生产力,而不是继续停留在“对象能显示颜色”的水平。 + +所以这条线的优先级: + +- 明显高于继续扩 RuntimeData +- 明显高于继续加更多行业 adapter +- 应该和模型导入、层级编辑并列为引擎基础主线 + +## 推荐实现顺序 + +建议按下面顺序推进: + +1. 定义 MetaCore 材质资源结构 +2. 建立材质资源持久化 +3. 建立材质到 simplepbr 参数的映射 +4. 打通编辑器材质编辑入口 +5. 打通场景对象材质指派 +6. 验证编辑器与 Player 一致性 +7. 再考虑材质实例、更多贴图槽和更丰富 feature + +## 第一阶段明确不要做的事 + +这些都属于以后,不应在当前阶段过早展开: + +- 完整的类 URP 自定义 render feature 体系 +- 大而全的后处理系统 +- 复杂透明与特效管线 +- 完整 shader graph +- 面向所有项目类型的通用渲染框架 + +## 长期方向 + +长期方向应该明确为: + +**MetaCore 第一阶段借助 `panda3d-simplepbr` 跑通基础 PBR 材质工作流,后续逐步演进为 MetaCore 自己的类 URP 渲染管线。** + +这句话同时保证了: + +- 当前可执行 +- 中期可演进 +- 长期不被第三方实现细节绑死 + +## 最终建议 + +当前最好的策略不是: + +- 直接做 URP +- 也不是完全依附 `simplepbr` + +而是: + +**MetaCore 自己拥有材质系统,第一版渲染后端借力 `panda3d-simplepbr`。** + +这是第一阶段最合理、风险最低、推进速度最快的选型。 diff --git a/docs/designs/metacore-packaging-runtime-delivery-design.md b/docs/designs/metacore-packaging-runtime-delivery-design.md new file mode 100644 index 0000000..82e2b65 --- /dev/null +++ b/docs/designs/metacore-packaging-runtime-delivery-design.md @@ -0,0 +1,374 @@ +# MetaCore 打包与交付运行时工作流设计 + +生成时间:2026-03-28 +状态:草案 +范围:M4 UI、持久化与打包循环 + +## 目的 + +这份文档用于明确 MetaCore 第一阶段的打包、Cook、运行时交付工作流应该如何设计。 + +目标不是只做到“Player 能单独启动”,而是建立一条真正可交付的链路: + +- 项目内容如何变成运行时可消费数据 +- 场景、资源、UI、运行时配置如何被带出 +- 打包后客户端如何在独立环境运行 +- 哪些文件属于开发期,哪些属于交付期 + +## 结论先说 + +MetaCore 第一阶段应明确采用下面这条交付链: + +```text +项目内容 + -> 资源与场景保存 + -> Cook + -> 生成运行时所需输出 + -> 打包 Player + 项目内容 + -> 交付 Windows 独立客户端 +``` + +这里必须把两件事分清楚: + +1. **Editor 项目工作目录** +2. **交付运行时目录** + +第一阶段不能再让 `Player` 长期依赖开发期目录的偶然结构或临时 fallback 才能运行。 + +## 当前代码基础 + +当前仓库里已经有较好的基础: + +- `MetaCore.project.json` +- Scene 二进制 package 方向 +- Package 服务 +- Cook 服务 +- `CookManifest` +- Player 能读取项目和启动场景 +- RuntimeData 已有独立二进制文档和项目级 runtime 配置 + +这说明第一阶段不是没有打包基础,而是还缺一套清晰的“交付工作流定义”。 + +当前最需要解决的是: + +- 什么内容属于交付包 +- Cook 输出和源包之间的关系 +- Player 在交付态下应该从哪里加载内容 +- clean machine 验证应如何定义 + +## 第一阶段设计目标 + +第一阶段打包与交付工作流要满足: + +- 开发者能从一个项目构建出独立 Windows 客户端 +- 客户端不依赖 Editor 才能运行 +- 启动场景正确加载 +- 场景依赖资源正确带出 +- UI 资源可带出 +- Runtime 配置可带出 +- clean machine 上可运行 + +## 第一阶段角色划分 + +### 1. 项目工作目录 + +这是开发期使用的目录,包含: + +- `Assets` +- `Scenes` +- `Runtime` +- `Library` +- `MetaCore.project.json` + +这部分是创作环境,不是最终交付目录。 + +### 2. Cook 输出目录 + +这是中间产物目录,用于: + +- 存放 cooked 资源 +- 存放 cooked scene +- 存放依赖清单 + +第一阶段建议继续使用: + +- `Library/Cooked/Windows` + +### 3. 打包输出目录 + +这是最终交付目录。 + +第一阶段建议显式区分出来,例如: + +- `Build/Windows/` + +里面应包含: + +- `MetaCorePlayer.exe` +- 运行时内容目录 +- 项目 runtime 描述 +- 必需依赖文件 + +## 第一阶段推荐交付目录结构 + +建议第一阶段打包后输出结构类似: + +```text +Build/Windows// + MetaCorePlayer.exe + Content/ + Scenes/ + Assets/ + UI/ + Runtime/ + Project/ + MetaCore.runtimeproject +``` + +第一阶段名称可以调整,但结构职责必须清晰。 + +## 第一阶段必须打包的内容 + +### 1. 启动场景 + +必须带出项目定义的 `startup_scene` 对应的 cooked scene。 + +### 2. 场景依赖资源 + +至少要带出: + +- Mesh 资源 +- Material 资源 +- Texture 资源 +- Prefab 资源 +- Scene 引用的 UI 资源 + +### 3. 运行时配置 + +如果项目使用 RuntimeData 或其他运行时配置,则应带出: + +- `ProjectRuntime.mcruntimecfg` +- `DataSources.mcruntime` +- `Bindings.mcruntime` +- 后续 UI runtime 配置 + +### 4. 项目运行时描述 + +建议打包时生成一个“运行时项目描述”,用于告诉 `Player`: + +- 启动场景在哪里 +- 内容根目录在哪里 +- runtime 配置在哪里 +- UI 资源在哪里 + +这和开发期的 `MetaCore.project.json` 不必完全相同。 + +## 项目描述与运行时描述分离建议 + +这是第一阶段很关键的一个设计点。 + +### 开发期项目描述 + +例如: + +- `MetaCore.project.json` + +它更适合: + +- 可读 +- 面向编辑器 +- 面向开发期目录 + +### 运行时项目描述 + +建议打包时生成一个更面向交付的二进制或轻量配置文件,例如: + +- `MetaCore.runtimeproject` + +它更适合: + +- 面向 Player +- 面向 cooked/packaged 内容 +- 不依赖开发目录结构 + +第一阶段不一定要把这层做得很复杂,但“开发期描述”和“交付期描述”最好现在就区分。 + +## Cook 的职责 + +第一阶段 Cook 应明确负责: + +- 把源 package 复制或转换为运行时可消费输出 +- 建立 `CookManifest` +- 确定资源 GUID 与 cooked 路径关系 +- 为打包阶段提供可收集依赖的基础 + +### Cook 不是最终打包 + +这两层要分开: + +- Cook:生成内容 +- Package/Build:组织交付目录、复制 Player、拼装最终产物 + +## 打包流程建议 + +第一阶段推荐下面这条顺序: + +1. 解析项目描述 +2. 确定启动场景 +3. Cook 启动场景 +4. 递归收集依赖资源并 Cook +5. Cook UI 资源 +6. 收集 Runtime 配置 +7. 生成运行时项目描述 +8. 复制 `MetaCorePlayer.exe` +9. 组装交付目录 +10. 运行最小验证 + +## 第一阶段 Player 行为建议 + +打包后的 `Player` 不应默认去猜开发期路径。 + +建议优先行为: + +- 读取与自身同目录或指定参数下的运行时项目描述 +- 根据运行时项目描述找到: + - 启动场景 + - 内容根目录 + - Runtime 配置 + - UI 资源 + +### 仅在开发态允许 fallback + +例如: + +- 直接运行仓库里的 `TestProject` +- 自动猜测 `MetaCore.project.json` + +这类行为可以保留给开发便利,但不应成为交付态主路径。 + +## 场景与资源的打包策略 + +第一阶段建议: + +- Scene 以 cooked scene 文件形式带出 +- 资源以 cooked asset 文件形式带出 +- Scene 中只保存 GUID/引用关系 +- Player 运行时通过 runtime 内容索引解析资源 + +不要让交付态仍然需要访问源文件或编辑器期 package 路径推断逻辑。 + +## UI 的打包策略 + +如果第一阶段引入正式 UI 资源工作流,则打包时应同样带出: + +- UI 文档 +- UI 引用图片资源 +- 必要字体资源 + +Scene 或运行时项目描述应能定位到这些 UI 内容。 + +## RuntimeData 的打包策略 + +RuntimeData 在第一阶段不再是主线,但如果项目启用它,打包时必须作为正式交付内容处理: + +- runtime project 配置 +- source / binding 配置 +- 必要的 replay 文件或外部连接参数 + +注意: + +- 开发态的调试 fallback 不应进入正式交付流程 + +## clean machine 验证 + +这是第一阶段打包能力是否成立的真正验收标准。 + +### 验证目标 + +在没有源码、没有编辑器、没有开发期目录结构假设的机器上,最终产物应能: + +- 启动 +- 读取运行时项目描述 +- 加载启动场景 +- 正确加载资源 +- 正确显示 UI +- 如果配置了 RuntimeData,则能正确加载相关配置 + +### 最低验证方式 + +第一阶段至少要有一条明确流程: + +- 将打包目录复制到 clean machine 或近似隔离环境 +- 启动 `MetaCorePlayer.exe` +- 验证项目主界面和主场景 + +## 错误与诊断 + +交付态必须能对这些问题给出明确提示: + +- 启动场景缺失 +- cooked 资源缺失 +- UI 文档缺失 +- runtime 配置缺失 +- 运行时项目描述损坏 + +错误不应只表现为黑屏或静默失败。 + +第一阶段至少应做到: + +- 日志输出 +- 控制台/日志文件 +- 基础错误分类 + +## 推荐实现顺序 + +建议按下面顺序推进: + +1. 固定运行时项目描述结构 +2. 固定交付目录结构 +3. 打通 Scene 与资源依赖收集 +4. 打通 UI 资源带出 +5. 打通 Runtime 配置带出 +6. 复制 Player 和组装交付目录 +7. 做 clean machine 验证 +8. 再补增量构建与更细的诊断 + +## 第一阶段 Definition of Done + +下面这些成立时,才能认为第一阶段打包与交付工作流真正成立: + +- 从项目可以生成独立 Windows 输出目录 +- 输出目录包含 Player 和项目必需内容 +- Player 不依赖 Editor 即可运行 +- 启动场景能正确加载 +- 资源和 UI 能正确显示 +- Runtime 配置能正确读取 +- clean machine 验证通过 + +## 与整体路线的关系 + +这份文档对应第一阶段中的: + +- `M4 UI、持久化与打包循环` + +它的位置必须在: + +- 项目系统 +- 资源导入 +- 场景编辑 +- 材质与光照 +- UI 资源工作流 + +之后。 + +只有这些内容工作流成立后,打包才有意义。 + +## 最终建议 + +第一阶段不要把“能运行 Player”误认为“完成打包”。 + +真正的打包与交付能力,应当是: + +**能把一个项目稳定转换成独立运行的 Windows 客户端,并在脱离编辑器和源码环境后仍能正确加载场景、资源、UI 和运行时配置。** + +这一步做实之后,MetaCore 才算真正具备了“项目可交付”的引擎特征。 diff --git a/docs/designs/metacore-phase1-foundation-checklist-and-acceptance-matrix.md b/docs/designs/metacore-phase1-foundation-checklist-and-acceptance-matrix.md new file mode 100644 index 0000000..3c20644 --- /dev/null +++ b/docs/designs/metacore-phase1-foundation-checklist-and-acceptance-matrix.md @@ -0,0 +1,479 @@ +# MetaCore 第一阶段引擎基础功能总清单与验收矩阵 + +生成时间:2026-03-28 +状态:草案 +范围:第一阶段引擎基础主线 + +## 目的 + +这份文档用于把前面已经形成的多份设计文档,收敛成一份真正可执行的总表。 + +它要解决的问题很直接: + +- 第一阶段到底有哪些基础功能块 +- 哪些是 `P0 / P1 / P2` +- 每一项做到什么程度才算完成 +- 每一项应该如何验收 + +这份文档不替代前面的专项设计,而是作为: + +- 排期总表 +- 阶段验收清单 +- 团队对齐基线 + +## 使用方式 + +建议后续所有第一阶段推进,都以这份文档作为总入口: + +- 想看为什么这样做:回到对应专项设计文档 +- 想排开发顺序:看这里的优先级 +- 想判断一项是否完成:看这里的 `Definition of Done` +- 想做测试和验收:看这里的“验证方式” + +## 优先级定义 + +### P0 + +第一阶段主线阻塞项。 +不做完,MetaCore 还不能被视为“可用于开发项目的引擎”。 + +### P1 + +紧随 P0 的高价值能力。 +不一定是主线第一步,但缺了会明显影响生产力与交付质量。 + +### P2 + +重要,但应建立在 P0/P1 成立之后。 +典型是数字孪生增强、更多适配器、更多格式和更深能力。 + +## 总体完成标准 + +MetaCore 第一阶段只有在下面这句话成立时,才算真正完成: + +**开发者能够使用 MetaCore 完成一个 Windows 工业三维应用的项目创建、资源导入、场景搭建、材质与光照编辑、UI 配置、保存打开、打包交付,并在此基础上接入第一版 RuntimeData。** + +## 总清单与验收矩阵 + +### 1. 项目系统 + +优先级:`P0` + +#### 范围 + +- 项目创建/打开 +- `MetaCore.project.json` +- 项目目录结构 +- 启动场景 +- Editor / Player 围绕项目工作 + +#### Definition of Done + +- 能稳定定位项目根目录 +- `MetaCore.project.json` 能被一致读取和保存 +- 启动场景行为在 Editor 和 Player 中一致 +- 样板项目目录结构固定可理解 + +#### 验收方式 + +- 从不同工作目录启动仍能正确打开项目 +- 修改启动场景后保存,再次打开结果一致 +- 坏项目描述文件能给出明确报错 + +#### 相关文档 + +- [metacore-product-plan.md](D:/MetaCore/docs/designs/metacore-product-plan.md) +- [metacore-phase1-implementation-plan.md](D:/MetaCore/docs/designs/metacore-phase1-implementation-plan.md) +- [metacore-m1-m2-detailed-task-breakdown.md](D:/MetaCore/docs/designs/metacore-m1-m2-detailed-task-breakdown.md) + +### 2. 资源数据库与导入循环 + +优先级:`P0` + +#### 范围 + +- 资产元数据 +- Asset Database +- Import Pipeline +- Reimport +- Package 基础 + +#### Definition of Done + +- 导入后的资源进入 Asset Database +- 每个资源有稳定 GUID +- 资源能在 Project 面板中出现 +- 资源能被重新导入而不破坏基础引用 + +#### 验收方式 + +- 导入一个模型后能在资源列表中找到 +- 删除/修改源文件时系统给出明确反馈 +- 重导入后场景引用尽量保持稳定 + +#### 相关文档 + +- [metacore-m1-m2-detailed-task-breakdown.md](D:/MetaCore/docs/designs/metacore-m1-m2-detailed-task-breakdown.md) +- [metacore-project-panel-asset-browser-design.md](D:/MetaCore/docs/designs/metacore-project-panel-asset-browser-design.md) + +### 3. `glTF/.glb` 首批导入器 + +优先级:`P0` + +#### 范围 + +- `.glb` +- `.gltf + 外部资源` +- 节点层级 +- 材质抽取 +- 贴图抽取 +- 导入结果文档 + +#### Definition of Done + +- 导入一个 `glTF/.glb` 文件后,生成正式 Mesh / Material / Texture 资产 +- 保留节点层级描述 +- 可将模型资源拖入场景生成对象树 +- 重导入默认尽量保持 GUID 稳定 + +#### 验收方式 + +- 单网格、单材质样例导入成功 +- 多材质、多层级样例导入成功 +- 场景保存重开后模型实例仍有效 + +#### 相关文档 + +- [metacore-gltf-glb-importer-design.md](D:/MetaCore/docs/designs/metacore-gltf-glb-importer-design.md) + +### 4. Project 面板与资源浏览 + +优先级:`P0` + +#### 范围 + +- 目录浏览 +- 资源列表 +- 资源详情 Inspector +- 资源拖拽 +- 刷新 / 重导入 / 定位 + +#### Definition of Done + +- 用户能看见项目资源结构 +- 资源选择与 Inspector 联动 +- 模型资源可拖入场景 +- 材质资源可拖到对象材质槽 +- 支持刷新和单资源重导入 + +#### 验收方式 + +- 导入资源后在 Project 面板中可见 +- 资源拖拽后场景对象或材质槽正确变化 +- 重导入后面板内容与资源状态同步 + +#### 相关文档 + +- [metacore-project-panel-asset-browser-design.md](D:/MetaCore/docs/designs/metacore-project-panel-asset-browser-design.md) + +### 5. 场景编辑与层级工作流 + +优先级:`P0` + +#### 范围 + +- Hierarchy +- 创建 / 删除 / 复制 / 重命名 +- 父子级 +- 拖拽重挂接 +- 多选 +- 模型资源拖入场景 + +#### Definition of Done + +- 用户能创建空对象和内置对象 +- 模型资源拖入场景后生成对象树 +- 支持复制、删除、重挂接、多选 +- 层级保存/重开一致 + +#### 验收方式 + +- 复杂对象树可以被正确组织 +- 删除和复制对子树行为正确 +- 重挂接后局部/世界变换行为符合约定 + +#### 相关文档 + +- [metacore-scene-hierarchy-workflow-design.md](D:/MetaCore/docs/designs/metacore-scene-hierarchy-workflow-design.md) + +### 6. Transform 与 Gizmo 工作流 + +优先级:`P0` + +#### 范围 + +- Move +- Rotate +- Scale +- Inspector 中 Position / Rotation / Scale +- Scene View 基础 Gizmo + +#### Definition of Done + +- 用户能在 Inspector 和视口中编辑 Transform +- 层级关系下局部变换与世界结果正确 +- 多选基础编辑成立 + +#### 验收方式 + +- 多对象摆放工作流顺畅 +- 父对象移动后子对象行为正确 +- 保存并重开后 Transform 一致 + +#### 相关文档 + +- [metacore-scene-hierarchy-workflow-design.md](D:/MetaCore/docs/designs/metacore-scene-hierarchy-workflow-design.md) + +### 7. 材质系统与 MeshRenderer 资源化 + +优先级:`P0` + +#### 范围 + +- Material Asset +- Mesh Asset +- Texture Asset 基础元信息 +- `MeshRenderer` 从内嵌颜色演进到资源引用 + +#### Definition of Done + +- Scene 中对象可以引用 Mesh Asset 和 Material Asset +- 材质资源具备第一阶段最小 PBR 字段 +- `MeshRenderer` 不再只依赖 `BuiltinMesh` +- Scene / Prefab 保存的是资源引用 + +#### 验收方式 + +- 导入模型后,材质资源可见且可被对象引用 +- 更换材质资源后,运行时渲染结果变化正确 +- 保存和重开后资源引用不丢失 + +#### 相关文档 + +- [metacore-material-render-pipeline-selection.md](D:/MetaCore/docs/designs/metacore-material-render-pipeline-selection.md) +- [metacore-material-import-integration-design.md](D:/MetaCore/docs/designs/metacore-material-import-integration-design.md) +- [metacore-material-meshrenderer-data-structure-design.md](D:/MetaCore/docs/designs/metacore-material-meshrenderer-data-structure-design.md) + +### 8. 基础 PBR 材质与光照 + +优先级:`P0` + +#### 范围 + +- 基于 `panda3d-simplepbr` 的第一版 PBR 后端 +- Directional / Point / Spot Light +- 基础阴影或稳定照明 +- 编辑器和 Player 尽量一致 + +#### Definition of Done + +- 基础 PBR 材质在 Editor / Player 中可见 +- 灯光可编辑且结果稳定 +- simplepbr 只作为后端,不反向绑定材质资源结构 + +#### 验收方式 + +- 导入模型的基础 PBR 参数能被正确显示 +- 灯光颜色/强度调整结果可见 +- 编辑器和 Player 画面不存在明显语义不一致 + +#### 相关文档 + +- [metacore-material-render-pipeline-selection.md](D:/MetaCore/docs/designs/metacore-material-render-pipeline-selection.md) + +### 9. 场景保存与打开 + +优先级:`P0` + +#### 范围 + +- Scene 新建 +- 保存 +- 另存 +- 打开 +- 启动场景 + +#### Definition of Done + +- Scene 中的层级、组件、资源引用都能稳定保存 +- Scene 能被重新打开并恢复一致状态 +- 启动场景机制稳定 + +#### 验收方式 + +- 编辑场景后保存并关闭,再打开结果一致 +- 改名或移动场景后,项目引用策略清晰 + +#### 相关文档 + +- [metacore-phase1-implementation-plan.md](D:/MetaCore/docs/designs/metacore-phase1-implementation-plan.md) +- [metacore-packaging-runtime-delivery-design.md](D:/MetaCore/docs/designs/metacore-packaging-runtime-delivery-design.md) + +### 10. UI 编辑与运行时 UI + +优先级:`P1` + +#### 范围 + +- UiDocument +- UiNode +- Panel / Text / Image / Button +- UI 层级树 +- UI Inspector +- Player 运行时显示 + +#### Definition of Done + +- 用户能创建和保存 UI 文档 +- 用户能编辑基础 UI 节点与层级 +- Scene 或项目可引用 UI 资源 +- Player 能显示 UI + +#### 验收方式 + +- 典型数字孪生界面可完成基础搭建 +- 保存并重开 UI 文档后结构一致 +- 打包后 UI 能正常显示 + +#### 相关文档 + +- [metacore-ui-workflow-design.md](D:/MetaCore/docs/designs/metacore-ui-workflow-design.md) + +### 11. 打包与交付运行时 + +优先级:`P0` + +#### 范围 + +- Cook +- 打包输出目录 +- 运行时项目描述 +- Player 交付态加载 +- clean machine 验证 + +#### Definition of Done + +- 能从项目生成独立 Windows 输出目录 +- 输出目录包含 Player 和项目必需内容 +- Player 不依赖 Editor 即可运行 +- 启动场景、资源、UI、runtime 配置可正确加载 + +#### 验收方式 + +- 在独立目录启动打包产物成功 +- clean machine 或近似隔离环境验证通过 +- 缺失关键内容时给出明确日志 + +#### 相关文档 + +- [metacore-packaging-runtime-delivery-design.md](D:/MetaCore/docs/designs/metacore-packaging-runtime-delivery-design.md) + +### 12. Prefab 基础复用 + +优先级:`P1` + +#### 范围 + +- 从对象树创建 Prefab +- Prefab 实例化 +- 应用 / 还原基础行为 + +#### Definition of Done + +- 可以将重复对象树提取为 Prefab +- Scene 中实例可复用资源和层级 +- 对基础修改可以应用和还原 + +#### 验收方式 + +- 重复设备对象可被抽成 Prefab 并实例化 +- 保存重开后 Prefab 实例关系仍然可识别 + +#### 相关文档 + +- [metacore-phase1-implementation-plan.md](D:/MetaCore/docs/designs/metacore-phase1-implementation-plan.md) + +### 13. RuntimeData 数字孪生集成 + +优先级:`P2` + +#### 范围 + +- source / point / binding +- adapter +- runtime diagnostics +- Editor 侧配置 + +#### Definition of Done + +- 打包后的运行时可接入至少一种 live source +- 数据变化能驱动场景或 UI 的第一批目标 +- 故障、断连、损坏配置可见 + +#### 验收方式 + +- mock / file_replay / tcp 至少一条真实链路可运行 +- binding 故障不会导致运行时崩溃 +- diagnostics 可定位问题 + +#### 相关文档 + +- [metacore-runtime-data-access-design.md](D:/MetaCore/docs/designs/metacore-runtime-data-access-design.md) +- [metacore-runtime-data-usage.md](D:/MetaCore/docs/designs/metacore-runtime-data-usage.md) + +## 当前建议执行顺序 + +按这份总表,推荐的第一阶段主路径应是: + +```text +项目系统 + -> 资源数据库与 glTF/glb 导入 + -> Project 面板 + -> 场景编辑与层级 + -> Transform/Gizmo + -> 材质资源化与基础 PBR + -> 场景保存/打开 + -> UI 工作流 + -> 打包与交付运行时 + -> RuntimeData 集成 +``` + +## 当前不应抢占主线的内容 + +这些内容都重要,但当前不应抢占上面的主路径: + +- 更多 live adapter +- 更复杂工业协议 +- VR/XR 训练工作流 +- B/S 产品化 +- AI 辅助能力 +- 更完整的后处理和高级渲染 feature + +## 验收建议 + +后续每完成一个功能域,建议都按下面四步做验收: + +1. 对照本表检查 `Definition of Done` +2. 对照专项设计文档确认没有走偏 +3. 补对应 smoke / integration tests +4. 用样板项目做一次真实工作流回归 + +## 最终建议 + +第一阶段不要再被零散功能牵着走,而应按这份总表推进。 + +最关键的判断只有一个: + +**MetaCore 第一阶段不是“做出很多点状能力”,而是把一条完整的引擎基础生产力链做通。** + +只有这条链做通之后,数字孪生、VR、国产化适配和后续商业化方向才会真正站得住。 diff --git a/docs/designs/metacore-project-panel-asset-browser-design.md b/docs/designs/metacore-project-panel-asset-browser-design.md new file mode 100644 index 0000000..4496d46 --- /dev/null +++ b/docs/designs/metacore-project-panel-asset-browser-design.md @@ -0,0 +1,420 @@ +# MetaCore Project 面板与资源浏览设计 + +生成时间:2026-03-28 +状态:草案 +范围:M2 资源与模型导入循环、M3 场景编辑工作流 + +## 目的 + +这份文档用于明确 MetaCore 第一阶段 `Project` 面板与资源浏览工作流应该怎么设计。 + +目标不是做一个“能列文件名”的面板,而是做出一套真正支撑内容生产的资源工作流。 + +它要解决的问题包括: + +- 项目里的资源怎样被看见 +- 资源怎样被定位、筛选、预览 +- 资源怎样被拖入场景 +- 资源怎样重新导入、刷新、定位引用 +- 源文件、元数据、生成资源之间如何组织 + +## 结论先说 + +MetaCore 第一阶段的 `Project` 面板应该明确承担下面这条工作流: + +```text +导入资源 + -> 资源进入 Asset Database + -> Project 面板可见 + -> 选择资源查看信息 + -> 拖入 Scene / Hierarchy 使用 + -> 需要时重导入、刷新、定位 +``` + +也就是说,`Project` 面板不是“附属视图”,而是引擎生产力主链的一部分。 + +如果没有这部分,导入能力就只是“文件进来了”,而不是“资源进入了引擎工作流”。 + +## 当前代码基础 + +从当前代码看,已经具备下面这些基础: + +- `MetaCoreProjectDescriptor` +- `MetaCoreAssetRecord` +- `MetaCoreIAssetDatabaseService` +- `MetaCoreIImportPipelineService` +- `MetaCoreICookService` +- `MetaCoreIAssetRegistryService` + +这说明 MetaCore 已经有了资源数据库和导入服务的基本边界。 + +当前更缺的是: + +- 明确 `Project` 面板展示什么 +- 明确源文件、资源、元数据三者关系 +- 明确资源交互动作 +- 明确资源拖拽进场景的规则 +- 明确“导入结果”与“项目文件结构”之间的用户心智 + +## 设计目标 + +第一阶段 `Project` 面板与资源浏览工作流要满足: + +- 用户能看懂项目资源结构 +- 用户能区分源文件和正式资源 +- 用户能找到模型、材质、纹理、场景、Prefab +- 用户能拖拽资源进入场景 +- 用户能重新导入和刷新资源 +- 用户能看到最基本的资源元信息 +- 用户能理解资源来自哪里、被什么引用 + +## 核心概念 + +### 1. 项目目录 + +项目目录是用户能直接看到和理解的结构,例如: + +- `Assets` +- `Scenes` +- `Runtime` +- `Library` + +### 2. 源文件 + +例如: + +- `.glb` +- `.gltf` +- `.fbx` +- 图片源文件 + +这是用户导入进来的输入文件。 + +### 3. 元数据 + +例如 `.mcmeta` 一类文件,主要用于保存: + +- AssetGuid +- AssetType +- ImporterId +- SourceHash +- PackagePath + +### 4. 正式资源 + +这是引擎真正管理和消费的对象,例如: + +- Mesh Asset +- Material Asset +- Texture Asset +- Prefab Asset + +### 5. 导入结果 + +一个源文件导入后,通常不是只得到一个资源,而是一组资源及其关系。 + +`Project` 面板必须允许用户理解这层关系。 + +## 第一阶段推荐的信息组织 + +### 用户看到的主目录 + +第一阶段建议在 `Project` 面板中,首先按项目目录浏览: + +- `Assets` +- `Scenes` +- `Runtime` + +`Library` 原则上不应作为用户的主工作目录。 + +### 用户看到的资源层级 + +在 `Assets` 下,第一阶段建议重点支持: + +- 模型源文件 +- 材质资源 +- 纹理资源 +- Prefab +- 其他后续资产 + +### 第一阶段推荐的展示心智 + +不要让用户只看到一堆 package 文件。 + +推荐心智应是: + +- 左侧按项目目录浏览 +- 中间按当前目录显示“资源项” +- 右侧或下方 Inspector 显示当前资源详情 + +这更接近 Unity / UE 用户习惯。 + +## Project 面板必须支持的能力 + +### P0:浏览 + +- 浏览目录 +- 浏览目录下资源 +- 显示资源名称 +- 显示资源类型 +- 显示图标或最小类型标识 + +### P0:选择 + +- 单选资源 +- 与 Inspector 联动 +- 双击打开适用资源 + +例如: + +- 双击场景资源 -> 打开 Scene +- 双击 Prefab -> 定位或打开 Prefab 编辑流 + +### P0:拖拽 + +- 将模型资源拖入 Scene View +- 将模型资源拖入 Hierarchy +- 将材质资源拖到对象材质槽 +- 将 Prefab 拖入场景 + +拖拽是资源工作流的核心生产力,第一阶段优先级很高。 + +### P0:刷新与重导入 + +- 刷新当前目录 +- 刷新 Asset Database +- 对单个资源执行重导入 + +### P0:定位 + +- 按路径定位 +- 按 GUID 查到资源记录 +- 在 Project 面板中高亮当前资源 + +## 第一阶段推荐的展示字段 + +对于 `Project` 面板中的每个资源项,第一阶段至少应显示: + +- 名称 +- 资源类型 +- 来源类型 + - 源文件 + - 元数据 + - 正式资源 +- 导入器类型(可选) + +不建议第一阶段直接把一大堆底层字段都堆到列表中。 + +## 资源详情 Inspector + +当用户在 `Project` 面板里选中一个资源时,Inspector 第一阶段至少应显示: + +- 名称 +- 资源 GUID +- 资源类型 +- 相对路径 +- 源文件路径 +- ImporterId +- SourceHash +- PackagePath + +### 针对模型资源 + +建议额外显示: + +- 包含几个 Mesh +- 包含几个 Material +- 包含几个 Texture +- 节点层级摘要 + +### 针对材质资源 + +建议额外显示: + +- ShaderModel +- BaseColor +- 贴图引用 + +### 针对场景资源 + +建议额外显示: + +- 是否是 Startup Scene + +## 源文件、元数据、正式资源的关系 + +第一阶段必须在设计上讲清楚,不然用户会迷惑。 + +### 推荐规则 + +- 源文件是输入 +- 元数据是追踪和导入控制文件 +- 正式资源是引擎消费对象 + +### Project 面板对这三者的展示建议 + +第一阶段建议用户主视角以“资源”为中心,而不是以 package 为中心。 + +也就是说: + +- 用户看到的是“模型资源”、“材质资源”、“纹理资源” +- 需要时可以在 Inspector 中看到它们来自哪个源文件 + +不建议把 package 内部细节直接变成主界面心智。 + +## 模型导入后的展示方式 + +这是第一阶段最关键的工作流之一。 + +当用户导入一个 `.glb` 文件后,建议在 `Project` 面板中: + +- 显示一个模型入口项 +- 允许展开或在 Inspector 中查看导入出的: + - Mesh 资源 + - Material 资源 + - Texture 资源 + +### 不建议的做法 + +不要让用户导入一个模型后,只看到: + +- 一个源文件 +- 一堆看不懂的 package 文件 + +这会让资源工作流非常不透明。 + +## 拖拽规则 + +### 模型资源拖入 Scene + +结果应为: + +- 根据模型节点文档实例化对象树 + +### Mesh 资源拖入 Scene + +结果应为: + +- 创建一个带 `MeshRenderer` 的对象 +- 默认绑定该 `Mesh Asset` +- 如果可推断默认材质,则一起指派 + +### 材质资源拖到对象 + +结果应为: + +- 替换当前对象或当前材质槽的材质引用 + +### Prefab 拖入 Scene + +结果应为: + +- 实例化 Prefab + +## 右键菜单建议 + +第一阶段推荐支持最小右键动作: + +- 新建文件夹 +- 刷新 +- 重导入 +- 显示到资源所在目录 +- 复制相对路径 + +### 针对适用资源可增加 + +- 设置为启动场景 +- 创建 Prefab +- 在场景中实例化 + +## 搜索与筛选 + +第一阶段建议至少支持: + +- 按名称搜索 +- 按资源类型筛选 + +例如: + +- Scene +- Material +- Mesh +- Texture +- Prefab + +这会显著提高后续工业场景项目的生产力。 + +## 与场景工作流的关系 + +`Project` 面板不是独立存在的,它必须和: + +- `Hierarchy` +- `Scene View` +- `Inspector` + +形成联动。 + +### 推荐联动 + +- 在 `Project` 里选资源 -> Inspector 显示资源详情 +- 从 `Project` 拖资源到 `Scene` -> 生成对象或修改对象 +- 场景对象引用资源 -> Inspector 可反向定位到 `Project` + +这条联动链一旦成立,引擎工作流才像 Unity/UE。 + +## 与导入器的关系 + +Project 面板必须对导入器结果可见。 + +也就是说: + +- 导入成功后,Asset Database 更新 +- `Project` 面板刷新后能立即看到结果 +- 重导入后,资源视图与 Inspector 同步变化 + +不能让导入器是一条孤立后台流程。 + +## 与 Cook / 打包的关系 + +第一阶段不要求 `Project` 面板直接承担完整打包流程,但至少应能: + +- 显示资产是否有 package 路径 +- 在必要时查看 cooked 输出位置 + +后续再扩: + +- 查看依赖 +- 查看 cook 状态 +- 查看增量构建状态 + +## 第一阶段推荐实现顺序 + +建议按下面顺序推进: + +1. 固定 `Project` 面板目录浏览模型 +2. 固定资源列表项和资源详情显示 +3. 打通 `Asset Database -> Project 面板` 刷新链 +4. 打通模型资源拖入 Scene / Hierarchy +5. 打通材质拖拽到对象槽位 +6. 打通单资源重导入 +7. 再补搜索、右键菜单和更强预览 + +## 第一阶段 Definition of Done + +下面这些成立时,才能认为第一阶段 `Project` 面板真正可用: + +- 用户能清楚看到项目资源结构 +- 导入后的资源能在 `Project` 面板中找到 +- 选中资源时能看到基本元信息 +- 模型资源可以拖入场景 +- 材质资源可以拖到对象材质槽 +- 能对资源执行刷新和重导入 +- Scene / Inspector / Project 三者之间联动成立 + +## 最终建议 + +第一阶段不要把 `Project` 面板做成“文件列表”,而要把它做成: + +**MetaCore 的资源工作台。** + +只有这样,模型导入、材质系统、场景编辑和后续 Prefab、UI、打包等能力,才会真正串成一个像 Unity/UE 的引擎工作流。 diff --git a/docs/designs/metacore-scene-hierarchy-workflow-design.md b/docs/designs/metacore-scene-hierarchy-workflow-design.md new file mode 100644 index 0000000..3fa6a2a --- /dev/null +++ b/docs/designs/metacore-scene-hierarchy-workflow-design.md @@ -0,0 +1,416 @@ +# MetaCore 场景编辑与层级工作流设计 + +生成时间:2026-03-28 +状态:草案 +范围:M3 场景编辑、材质与光照工作流 + +## 目的 + +这份文档用于明确 MetaCore 第一阶段的场景编辑与层级工作流应该怎么设计。 + +目标不是做一个“能显示对象树”的面板,而是做出一套真正能支撑工业数字孪生项目内容生产的基础编辑工作流。 + +它要解决的问题包括: + +- 对象在场景中怎么组织 +- 父子级怎么工作 +- 拖拽重挂接怎么工作 +- 多选、复制、删除怎么工作 +- 模型资源拖入场景后生成什么 +- Gizmo 和 Transform 编辑如何与层级系统配合 + +## 结论先说 + +MetaCore 第一阶段的场景编辑工作流,应该明确对标: + +**Unity / UE 风格的“Hierarchy + Scene View + Inspector”最小生产力闭环** + +它的核心不是功能多,而是这条链必须顺: + +```text +资源进入项目 + -> 拖入场景 + -> 生成对象树 + -> 在 Hierarchy 中组织 + -> 在 Scene View 中摆放 + -> 在 Inspector 中调整 + -> 保存并重开仍然一致 +``` + +如果这条链不成立,前面的模型导入、材质系统和后面的 RuntimeData 都很难真正落地。 + +## 当前代码基础 + +当前代码里已经具备一些可复用的基础能力: + +- 创建对象 +- 查找对象 +- 获取根对象和子对象 +- 构建层级前序遍历 +- 删除对象及其子树 +- 复制对象及其子树 +- 批量重挂接对象 +- 场景快照、撤销重做基础 + +这说明 MetaCore 已经不是从零开始,而是已经有了场景数据层基础。 + +当前更缺的是: + +- 把这些能力组织成清晰的编辑工作流 +- 明确对象、资源、层级三者的关系 +- 明确模型拖入场景如何生成对象树 +- 明确 Gizmo、多选、重挂接、Inspector 的交互约定 + +## 第一阶段设计目标 + +第一阶段场景编辑工作流要满足: + +- 支撑工业场景对象组织 +- 支撑模型导入后的对象树实例化 +- 支撑 Transform 基础编辑 +- 支撑对象复制、删除、重命名、重挂接 +- 支撑对象与资源分离 +- 支撑 Scene 保存/重开一致性 +- 为 Prefab 和后续 RuntimeData 留出空间 + +## 核心概念 + +### 1. Scene + +Scene 是对象实例的容器,负责: + +- 保存对象树 +- 保存对象组件数据 +- 保存对象之间的父子关系 + +Scene 不直接保存资源本体,只保存对资源的引用。 + +### 2. GameObject + +GameObject 是场景中的实例节点,负责: + +- 名称 +- 唯一 ID +- 父子关系 +- 组件集合 + +### 3. Resource + +资源是被场景对象引用的外部资产,例如: + +- Mesh Asset +- Material Asset +- Texture Asset +- Prefab Asset + +资源不是场景节点本身。 + +### 4. Hierarchy + +Hierarchy 是 Scene 对象树的编辑视图,不是单独的一份数据。 + +它负责表达: + +- 根对象 +- 子对象 +- 展开折叠 +- 选择状态 +- 拖拽重挂接 + +## 第一阶段工作流总图 + +```mermaid +flowchart LR + A["Project 面板"] --> B["将模型/Prefab 拖入场景"] + B --> C["根据资源生成 GameObject 树"] + C --> D["Hierarchy 中显示对象树"] + D --> E["Scene View 中摆放与选择"] + E --> F["Inspector 中编辑 Transform/组件/资源引用"] + F --> G["保存 Scene"] + G --> H["重新打开 Scene 后结果一致"] +``` + +## 对象创建规则 + +第一阶段应明确三类创建方式: + +### 1. 空对象创建 + +用于: + +- 逻辑分组 +- 组织层级 +- 作为父节点容器 + +### 2. 内置对象创建 + +用于: + +- Cube +- 未来的 Plane、Sphere 等原型几何 + +这有利于: + +- 快速搭测试场景 +- 不依赖导入资源也能编辑布局 + +### 3. 资源实例创建 + +用于: + +- 模型资源拖入场景 +- Prefab 拖入场景 + +这是第一阶段真正需要打通的重点。 + +## 模型资源拖入场景规则 + +这是场景编辑工作流里最关键的新增点之一。 + +### 规则 1:拖入模型资源时,生成对象树而不是单一节点 + +如果导入的模型保留了节点层级,那么拖入场景后应根据模型节点描述生成对应的 `GameObject` 树。 + +不要把整个模型强行压成一个单节点对象。 + +### 规则 2:每个节点保留局部变换 + +这样: + +- 装配关系 +- 部件位置 +- 工业模型层级 + +才能正确进入 Scene。 + +### 规则 3:带 Mesh 的节点生成 MeshRenderer + +节点如果引用了某个 `Mesh Asset`,则实例化对象应带: + +- `Transform` +- `MeshRenderer` + +并正确引用: + +- `MeshAssetGuid` +- `MaterialAssetGuids` + +### 规则 4:根节点命名应可读 + +拖入后根对象名称应优先来自: + +- 模型名 +- 或根节点名 + +避免出现一堆无意义自动名。 + +## Hierarchy 面板设计 + +### 第一阶段必须支持 + +- 树状显示对象 +- 展开 / 折叠 +- 单选 +- 多选 +- 当前激活对象高亮 +- 重命名 +- 拖拽重挂接 +- 删除 +- 复制 + +### 第一阶段推荐支持 + +- 搜索过滤 +- 根节点快速创建 +- 右键菜单 + +### 第一阶段可以后置 + +- 复杂标签系统 +- 场景分层/层管理器 +- 多场景同时编辑 + +## 选择模型 + +第一阶段建议采用: + +- `Hierarchy` 和 `Scene View` 共享同一选择状态 +- `Inspector` 始终展示当前激活对象 +- 多选时 Inspector 支持基础公共字段编辑 + +### 多选第一阶段建议支持的字段 + +- `Visible` +- `Transform` 的基础值 +- 灯光颜色 / 强度 +- MeshRenderer 的简单开关 + +复杂差异编辑可以后置。 + +## 重挂接规则 + +第一阶段必须明确重挂接时的行为。 + +### 规则 1:不能挂到自己的子树下面 + +这会导致层级环。 + +### 规则 2:支持保留世界变换 + +这是最符合编辑器习惯的行为。 + +建议第一阶段默认: + +- Hierarchy 拖拽重挂接时保留世界变换 + +### 规则 3:多对象重挂接应视为一个原子编辑动作 + +这样撤销重做才合理。 + +## 复制与删除规则 + +### 删除 + +第一阶段应明确: + +- 删除对象时默认删除其整个子树 +- 多选删除时要去重,避免重复删除子节点 + +### 复制 + +第一阶段应明确: + +- 复制对象时复制整个子树 +- 新对象保留组件结构 +- 新对象保留资源引用 +- 新对象生成新的对象 ID + +这对工业设备、告警组件、UI 锚点等复用很重要。 + +## Transform 与 Gizmo 工作流 + +这一部分是 Scene 编辑的生产力核心。 + +### 第一阶段必须支持 + +- Move +- Rotate +- Scale +- Position / Rotation / Scale 在 Inspector 中可编辑 +- Scene View 中基础 Gizmo 操作 + +### 第一阶段推荐支持 + +- Local / World 切换 +- Pivot / Center 行为预留 +- 聚焦当前对象 + +### 与层级的关系 + +Transform 编辑必须基于对象的父子层级来解释局部变换。 + +也就是说: + +- Scene 中保存的是局部变换 +- 世界变换在运行时和编辑计算时推导 + +不要把世界变换直接存回 Scene 作为主数据。 + +## Inspector 规则 + +第一阶段 Inspector 至少应清晰分层: + +### 对象级 + +- 名称 +- 启用状态(后续可加) +- 父对象信息 + +### Transform + +- Position +- Rotation +- Scale + +### 组件级 + +- MeshRenderer +- Light +- Camera +- 后续其他组件 + +Inspector 要做的是“编辑当前对象”,不要把资源浏览、运行时诊断、复杂项目管理都混进来。 + +## 保存与恢复规则 + +第一阶段必须保证: + +- Hierarchy 结构可保存 +- 父子关系可恢复 +- 组件数据可恢复 +- 资源引用可恢复 +- 重新打开 Scene 后结构不乱 + +如果导入模型拖入场景后保存重开不能保持一致,那整个工作流就是不稳定的。 + +## 与 Prefab 的关系 + +第一阶段场景工作流必须考虑后续 Prefab 复用。 + +建议规则: + +- 场景中的对象树可以被抽成 Prefab +- Prefab 实例进入 Scene 后仍表现为普通对象树 +- 层级结构和资源引用规则保持一致 + +这样设备、阀门、泵、面板、指示灯这类重复对象才能真正规模化复用。 + +## 与 RuntimeData 的关系 + +RuntimeData 不应主导场景编辑结构。 + +更合理的关系是: + +- Scene 负责对象树和组件结构 +- RuntimeData 只负责在运行时驱动这些对象的显示状态或组件参数 + +也就是说: + +- 先有稳定场景工作流 +- 再把 RuntimeData 挂进去 + +这才符合引擎基础优先的路线。 + +## 推荐实现顺序 + +建议按下面顺序推进: + +1. 固定 Hierarchy 数据和交互规则 +2. 固定选择模型 +3. 固定复制、删除、重挂接规则 +4. 打通 Transform Inspector 与基础 Gizmo +5. 打通模型资源拖入场景生成对象树 +6. 验证 Scene 保存/重开一致性 +7. 再增强多选、搜索、右键菜单等体验 + +## 第一阶段 Definition of Done + +下面这些成立时,才能认为第一阶段场景编辑工作流真正可用: + +- 用户可以创建空对象和内置对象 +- 用户可以将模型资源拖入场景并生成对象树 +- Hierarchy 能正确显示和编辑对象树 +- 支持多选、复制、删除、重命名、重挂接 +- Scene View 可选择和操作对象 +- Inspector 可编辑 Transform 和基础组件 +- Scene 保存并重开后结果一致 + +## 最终建议 + +第一阶段最重要的不是把编辑器做得花,而是把工作流做顺。 + +所以最合理的方向是: + +**把 Scene、Hierarchy、Scene View、Inspector 串成一条稳定的对象编辑链,让导入进来的模型真正能被组织、摆放、调整和保存。** + +这一步一旦做稳,MetaCore 才真正开始像一个能开发工业数字孪生项目的引擎,而不是一个只有零散功能的工具集合。 diff --git a/docs/designs/metacore-ui-workflow-design.md b/docs/designs/metacore-ui-workflow-design.md new file mode 100644 index 0000000..bc4e21c --- /dev/null +++ b/docs/designs/metacore-ui-workflow-design.md @@ -0,0 +1,453 @@ +# MetaCore UI 编辑与运行时 UI 工作流设计 + +生成时间:2026-03-28 +状态:草案 +范围:M4 UI、持久化与打包循环 + +## 目的 + +这份文档用于明确 MetaCore 第一阶段 UI 系统应该做到什么程度,以及 UI 编辑、UI 资源、运行时 UI 三者之间的关系。 + +目标不是一步到位做出 Unity UGUI 或 UI Toolkit 的完整等价物,而是先建立一套足够支撑工业数字孪生项目的基础 UI 工作流。 + +它要解决的问题包括: + +- 第一阶段 UI 系统到底是“调试叠层”还是正式项目 UI +- UI 应该如何被编辑 +- UI 资源如何保存 +- UI 如何进入运行时 +- UI 如何与场景对象和 RuntimeData 建立关系 + +## 结论先说 + +MetaCore 第一阶段的 UI 系统应明确定位为: + +**可用于搭建工业三维应用静态界面和基础交互界面的正式项目 UI 工作流** + +它不是: + +- 只给编辑器自己用的 ImGui 面板 +- 只给调试用的运行时 overlay +- 一套完整的大而全前端框架 + +第一阶段只要先把下面这条链做顺就够了: + +```text +创建 UI 资源 + -> 在编辑器中组织面板层级 + -> 配置文本/图片/按钮/布局 + -> 挂到项目或场景 + -> Player 运行时正确显示 + -> 可做基础状态展示与简单交互 +``` + +## 为什么 UI 是第一阶段基础能力 + +工业数字孪生项目的交付不只是 3D 场景。 + +真正交付时几乎一定会需要: + +- 顶部标题栏 +- 左右信息面板 +- 状态文本 +- 设备详情卡片 +- 告警提示 +- 操作按钮 +- 图标和图片 + +如果没有稳定 UI 工作流,项目最后一定会退化成: + +- 场景里有模型 +- 但没有正式业务界面 + +这就不算可交付引擎。 + +## 当前代码基础判断 + +当前仓库里已经有: + +- 基于 ImGui 的编辑器 UI +- Inspector / Hierarchy / Console 等编辑器面板 + +但这不等于已经有项目 UI 系统。 + +当前真正缺的是: + +- 正式的 UI 资源模型 +- 运行时 UI 组件体系 +- UI 编辑工作流 +- UI 与项目/场景的关系 +- UI 在 Player 中的加载与显示 + +因此第一阶段必须把“编辑器面板框架”和“项目运行时 UI 系统”明确分开。 + +## 第一阶段 UI 系统定位 + +第一阶段建议明确分成两套概念: + +### 1. 编辑器 UI + +这部分继续使用当前编辑器自己的 UI 技术栈,用于: + +- Hierarchy +- Project +- Inspector +- Console +- Runtime 面板 + +这不是项目交付用 UI。 + +### 2. 项目运行时 UI + +这是用户用 MetaCore 制作数字孪生项目时真正需要的 UI 系统,用于: + +- 顶部/侧边面板 +- 状态栏 +- 文本信息 +- 图片和图标 +- 按钮 +- 基础交互界面 + +后续讨论“UI 编辑功能”时,默认指的是这一套。 + +## 第一阶段设计目标 + +第一阶段 UI 工作流要满足: + +- 用户能创建正式 UI 资源 +- 用户能编辑 UI 层级 +- 用户能配置基础 UI 元素 +- 用户能把 UI 作为项目内容保存 +- Player 能加载并显示 UI +- UI 能承载数字孪生最基本的信息展示和操作入口 + +## 第一阶段范围 + +### 必须支持 + +- UI 文档或 UI 资产 +- UI 根节点 +- 面板 +- 文本 +- 图片 +- 按钮 +- 基础布局 +- 运行时加载与显示 + +### 推荐支持 + +- 可见性开关 +- 锚点与对齐 +- 简单层级重组 +- 基础样式属性 + +### 可以后置 + +- 复杂动画 +- 自适应布局高级特性 +- 富文本 +- 数据表格系统 +- UI 状态机 +- 可视化脚本逻辑 + +## 推荐 UI 资源模型 + +第一阶段建议正式引入: + +- `UiDocument` +- `UiNode` +- `UiStyle` + +### UiDocument + +负责保存: + +- UI 名称 +- 根节点列表 +- 默认分辨率或参考尺寸 +- 画布配置 + +### UiNode + +第一阶段建议统一节点模型,节点通过 `Type` 区分: + +- `Panel` +- `Text` +- `Image` +- `Button` + +每个节点至少应有: + +- `Id` +- `Name` +- `Type` +- `ParentId` +- `Children` +- `RectTransform` +- `Style` +- `Visible` + +### RectTransform + +第一阶段建议至少包含: + +- `AnchorMin` +- `AnchorMax` +- `Pivot` +- `Position` +- `Size` + +不要一开始就过度简化成只有绝对位置。 + +因为工业界面里: + +- 顶栏 +- 左面板 +- 右面板 +- 底栏 + +都强依赖基本锚点布局。 + +### Style + +第一阶段建议做最小样式集合,例如: + +- 背景颜色 +- 文本颜色 +- 字体大小 +- 图片资源引用 +- 边距 +- 对齐方式 + +## 第一阶段推荐节点类型 + +### Panel + +用于: + +- 容器 +- 背景区域 +- 信息卡片 +- 布局分组 + +### Text + +用于: + +- 标题 +- 状态文本 +- 标签 +- 数值显示 + +### Image + +用于: + +- 图标 +- Logo +- 状态图片 + +### Button + +用于: + +- 基础交互入口 +- 切换页面 +- 打开/关闭面板 +- 简单控制命令 + +第一阶段 Button 不需要做成复杂事件系统,但至少要有: + +- 点击事件入口 +- 可与后续逻辑系统连接的绑定点 + +## UI 编辑工作流建议 + +第一阶段推荐的 UI 编辑工作流不要太复杂,但必须正式存在。 + +### 建议的最小编辑器形态 + +- 左侧:UI 层级树 +- 中间:UI 预览区域 +- 右侧:当前节点 Inspector + +这和 Scene 编辑工作流保持一致,学习成本最低。 + +### 第一阶段必须支持的编辑动作 + +- 创建 Panel/Text/Image/Button +- 删除节点 +- 重命名节点 +- 拖拽重排父子级 +- 修改 RectTransform +- 修改基础样式 +- 选择资源 + +### 推荐支持 + +- 复制节点 +- 多选节点 +- 对齐辅助线 + +## UI 与项目/场景的关系 + +第一阶段建议明确: + +- UI 作为项目资源存在 +- Scene 可以引用一个或多个 UI 文档 +- Player 启动后加载 Scene 时,再加载对应 UI + +### 不建议的做法 + +不要把 UI 直接塞进 Scene 文档本体里做成内嵌大对象。 + +更合理的做法是: + +- Scene 引用 `UiDocumentAssetGuid` + +这样好处是: + +- UI 可以复用 +- UI 可单独编辑 +- UI 可单独保存和版本化 + +## UI 与 RuntimeData 的关系 + +这是数字孪生方向非常关键的一点。 + +第一阶段建议明确分层: + +### UI 资源层 + +定义界面长什么样。 + +### UI 运行时层 + +负责显示节点、接收输入、刷新显示内容。 + +### 数据绑定层 + +后续可让 RuntimeData 驱动: + +- Text 内容 +- Visible +- Image 状态 +- 颜色 + +但第一阶段不要反过来让 RuntimeData 定义 UI 数据结构。 + +也就是说: + +- 先把 UI 工作流做稳 +- 再把 RuntimeData 挂进 UI + +## 第一阶段运行时能力 + +Player 至少应能: + +- 加载一个或多个 UiDocument +- 构建 UI 节点树 +- 正确显示 Panel/Text/Image/Button +- 处理最基础点击输入 +- 与场景渲染共存 + +### 第一阶段不要求 + +- 完整输入系统生态 +- 页面导航框架 +- 表单系统 +- 动画系统 + +## Inspector 建议 + +### 对 UI 节点的 Inspector + +第一阶段至少应支持: + +- 名称 +- 类型 +- Visible +- RectTransform +- Style 基础字段 + +### 对 Text 节点 + +至少支持: + +- 文本内容 +- 字体大小 +- 颜色 +- 对齐 + +### 对 Image 节点 + +至少支持: + +- 图片资源 +- Tint +- PreserveAspect + +### 对 Button 节点 + +至少支持: + +- 文本标题或子节点文本 +- 可点击状态 +- 基础点击动作入口 + +## 推荐实现顺序 + +建议按下面顺序推进: + +1. 定义 `UiDocument / UiNode / UiStyle` +2. 定义最小 `RectTransform` +3. 打通 UI 资源保存与加载 +4. 建立 UI 层级树与 Inspector +5. 建立运行时基础显示 +6. 支持 Scene 引用 UI 文档 +7. 再补基础交互和数据驱动入口 + +## 第一阶段 Definition of Done + +下面这些成立时,才能认为第一阶段 UI 工作流真正可用: + +- 用户能创建 UI 文档 +- 用户能创建 Panel/Text/Image/Button +- 用户能编辑 UI 层级与基本属性 +- UI 文档可保存并重新打开 +- Scene 或项目能引用 UI 资源 +- Player 运行时能显示 UI +- UI 能承载数字孪生最基本的状态展示和操作入口 + +## 与整体引擎路线的关系 + +这份文档对应的是第一阶段主线中的: + +- 项目与内容工作流 +- 场景与对象工作流 +- UI 工作流 + +它的位置应当在: + +- 模型导入 +- 场景编辑 +- 材质与光照 + +之后,且明显先于: + +- 更复杂的 RuntimeData 扩展 +- VR 训练 UI 工作流 +- 浏览器嵌入产品化 + +## 最终建议 + +第一阶段最合理的 UI 路线不是: + +- 继续把项目 UI 混在编辑器 ImGui 里 +- 也不是一口气做成完整前端框架 + +而是: + +**先建立一套正式的项目运行时 UI 资源、编辑和显示闭环,让数字孪生项目至少具备“场景 + 界面 + 基础交互”三件套。** + +只有这样,MetaCore 第一阶段才像一个可用于开发工业三维应用的真正引擎,而不是只有 3D 场景能力的半成品。 diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 7301fe5..3b05168 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -396,6 +396,195 @@ void MetaCoreTestImportPipelineAndCook() { std::filesystem::remove_all(tempProjectRoot); } +void MetaCoreTestProjectDescriptorAndGltfImporterSkeleton() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreProjectGltfImporterProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "ConfigRuntime"); + std::filesystem::create_directories(tempProjectRoot / "UiDocuments"); + std::filesystem::create_directories(tempProjectRoot / "BuildOutput"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"ImporterProject\",\n" + << " \"version\": \"0.2.0\",\n" + << " \"runtime_directory\": \"ConfigRuntime\",\n" + << " \"ui_directory\": \"UiDocuments\",\n" + << " \"build_directory\": \"BuildOutput\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + { + std::ofstream gltfFile(tempProjectRoot / "Assets" / "Valve.gltf", std::ios::trunc); + gltfFile + << "{\n" + << " \"meshes\": [{\"name\": \"ValveMesh\"}],\n" + << " \"materials\": [{\"name\": \"ValveMaterial\"}],\n" + << " \"images\": [{\"uri\": \"Textures/ValveBaseColor.png\"}]\n" + << "}\n"; + } + + { + std::ofstream glbFile(tempProjectRoot / "Assets" / "Tank.glb", std::ios::binary | std::ios::trunc); + glbFile << "glTF_BINARY_PLACEHOLDER"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto assetDatabase = moduleRegistry.ResolveService(); + const auto packageService = moduleRegistry.ResolveService(); + const auto reflectionRegistry = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); + MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); + MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); + + const auto& project = assetDatabase->GetProjectDescriptor(); + MetaCoreExpect(project.RuntimePath.filename() == "ConfigRuntime", "项目应读取 runtime_directory"); + MetaCoreExpect(project.UiPath.filename() == "UiDocuments", "项目应读取 ui_directory"); + MetaCoreExpect(project.BuildPath.filename() == "BuildOutput", "项目应读取 build_directory"); + + const auto gltfRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Valve.gltf"); + MetaCoreExpect(gltfRecord.has_value(), "应能找到 glTF 资源记录"); + MetaCoreExpect(gltfRecord->ImporterId == "GltfModelImporter", "glTF 应使用 GltfModelImporter"); + MetaCoreExpect(gltfRecord->Type == "model", "glTF 资源类型应为 model"); + + const auto gltfPackage = packageService->ReadPackage(tempProjectRoot / gltfRecord->PackagePath); + MetaCoreExpect(gltfPackage.has_value(), "glTF 应生成包文件"); + MetaCore::MetaCoreImportedGltfAssetDocument gltfDocument; + MetaCoreExpect( + !gltfPackage->PayloadSections.empty() && + MetaCore::MetaCoreDeserializeFromBytes( + gltfPackage->PayloadSections.front(), + gltfDocument, + reflectionRegistry->GetTypeRegistry() + ), + "glTF 导入包应能反序列化为专用导入文档" + ); + MetaCoreExpect(gltfDocument.SourceFormat == "gltf", "glTF 导入文档应标记源格式"); + MetaCoreExpect(!gltfDocument.Nodes.empty() && gltfDocument.Nodes.front().Name == "Valve", "glTF 骨架文档应写入默认根节点名"); + MetaCoreExpect(!gltfDocument.Meshes.empty() && gltfDocument.Meshes.front().Name == "ValveMesh", "glTF 导入文档应提取 mesh 名称"); + MetaCoreExpect(gltfDocument.Meshes.front().PrimitiveCount == 1, "glTF mesh 骨架应写入 primitive 数"); + MetaCoreExpect(!gltfDocument.Materials.empty() && gltfDocument.Materials.front().Name == "ValveMaterial", "glTF 导入文档应提取材质名称"); + MetaCoreExpect(!gltfDocument.Textures.empty(), "glTF 导入文档应记录引用贴图"); + MetaCoreExpect(gltfDocument.Textures.front().SourcePath == std::filesystem::path("Textures") / "ValveBaseColor.png", "glTF 导入文档应保存贴图路径"); + MetaCoreExpect(gltfDocument.GeneratedMeshAssets.size() == gltfDocument.Meshes.size(), "glTF 导入文档应生成 mesh 资源骨架"); + MetaCoreExpect(gltfDocument.GeneratedMaterialAssets.size() == gltfDocument.Materials.size(), "glTF 导入文档应生成材质资源骨架"); + MetaCoreExpect(gltfDocument.GeneratedTextureAssets.size() == gltfDocument.Textures.size(), "glTF 导入文档应生成纹理资源骨架"); + MetaCoreExpect(gltfDocument.GeneratedMeshAssets.front().AssetGuid.IsValid(), "生成的 mesh 资源应有 GUID"); + MetaCoreExpect(gltfDocument.GeneratedMaterialAssets.front().AssetGuid.IsValid(), "生成的材质资源应有 GUID"); + MetaCoreExpect(gltfDocument.GeneratedTextureAssets.front().AssetGuid.IsValid(), "生成的纹理资源应有 GUID"); + MetaCoreExpect(gltfDocument.GeneratedMaterialAssets.front().BaseColorTexture.IsValid(), "材质资源应引用生成的贴图资源"); + + const auto glbRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Tank.glb"); + MetaCoreExpect(glbRecord.has_value(), "应能找到 glb 资源记录"); + MetaCoreExpect(glbRecord->ImporterId == "GltfModelImporter", "glb 应使用 GltfModelImporter"); + + const auto glbPackage = packageService->ReadPackage(tempProjectRoot / glbRecord->PackagePath); + MetaCoreExpect(glbPackage.has_value(), "glb 应生成包文件"); + MetaCore::MetaCoreImportedGltfAssetDocument glbDocument; + MetaCoreExpect( + !glbPackage->PayloadSections.empty() && + MetaCore::MetaCoreDeserializeFromBytes( + glbPackage->PayloadSections.front(), + glbDocument, + reflectionRegistry->GetTypeRegistry() + ), + "glb 导入包应能反序列化为专用导入文档" + ); + MetaCoreExpect(glbDocument.SourceFormat == "glb", "glb 导入文档应标记源格式"); + MetaCoreExpect(!glbDocument.Meshes.empty() && glbDocument.Meshes.front().Name == "Tank", "glb 骨架文档应生成默认 mesh 描述"); + MetaCoreExpect(!glbDocument.GeneratedMeshAssets.empty(), "glb 导入文档应生成默认 mesh 资源骨架"); + MetaCoreExpect(!glbDocument.GeneratedMaterialAssets.empty(), "glb 导入文档应生成默认材质资源骨架"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestInstantiateImportedModelAssetIntoScene() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreInstantiateModelProject"; + 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\": \"InstantiateProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + { + std::ofstream gltfFile(tempProjectRoot / "Assets" / "Pump.gltf", std::ios::trunc); + gltfFile + << "{\n" + << " \"meshes\": [{\"name\": \"PumpMesh\"}],\n" + << " \"materials\": [{\"name\": \"PumpMaterial\"}],\n" + << " \"images\": [{\"uri\": \"Textures/PumpBaseColor.png\"}]\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + const auto assetDatabase = moduleRegistry.ResolveService(); + const auto sceneEditingService = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); + MetaCoreExpect(sceneEditingService != nullptr, "应解析到 SceneEditingService"); + + const auto modelRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Pump.gltf"); + MetaCoreExpect(modelRecord.has_value(), "应能找到导入后的模型资源"); + + const auto instantiatedRootId = sceneEditingService->InstantiateModelAsset(editorContext, modelRecord->Guid, std::nullopt); + MetaCoreExpect(instantiatedRootId.has_value(), "应能将导入模型实例化到场景"); + + const MetaCore::MetaCoreGameObject* instantiatedRoot = scene.FindGameObject(*instantiatedRootId); + MetaCoreExpect(instantiatedRoot != nullptr, "实例化后应能找到根对象"); + MetaCoreExpect(instantiatedRoot->Name == "Pump", "实例化根对象名称应来自导入节点"); + MetaCoreExpect(instantiatedRoot->MeshRenderer.has_value(), "导入模型实例应带 MeshRenderer"); + MetaCoreExpect(instantiatedRoot->MeshRenderer->MeshSource == MetaCore::MetaCoreMeshSourceKind::Asset, "导入模型实例应使用 Asset MeshSource"); + MetaCoreExpect(instantiatedRoot->MeshRenderer->MeshAssetGuid.IsValid(), "导入模型实例应绑定 MeshAssetGuid"); + MetaCoreExpect(!instantiatedRoot->MeshRenderer->MaterialAssetGuids.empty(), "导入模型实例应绑定材质资源"); + MetaCoreExpect(editorContext.GetActiveObjectId() == *instantiatedRootId, "实例化后应选中新对象"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + void MetaCoreTestBootstrapStartupSceneCreation() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreBootstrapSceneProject"; @@ -499,10 +688,11 @@ void MetaCoreTestPrefabWorkflow() { MetaCore::MetaCoreGameObject& root = scene.CreateGameObject("PrefabRoot"); root.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; root.Transform.Position = glm::vec3(1.0F, 2.0F, 3.0F); - MetaCore::MetaCoreGameObject& child = scene.CreateGameObject("PrefabChild", root.Id); + const MetaCore::MetaCoreId rootId = root.Id; + MetaCore::MetaCoreGameObject& child = scene.CreateGameObject("PrefabChild", rootId); child.Light = MetaCore::MetaCoreLightComponent{}; - editorContext.SelectOnly(root.Id); + editorContext.SelectOnly(rootId); const auto prefabService = moduleRegistry.ResolveService(); const auto assetDatabaseService = moduleRegistry.ResolveService(); @@ -712,6 +902,33 @@ void MetaCoreTestRuntimeDataProjectDocumentSerialization() { MetaCoreExpect(roundTripDocument.Sources.front().Id == "mock-source", "RuntimeDataSourcesDocument source id 应保留"); } +void MetaCoreTestMeshRendererResourceSerialization() { + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterFoundationGeneratedTypes(registry); + MetaCoreRegisterSceneGeneratedTypes(registry); + + MetaCore::MetaCoreMeshRendererComponent component; + component.MeshSource = MetaCore::MetaCoreMeshSourceKind::Asset; + component.MeshAssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); + component.MaterialAssetGuids.push_back(MetaCore::MetaCoreAssetGuid::Generate()); + component.MaterialAssetGuids.push_back(MetaCore::MetaCoreAssetGuid::Generate()); + component.Visible = false; + + const auto serialized = MetaCore::MetaCoreSerializeToBytes(component, registry); + MetaCoreExpect(serialized.has_value(), "MeshRenderer 资源化结构应可序列化"); + + MetaCore::MetaCoreMeshRendererComponent roundTripComponent; + MetaCoreExpect( + MetaCore::MetaCoreDeserializeFromBytes(*serialized, roundTripComponent, registry), + "MeshRenderer 资源化结构应可反序列化" + ); + MetaCoreExpect(roundTripComponent.MeshSource == MetaCore::MetaCoreMeshSourceKind::Asset, "MeshSource 应保留"); + MetaCoreExpect(roundTripComponent.MeshAssetGuid == component.MeshAssetGuid, "MeshAssetGuid 应保留"); + MetaCoreExpect(roundTripComponent.MaterialAssetGuids.size() == 2, "材质槽引用数量应保留"); + MetaCoreExpect(roundTripComponent.MaterialAssetGuids.front() == component.MaterialAssetGuids.front(), "第一个材质引用应保留"); + MetaCoreExpect(!roundTripComponent.Visible, "Visible 应保留"); +} + void MetaCoreTestRuntimeDataBinaryDocumentIo() { MetaCore::MetaCoreTypeRegistry registry; MetaCoreRegisterRuntimeDataGeneratedTypes(registry); @@ -1401,6 +1618,81 @@ void MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave() { std::filesystem::remove_all(tempProjectRoot); } +void MetaCoreTestUiDocumentSerialization() { + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterFoundationGeneratedTypes(registry); + MetaCoreRegisterSceneGeneratedTypes(registry); + MetaCoreRegisterEditorGeneratedTypes(registry); + + MetaCore::MetaCoreUiDocument document; + document.Name = "MainHud"; + document.ReferenceWidth = 1920; + document.ReferenceHeight = 1080; + document.RootNodeIds = {"root.panel"}; + + MetaCore::MetaCoreUiNodeDocument rootNode; + rootNode.Id = "root.panel"; + rootNode.Name = "Root Panel"; + rootNode.Type = MetaCore::MetaCoreUiNodeType::Panel; + rootNode.Visible = true; + rootNode.Children = {"header.text", "status.button"}; + rootNode.RectTransform.AnchorMin = glm::vec3(0.0F, 0.0F, 0.0F); + rootNode.RectTransform.AnchorMax = glm::vec3(1.0F, 1.0F, 0.0F); + rootNode.RectTransform.Size = glm::vec3(0.0F, 0.0F, 0.0F); + rootNode.Style.BackgroundColor = glm::vec3(0.05F, 0.08F, 0.12F); + + MetaCore::MetaCoreUiNodeDocument textNode; + textNode.Id = "header.text"; + textNode.Name = "Header"; + textNode.Type = MetaCore::MetaCoreUiNodeType::Text; + textNode.ParentId = "root.panel"; + textNode.Text = "MetaCore"; + textNode.RectTransform.AnchorMin = glm::vec3(0.0F, 1.0F, 0.0F); + textNode.RectTransform.AnchorMax = glm::vec3(1.0F, 1.0F, 0.0F); + textNode.RectTransform.Position = glm::vec3(0.0F, -24.0F, 0.0F); + textNode.Style.FontSize = 28.0F; + textNode.Style.HorizontalAlignment = MetaCore::MetaCoreUiHorizontalAlignment::Center; + textNode.Style.TextColor = glm::vec3(0.95F, 0.95F, 0.95F); + + MetaCore::MetaCoreUiNodeDocument buttonNode; + buttonNode.Id = "status.button"; + buttonNode.Name = "Status Button"; + buttonNode.Type = MetaCore::MetaCoreUiNodeType::Button; + buttonNode.ParentId = "root.panel"; + buttonNode.Text = "Start"; + buttonNode.Interactable = true; + buttonNode.RectTransform.AnchorMin = glm::vec3(1.0F, 0.0F, 0.0F); + buttonNode.RectTransform.AnchorMax = glm::vec3(1.0F, 0.0F, 0.0F); + buttonNode.RectTransform.Pivot = glm::vec3(1.0F, 0.0F, 0.0F); + buttonNode.RectTransform.Position = glm::vec3(-32.0F, 32.0F, 0.0F); + buttonNode.RectTransform.Size = glm::vec3(180.0F, 48.0F, 0.0F); + buttonNode.Style.BackgroundColor = glm::vec3(0.12F, 0.42F, 0.82F); + + document.Nodes = {rootNode, textNode, buttonNode}; + + const auto bytes = MetaCore::MetaCoreSerializeToBytes(document, registry); + MetaCoreExpect(bytes.has_value(), "UiDocument 应能序列化"); + + MetaCore::MetaCoreUiDocument roundTrip; + MetaCoreExpect( + MetaCore::MetaCoreDeserializeFromBytes(*bytes, roundTrip, registry), + "UiDocument 应能反序列化" + ); + + MetaCoreExpect(roundTrip.Name == document.Name, "UiDocument 名称应保持"); + MetaCoreExpect(roundTrip.ReferenceWidth == 1920, "UiDocument 参考宽度应保持"); + MetaCoreExpect(roundTrip.ReferenceHeight == 1080, "UiDocument 参考高度应保持"); + MetaCoreExpect(roundTrip.RootNodeIds.size() == 1, "UiDocument 根节点数应保持"); + MetaCoreExpect(roundTrip.Nodes.size() == 3, "UiDocument 节点数量应保持"); + MetaCoreExpect(roundTrip.Nodes[0].Children.size() == 2, "UiNode 子节点列表应保持"); + MetaCoreExpect(roundTrip.Nodes[1].Type == MetaCore::MetaCoreUiNodeType::Text, "UiNode 类型应保持"); + MetaCoreExpect(roundTrip.Nodes[1].Text == "MetaCore", "Ui Text 内容应保持"); + MetaCoreExpect(roundTrip.Nodes[1].Style.FontSize == 28.0F, "Ui 字体大小应保持"); + MetaCoreExpect(roundTrip.Nodes[2].Interactable, "Ui Button 可交互状态应保持"); + MetaCoreExpect(roundTrip.Nodes[2].Style.HorizontalAlignment == MetaCore::MetaCoreUiHorizontalAlignment::Left, "默认水平对齐应保持"); + MetaCoreExpectVec3Near(roundTrip.Nodes[0].Style.BackgroundColor, glm::vec3(0.05F, 0.08F, 0.12F), "Ui 背景色应保持"); +} + } // namespace int main() { @@ -1436,11 +1728,14 @@ int main() { MetaCoreTestComponentRegistryDescriptors(); MetaCoreTestScenePersistenceRoundTrip(); MetaCoreTestImportPipelineAndCook(); + MetaCoreTestProjectDescriptorAndGltfImporterSkeleton(); + MetaCoreTestInstantiateImportedModelAssetIntoScene(); MetaCoreTestBootstrapStartupSceneCreation(); MetaCoreTestPrefabWorkflow(); MetaCoreTestComponentRegistryOperations(); MetaCoreTestRuntimeDataTypeSerialization(); MetaCoreTestRuntimeDataProjectDocumentSerialization(); + MetaCoreTestMeshRendererResourceSerialization(); MetaCoreTestRuntimeDataBinaryDocumentIo(); MetaCoreTestRuntimeProjectDocumentIo(); MetaCoreTestRuntimeDiagnosticsSnapshotIo(); @@ -1458,6 +1753,7 @@ int main() { MetaCoreTestRuntimeDataConfigValidationRejectsMissingTcpPort(); MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave(); MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream(); + MetaCoreTestUiDocumentSerialization(); std::cout << "MetaCoreSmokeTests passed\n"; return 0; diff --git a/third_party/simplepbr-shaders/LICENSE.txt b/third_party/simplepbr-shaders/LICENSE.txt new file mode 100644 index 0000000..46c371b --- /dev/null +++ b/third_party/simplepbr-shaders/LICENSE.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2019, Mitchell Stokes +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/simplepbr-shaders/shaders/post.vert b/third_party/simplepbr-shaders/shaders/post.vert new file mode 100644 index 0000000..6dc2087 --- /dev/null +++ b/third_party/simplepbr-shaders/shaders/post.vert @@ -0,0 +1,13 @@ +#version 120 + +uniform mat4 p3d_ModelViewProjectionMatrix; + +attribute vec4 p3d_Vertex; +attribute vec2 p3d_MultiTexCoord0; + +varying vec2 v_texcoord; + +void main() { + v_texcoord = p3d_MultiTexCoord0; + gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; +} diff --git a/third_party/simplepbr-shaders/shaders/shadow.frag b/third_party/simplepbr-shaders/shaders/shadow.frag new file mode 100644 index 0000000..09f0bc0 --- /dev/null +++ b/third_party/simplepbr-shaders/shaders/shadow.frag @@ -0,0 +1,28 @@ +#version 120 + +#ifdef USE_330 + #define texture2D texture +#endif + +uniform struct p3d_MaterialParameters { + vec4 baseColor; +} p3d_Material; + +uniform vec4 p3d_ColorScale; + +uniform sampler2D p3d_TextureBaseColor; +varying vec4 v_color; +varying vec2 v_texcoord; + +#ifdef USE_330 +out vec4 o_color; +#endif + +void main() { + vec4 base_color = p3d_Material.baseColor * v_color * p3d_ColorScale * texture2D(p3d_TextureBaseColor, v_texcoord); +#ifdef USE_330 + o_color = base_color; +#else + gl_FragColor = base_color; +#endif +} diff --git a/third_party/simplepbr-shaders/shaders/shadow.vert b/third_party/simplepbr-shaders/shaders/shadow.vert new file mode 100644 index 0000000..965d7be --- /dev/null +++ b/third_party/simplepbr-shaders/shaders/shadow.vert @@ -0,0 +1,35 @@ +#version 120 + +uniform mat4 p3d_ModelViewProjectionMatrix; +#ifdef ENABLE_SKINNING +uniform mat4 p3d_TransformTable[100]; +#endif + +attribute vec4 p3d_Vertex; +attribute vec4 p3d_Color; +attribute vec2 p3d_MultiTexCoord0; +#ifdef ENABLE_SKINNING +attribute vec4 transform_weight; +attribute vec4 transform_index; +#endif + + +varying vec4 v_color; +varying vec2 v_texcoord; + +void main() { +#ifdef ENABLE_SKINNING + mat4 skin_matrix = ( + p3d_TransformTable[int(transform_index.x)] * transform_weight.x + + p3d_TransformTable[int(transform_index.y)] * transform_weight.y + + p3d_TransformTable[int(transform_index.z)] * transform_weight.z + + p3d_TransformTable[int(transform_index.w)] * transform_weight.w + ); + vec4 vert_pos4 = skin_matrix * p3d_Vertex; +#else + vec4 vert_pos4 = p3d_Vertex; +#endif + v_color = p3d_Color; + v_texcoord = p3d_MultiTexCoord0; + gl_Position = p3d_ModelViewProjectionMatrix * vert_pos4; +} diff --git a/third_party/simplepbr-shaders/shaders/simplepbr.frag b/third_party/simplepbr-shaders/shaders/simplepbr.frag new file mode 100644 index 0000000..1a03394 --- /dev/null +++ b/third_party/simplepbr-shaders/shaders/simplepbr.frag @@ -0,0 +1,309 @@ +// Based on code from https://github.com/KhronosGroup/glTF-Sample-Viewer + +#version 120 + +#ifndef MAX_LIGHTS + #define MAX_LIGHTS 8 +#endif + +#ifndef USE_NORMAL_MAP + #define USE_NORMAL_MAP +#endif + +#ifndef USE_EMISSION_MAP + #define USE_EMISSION_MAP +#endif + +#ifndef USE_OCCLUSION_MAP + #define USE_OCCLUSION_MAP +#endif + +#ifdef USE_330 + #define texture2D texture + #define textureCube texture + #define textureCubeLod textureLod +#else + #extension GL_ARB_shader_texture_lod : require +#endif + +uniform struct p3d_MaterialParameters { + vec4 baseColor; + vec4 emission; + float roughness; + float metallic; +} p3d_Material; + +uniform struct p3d_LightSourceParameters { + vec4 position; + vec4 diffuse; + vec4 specular; + vec3 attenuation; + vec3 spotDirection; + float spotCosCutoff; +#ifdef ENABLE_SHADOWS + sampler2DShadow shadowMap; + mat4 shadowViewMatrix; +#endif +} p3d_LightSource[MAX_LIGHTS]; + +uniform struct p3d_LightModelParameters { + vec4 ambient; +} p3d_LightModel; + +#ifdef ENABLE_FOG +uniform struct p3d_FogParameters { + vec4 color; + float density; +} p3d_Fog; +#endif + +uniform vec4 p3d_ColorScale; +uniform vec4 p3d_TexAlphaOnly; + +uniform vec3 sh_coeffs[9]; +uniform vec3 camera_world_position; + +struct FunctionParamters { + float n_dot_l; + float n_dot_v; + float n_dot_h; + float l_dot_h; + float v_dot_h; + float roughness; + float metallic; + vec3 reflection0; + vec3 diffuse_color; + vec3 specular_color; +}; + +uniform sampler2D p3d_TextureBaseColor; +uniform sampler2D p3d_TextureMetalRoughness; +uniform sampler2D p3d_TextureNormal; +uniform sampler2D p3d_TextureEmission; +uniform sampler2D p3d_TextureOcclusion; + +uniform sampler2D brdf_lut; +uniform samplerCube filtered_env_map; +uniform float max_reflection_lod; +uniform float metacore_AlphaCutoff; +uniform float metacore_AlphaMode; + +#ifdef ENABLE_SHADOWS +uniform float global_shadow_bias; +#endif + +const vec3 F0 = vec3(0.04); +const float PI = 3.141592653589793; +const float SPOTSMOOTH = 0.001; +const float LIGHT_CUTOFF = 0.001; + +varying vec3 v_view_position; +varying vec3 v_world_position; +varying vec4 v_color; +varying vec2 v_texcoord; +varying mat3 v_view_tbn; +varying mat3 v_world_tbn; +#ifdef ENABLE_SHADOWS +varying vec4 v_shadow_pos[MAX_LIGHTS]; +#endif + +#ifdef USE_330 +out vec4 o_color; +#endif + + +// Schlick's Fresnel approximation with Spherical Gaussian approximation to replace the power +vec3 specular_reflection(FunctionParamters func_params) { + vec3 f0 = func_params.reflection0; + float v_dot_h= func_params.v_dot_h; + return f0 + (vec3(1.0) - f0) * pow(2.0, (-5.55473 * v_dot_h - 6.98316) * v_dot_h); +} + +vec3 fresnelSchlickRoughness(float u, vec3 f0, float roughness) { + return f0 + (max(vec3(1.0 - roughness), f0) - f0) * pow(clamp(1.0 - u, 0.0, 1.0), 5.0); +} + +// Smith GGX with optional fast sqrt approximation (see https://google.github.io/filament/Filament.md.html#materialsystem/specularbrdf/geometricshadowing(specularg)) +float visibility_occlusion(FunctionParamters func_params) { + float r = func_params.roughness; + float n_dot_l = func_params.n_dot_l; + float n_dot_v = func_params.n_dot_v; +#ifdef SMITH_SQRT_APPROX + float ggxv = n_dot_l * (n_dot_v * (1.0 - r) + r); + float ggxl = n_dot_v * (n_dot_l * (1.0 - r) + r); +#else + float r2 = r * r; + float ggxv = n_dot_l * sqrt(n_dot_v * n_dot_v * (1.0 - r2) + r2); + float ggxl = n_dot_v * sqrt(n_dot_l * n_dot_l * (1.0 - r2) + r2); +#endif + + float ggx = ggxv + ggxl; + if (ggx > 0.0) { + return 0.5 / ggx; + } + return 0.0; +} + +// GGX/Trowbridge-Reitz +float microfacet_distribution(FunctionParamters func_params) { + float roughness2 = func_params.roughness * func_params.roughness; + float f = (func_params.n_dot_h * func_params.n_dot_h) * (roughness2 - 1.0) + 1.0; + return roughness2 / (PI * f * f); +} + +// Lambert +float diffuse_function() { + return 1.0 / PI; +} + +#ifdef ENABLE_SHADOWS +float shadow_caster_contrib(sampler2DShadow shadowmap, vec4 shadowpos) { + vec3 light_space_coords = shadowpos.xyz / shadowpos.w; + light_space_coords.z -= global_shadow_bias; +#ifdef USE_330 + float shadow = texture(shadowmap, light_space_coords); +#else + float shadow = shadow2D(shadowmap, light_space_coords).r; +#endif + return shadow; +} +#endif + +vec3 get_normalmap_data() { +#ifdef CALC_NORMAL_Z + vec2 normalXY = 2.0 * texture2D(p3d_TextureNormal, v_texcoord).rg - 1.0; + float normalZ = sqrt(clamp(1.0 - dot(normalXY, normalXY), 0.0, 1.0)); + return vec3( + normalXY, + normalZ + ); +#else + return 2.0 * texture2D(p3d_TextureNormal, v_texcoord).rgb - 1.0; +#endif +} + +vec3 irradiance_from_sh(vec3 normal) { + return + + sh_coeffs[0] * 0.282095 + + sh_coeffs[1] * 0.488603 * normal.x + + sh_coeffs[2] * 0.488603 * normal.z + + sh_coeffs[3] * 0.488603 * normal.y + + sh_coeffs[4] * 1.092548 * normal.x * normal.z + + sh_coeffs[5] * 1.092548 * normal.y * normal.z + + sh_coeffs[6] * 1.092548 * normal.y * normal.x + + sh_coeffs[7] * (0.946176 * normal.z * normal.z - 0.315392) + + sh_coeffs[8] * 0.546274 * (normal.x * normal.x - normal.y * normal.y); +} + +void main() { + vec4 metal_rough = texture2D(p3d_TextureMetalRoughness, v_texcoord); + float metallic = clamp(p3d_Material.metallic * metal_rough.b, 0.0, 1.0); + float perceptual_roughness = clamp(p3d_Material.roughness * metal_rough.g, 0.0, 1.0); + float alpha_roughness = perceptual_roughness * perceptual_roughness; + vec4 base_color = p3d_Material.baseColor * v_color * p3d_ColorScale * (texture2D(p3d_TextureBaseColor, v_texcoord) + p3d_TexAlphaOnly); + vec3 diffuse_color = (base_color.rgb * (vec3(1.0) - F0)) * (1.0 - metallic); + vec3 spec_color = mix(F0, base_color.rgb, metallic); +#ifdef USE_NORMAL_MAP + vec3 normalmap = get_normalmap_data(); +#else + vec3 normalmap = vec3(0, 0, 1); +#endif + vec3 n = normalize(v_view_tbn * normalmap); + vec3 world_normal = normalize(v_world_tbn * normalmap); + vec3 v = normalize(-v_view_position); + +#ifdef USE_OCCLUSION_MAP + float ambient_occlusion = texture2D(p3d_TextureOcclusion, v_texcoord).r; +#else + float ambient_occlusion = 1.0; +#endif + +#ifdef USE_EMISSION_MAP + vec3 emission = p3d_Material.emission.rgb * texture2D(p3d_TextureEmission, v_texcoord).rgb; +#else + vec3 emission = vec3(0.0); +#endif + + vec4 color = vec4(vec3(0.0), base_color.a); + + if (metacore_AlphaMode > 0.5 && metacore_AlphaMode < 1.5 && base_color.a < metacore_AlphaCutoff) { + discard; + } + + float n_dot_v = clamp(abs(dot(n, v)), 0.0, 1.0); + + for (int i = 0; i < p3d_LightSource.length(); ++i) { + vec3 lightcol = p3d_LightSource[i].diffuse.rgb; + + if (dot(lightcol, lightcol) < LIGHT_CUTOFF) { + continue; + } + + vec3 light_pos = p3d_LightSource[i].position.xyz - v_view_position * p3d_LightSource[i].position.w; + vec3 l = normalize(light_pos); + vec3 h = normalize(l + v); + float dist = length(light_pos); + vec3 att_const = p3d_LightSource[i].attenuation; + float attenuation_factor = 1.0 / (att_const.x + att_const.y * dist + att_const.z * dist * dist); + float spotcos = dot(normalize(p3d_LightSource[i].spotDirection), -l); + float spotcutoff = p3d_LightSource[i].spotCosCutoff; + float shadowSpot = (spotcutoff > SPOTSMOOTH) ? smoothstep(spotcutoff-SPOTSMOOTH, spotcutoff+SPOTSMOOTH, spotcos) : 1.0; +#ifdef ENABLE_SHADOWS + float shadow_caster = shadow_caster_contrib(p3d_LightSource[i].shadowMap, v_shadow_pos[i]); +#else + float shadow_caster = 1.0; +#endif + float shadow = shadowSpot * shadow_caster * attenuation_factor; + + FunctionParamters func_params; + func_params.n_dot_l = clamp(dot(n, l), 0.0, 1.0); + func_params.n_dot_v = n_dot_v; + func_params.n_dot_h = clamp(dot(n, h), 0.0, 1.0); + func_params.l_dot_h = clamp(dot(l, h), 0.0, 1.0); + func_params.v_dot_h = clamp(dot(v, h), 0.0, 1.0); + func_params.roughness = alpha_roughness; + func_params.metallic = metallic; + func_params.reflection0 = spec_color; + func_params.diffuse_color = diffuse_color; + func_params.specular_color = spec_color; + + vec3 F = specular_reflection(func_params); + float V = visibility_occlusion(func_params); // V = G / (4 * n_dot_l * n_dot_v) + float D = microfacet_distribution(func_params); + + vec3 diffuse_contrib = diffuse_color * diffuse_function(); + vec3 spec_contrib = vec3(F * V * D); + color.rgb += func_params.n_dot_l * lightcol * (diffuse_contrib + spec_contrib) * shadow; + } + + // Indirect diffuse + specular (IBL) + vec3 ibl_f = fresnelSchlickRoughness(n_dot_v, spec_color, perceptual_roughness); + vec3 ibl_kd = (1.0 - ibl_f) * (1.0 - metallic); + vec3 ibl_diff = base_color.rgb * max(irradiance_from_sh(world_normal), 0.0) * diffuse_function(); + + vec3 world_view = normalize(camera_world_position - v_world_position); + vec3 ibl_r = reflect(-world_view, world_normal); + vec2 env_brdf = texture2D(brdf_lut, vec2(n_dot_v, perceptual_roughness)).rg; + vec3 ibl_spec_color = textureCubeLod(filtered_env_map, ibl_r, perceptual_roughness * max_reflection_lod).rgb; + vec3 ibl_spec = ibl_spec_color * (ibl_f * env_brdf.x + env_brdf.y); + color.rgb += (ibl_kd * ibl_diff + ibl_spec) * ambient_occlusion; + + // Indirect diffuse (ambient light) + color.rgb += (diffuse_color + spec_color) * p3d_LightModel.ambient.rgb * ambient_occlusion; + + // Emission + color.rgb += emission; + +#ifdef ENABLE_FOG + // Exponential fog + float fog_distance = length(v_view_position); + float fog_factor = clamp(1.0 / exp(fog_distance * p3d_Fog.density), 0.0, 1.0); + color = mix(p3d_Fog.color, color, fog_factor); +#endif + +#ifdef USE_330 + o_color = color; +#else + gl_FragColor = color; +#endif +} diff --git a/third_party/simplepbr-shaders/shaders/simplepbr.vert b/third_party/simplepbr-shaders/shaders/simplepbr.vert new file mode 100644 index 0000000..12f958f --- /dev/null +++ b/third_party/simplepbr-shaders/shaders/simplepbr.vert @@ -0,0 +1,101 @@ +#version 120 + +#ifndef MAX_LIGHTS + #define MAX_LIGHTS 8 +#endif + +#ifdef ENABLE_SHADOWS +uniform struct p3d_LightSourceParameters { + vec4 position; + vec4 diffuse; + vec4 specular; + vec3 attenuation; + vec3 spotDirection; + float spotCosCutoff; + sampler2DShadow shadowMap; + mat4 shadowViewMatrix; +} p3d_LightSource[MAX_LIGHTS]; +#endif + +#ifdef ENABLE_SKINNING +uniform mat4 p3d_TransformTable[100]; +#endif + +uniform mat4 p3d_ProjectionMatrix; +uniform mat4 p3d_ModelViewMatrix; +uniform mat4 p3d_ViewMatrix; +uniform mat4 p3d_ModelMatrix; +uniform mat3 p3d_NormalMatrix; +uniform mat4 p3d_TextureMatrix; +uniform mat4 p3d_ModelMatrixInverseTranspose; + +attribute vec4 p3d_Vertex; +attribute vec4 p3d_Color; +attribute vec3 p3d_Normal; +attribute vec4 p3d_Tangent; +attribute vec2 p3d_MultiTexCoord0; +#ifdef ENABLE_SKINNING +attribute vec4 transform_weight; +attribute vec4 transform_index; +#endif + + +varying vec3 v_view_position; +varying vec3 v_world_position; +varying vec4 v_color; +varying vec2 v_texcoord; +varying mat3 v_view_tbn; +varying mat3 v_world_tbn; +#ifdef ENABLE_SHADOWS +varying vec4 v_shadow_pos[MAX_LIGHTS]; +#endif + +void main() { +#ifdef ENABLE_SKINNING + mat4 skin_matrix = ( + p3d_TransformTable[int(transform_index.x)] * transform_weight.x + + p3d_TransformTable[int(transform_index.y)] * transform_weight.y + + p3d_TransformTable[int(transform_index.z)] * transform_weight.z + + p3d_TransformTable[int(transform_index.w)] * transform_weight.w + ); + vec4 model_position = skin_matrix * p3d_Vertex; + mat3 skin_matrix3 = mat3(skin_matrix); + vec3 model_normal = skin_matrix3 * p3d_Normal; + vec3 model_tangent = skin_matrix3 * p3d_Tangent.xyz; +#else + vec4 model_position = p3d_Vertex; + vec3 model_normal = p3d_Normal; + vec3 model_tangent = p3d_Tangent.xyz; +#endif + vec4 view_position = p3d_ModelViewMatrix * model_position; + v_view_position = (view_position).xyz; + v_world_position = (p3d_ModelMatrix * model_position).xyz; + v_color = p3d_Color; + v_texcoord = (p3d_TextureMatrix * vec4(p3d_MultiTexCoord0, 0, 1)).xy; +#ifdef ENABLE_SHADOWS + for (int i = 0; i < p3d_LightSource.length(); ++i) { + v_shadow_pos[i] = p3d_LightSource[i].shadowViewMatrix * view_position; + } +#endif + + vec3 view_normal = normalize(p3d_NormalMatrix * model_normal); + vec3 view_tangent = normalize(p3d_NormalMatrix * model_tangent); + vec3 view_bitangent = cross(view_normal, view_tangent) * p3d_Tangent.w; + v_view_tbn = mat3( + view_tangent, + view_bitangent, + view_normal + ); + + mat3 world_normal_mat = mat3(p3d_ModelMatrixInverseTranspose); + vec3 world_normal = normalize(world_normal_mat * model_normal); + vec3 world_tangent = normalize(world_normal_mat * model_tangent); + vec3 world_bitangent = cross(world_normal, world_tangent) * p3d_Tangent.w; + v_world_tbn = mat3( + world_tangent, + world_bitangent, + world_normal + ); + + gl_Position = p3d_ProjectionMatrix * view_position; +} diff --git a/third_party/simplepbr-shaders/shaders/skybox.frag b/third_party/simplepbr-shaders/shaders/skybox.frag new file mode 100644 index 0000000..6560d33 --- /dev/null +++ b/third_party/simplepbr-shaders/shaders/skybox.frag @@ -0,0 +1,20 @@ +#version 120 + +#ifdef USE_330 + #define textureCube texture + + out vec4 o_color; +#endif + +uniform samplerCube skybox; + +varying vec3 v_texcoord; + +void main() { + vec4 color = textureCube(skybox, v_texcoord); +#ifdef USE_330 + o_color = color; +#else + gl_FragColor = color; +#endif +} diff --git a/third_party/simplepbr-shaders/shaders/skybox.vert b/third_party/simplepbr-shaders/shaders/skybox.vert new file mode 100644 index 0000000..f1932ac --- /dev/null +++ b/third_party/simplepbr-shaders/shaders/skybox.vert @@ -0,0 +1,14 @@ +#version 120 + +uniform mat4 p3d_ProjectionMatrixInverse; +uniform mat4 p3d_ModelViewMatrix; + +attribute vec4 p3d_Vertex; + +varying vec3 v_texcoord; + +void main() { + mat3 inv_view = transpose(mat3(p3d_ModelViewMatrix)); + v_texcoord = inv_view * (p3d_ProjectionMatrixInverse * p3d_Vertex).xyz; + gl_Position = p3d_Vertex; +} diff --git a/third_party/simplepbr-shaders/shaders/tonemap.frag b/third_party/simplepbr-shaders/shaders/tonemap.frag new file mode 100644 index 0000000..f2ceb8c --- /dev/null +++ b/third_party/simplepbr-shaders/shaders/tonemap.frag @@ -0,0 +1,40 @@ +#version 120 + +#ifdef USE_330 + #define texture2D texture + #define texture3D texture +#endif + +uniform sampler2D tex; +#ifdef USE_SDR_LUT + uniform sampler3D sdr_lut; + uniform float sdr_lut_factor; +#endif +uniform float exposure; + +varying vec2 v_texcoord; + +#ifdef USE_330 +out vec4 o_color; +#endif + +void main() { + vec4 tex_color = texture2D(tex, v_texcoord); + vec3 color = tex_color.rgb; + + color *= exposure; + color = max(vec3(0.0), color - vec3(0.004)); + color = (color * (vec3(6.2) * color + vec3(0.5))) / (color * (vec3(6.2) * color + vec3(1.7)) + vec3(0.06)); + +#ifdef USE_SDR_LUT + vec3 lut_size = vec3(textureSize(sdr_lut, 0)); + vec3 lut_uvw = (color.rgb * float(lut_size - 1.0) + 0.5) / lut_size; + vec3 lut_color = texture3D(sdr_lut, lut_uvw).rgb; + color = mix(color, lut_color, sdr_lut_factor); +#endif +#ifdef USE_330 + o_color = vec4(color, tex_color.a); +#else + gl_FragColor = vec4(color, tex_color.a); +#endif +} diff --git a/third_party/simplepbr-shaders/textures/brdf_lut.txo b/third_party/simplepbr-shaders/textures/brdf_lut.txo new file mode 100644 index 0000000..e3fa9af Binary files /dev/null and b/third_party/simplepbr-shaders/textures/brdf_lut.txo differ