启动优化

This commit is contained in:
Rowland 2026-07-02 10:18:02 +08:00
parent 2012ab74d1
commit 758aa6ff83
47 changed files with 209718 additions and 119 deletions

View File

@ -4,5 +4,7 @@
"importer_id": "MaterialImporter",
"package_path": "Assets/Materials/Material 1.mcmaterial.json",
"source_hash": 4176055334418841915,
"source_path": "Assets/Materials/Material 1.mcmaterial.json"
"source_mtime_ns": 1781677768840654120,
"source_path": "Assets/Materials/Material 1.mcmaterial.json",
"source_size_bytes": 563
}

View File

@ -4,5 +4,7 @@
"importer_id": "MaterialImporter",
"package_path": "Assets/Materials/Material 2.mcmaterial.json",
"source_hash": 12206391660553642631,
"source_path": "Assets/Materials/Material 2.mcmaterial.json"
"source_mtime_ns": 1781573175016906643,
"source_path": "Assets/Materials/Material 2.mcmaterial.json",
"source_size_bytes": 574
}

View File

@ -4,5 +4,7 @@
"importer_id": "MaterialImporter",
"package_path": "Assets/Materials/Material 3.mcmaterial.json",
"source_hash": 7449433069631146521,
"source_path": "Assets/Materials/Material 3.mcmaterial.json"
"source_mtime_ns": 1781574603045608609,
"source_path": "Assets/Materials/Material 3.mcmaterial.json",
"source_size_bytes": 591
}

View File

@ -4,5 +4,7 @@
"importer_id": "MaterialImporter",
"package_path": "Assets/Materials/Material 4.mcmaterial.json",
"source_hash": 12594992031322235982,
"source_path": "Assets/Materials/Material 4.mcmaterial.json"
"source_mtime_ns": 1781574003827012347,
"source_path": "Assets/Materials/Material 4.mcmaterial.json",
"source_size_bytes": 591
}

View File

@ -4,5 +4,7 @@
"importer_id": "MaterialImporter",
"package_path": "Assets/Materials/Material 5.mcmaterial.json",
"source_hash": 10101379247114018828,
"source_path": "Assets/Materials/Material 5.mcmaterial.json"
"source_mtime_ns": 1781574163019728677,
"source_path": "Assets/Materials/Material 5.mcmaterial.json",
"source_size_bytes": 561
}

View File

@ -4,5 +4,7 @@
"importer_id": "MaterialImporter",
"package_path": "Assets/Materials/Material 6.mcmaterial.json",
"source_hash": 2546970018452484703,
"source_path": "Assets/Materials/Material 6.mcmaterial.json"
"source_mtime_ns": 1781575611817940101,
"source_path": "Assets/Materials/Material 6.mcmaterial.json",
"source_size_bytes": 576
}

View File

@ -4,5 +4,7 @@
"importer_id": "MaterialImporter",
"package_path": "Assets/Materials/Material 7.mcmaterial.json",
"source_hash": 12622570808854545607,
"source_path": "Assets/Materials/Material 7.mcmaterial.json"
"source_mtime_ns": 1781678340396816708,
"source_path": "Assets/Materials/Material 7.mcmaterial.json",
"source_size_bytes": 548
}

View File

@ -4,5 +4,7 @@
"importer_id": "MaterialImporter",
"package_path": "Assets/Materials/Material.mcmaterial.json",
"source_hash": 7326866736359587430,
"source_path": "Assets/Materials/Material.mcmaterial.json"
"source_mtime_ns": 1781572763657368309,
"source_path": "Assets/Materials/Material.mcmaterial.json",
"source_size_bytes": 559
}

View File

@ -4,5 +4,7 @@
"importer_id": "BinaryImporter",
"package_path": "Assets/Materials/default_pbr.material.json.mcasset",
"source_hash": 12650993429013095480,
"source_path": "Assets/Materials/default_pbr.material.json"
"source_mtime_ns": 1779698309758483890,
"source_path": "Assets/Materials/default_pbr.material.json",
"source_size_bytes": 136
}

View File

@ -2102,7 +2102,9 @@
"importer_id": "GltfModelImporter",
"package_path": "Assets/Models/111.glb.mcasset",
"source_hash": 17894180331051193223,
"source_mtime_ns": 1781575411600895336,
"source_path": "Assets/Models/111.glb",
"source_size_bytes": 157690480,
"sub_assets": [
{
"guid": "86c72638-58f3-95a7-c6a5-d6661e2df677",

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,126 @@
{
"asset_type": "model",
"dependencies": [
"e863fe4d-79a8-84ae-3102-b357ad7b4823",
"535c5229-72b7-7b5b-e026-720fbbe2b5ed",
"83b0a33f-5d17-50f3-3c6b-aabfd47f8b22",
"556e58ff-62bc-ee21-e111-7e2258efc264",
"9c361eb5-975b-12a5-1a35-14c9a253cf8b",
"733608c9-f0b8-e1f7-484f-8c72208d8900",
"691a7e27-0773-5c06-d0f5-e4d7ea1ae95f",
"c04721a7-3683-9df4-0f57-6e1eef3a14d1",
"cde17669-b567-ece4-5f1b-3c961cf5bad1",
"324d1a6e-cdfe-75d4-99e3-5447bc40ebc7",
"2b0729d1-4850-cb27-42dc-e914e021039a",
"6d2002b7-45aa-0005-5752-7fc224ed6c07",
"4881b914-e23c-4c2c-7900-7aeefdef6ff3"
],
"guid": "4c35d56e-2bba-4d35-8676-5030e502368a",
"importer_id": "GltfModelImporter",
"package_path": "Assets/Models/Angry.glb.mcasset",
"source_hash": 4454436885812718669,
"source_mtime_ns": 1782811201861152889,
"source_path": "Assets/Models/Angry.glb",
"source_size_bytes": 131839676,
"sub_assets": [
{
"guid": "e863fe4d-79a8-84ae-3102-b357ad7b4823",
"index": 0,
"name": "Mesh",
"stable_import_key": "mesh:Mesh:0",
"type": "mesh"
},
{
"dependencies": [
"691a7e27-0773-5c06-d0f5-e4d7ea1ae95f",
"c04721a7-3683-9df4-0f57-6e1eef3a14d1"
],
"guid": "535c5229-72b7-7b5b-e026-720fbbe2b5ed",
"index": 0,
"name": "Ch50_body1",
"stable_import_key": "material:Ch50_body1:0",
"type": "material"
},
{
"dependencies": [
"6d2002b7-45aa-0005-5752-7fc224ed6c07",
"4881b914-e23c-4c2c-7900-7aeefdef6ff3"
],
"guid": "83b0a33f-5d17-50f3-3c6b-aabfd47f8b22",
"index": 1,
"name": "Ch50_body",
"stable_import_key": "material:Ch50_body:1",
"type": "material"
},
{
"guid": "556e58ff-62bc-ee21-e111-7e2258efc264",
"index": 0,
"name": "Ch50_1002_Emissive",
"stable_import_key": "texture:Ch50_1002_Emissive:0",
"type": "texture"
},
{
"guid": "9c361eb5-975b-12a5-1a35-14c9a253cf8b",
"index": 1,
"name": "Ch50_1002_Specular",
"stable_import_key": "texture:Ch50_1002_Specular:1",
"type": "texture"
},
{
"guid": "733608c9-f0b8-e1f7-484f-8c72208d8900",
"index": 2,
"name": "Ch50_1002_Normal",
"stable_import_key": "texture:Ch50_1002_Normal:2",
"type": "texture"
},
{
"guid": "691a7e27-0773-5c06-d0f5-e4d7ea1ae95f",
"index": 3,
"name": "Ch50_1002_Diffuse",
"stable_import_key": "texture:Ch50_1002_Diffuse:3",
"type": "texture"
},
{
"guid": "c04721a7-3683-9df4-0f57-6e1eef3a14d1",
"index": 4,
"name": "Ch50_1002_Glossiness",
"stable_import_key": "texture:Ch50_1002_Glossiness:4",
"type": "texture"
},
{
"guid": "cde17669-b567-ece4-5f1b-3c961cf5bad1",
"index": 5,
"name": "Ch50_1001_Emissive",
"stable_import_key": "texture:Ch50_1001_Emissive:5",
"type": "texture"
},
{
"guid": "324d1a6e-cdfe-75d4-99e3-5447bc40ebc7",
"index": 6,
"name": "Ch50_1001_Specular",
"stable_import_key": "texture:Ch50_1001_Specular:6",
"type": "texture"
},
{
"guid": "2b0729d1-4850-cb27-42dc-e914e021039a",
"index": 7,
"name": "Ch50_1001_Normal",
"stable_import_key": "texture:Ch50_1001_Normal:7",
"type": "texture"
},
{
"guid": "6d2002b7-45aa-0005-5752-7fc224ed6c07",
"index": 8,
"name": "Ch50_1001_Diffuse",
"stable_import_key": "texture:Ch50_1001_Diffuse:8",
"type": "texture"
},
{
"guid": "4881b914-e23c-4c2c-7900-7aeefdef6ff3",
"index": 9,
"name": "Ch50_1001_Glossiness",
"stable_import_key": "texture:Ch50_1001_Glossiness:9",
"type": "texture"
}
]
}

View File

@ -4,7 +4,9 @@
"importer_id": "GltfModelImporter",
"package_path": "Assets/Models/Cube.glb.mcasset",
"source_hash": 2668036311376285448,
"source_mtime_ns": 1779698309758483890,
"source_path": "Assets/Models/Cube.glb",
"source_size_bytes": 1936,
"sub_assets": [
{
"guid": "9cd50d41-df30-26ff-da7e-e01bb57f7135",

Binary file not shown.

View File

@ -1,24 +1,22 @@
{
"asset_type": "model",
"dependencies": [
"a718a4d3-f5ce-7f3a-6fe6-877661f2ea55"
],
"guid": "b9ff132c-4c3e-4d51-bf21-eb810482ec51",
"importer_id": "GltfModelImporter",
"package_path": "Assets/Models/Plane.glb.mcasset",
"source_hash": 7189019093832532692,
"source_mtime_ns": 1779698309758483890,
"source_path": "Assets/Models/Plane.glb",
"source_size_bytes": 1036,
"sub_assets": [
{
"guid": "804bca6d-b9ea-f7af-3f0a-f078d5de0d45",
"guid": "a718a4d3-f5ce-7f3a-6fe6-877661f2ea55",
"index": 0,
"name": "Plane",
"stable_import_key": "",
"name": "平面",
"stable_import_key": "mesh:平面:0",
"type": "mesh"
},
{
"guid": "6c337003-dc9a-da7c-d685-6d27c673046a",
"index": 0,
"name": "Plane_Material",
"stable_import_key": "",
"type": "material"
}
]
}

Binary file not shown.

View File

@ -1,23 +1,85 @@
{
"asset_type": "model",
"dependencies": [
"7998c3fb-b5f3-3fcf-444b-c84c7b86f10b",
"6ce77fdf-b580-a89c-d644-7140b259e38e",
"141a8537-64cc-e883-7080-d901ed1bc65f",
"cfd33be3-b5ab-f0a3-5d64-c36d7d08d845",
"940496ed-b586-e6b5-9071-8dc53e5cfadd",
"cc9b68f1-b5a0-49bd-1e73-ddecb981f5ff",
"7caf07f8-b5ca-f7c7-c236-253f38e10537",
"8005dae9-b550-9eae-bfb5-f6d1bf1da33a",
"c1db65ae-1bb6-5c96-ceed-2062cf899aeb"
],
"guid": "7033a4a0-8f64-4826-8fec-22ce0fccb514",
"importer_id": "GltfModelImporter",
"package_path": "Assets/Models/box1.glb.mcasset",
"source_hash": 17764949482612063346,
"source_mtime_ns": 1779698309758483890,
"source_path": "Assets/Models/box1.glb",
"source_size_bytes": 11784,
"sub_assets": [
{
"guid": "34a6ed73-d4a5-83a3-5863-742c204c4869",
"guid": "7998c3fb-b5f3-3fcf-444b-c84c7b86f10b",
"index": 0,
"name": "box1",
"stable_import_key": "",
"name": "立方体.007",
"stable_import_key": "mesh:立方体.007:0",
"type": "mesh"
},
{
"guid": "9833a707-d635-426e-5db2-4cb58fbb5d03",
"guid": "6ce77fdf-b580-a89c-d644-7140b259e38e",
"index": 1,
"name": "立方体.001",
"stable_import_key": "mesh:立方体.001:1",
"type": "mesh"
},
{
"guid": "141a8537-64cc-e883-7080-d901ed1bc65f",
"index": 2,
"name": "立方体",
"stable_import_key": "mesh:立方体:2",
"type": "mesh"
},
{
"guid": "cfd33be3-b5ab-f0a3-5d64-c36d7d08d845",
"index": 3,
"name": "立方体.002",
"stable_import_key": "mesh:立方体.002:3",
"type": "mesh"
},
{
"guid": "940496ed-b586-e6b5-9071-8dc53e5cfadd",
"index": 4,
"name": "立方体.004",
"stable_import_key": "mesh:立方体.004:4",
"type": "mesh"
},
{
"guid": "cc9b68f1-b5a0-49bd-1e73-ddecb981f5ff",
"index": 5,
"name": "立方体.005",
"stable_import_key": "mesh:立方体.005:5",
"type": "mesh"
},
{
"guid": "7caf07f8-b5ca-f7c7-c236-253f38e10537",
"index": 6,
"name": "立方体.006",
"stable_import_key": "mesh:立方体.006:6",
"type": "mesh"
},
{
"guid": "8005dae9-b550-9eae-bfb5-f6d1bf1da33a",
"index": 7,
"name": "立方体.003",
"stable_import_key": "mesh:立方体.003:7",
"type": "mesh"
},
{
"guid": "c1db65ae-1bb6-5c96-ceed-2062cf899aeb",
"index": 0,
"name": "box1_Material",
"stable_import_key": "",
"name": "材质",
"stable_import_key": "material:材质:0",
"type": "material"
}
]

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,65 @@
{
"asset_type": "model",
"dependencies": [
"691e9f45-775d-4beb-d02e-fbc5e69de025",
"3717f487-e79d-9e16-5bb5-c3a15931ce78",
"d7627862-ce92-d86f-83ff-931f60f1c4bc",
"64d966a0-53c1-b645-5c07-62e9cc51a905",
"6b13ab8c-2227-397f-127d-8f99ecba20d0",
"e1aa9ec5-14d2-8cff-369b-d9250c97951b"
],
"guid": "eb4eb535-69de-4292-bf7a-b16ca66e39f1",
"importer_id": "GltfModelImporter",
"package_path": "Assets/Models/defeat.glb.mcasset",
"source_hash": 11308311888863755122,
"source_mtime_ns": 1782811597595352702,
"source_path": "Assets/Models/defeat.glb",
"source_size_bytes": 4669588,
"sub_assets": [
{
"guid": "691e9f45-775d-4beb-d02e-fbc5e69de025",
"index": 0,
"name": "ely_vanguardsoldier_kerwinatienza_Mesh",
"stable_import_key": "mesh:ely_vanguardsoldier_kerwinatienza_Mesh:0",
"type": "mesh"
},
{
"dependencies": [
"e1aa9ec5-14d2-8cff-369b-d9250c97951b"
],
"guid": "3717f487-e79d-9e16-5bb5-c3a15931ce78",
"index": 0,
"name": "ely_vanguardsoldier_kerwinatienza_M2",
"stable_import_key": "material:ely_vanguardsoldier_kerwinatienza_M2:0",
"type": "material"
},
{
"guid": "d7627862-ce92-d86f-83ff-931f60f1c4bc",
"index": 0,
"name": "ely-vanguardsoldier-kerwinatienza_glow",
"stable_import_key": "texture:ely-vanguardsoldier-kerwinatienza_glow:0",
"type": "texture"
},
{
"guid": "64d966a0-53c1-b645-5c07-62e9cc51a905",
"index": 1,
"name": "ely-vanguardsoldier-kerwinatienza_specular",
"stable_import_key": "texture:ely-vanguardsoldier-kerwinatienza_specular:1",
"type": "texture"
},
{
"guid": "6b13ab8c-2227-397f-127d-8f99ecba20d0",
"index": 2,
"name": "ely-vanguardsoldier-kerwinatienza_normal",
"stable_import_key": "texture:ely-vanguardsoldier-kerwinatienza_normal:2",
"type": "texture"
},
{
"guid": "e1aa9ec5-14d2-8cff-369b-d9250c97951b",
"index": 3,
"name": "ely-vanguardsoldier-kerwinatienza_diffuse",
"stable_import_key": "texture:ely-vanguardsoldier-kerwinatienza_diffuse:3",
"type": "texture"
}
]
}

Binary file not shown.

View File

@ -21,11 +21,13 @@
"6b556bd8-16f3-f182-92ab-4fc0c83ecb1c",
"6b1cedd6-4af2-ba3f-d22c-da6ee0281680"
],
"guid": "f808e65c-6950-4639-9e1a-34487f9dfdd1",
"guid": "35e7fee9-a67f-4a7f-ad9f-6479117b08b5",
"importer_id": "GltfModelImporter",
"package_path": "Assets/Models/jxb.glb.mcasset",
"source_hash": 3452613311117949482,
"source_mtime_ns": 1782811325693939312,
"source_path": "Assets/Models/jxb.glb",
"source_size_bytes": 10369368,
"sub_assets": [
{
"guid": "355dbe1a-2ddd-f8d3-59ee-c527d9d0cf27",

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -48,7 +48,9 @@
"importer_id": "GltfModelImporter",
"package_path": "Assets/Models/超精密磨床.glb.mcasset",
"source_hash": 4002013065180687563,
"source_mtime_ns": 1781595964581304216,
"source_path": "Assets/Models/超精密磨床.glb",
"source_size_bytes": 25047416,
"sub_assets": [
{
"guid": "f18830da-5294-9e8e-aaa5-d3537fd1d002",

View File

@ -4,5 +4,7 @@
"importer_id": "UiStylesheetImporter",
"package_path": "Assets/UI/main_menu.rcss.mcasset",
"source_hash": 8787559501102875347,
"source_path": "Assets/UI/main_menu.rcss"
"source_mtime_ns": 1779698310955569237,
"source_path": "Assets/UI/main_menu.rcss",
"source_size_bytes": 162
}

View File

@ -4,5 +4,7 @@
"importer_id": "UiDocumentImporter",
"package_path": "Assets/UI/main_menu.rml.mcasset",
"source_hash": 11300316873669048992,
"source_path": "Assets/UI/main_menu.rml"
"source_mtime_ns": 1779698310955569237,
"source_path": "Assets/UI/main_menu.rml",
"source_size_bytes": 200
}

View File

@ -160,6 +160,52 @@ void MetaCoreReadGuidArray(const nlohmann::json& json, const char* key, std::vec
}
}
struct MetaCoreSourceFileStamp {
std::uint64_t SizeBytes = 0;
std::int64_t LastWriteTime = 0;
};
[[nodiscard]] std::optional<MetaCoreSourceFileStamp> MetaCoreGetSourceFileStamp(
const std::filesystem::path& absolutePath
) {
std::error_code errorCode;
const auto size = std::filesystem::file_size(absolutePath, errorCode);
if (errorCode) {
return std::nullopt;
}
const auto writeTime = std::filesystem::last_write_time(absolutePath, errorCode);
if (errorCode) {
return std::nullopt;
}
return MetaCoreSourceFileStamp{
static_cast<std::uint64_t>(size),
std::chrono::duration_cast<std::chrono::nanoseconds>(writeTime.time_since_epoch()).count()
};
}
[[nodiscard]] std::int64_t MetaCoreGetFileLastWriteTimeValue(const std::filesystem::path& absolutePath) {
std::error_code errorCode;
const auto writeTime = std::filesystem::last_write_time(absolutePath, errorCode);
if (errorCode) {
return 0;
}
return std::chrono::duration_cast<std::chrono::nanoseconds>(writeTime.time_since_epoch()).count();
}
[[nodiscard]] bool MetaCoreMetadataHasSourceStamp(const MetaCoreAssetMetadataDocument& metadata) {
return metadata.SourceSizeBytes != 0 || metadata.SourceLastWriteTime != 0;
}
[[nodiscard]] bool MetaCoreMetadataMatchesSourceStamp(
const MetaCoreAssetMetadataDocument& metadata,
const MetaCoreSourceFileStamp& stamp
) {
return metadata.SourceSizeBytes == stamp.SizeBytes &&
metadata.SourceLastWriteTime == stamp.LastWriteTime;
}
bool MetaCoreWriteMetaJson(
const std::filesystem::path& absoluteMetaPath,
const MetaCoreAssetMetadataDocument& metadata
@ -172,6 +218,8 @@ bool MetaCoreWriteMetaJson(
json["source_path"] = metadata.SourcePath.generic_string();
json["package_path"] = metadata.PackagePath.generic_string();
json["source_hash"] = metadata.SourceHash;
json["source_size_bytes"] = metadata.SourceSizeBytes;
json["source_mtime_ns"] = metadata.SourceLastWriteTime;
// 序列化子资产列表
if (!metadata.SubAssets.empty()) {
@ -233,6 +281,12 @@ bool MetaCoreReadMetaJson(
if (json.contains("source_hash") && json["source_hash"].is_number_integer()) {
outMetadata.SourceHash = json["source_hash"].get<std::uint64_t>();
}
if (json.contains("source_size_bytes") && json["source_size_bytes"].is_number_unsigned()) {
outMetadata.SourceSizeBytes = json["source_size_bytes"].get<std::uint64_t>();
}
if (json.contains("source_mtime_ns") && json["source_mtime_ns"].is_number_integer()) {
outMetadata.SourceLastWriteTime = json["source_mtime_ns"].get<std::int64_t>();
}
// 解析子资产列表
outMetadata.SubAssets.clear();
@ -2530,6 +2584,225 @@ void MetaCoreDrawRotatorComponentInspector(MetaCoreEditorContext& editorContext,
}
}
[[nodiscard]] std::optional<MetaCoreModelAssetDocument> MetaCoreLoadAnimatorSourceModelDocument(
MetaCoreEditorContext& editorContext,
const MetaCoreAnimatorComponent& animator
) {
const auto assetEditingService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetEditingService>();
if (assetEditingService == nullptr) {
return std::nullopt;
}
if (animator.SourceModelAssetGuid.IsValid()) {
if (auto document = assetEditingService->LoadModelAsset(animator.SourceModelAssetGuid); document.has_value()) {
return document;
}
}
if (!animator.SourceModelPath.empty()) {
const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
if (assetDatabaseService != nullptr) {
if (const auto record = assetDatabaseService->FindAssetByRelativePath(std::filesystem::path(animator.SourceModelPath));
record.has_value() && record->Type == "model") {
return assetEditingService->LoadModelAsset(record->Guid);
}
}
}
return std::nullopt;
}
[[nodiscard]] float MetaCoreGetAnimatorClipDuration(
const std::optional<MetaCoreModelAssetDocument>& modelDocument,
std::int32_t clipIndex
) {
if (!modelDocument.has_value() ||
clipIndex < 0 ||
static_cast<std::size_t>(clipIndex) >= modelDocument->Animations.size()) {
return 0.0F;
}
return std::max(0.0F, modelDocument->Animations[static_cast<std::size_t>(clipIndex)].DurationSeconds);
}
void MetaCoreDrawAnimatorComponentInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) {
if (!gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
return;
}
const bool multiEdit = editorContext.GetSelectedObjectIds().size() > 1;
auto& animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
const auto modelDocument = MetaCoreLoadAnimatorSourceModelDocument(editorContext, animator);
std::string sourceLabel = animator.SourceModelPath.empty() ? "<none>" : animator.SourceModelPath;
if (animator.SourceModelAssetGuid.IsValid()) {
sourceLabel += " [" + animator.SourceModelAssetGuid.ToString() + "]";
}
ImGui::TextDisabled("Source: %s", sourceLabel.c_str());
if (modelDocument.has_value()) {
ImGui::TextDisabled("Clips: %d", static_cast<int>(modelDocument->Animations.size()));
} else {
ImGui::TextColored(ImVec4(0.95F, 0.55F, 0.20F, 1.0F), "模型动画元数据不可用");
}
auto drawPersistedBool = [&](const char* label, bool MetaCoreAnimatorComponent::* member) {
const auto sharedValue = MetaCoreGetSharedSelectedValue<bool>(
editorContext,
[member](MetaCoreGameObject& selectedObject) -> std::optional<bool> {
return selectedObject.HasComponent<MetaCoreAnimatorComponent>()
? std::optional<bool>(selectedObject.GetComponent<MetaCoreAnimatorComponent>().*member)
: std::nullopt;
},
[](bool lhs, bool rhs) { return lhs == rhs; }
);
bool value = sharedValue.value_or(animator.*member);
if (multiEdit && !sharedValue.has_value()) {
ImGui::TextDisabled("%s: 混合值", label);
}
if (ImGui::Checkbox(label, &value)) {
(void)editorContext.ExecuteSnapshotCommand("修改 Animator", [&]() {
MetaCoreApplyValueToSelectedObjects(
editorContext,
[member](MetaCoreGameObject& selectedObject) -> bool* {
return selectedObject.HasComponent<MetaCoreAnimatorComponent>()
? &(selectedObject.GetComponent<MetaCoreAnimatorComponent>().*member)
: nullptr;
},
value
);
editorContext.GetScene().IncrementRevision();
return true;
});
}
};
drawPersistedBool("启用##Animator", &MetaCoreAnimatorComponent::Enabled);
drawPersistedBool("Play On Start", &MetaCoreAnimatorComponent::PlayOnStart);
drawPersistedBool("Loop", &MetaCoreAnimatorComponent::Loop);
if (modelDocument.has_value() && !modelDocument->Animations.empty()) {
std::int32_t clipIndex = std::clamp<std::int32_t>(
animator.ClipIndex,
0,
static_cast<std::int32_t>(modelDocument->Animations.size() - 1)
);
const std::string currentClipLabel =
modelDocument->Animations[static_cast<std::size_t>(clipIndex)].Name.empty()
? ("Clip " + std::to_string(clipIndex))
: modelDocument->Animations[static_cast<std::size_t>(clipIndex)].Name;
ImGui::SetNextItemWidth(-1);
if (ImGui::BeginCombo("Clip", currentClipLabel.c_str())) {
for (std::size_t index = 0; index < modelDocument->Animations.size(); ++index) {
const auto& clip = modelDocument->Animations[index];
const std::string label = clip.Name.empty()
? ("Clip " + std::to_string(index))
: clip.Name;
const bool selected = static_cast<std::size_t>(clipIndex) == index;
if (ImGui::Selectable(label.c_str(), selected)) {
const auto newClipIndex = static_cast<std::int32_t>(index);
const std::string newClipName = clip.Name;
(void)editorContext.ExecuteSnapshotCommand("修改 Animator Clip", [&]() {
MetaCoreForEachSelectedGameObject(editorContext, [&](MetaCoreGameObject& selectedObject) {
if (!selectedObject.HasComponent<MetaCoreAnimatorComponent>()) {
return;
}
auto& selectedAnimator = selectedObject.GetComponent<MetaCoreAnimatorComponent>();
selectedAnimator.ClipIndex = newClipIndex;
selectedAnimator.ClipName = newClipName;
selectedAnimator.RuntimeTimeSeconds = 0.0F;
});
editorContext.GetScene().IncrementRevision();
return true;
});
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
} else {
ImGui::TextDisabled("没有可播放动画 Clip");
}
float speed = MetaCoreGetSharedSelectedValue<float>(
editorContext,
[](MetaCoreGameObject& selectedObject) -> std::optional<float> {
return selectedObject.HasComponent<MetaCoreAnimatorComponent>()
? std::optional<float>(selectedObject.GetComponent<MetaCoreAnimatorComponent>().Speed)
: std::nullopt;
},
[](float lhs, float rhs) { return MetaCoreNearlyEqual(lhs, rhs); }
).value_or(animator.Speed);
ImGui::DragFloat("Speed", &speed, 0.05F, 0.0F, 8.0F);
MetaCoreTrackComponentInspectorEdit(editorContext, "修改 Animator", true);
if (ImGui::IsItemEdited()) {
MetaCoreApplyValueToSelectedObjects(
editorContext,
[](MetaCoreGameObject& selectedObject) -> float* {
return selectedObject.HasComponent<MetaCoreAnimatorComponent>()
? &selectedObject.GetComponent<MetaCoreAnimatorComponent>().Speed
: nullptr;
},
std::max(0.0F, speed)
);
editorContext.GetScene().IncrementRevision();
}
const float durationSeconds = MetaCoreGetAnimatorClipDuration(modelDocument, animator.ClipIndex);
float timeSeconds = std::max(0.0F, animator.RuntimeTimeSeconds);
const float sliderMax = durationSeconds > 0.0F ? durationSeconds : std::max(1.0F, timeSeconds);
if (ImGui::Button(animator.RuntimePlaying ? "Pause" : "Play")) {
const bool play = !animator.RuntimePlaying;
MetaCoreForEachSelectedGameObject(editorContext, [&](MetaCoreGameObject& selectedObject) {
if (!selectedObject.HasComponent<MetaCoreAnimatorComponent>()) {
return;
}
auto& selectedAnimator = selectedObject.GetComponent<MetaCoreAnimatorComponent>();
selectedAnimator.RuntimePlaying = play;
if (play && durationSeconds > 0.0F && selectedAnimator.RuntimeTimeSeconds >= durationSeconds && !selectedAnimator.Loop) {
selectedAnimator.RuntimeTimeSeconds = 0.0F;
}
});
editorContext.GetScene().IncrementRevision();
}
ImGui::SameLine();
if (ImGui::Button("Stop")) {
MetaCoreForEachSelectedGameObject(editorContext, [&](MetaCoreGameObject& selectedObject) {
if (!selectedObject.HasComponent<MetaCoreAnimatorComponent>()) {
return;
}
auto& selectedAnimator = selectedObject.GetComponent<MetaCoreAnimatorComponent>();
selectedAnimator.RuntimePlaying = false;
selectedAnimator.RuntimeTimeSeconds = 0.0F;
});
editorContext.GetScene().IncrementRevision();
timeSeconds = 0.0F;
}
ImGui::SliderFloat("Time", &timeSeconds, 0.0F, sliderMax, "%.3f s");
if (ImGui::IsItemEdited()) {
MetaCoreForEachSelectedGameObject(editorContext, [&](MetaCoreGameObject& selectedObject) {
if (!selectedObject.HasComponent<MetaCoreAnimatorComponent>()) {
return;
}
auto& selectedAnimator = selectedObject.GetComponent<MetaCoreAnimatorComponent>();
const float selectedDuration = MetaCoreGetAnimatorClipDuration(
MetaCoreLoadAnimatorSourceModelDocument(editorContext, selectedAnimator),
selectedAnimator.ClipIndex
);
selectedAnimator.RuntimeTimeSeconds = selectedDuration > 0.0F
? std::clamp(timeSeconds, 0.0F, selectedDuration)
: std::max(0.0F, timeSeconds);
});
editorContext.GetScene().IncrementRevision();
}
if (durationSeconds > 0.0F) {
ImGui::TextDisabled("Duration: %.3f s", durationSeconds);
}
}
void MetaCoreDrawScriptComponentInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) {
if (!gameObject.HasComponent<MetaCoreScriptComponent>()) {
return;
@ -3920,6 +4193,36 @@ template <typename T>
prefabObjectIterator->Light->Enabled = selectedObject.GetComponent<MetaCoreLightComponent>().Enabled;
changed = true;
}
} else if (componentTypeId == "Animator" && selectedObject.HasComponent<MetaCoreAnimatorComponent>()) {
if (!prefabObjectIterator->Animator.has_value()) {
prefabObjectIterator->Animator = MetaCoreAnimatorComponent{};
}
const auto& animator = selectedObject.GetComponent<MetaCoreAnimatorComponent>();
if (fieldId == "enabled") {
prefabObjectIterator->Animator->Enabled = animator.Enabled;
changed = true;
} else if (fieldId == "source_model_asset_guid") {
prefabObjectIterator->Animator->SourceModelAssetGuid = animator.SourceModelAssetGuid;
changed = true;
} else if (fieldId == "source_model_path") {
prefabObjectIterator->Animator->SourceModelPath = animator.SourceModelPath;
changed = true;
} else if (fieldId == "clip_index") {
prefabObjectIterator->Animator->ClipIndex = animator.ClipIndex;
changed = true;
} else if (fieldId == "clip_name") {
prefabObjectIterator->Animator->ClipName = animator.ClipName;
changed = true;
} else if (fieldId == "play_on_start") {
prefabObjectIterator->Animator->PlayOnStart = animator.PlayOnStart;
changed = true;
} else if (fieldId == "loop") {
prefabObjectIterator->Animator->Loop = animator.Loop;
changed = true;
} else if (fieldId == "speed") {
prefabObjectIterator->Animator->Speed = animator.Speed;
changed = true;
}
} else if (componentTypeId == "MeshRenderer" && selectedObject.HasComponent<MetaCoreMeshRendererComponent>()) {
if (!prefabObjectIterator->MeshRenderer.has_value()) {
prefabObjectIterator->MeshRenderer = MetaCoreMeshRendererComponent{};
@ -4036,6 +4339,45 @@ template <typename T>
}
}
if (componentTypeId == "Animator" && prefabObject->Animator.has_value()) {
if (!selectedObject.HasComponent<MetaCoreAnimatorComponent>()) {
selectedObject.AddComponent<MetaCoreAnimatorComponent>(MetaCoreAnimatorComponent{});
}
auto& animator = selectedObject.GetComponent<MetaCoreAnimatorComponent>();
if (fieldId == "enabled") {
animator.Enabled = prefabObject->Animator->Enabled;
return true;
}
if (fieldId == "source_model_asset_guid") {
animator.SourceModelAssetGuid = prefabObject->Animator->SourceModelAssetGuid;
return true;
}
if (fieldId == "source_model_path") {
animator.SourceModelPath = prefabObject->Animator->SourceModelPath;
return true;
}
if (fieldId == "clip_index") {
animator.ClipIndex = prefabObject->Animator->ClipIndex;
return true;
}
if (fieldId == "clip_name") {
animator.ClipName = prefabObject->Animator->ClipName;
return true;
}
if (fieldId == "play_on_start") {
animator.PlayOnStart = prefabObject->Animator->PlayOnStart;
return true;
}
if (fieldId == "loop") {
animator.Loop = prefabObject->Animator->Loop;
return true;
}
if (fieldId == "speed") {
animator.Speed = prefabObject->Animator->Speed;
return true;
}
}
if (componentTypeId == "MeshRenderer" && prefabObject->MeshRenderer.has_value()) {
if (!selectedObject.HasComponent<MetaCoreMeshRendererComponent>()) {
selectedObject.AddComponent<MetaCoreMeshRendererComponent>(MetaCoreMeshRendererComponent{});
@ -5866,6 +6208,10 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() {
metadata.SourcePath = relativePath;
metadata.PackagePath = MetaCoreIsJsonSourceAssetPath(relativePath) ? relativePath : MetaCoreBuildPackagePathFromSource(relativePath);
metadata.SourceHash = MetaCoreHashFile(entry.path()).value_or(0);
if (const auto sourceStamp = MetaCoreGetSourceFileStamp(entry.path())) {
metadata.SourceSizeBytes = sourceStamp->SizeBytes;
metadata.SourceLastWriteTime = sourceStamp->LastWriteTime;
}
if (MetaCoreWriteMetaJson(metaPath, metadata)) {
hasMetadata = true;
}
@ -6287,15 +6633,19 @@ bool MetaCoreBuiltinCookService::CookAsset(const MetaCoreAssetGuid& assetGuid) {
}
const std::filesystem::path absoluteSourcePath = AssetDatabaseService_->GetProjectDescriptor().RootPath / record->RelativePath;
const auto sourceHash = MetaCoreHashFile(absoluteSourcePath);
if (!sourceHash.has_value()) {
return false;
std::uint64_t sourceHash = record->SourceHash;
if (sourceHash == 0) {
const auto computedSourceHash = MetaCoreHashFile(absoluteSourcePath);
if (!computedSourceHash.has_value()) {
return false;
}
sourceHash = *computedSourceHash;
}
LoadManifest();
const std::uint64_t cookedKey = MetaCoreHashCombine(
MetaCoreHashString(assetGuid.ToString()),
MetaCoreHashCombine(*sourceHash, MetaCoreHashString(record->ImporterId))
MetaCoreHashCombine(sourceHash, MetaCoreHashString(record->ImporterId))
);
auto manifestIterator = std::find_if(Manifest_.Entries.begin(), Manifest_.Entries.end(), [&](const MetaCoreCookManifestEntry& entry) {
@ -6402,13 +6752,13 @@ bool MetaCoreBuiltinCookService::CookAsset(const MetaCoreAssetGuid& assetGuid) {
assetGuid,
record->RelativePath,
relativeCookedPath,
*sourceHash,
sourceHash,
cookedKey
});
} else {
manifestIterator->SourcePackagePath = record->RelativePath;
manifestIterator->CookedPath = relativeCookedPath;
manifestIterator->SourceHash = *sourceHash;
manifestIterator->SourceHash = sourceHash;
manifestIterator->CookedKey = cookedKey;
}
@ -6928,8 +7278,8 @@ bool MetaCoreBuiltinImportPipelineService::ReimportAsset(const MetaCoreAssetGuid
}
bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesystem::path& absoluteSourcePath, bool forcePackageWrite) {
const auto sourceHash = MetaCoreHashFile(absoluteSourcePath);
if (!sourceHash.has_value()) {
const auto sourceStamp = MetaCoreGetSourceFileStamp(absoluteSourcePath);
if (!sourceStamp.has_value()) {
return false;
}
@ -6951,6 +7301,14 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste
const std::uint64_t previousSourceHash = metadata.SourceHash;
const std::string previousImporterId = metadata.ImporterId;
const std::vector<MetaCoreSubAssetMetadata> previousSubAssets = metadata.SubAssets;
const bool hadSourceStamp = hasMetadata && MetaCoreMetadataHasSourceStamp(metadata);
const bool sourceStampMatches =
hasMetadata && hadSourceStamp && MetaCoreMetadataMatchesSourceStamp(metadata, *sourceStamp);
const bool legacyMetadataCanTrustHash =
hasMetadata &&
!hadSourceStamp &&
metadata.SourceHash != 0 &&
MetaCoreGetFileLastWriteTimeValue(absoluteSourcePath) <= MetaCoreGetFileLastWriteTimeValue(absoluteMetaPath);
if (!metadata.AssetGuid.IsValid()) {
metadata.AssetGuid = MetaCoreAssetGuid::Generate();
@ -6960,21 +7318,37 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste
metadata.ImporterId = MetaCoreDetectImporterId(absoluteSourcePath);
metadata.SourcePath = relativeSourcePath;
metadata.PackagePath = relativePackagePath;
metadata.SourceHash = *sourceHash;
metadata.SourceSizeBytes = sourceStamp->SizeBytes;
metadata.SourceLastWriteTime = sourceStamp->LastWriteTime;
if (jsonSourceAsset) {
metadata.Dependencies = MetaCoreReadJsonGuidDependencies(absoluteSourcePath, metadata.AssetGuid);
}
const bool sourceStampChanged = !hasMetadata || (!sourceStampMatches && !legacyMetadataCanTrustHash);
if (forcePackageWrite || sourceStampChanged || metadata.SourceHash == 0) {
const auto sourceHash = MetaCoreHashFile(absoluteSourcePath);
if (!sourceHash.has_value()) {
return false;
}
metadata.SourceHash = *sourceHash;
}
const MetaCoreModelImportSettingsDocument existingModelImportSettings =
(metadata.AssetType == "model")
? MetaCoreReadExistingModelImportSettings(*PackageService_, registry, absolutePackagePath)
: MetaCoreBuildDefaultModelImportSettings();
const bool gltfModelPackageMissing =
metadata.AssetType == "model" &&
metadata.ImporterId == "GltfModelImporter" &&
!jsonSourceAsset &&
!std::filesystem::exists(absolutePackagePath);
const bool needsPackageWrite =
forcePackageWrite ||
!hasMetadata ||
previousSourceHash != *sourceHash ||
previousImporterId != metadata.ImporterId;
previousSourceHash != metadata.SourceHash ||
previousImporterId != metadata.ImporterId ||
gltfModelPackageMissing;
if (needsPackageWrite) {
std::optional<std::vector<std::byte>> importedAssetBytes;
@ -7005,22 +7379,12 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste
*importedAssetBytes
);
// Add mesh payloads to the package
for (auto& payload : meshPayloads) {
assetPackage.PayloadSections.push_back(std::move(payload));
// Keep rendering on the source GLB/GLTF path, but persist the lightweight
// model document so Inspector and scene instantiation can read animation metadata.
(void)meshPayloads;
if (!PackageService_->WritePackage(absolutePackagePath, std::move(assetPackage))) {
return false;
}
// 【敏捷开发决策】:开发迭代期暂不生成与烘焙体积沉重的 .mcasset 二进制包,仅写入 .mcmeta 索引,直接加载源 GLB 文件!
// 原始二进制烘焙代码将在下一个阶段(打包发版)由 Cooker 模块启用。
/*
(void)MetaCoreWriteModelAssetPackageWithRuntimeCopy(
*PackageService_,
AssetDatabaseService_->GetProjectDescriptor().RootPath,
absolutePackagePath,
metadata.AssetGuid,
assetPackage
);
*/
if (MetaCoreWriteMetaJson(absoluteMetaPath, metadata)) {
// 暂时忽略纹理等子资产运行时二进制包的物理生成
/*
@ -7693,6 +8057,51 @@ public:
},
MetaCoreDrawRotatorComponentInspector
});
Descriptors_.push_back(MetaCoreComponentDescriptor{
"Animator",
"Animator",
[](const MetaCoreGameObject& gameObject) { return gameObject.HasComponent<MetaCoreAnimatorComponent>(); },
[](MetaCoreGameObject& gameObject) {
if (gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
return false;
}
gameObject.AddComponent<MetaCoreAnimatorComponent>(MetaCoreAnimatorComponent{});
return true;
},
[](MetaCoreGameObject& gameObject) {
if (!gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
return false;
}
gameObject.RemoveComponent<MetaCoreAnimatorComponent>();
return true;
},
[](MetaCoreGameObject& gameObject) {
if (!gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
return false;
}
gameObject.GetComponent<MetaCoreAnimatorComponent>() = MetaCoreAnimatorComponent{};
return true;
},
[](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) -> std::optional<std::vector<std::byte>> {
if (!gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
return std::nullopt;
}
return MetaCoreSerializeComponentValue(gameObject.GetComponent<MetaCoreAnimatorComponent>(), registry);
},
[](MetaCoreGameObject& gameObject, std::span<const std::byte> payload, const MetaCoreTypeRegistry& registry) {
MetaCoreAnimatorComponent component{};
if (!MetaCoreDeserializeComponentValue(payload, component, registry)) {
return false;
}
if (gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
gameObject.GetComponent<MetaCoreAnimatorComponent>() = component;
} else {
gameObject.AddComponent<MetaCoreAnimatorComponent>(component);
}
return true;
},
MetaCoreDrawAnimatorComponentInspector
});
Descriptors_.push_back(MetaCoreComponentDescriptor{
"Script",
"脚本",
@ -8385,6 +8794,85 @@ public:
}
};
class MetaCoreAnimationPlayModeComponentSystem final : public MetaCoreIPlayModeComponentSystem {
public:
[[nodiscard]] std::string GetSystemId() const override { return "MetaCore.PlayMode.Animation"; }
void OnPlayStart(MetaCoreEditorContext& editorContext) override {
bool changed = false;
for (MetaCoreGameObject gameObject : editorContext.GetScene().GetGameObjects()) {
if (!gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
continue;
}
auto& animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
animator.RuntimeTimeSeconds = 0.0F;
animator.RuntimePlaying = animator.Enabled && animator.PlayOnStart;
changed = true;
}
if (changed) {
editorContext.GetScene().IncrementRevision();
}
}
void Update(MetaCoreEditorContext& editorContext, double deltaSeconds) override {
if (deltaSeconds <= 0.0) {
return;
}
bool changed = false;
for (MetaCoreGameObject gameObject : editorContext.GetScene().GetGameObjects()) {
if (!gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
continue;
}
auto& animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
if (!animator.Enabled || !animator.RuntimePlaying) {
continue;
}
const float speed = std::max(0.0F, animator.Speed);
if (speed <= 0.0F) {
continue;
}
const auto modelDocument = MetaCoreLoadAnimatorSourceModelDocument(editorContext, animator);
const float durationSeconds = MetaCoreGetAnimatorClipDuration(modelDocument, animator.ClipIndex);
float nextTime = animator.RuntimeTimeSeconds + static_cast<float>(deltaSeconds) * speed;
if (durationSeconds > 0.0F) {
if (animator.Loop) {
nextTime = std::fmod(std::max(0.0F, nextTime), durationSeconds);
} else if (nextTime >= durationSeconds) {
nextTime = durationSeconds;
animator.RuntimePlaying = false;
}
}
animator.RuntimeTimeSeconds = std::max(0.0F, nextTime);
changed = true;
}
if (changed) {
editorContext.GetScene().IncrementRevision();
}
}
void OnPlayStop(MetaCoreEditorContext& editorContext) override {
bool changed = false;
for (MetaCoreGameObject gameObject : editorContext.GetScene().GetGameObjects()) {
if (!gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
continue;
}
auto& animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
animator.RuntimePlaying = false;
animator.RuntimeTimeSeconds = 0.0F;
changed = true;
}
if (changed) {
editorContext.GetScene().IncrementRevision();
}
}
};
class MetaCoreBuiltinPlayModeComponentSystemRegistry final : public MetaCoreIPlayModeComponentSystemRegistry {
public:
[[nodiscard]] std::string GetServiceId() const override { return "MetaCore.PlayModeComponentSystemRegistry"; }
@ -8392,6 +8880,7 @@ public:
void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override {
(void)moduleRegistry;
Systems_.clear();
RegisterSystem(std::make_shared<MetaCoreAnimationPlayModeComponentSystem>());
RegisterSystem(std::make_shared<MetaCoreRotatorPlayModeComponentSystem>());
RegisterSystem(std::make_shared<MetaCoreScriptPlayModeComponentSystem>());
}
@ -8869,6 +9358,7 @@ std::optional<std::filesystem::path> MetaCoreBuiltinPrefabService::CreatePrefabF
if (gameObject.HasComponent<MetaCoreCameraComponent>()) data.Camera = gameObject.GetComponent<MetaCoreCameraComponent>();
if (gameObject.HasComponent<MetaCoreLightComponent>()) data.Light = gameObject.GetComponent<MetaCoreLightComponent>();
if (gameObject.HasComponent<MetaCoreRotatorComponent>()) data.Rotator = gameObject.GetComponent<MetaCoreRotatorComponent>();
if (gameObject.HasComponent<MetaCoreAnimatorComponent>()) data.Animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
if (gameObject.HasComponent<MetaCoreScriptComponent>()) data.Script = gameObject.GetComponent<MetaCoreScriptComponent>();
if (gameObject.HasComponent<MetaCoreUiRendererComponent>()) data.UiRenderer = gameObject.GetComponent<MetaCoreUiRendererComponent>();
if (gameObject.HasComponent<MetaCoreMeshRendererComponent>()) data.MeshRenderer = gameObject.GetComponent<MetaCoreMeshRendererComponent>();
@ -8933,6 +9423,7 @@ std::optional<MetaCoreId> MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen
if (sourceObject.Camera.has_value()) clonedObject.AddComponent<MetaCoreCameraComponent>(*sourceObject.Camera);
if (sourceObject.Light.has_value()) clonedObject.AddComponent<MetaCoreLightComponent>(*sourceObject.Light);
if (sourceObject.Rotator.has_value()) clonedObject.AddComponent<MetaCoreRotatorComponent>(*sourceObject.Rotator);
if (sourceObject.Animator.has_value()) clonedObject.AddComponent<MetaCoreAnimatorComponent>(*sourceObject.Animator);
if (sourceObject.Script.has_value()) clonedObject.AddComponent<MetaCoreScriptComponent>(*sourceObject.Script);
if (sourceObject.UiRenderer.has_value()) clonedObject.AddComponent<MetaCoreUiRendererComponent>(*sourceObject.UiRenderer);
if (sourceObject.MeshRenderer.has_value()) clonedObject.AddComponent<MetaCoreMeshRendererComponent>(*sourceObject.MeshRenderer);
@ -9046,6 +9537,7 @@ bool MetaCoreBuiltinPrefabService::ApplySelectedPrefabInstance(MetaCoreEditorCon
if (gameObject.HasComponent<MetaCoreCameraComponent>()) data.Camera = gameObject.GetComponent<MetaCoreCameraComponent>();
if (gameObject.HasComponent<MetaCoreLightComponent>()) data.Light = gameObject.GetComponent<MetaCoreLightComponent>();
if (gameObject.HasComponent<MetaCoreRotatorComponent>()) data.Rotator = gameObject.GetComponent<MetaCoreRotatorComponent>();
if (gameObject.HasComponent<MetaCoreAnimatorComponent>()) data.Animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
if (gameObject.HasComponent<MetaCoreScriptComponent>()) data.Script = gameObject.GetComponent<MetaCoreScriptComponent>();
if (gameObject.HasComponent<MetaCoreUiRendererComponent>()) data.UiRenderer = gameObject.GetComponent<MetaCoreUiRendererComponent>();
if (gameObject.HasComponent<MetaCoreMeshRendererComponent>()) data.MeshRenderer = gameObject.GetComponent<MetaCoreMeshRendererComponent>();
@ -9306,6 +9798,18 @@ std::optional<MetaCoreId> MetaCoreBuiltinSceneEditingService::InstantiateModelAs
std::vector<MetaCoreId> nodeToIdMap(importedDocument->Nodes.size(), 0);
if (!importedDocument->Animations.empty() && !modelRootObject.HasComponent<MetaCoreAnimatorComponent>()) {
MetaCoreAnimatorComponent animator;
animator.SourceModelAssetGuid = assetGuid;
animator.SourceModelPath = resolvedModelPath.generic_string();
animator.ClipIndex = 0;
animator.ClipName = importedDocument->Animations.front().Name;
animator.PlayOnStart = true;
animator.Loop = true;
animator.Speed = 1.0F;
modelRootObject.AddComponent<MetaCoreAnimatorComponent>(animator);
}
if (importedDocument->Nodes.size() == 1) {
// 给顶级根节点打上 Tag用以在渲染同步回溯时精确定位宿主根
if (!modelRootObject.HasComponent<MetaCoreModelRootTag>()) {
@ -9596,7 +10100,7 @@ public:
private:
struct FileSnapshot {
std::uintmax_t Size = 0;
std::uint64_t Hash = 0;
std::int64_t LastWriteTime = 0;
bool operator==(const FileSnapshot&) const = default;
};
@ -9615,6 +10119,24 @@ private:
using SnapshotMap = std::unordered_map<std::string, FileSnapshot>;
[[nodiscard]] static std::optional<FileSnapshot> BuildFileSnapshot(const std::filesystem::directory_entry& entry) {
std::error_code errorCode;
const std::uintmax_t size = entry.file_size(errorCode);
if (errorCode) {
return std::nullopt;
}
const auto writeTime = entry.last_write_time(errorCode);
if (errorCode) {
return std::nullopt;
}
return FileSnapshot{
size,
std::chrono::duration_cast<std::chrono::nanoseconds>(writeTime.time_since_epoch()).count()
};
}
[[nodiscard]] static bool ShouldIgnoreFile(const std::filesystem::path& path) {
const std::string extension = path.extension().string();
const std::string filename = path.filename().string();
@ -9644,14 +10166,11 @@ private:
if (!entry.is_regular_file() || ShouldIgnoreFile(entry.path())) {
continue;
}
const auto hash = MetaCoreHashFile(entry.path());
if (!hash.has_value()) {
const auto fileSnapshot = BuildFileSnapshot(entry);
if (!fileSnapshot.has_value()) {
continue;
}
snapshot[entry.path().lexically_relative(projectRoot).generic_string()] = FileSnapshot{
entry.file_size(),
*hash
};
snapshot[entry.path().lexically_relative(projectRoot).generic_string()] = *fileSnapshot;
}
}
return snapshot;

View File

@ -89,6 +89,17 @@ static HANDLE PlayerProcessHandle_ = NULL;
static HANDLE PlayerStdoutRead_ = NULL;
#endif
struct MetaCoreModelAnimationPreviewState {
std::int32_t ClipIndex = 0;
float TimeSeconds = 0.0F;
float Speed = 1.0F;
bool Loop = true;
bool Playing = false;
std::chrono::steady_clock::time_point LastTick{};
};
static std::unordered_map<std::string, MetaCoreModelAnimationPreviewState> ModelAnimationPreviewStates_{};
[[nodiscard]] MetaCoreTypeRegistry MetaCoreBuildRuntimePanelTypeRegistry() {
MetaCoreTypeRegistry registry;
MetaCoreRegisterFoundationGeneratedTypes(registry);
@ -5179,27 +5190,84 @@ void DrawGeneratedTextureDetails(
MetaCoreEditorContext& editorContext,
const MetaCoreGameObject& gameObject
) {
if (!gameObject.HasComponent<MetaCoreMeshRendererComponent>()) {
return std::nullopt;
}
const auto& meshRenderer = gameObject.GetComponent<MetaCoreMeshRendererComponent>();
MetaCoreAssetGuid modelGuid = meshRenderer.SourceModelAssetGuid;
if (!modelGuid.IsValid()) {
if (!meshRenderer.SourceModelPath.empty()) {
modelGuid = MetaCoreAssetRegistry::Get().ResolvePathToGuid(meshRenderer.SourceModelPath);
}
}
if (!modelGuid.IsValid()) {
return std::nullopt;
}
const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetDatabaseService>();
if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) {
return std::nullopt;
}
return assetDatabaseService->FindAssetByGuid(modelGuid);
MetaCoreAssetGuid modelGuid;
std::filesystem::path sourceModelPath;
if (gameObject.HasComponent<MetaCoreMeshRendererComponent>()) {
const auto& meshRenderer = gameObject.GetComponent<MetaCoreMeshRendererComponent>();
modelGuid = meshRenderer.SourceModelAssetGuid;
sourceModelPath = meshRenderer.SourceModelPath;
}
if (!modelGuid.IsValid() && gameObject.HasComponent<MetaCoreModelRootTag>()) {
const auto& modelRoot = gameObject.GetComponent<MetaCoreModelRootTag>();
sourceModelPath = modelRoot.SourceModelPath;
}
if (!modelGuid.IsValid() && !sourceModelPath.empty()) {
modelGuid = MetaCoreAssetRegistry::Get().ResolvePathToGuid(sourceModelPath);
}
if (modelGuid.IsValid()) {
return assetDatabaseService->FindAssetByGuid(modelGuid);
}
if (!sourceModelPath.empty()) {
return assetDatabaseService->FindAssetByRelativePath(sourceModelPath.lexically_normal());
}
return std::nullopt;
}
void DrawMissingAnimatorPrompt(MetaCoreEditorContext& editorContext, MetaCoreGameObject& selectedObject) {
if (!selectedObject.HasComponent<MetaCoreModelRootTag>() ||
selectedObject.HasComponent<MetaCoreAnimatorComponent>()) {
return;
}
const auto sourceAsset = FindSourceModelAssetForGameObject(editorContext, selectedObject);
if (!sourceAsset.has_value()) {
return;
}
const auto modelDocument = MetaCoreLoadModelAssetDocument(editorContext, *sourceAsset);
if (!modelDocument.has_value() || modelDocument->Animations.empty()) {
return;
}
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.18F, 0.16F, 0.10F, 1.0F));
ImGui::BeginChild("MissingAnimatorPrompt", ImVec2(0, 58), true, ImGuiWindowFlags_NoScrollbar);
ImGui::Text("该模型包含 %d 个动画 Clip但当前根对象没有 Animator。", static_cast<int>(modelDocument->Animations.size()));
if (ImGui::Button("添加 Animator 并播放", ImVec2(-1, 24))) {
const MetaCoreId objectId = selectedObject.GetId();
const MetaCoreAssetRecord sourceRecord = *sourceAsset;
const MetaCoreModelAssetDocument sourceDocument = *modelDocument;
(void)editorContext.ExecuteSnapshotCommand("添加 Animator", [&]() {
MetaCoreGameObject object = editorContext.GetScene().FindGameObject(objectId);
if (!object || object.HasComponent<MetaCoreAnimatorComponent>()) {
return false;
}
MetaCoreAnimatorComponent animator;
animator.Enabled = true;
animator.SourceModelAssetGuid = sourceRecord.Guid;
animator.SourceModelPath = !sourceDocument.SourcePath.empty()
? sourceDocument.SourcePath.generic_string()
: sourceRecord.RelativePath.generic_string();
animator.ClipIndex = 0;
animator.ClipName = sourceDocument.Animations.front().Name;
animator.PlayOnStart = true;
animator.Loop = true;
animator.Speed = 1.0F;
animator.RuntimePlaying = true;
animator.RuntimeTimeSeconds = 0.0F;
object.AddComponent<MetaCoreAnimatorComponent>(animator);
editorContext.GetScene().IncrementRevision();
return true;
});
}
ImGui::EndChild();
ImGui::PopStyleColor();
}
void OpenSourceModelForGameObject(MetaCoreEditorContext& editorContext, const MetaCoreGameObject& gameObject) {
@ -5625,6 +5693,136 @@ void DrawModelAssetDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Animations")) {
if (importedDocument->Animations.empty()) {
ImGui::TextDisabled("No animations imported.");
} else {
const std::string previewKey = selectedAsset.Guid.ToString();
auto& previewState = ModelAnimationPreviewStates_[previewKey];
previewState.ClipIndex = std::clamp<std::int32_t>(
previewState.ClipIndex,
0,
static_cast<std::int32_t>(importedDocument->Animations.size() - 1)
);
const auto now = std::chrono::steady_clock::now();
if (previewState.LastTick.time_since_epoch().count() == 0) {
previewState.LastTick = now;
}
const float deltaSeconds = std::chrono::duration<float>(now - previewState.LastTick).count();
previewState.LastTick = now;
const auto& selectedClip =
importedDocument->Animations[static_cast<std::size_t>(previewState.ClipIndex)];
const float durationSeconds = std::max(0.0F, selectedClip.DurationSeconds);
if (previewState.Playing) {
previewState.TimeSeconds += std::max(0.0F, deltaSeconds) * std::max(0.0F, previewState.Speed);
if (durationSeconds > 0.0F) {
if (previewState.Loop) {
previewState.TimeSeconds = std::fmod(previewState.TimeSeconds, durationSeconds);
} else if (previewState.TimeSeconds >= durationSeconds) {
previewState.TimeSeconds = durationSeconds;
previewState.Playing = false;
}
}
}
const std::string currentClipLabel = selectedClip.Name.empty()
? ("Clip " + std::to_string(previewState.ClipIndex))
: selectedClip.Name;
ImGui::SetNextItemWidth(-1);
if (ImGui::BeginCombo("Clip##ModelAnimationPreview", currentClipLabel.c_str())) {
for (std::size_t clipIndex = 0; clipIndex < importedDocument->Animations.size(); ++clipIndex) {
const auto& clip = importedDocument->Animations[clipIndex];
const std::string clipLabel = clip.Name.empty()
? ("Clip " + std::to_string(clipIndex))
: clip.Name;
const bool selected = clipIndex == static_cast<std::size_t>(previewState.ClipIndex);
if (ImGui::Selectable(clipLabel.c_str(), selected)) {
previewState.ClipIndex = static_cast<std::int32_t>(clipIndex);
previewState.TimeSeconds = 0.0F;
previewState.Playing = false;
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
if (ImGui::Button(previewState.Playing ? "Pause##ModelAnimationPreview" : "Play##ModelAnimationPreview")) {
previewState.Playing = !previewState.Playing;
}
ImGui::SameLine();
if (ImGui::Button("Stop##ModelAnimationPreview")) {
previewState.Playing = false;
previewState.TimeSeconds = 0.0F;
}
ImGui::SameLine();
if (ImGui::Button("Instantiate and Play##ModelAnimationPreview")) {
if (const auto instantiatedRootId = MetaCoreInstantiateModelAsset(editorContext, selectedAsset.Guid, std::nullopt);
instantiatedRootId.has_value()) {
if (MetaCoreGameObject rootObject = editorContext.GetScene().FindGameObject(*instantiatedRootId);
rootObject && rootObject.HasComponent<MetaCoreAnimatorComponent>()) {
auto& animator = rootObject.GetComponent<MetaCoreAnimatorComponent>();
animator.Enabled = true;
animator.SourceModelAssetGuid = selectedAsset.Guid;
animator.SourceModelPath = importedDocument->SourcePath.generic_string();
animator.ClipIndex = previewState.ClipIndex;
animator.ClipName = currentClipLabel;
animator.PlayOnStart = true;
animator.Loop = previewState.Loop;
animator.Speed = std::max(0.0F, previewState.Speed);
animator.RuntimeTimeSeconds = std::max(0.0F, previewState.TimeSeconds);
animator.RuntimePlaying = true;
editorContext.GetScene().IncrementRevision();
}
}
}
const float sliderMax = durationSeconds > 0.0F ? durationSeconds : std::max(1.0F, previewState.TimeSeconds);
ImGui::SliderFloat("Time##ModelAnimationPreview", &previewState.TimeSeconds, 0.0F, sliderMax, "%.3f s");
ImGui::DragFloat("Speed##ModelAnimationPreview", &previewState.Speed, 0.05F, 0.0F, 8.0F);
previewState.Speed = std::max(0.0F, previewState.Speed);
ImGui::Checkbox("Loop##ModelAnimationPreview", &previewState.Loop);
if (ImGui::BeginTable("AnimationsTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Duration", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableSetupColumn("Channels", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("Samplers", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("Targets", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableHeadersRow();
for (std::size_t clipIndex = 0; clipIndex < importedDocument->Animations.size(); ++clipIndex) {
const auto& clip = importedDocument->Animations[clipIndex];
ImGui::TableNextRow();
ImGui::TableNextColumn();
const bool selected = clipIndex == static_cast<std::size_t>(previewState.ClipIndex);
const std::string label = clip.Name.empty()
? ("Clip " + std::to_string(clipIndex))
: clip.Name;
if (ImGui::Selectable(label.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) {
previewState.ClipIndex = static_cast<std::int32_t>(clipIndex);
previewState.TimeSeconds = 0.0F;
previewState.Playing = false;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", clip.StableImportKey.c_str());
}
ImGui::TableNextColumn();
ImGui::Text("%.3f s", clip.DurationSeconds);
ImGui::TableNextColumn();
ImGui::Text("%d", clip.ChannelCount);
ImGui::TableNextColumn();
ImGui::Text("%d", clip.SamplerCount);
ImGui::TableNextColumn();
ImGui::Text("%d", static_cast<int>(clip.TargetNodeIndices.size()));
}
ImGui::EndTable();
}
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Import Settings")) {
// 当前阶段 Import Settings 为只读显示,后续迭代再开放编辑
const char* upAxisNames[] = {"Preserve Source", "Y-Up", "Z-Up", "X-Up"};
@ -7827,6 +8025,8 @@ public:
}
}
DrawMissingAnimatorPrompt(editorContext, selectedObject);
if (const auto componentRegistry = editorContext.GetModuleRegistry().ResolveService<MetaCoreIComponentTypeRegistry>();
componentRegistry != nullptr) {
for (const auto& descriptor : componentRegistry->GetComponentDescriptors()) {

View File

@ -1573,9 +1573,28 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) {
MetaCoreTraceStartup(("metacore.app: --project=" + resolvedProjectRoot->string()).c_str());
}
MetaCoreTraceStartup("metacore.app: create bootstrap scene");
Scene_ = MetaCoreCreateDefaultScene();
MetaCoreTraceStartup("metacore.app: create modules");
Modules_.push_back(MetaCoreCreateBuiltinCoreServicesModule());
Modules_.push_back(MetaCoreCreateBuiltinEditorViewsModule());
MetaCoreTraceStartup("metacore.app: startup modules");
for (const auto& module : Modules_) {
module->Startup(ModuleRegistry_);
}
auto shutdownStartedModules = [&]() {
for (const auto& module : Modules_) {
module->Shutdown(ModuleRegistry_);
}
ModuleRegistry_.ShutdownServices();
Modules_.clear();
};
MetaCoreTraceStartup("metacore.app: initialize window");
if (!Window_.Initialize(1600, 900, "MetaCore Editor")) {
MetaCoreTraceStartup("metacore.app: initialize window failed");
shutdownStartedModules();
return false;
}
@ -1589,31 +1608,30 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) {
MetaCoreTraceStartup("metacore.app: initialize render device");
if (!RenderDevice_.Initialize(Window_)) {
MetaCoreTraceStartup("metacore.app: initialize render device failed");
Window_.Shutdown();
shutdownStartedModules();
return false;
}
MetaCoreTraceStartup("metacore.app: initialize ui");
if (!InitializeImGui()) {
MetaCoreTraceStartup("metacore.app: initialize ui failed");
RenderDevice_.Shutdown();
Window_.Shutdown();
shutdownStartedModules();
return false;
}
MetaCoreTraceStartup("metacore.app: initialize viewport");
if (!ViewportRenderer_.Initialize(RenderDevice_, Window_)) {
MetaCoreTraceStartup("metacore.app: initialize viewport failed");
ShutdownImGui();
RenderDevice_.Shutdown();
Window_.Shutdown();
shutdownStartedModules();
return false;
}
MetaCoreTraceStartup("metacore.app: create bootstrap scene");
Scene_ = MetaCoreCreateDefaultScene();
MetaCoreTraceStartup("metacore.app: create modules");
Modules_.push_back(MetaCoreCreateBuiltinCoreServicesModule());
Modules_.push_back(MetaCoreCreateBuiltinEditorViewsModule());
MetaCoreTraceStartup("metacore.app: startup modules");
for (const auto& module : Modules_) {
module->Startup(ModuleRegistry_);
}
MetaCoreTraceStartup("metacore.app: create editor context");
EditorContext_ = std::make_unique<MetaCoreEditorContext>(
Window_,

View File

@ -164,6 +164,22 @@ private:
}
}
if (lhs.Animator.has_value() != rhs.Animator.has_value()) {
return false;
}
if (lhs.Animator.has_value()) {
if (lhs.Animator->Enabled != rhs.Animator->Enabled ||
lhs.Animator->SourceModelAssetGuid != rhs.Animator->SourceModelAssetGuid ||
lhs.Animator->SourceModelPath != rhs.Animator->SourceModelPath ||
lhs.Animator->ClipIndex != rhs.Animator->ClipIndex ||
lhs.Animator->ClipName != rhs.Animator->ClipName ||
lhs.Animator->PlayOnStart != rhs.Animator->PlayOnStart ||
lhs.Animator->Loop != rhs.Animator->Loop ||
!MetaCoreNearlyEqual(lhs.Animator->Speed, rhs.Animator->Speed)) {
return false;
}
}
if (lhs.Script.has_value() != rhs.Script.has_value()) {
return false;
}

View File

@ -12,6 +12,7 @@
#include <filesystem>
#include <vector>
#include <string>
#include <string_view>
#include <cstddef>
#include <cstring>
#include <cstdio>
@ -139,6 +140,64 @@ void MetaCoreSetImportedNodeTransformFromCgltfNode(MetaCoreImportedGltfNodeDocum
return stablePath;
}
[[nodiscard]] std::string MetaCoreGltfDisplayName(const char* rawName, std::string_view fallbackName);
void MetaCoreImportGltfAnimationMetadata(MetaCoreModelAssetDocument& document, const cgltf_data& data) {
document.Animations.clear();
if (data.animations_count == 0) {
return;
}
document.Animations.reserve(data.animations_count);
for (std::size_t animationIndex = 0; animationIndex < data.animations_count; ++animationIndex) {
const cgltf_animation& animation = data.animations[animationIndex];
MetaCoreImportedGltfAnimationDocument animationDocument;
animationDocument.Name = MetaCoreGltfDisplayName(animation.name, "Animation_" + std::to_string(animationIndex));
animationDocument.StableImportKey = MetaCoreBuildStableImportKey("animation", animationDocument.Name, animationIndex);
animationDocument.ChannelCount = static_cast<std::int32_t>(animation.channels_count);
animationDocument.SamplerCount = static_cast<std::int32_t>(animation.samplers_count);
float durationSeconds = 0.0F;
for (std::size_t samplerIndex = 0; samplerIndex < animation.samplers_count; ++samplerIndex) {
const cgltf_animation_sampler& sampler = animation.samplers[samplerIndex];
const cgltf_accessor* input = sampler.input;
if (input == nullptr) {
continue;
}
for (std::size_t keyIndex = 0; keyIndex < input->count; ++keyIndex) {
float timeSeconds = 0.0F;
if (cgltf_accessor_read_float(input, keyIndex, &timeSeconds, 1)) {
durationSeconds = std::max(durationSeconds, timeSeconds);
}
}
}
animationDocument.DurationSeconds = durationSeconds;
for (std::size_t channelIndex = 0; channelIndex < animation.channels_count; ++channelIndex) {
const cgltf_animation_channel& channel = animation.channels[channelIndex];
if (channel.target_node == nullptr || data.nodes == nullptr) {
continue;
}
const std::ptrdiff_t nodeIndex = channel.target_node - data.nodes;
if (nodeIndex < 0 || static_cast<std::size_t>(nodeIndex) >= data.nodes_count) {
continue;
}
const auto targetNodeIndex = static_cast<std::int32_t>(nodeIndex);
if (std::find(
animationDocument.TargetNodeIndices.begin(),
animationDocument.TargetNodeIndices.end(),
targetNodeIndex
) == animationDocument.TargetNodeIndices.end()) {
animationDocument.TargetNodeIndices.push_back(targetNodeIndex);
}
}
std::sort(animationDocument.TargetNodeIndices.begin(), animationDocument.TargetNodeIndices.end());
document.Animations.push_back(std::move(animationDocument));
}
}
// 辅助浮点数比较
bool MetaCoreNearlyEqual(float a, float b, float epsilon = 1e-5f) {
return std::abs(a - b) < epsilon;
@ -1075,6 +1134,8 @@ struct MetaCoreGltfMaterialScalarPresence {
);
}
MetaCoreImportGltfAnimationMetadata(document, *data);
cgltf_free(data);
MetaCoreFinalizeImportReport(document.ImportReport);
MetaCoreApplyModelImportSettings(document);

View File

@ -8,6 +8,7 @@
#include <glm/vec3.hpp>
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
@ -59,6 +60,12 @@ struct MetaCoreAssetMetadataDocument {
MC_PROPERTY()
std::uint64_t SourceHash = 0;
MC_PROPERTY()
std::uint64_t SourceSizeBytes = 0;
MC_PROPERTY()
std::int64_t SourceLastWriteTime = 0;
MC_PROPERTY()
std::vector<MetaCoreSubAssetMetadata> SubAssets{};

View File

@ -313,6 +313,29 @@ struct MetaCoreImportedGltfNodeDocument {
glm::vec3 Scale{1.0F, 1.0F, 1.0F};
};
MC_STRUCT()
struct MetaCoreImportedGltfAnimationDocument {
MC_GENERATED_BODY()
MC_PROPERTY()
std::string Name{};
MC_PROPERTY()
std::string StableImportKey{};
MC_PROPERTY()
float DurationSeconds = 0.0F;
MC_PROPERTY()
std::int32_t ChannelCount = 0;
MC_PROPERTY()
std::int32_t SamplerCount = 0;
MC_PROPERTY()
std::vector<std::int32_t> TargetNodeIndices{};
};
MC_STRUCT()
struct MetaCoreModelAssetDocument {
@ -365,6 +388,9 @@ struct MetaCoreModelAssetDocument {
MC_PROPERTY()
std::vector<MetaCoreImportedGltfNodeDocument> Nodes{};
MC_PROPERTY()
std::vector<MetaCoreImportedGltfAnimationDocument> Animations{};
};

View File

@ -342,6 +342,18 @@ void MetaCoreWindow::Shutdown() {
Impl_->Initialized = false;
}
void MetaCoreWindow::PollEvents() {
if (!Impl_->Initialized) {
return;
}
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
void MetaCoreWindow::BeginFrame() {
if (!Impl_->Initialized) {
return;
@ -357,11 +369,7 @@ void MetaCoreWindow::BeginFrame() {
Impl_->Input.BeginFrame();
// Win32 消息循环
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
PollEvents();
Impl_->Input.SetKeyState(MetaCoreInputKey::LeftAlt, MetaCoreIsVirtualKeyDown(VK_LMENU));
Impl_->Input.SetKeyState(MetaCoreInputKey::RightAlt, MetaCoreIsVirtualKeyDown(VK_RMENU));

View File

@ -186,6 +186,13 @@ void MetaCoreWindow::Shutdown() {
Impl_->CloseRequested = false;
}
void MetaCoreWindow::PollEvents() {
if (!Impl_->Initialized || Impl_->NativeHandle == nullptr) {
return;
}
glfwPollEvents();
}
void MetaCoreWindow::BeginFrame() {
if (!Impl_->Initialized || Impl_->NativeHandle == nullptr) {
return;
@ -199,7 +206,7 @@ void MetaCoreWindow::BeginFrame() {
Impl_->LastFrameTime = now;
Impl_->Input.BeginFrame();
glfwPollEvents();
PollEvents();
GLFWwindow* window = Impl_->NativeHandle;
Impl_->Input.SetKeyState(MetaCoreInputKey::LeftAlt, MetaCoreIsGlfwKeyDown(window, GLFW_KEY_LEFT_ALT));

View File

@ -25,6 +25,7 @@ public:
bool Initialize(int width, int height, const std::string& title);
void Shutdown();
void PollEvents();
void BeginFrame();
void EndFrame();

View File

@ -32,7 +32,9 @@
#include <filament/Color.h>
#include <gltfio/AssetLoader.h>
#include <gltfio/Animator.h>
#include <gltfio/FilamentAsset.h>
#include <gltfio/FilamentInstance.h>
#include <gltfio/ResourceLoader.h>
#include <gltfio/MaterialProvider.h>
#include <gltfio/TextureProvider.h>
@ -92,6 +94,7 @@ namespace {
constexpr std::uint8_t kSceneContentLayerMask = 0x01U;
constexpr std::uint8_t kEnvironmentSkyboxLayerMask = 0x02U;
constexpr std::uint8_t kEditorGridLayerMask = 0x04U;
constexpr std::size_t kGltfFileReadChunkBytes = 4U * 1024U * 1024U;
void* MetaCoreGetFilamentNativeWindowHandle(MetaCoreWindow& window) {
#if defined(_WIN32)
@ -262,6 +265,11 @@ private:
class MetaCoreFilamentSceneBridge::MetaCoreFilamentSceneBridgeImpl {
using EntityChildrenMap = std::unordered_map<utils::Entity, std::vector<utils::Entity>, utils::Entity::Hasher>;
struct PendingResourceLoad {
std::string GltfPath{};
std::unique_ptr<filament::gltfio::ResourceLoader> Loader{};
};
public:
MetaCoreFilamentSceneBridgeImpl() = default;
@ -276,6 +284,7 @@ public:
bool Initialize(MetaCoreWindow& window, bool offscreen = true) {
std::cout << "FilamentSceneBridge: Initializing..." << std::endl;
Window_ = &window;
// 创建 Filament Engine (不共享上下文,避免闪退)
Engine_ = filament::Engine::create(MetaCoreResolveFilamentBackend());
@ -482,6 +491,12 @@ public:
DebugRenderTargetState("Shutdown begin");
DestroyEditorGridRenderable();
for (auto& [hostRootId, pendingLoad] : PendingResourceLoads_) {
if (pendingLoad.Loader != nullptr) {
pendingLoad.Loader->asyncCancelLoad();
}
}
PendingResourceLoads_.clear();
// 1. 销毁所有加载的资产,切断其内部 Renderable 对克隆材质实例的占用引用
std::cout << "[DEBUG] LoadedAssets_ size: " << LoadedAssets_.size() << std::endl;
@ -647,6 +662,7 @@ public:
filament::Engine::destroy(&Engine_);
Engine_ = nullptr;
}
Window_ = nullptr;
}
void SetProjectRootPath(const std::filesystem::path& projectRootPath) {
@ -1280,6 +1296,109 @@ public:
return path;
}
[[nodiscard]] std::optional<std::vector<char>> ReadGltfFileResponsive(
const std::filesystem::path& absolutePath
) {
std::ifstream file(absolutePath, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
return std::nullopt;
}
const std::streamoff fileSize = file.tellg();
if (fileSize < 0 || static_cast<std::uint64_t>(fileSize) > std::numeric_limits<std::uint32_t>::max()) {
return std::nullopt;
}
file.seekg(0, std::ios::beg);
std::vector<char> buffer(static_cast<std::size_t>(fileSize));
std::size_t offset = 0;
while (offset < buffer.size()) {
const std::size_t bytesToRead = std::min(kGltfFileReadChunkBytes, buffer.size() - offset);
file.read(buffer.data() + offset, static_cast<std::streamsize>(bytesToRead));
const std::streamsize bytesRead = file.gcount();
if (bytesRead <= 0) {
return std::nullopt;
}
offset += static_cast<std::size_t>(bytesRead);
if (Window_ != nullptr) {
Window_->PollEvents();
}
}
return buffer;
}
void CancelPendingResourceLoad(MetaCoreId hostRootId) {
const auto iterator = PendingResourceLoads_.find(hostRootId);
if (iterator == PendingResourceLoads_.end()) {
return;
}
if (iterator->second.Loader != nullptr) {
iterator->second.Loader->asyncCancelLoad();
}
PendingResourceLoads_.erase(iterator);
}
void UpdatePendingResourceLoads(const std::unordered_set<MetaCoreId>& activeHostRootIds) {
for (auto iterator = PendingResourceLoads_.begin(); iterator != PendingResourceLoads_.end();) {
const MetaCoreId hostRootId = iterator->first;
PendingResourceLoad& pendingLoad = iterator->second;
if (!activeHostRootIds.contains(hostRootId) || !LoadedAssets_.contains(hostRootId)) {
if (pendingLoad.Loader != nullptr) {
pendingLoad.Loader->asyncCancelLoad();
}
iterator = PendingResourceLoads_.erase(iterator);
continue;
}
if (pendingLoad.Loader == nullptr) {
iterator = PendingResourceLoads_.erase(iterator);
continue;
}
pendingLoad.Loader->asyncUpdateLoad();
if (pendingLoad.Loader->asyncGetLoadProgress() >= 1.0F) {
MetaCoreFilamentDebugLog(
"Load async resources complete hostRootId=" + std::to_string(hostRootId)
);
iterator = PendingResourceLoads_.erase(iterator);
continue;
}
++iterator;
}
}
bool BeginAsyncResourceLoad(
MetaCoreId hostRootId,
filament::gltfio::FilamentAsset* asset,
const std::filesystem::path& absolutePath
) {
auto [iterator, inserted] = PendingResourceLoads_.try_emplace(hostRootId);
PendingResourceLoad& pendingLoad = iterator->second;
pendingLoad.GltfPath = absolutePath.string();
filament::gltfio::ResourceConfiguration resourceConfig{};
resourceConfig.engine = Engine_;
resourceConfig.gltfPath = pendingLoad.GltfPath.c_str();
resourceConfig.normalizeSkinningWeights = true;
pendingLoad.Loader = std::make_unique<filament::gltfio::ResourceLoader>(resourceConfig);
if (StbTextureProvider_) {
pendingLoad.Loader->addTextureProvider("image/png", StbTextureProvider_);
pendingLoad.Loader->addTextureProvider("image/jpeg", StbTextureProvider_);
}
if (!pendingLoad.Loader->asyncBeginLoad(asset)) {
pendingLoad.Loader.reset();
PendingResourceLoads_.erase(iterator);
return false;
}
MetaCoreFilamentDebugLog(
"Load async resources begin hostRootId=" + std::to_string(hostRootId)
);
return true;
}
void SyncScene(const MetaCoreSceneRenderSyncSnapshot& snapshot, MetaCoreScene* scene, bool compatibilityMeshOnly, bool useScenePrimaryCamera) {
if (!AssetLoader_) return;
(void)compatibilityMeshOnly;
@ -1387,6 +1506,8 @@ public:
LastDebugRenderableCount_ = snapshot.Renderables.size();
}
UpdatePendingResourceLoads(activeHostRootIds);
// 2. 检查所有已加载的资产,若其 Root ID 不在 activeHostRootIds 中,则说明已被删除,执行清理
for (auto it = LoadedAssets_.begin(); it != LoadedAssets_.end(); ) {
MetaCoreId hostRootId = it->first;
@ -1438,6 +1559,7 @@ public:
// 等待后台渲染线程消费完上一帧命令,再销毁 GLTF 资产持有的纹理/材质。
// 脚本播放会每帧推进 Transform暂停/热更新等操作可能和渲染同步落在相邻帧,
// 不等待会让 OpenGL descriptor 绑定已释放的 FTexture。
CancelPendingResourceLoad(hostRootId);
if (Engine_) {
MetaCoreFilamentDebugLog(
"Unload flush before destroyAsset hostRootId=" + std::to_string(hostRootId)
@ -1550,17 +1672,15 @@ public:
" loadedAssets=" + std::to_string(LoadedAssets_.size())
);
std::ifstream file(absolutePath, std::ios::binary);
if (!file.is_open()) {
auto buffer = ReadGltfFileResponsive(absolutePath);
if (!buffer.has_value()) {
std::cerr << "Failed to open file: " << absolutePath << std::endl;
continue;
}
std::vector<char> buffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
filament::gltfio::FilamentAsset* asset = AssetLoader_->createAsset(
reinterpret_cast<const uint8_t*>(buffer.data()),
static_cast<uint32_t>(buffer.size())
reinterpret_cast<const uint8_t*>(buffer->data()),
static_cast<uint32_t>(buffer->size())
);
if (!asset) {
@ -1573,19 +1693,9 @@ public:
" entities=" + std::to_string(asset->getEntityCount())
);
// 加载贴图等资源。gltfio 不会默认解码 PNG/JPEG必须注册 TextureProvider。
const std::string gltfPath = absolutePath.string();
filament::gltfio::ResourceConfiguration resourceConfig{};
resourceConfig.engine = Engine_;
resourceConfig.gltfPath = gltfPath.c_str();
resourceConfig.normalizeSkinningWeights = true;
filament::gltfio::ResourceLoader resourceLoader(resourceConfig);
if (StbTextureProvider_) {
resourceLoader.addTextureProvider("image/png", StbTextureProvider_);
resourceLoader.addTextureProvider("image/jpeg", StbTextureProvider_);
}
if (!resourceLoader.loadResources(asset)) {
std::cerr << "Warning: GLTF resources were not fully loaded for: " << absolutePath << std::endl;
// Use gltfio async loading so textures and buffers do not stall the first sync frame.
if (!BeginAsyncResourceLoad(hostRootId, asset, absolutePath)) {
std::cerr << "Warning: GLTF async resources could not start for: " << absolutePath << std::endl;
}
// 添加到场景
@ -2150,6 +2260,55 @@ public:
}
}
}
ApplyAnimationStates(snapshot);
}
void ApplyAnimationStates(const MetaCoreSceneRenderSyncSnapshot& snapshot) {
for (const MetaCoreRenderSyncAnimationState& animationState : snapshot.Animations) {
if (animationState.HostRootId == 0) {
continue;
}
const auto assetIterator = LoadedAssets_.find(animationState.HostRootId);
if (assetIterator == LoadedAssets_.end() || assetIterator->second == nullptr) {
continue;
}
filament::gltfio::FilamentAsset* asset = assetIterator->second;
filament::gltfio::FilamentInstance* instance = asset->getInstance();
if (instance == nullptr) {
continue;
}
filament::gltfio::Animator* animator = instance->getAnimator();
if (animator == nullptr) {
continue;
}
const std::size_t animationCount = animator->getAnimationCount();
const bool validClip =
animationState.ClipIndex >= 0 &&
static_cast<std::size_t>(animationState.ClipIndex) < animationCount;
if (!animationState.Enabled || !validClip) {
animator->resetBoneMatrices();
continue;
}
const std::size_t clipIndex = static_cast<std::size_t>(animationState.ClipIndex);
const float durationSeconds = animator->getAnimationDuration(clipIndex);
float sampleTimeSeconds = std::isfinite(animationState.TimeSeconds)
? std::max(0.0F, animationState.TimeSeconds)
: 0.0F;
if (durationSeconds > 0.0F) {
sampleTimeSeconds = animationState.Loop
? std::fmod(sampleTimeSeconds, durationSeconds)
: std::min(sampleTimeSeconds, durationSeconds);
}
animator->applyAnimation(clipIndex, sampleTimeSeconds);
animator->updateBoneMatrices();
}
}
void ApplySceneView(const MetaCoreSceneView& sceneView) {
@ -3354,6 +3513,7 @@ private:
filament::gltfio::MaterialProvider* MaterialProvider_ = nullptr;
filament::gltfio::TextureProvider* StbTextureProvider_ = nullptr;
utils::NameComponentManager* NameManager_ = nullptr;
MetaCoreWindow* Window_ = nullptr;
MetaCoreSceneRenderSync RenderSync_{};
MetaCoreSceneRenderSyncSnapshot LastSyncSnapshot_{};
@ -3365,6 +3525,7 @@ private:
std::unordered_map<MetaCoreId, glm::mat4> ObjectWorldMatrices_{};
std::unordered_map<MetaCoreId, std::pair<filament::gltfio::FilamentAsset*, utils::Entity>> ObjectToFilamentEntity_;
std::unordered_map<MetaCoreId, filament::gltfio::FilamentAsset*> LoadedAssets_;
std::unordered_map<MetaCoreId, PendingResourceLoad> PendingResourceLoads_;
std::unordered_map<MetaCoreId, MetaCoreAssetGuid> HostRootSourceModelGuids_;
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> InvalidatedModelGuids_;
std::unordered_map<MetaCoreId, std::vector<filament::MaterialInstance*>> AssetDuplicatedMaterials_;

View File

@ -267,6 +267,25 @@ MetaCoreSceneRenderSyncSnapshot MetaCoreSceneRenderSync::BuildSnapshot(const Met
light.Enabled
});
}
if (gameObject.HasComponent<MetaCoreAnimatorComponent>()) {
const auto& animator = gameObject.GetComponent<MetaCoreAnimatorComponent>();
std::string sourceModelPath = animator.SourceModelPath;
if (sourceModelPath.empty() && gameObject.HasComponent<MetaCoreModelRootTag>()) {
sourceModelPath = gameObject.GetComponent<MetaCoreModelRootTag>().SourceModelPath;
}
snapshot.Animations.push_back(MetaCoreRenderSyncAnimationState{
objectId,
animator.SourceModelAssetGuid,
sourceModelPath,
animator.ClipIndex,
animator.RuntimeTimeSeconds,
animator.Enabled,
animator.RuntimePlaying,
animator.Loop,
animator.Speed
});
}
}
if (!snapshot.PrimaryCamera.has_value() && !snapshot.Cameras.empty()) {

View File

@ -76,6 +76,18 @@ struct MetaCoreRenderSyncLight {
bool Enabled = true;
};
struct MetaCoreRenderSyncAnimationState {
MetaCoreId HostRootId = 0;
MetaCoreAssetGuid SourceModelAssetGuid{};
std::string SourceModelPath{};
std::int32_t ClipIndex = 0;
float TimeSeconds = 0.0F;
bool Enabled = true;
bool Playing = false;
bool Loop = true;
float Speed = 1.0F;
};
struct MetaCoreSceneRenderSyncSnapshot {
MetaCoreSceneEnvironmentSettings Environment{};
std::unordered_map<MetaCoreId, glm::mat4> WorldMatrices{};
@ -83,6 +95,7 @@ struct MetaCoreSceneRenderSyncSnapshot {
std::vector<MetaCoreRenderSyncRenderable> Renderables{};
std::vector<MetaCoreRenderSyncCamera> Cameras{};
std::vector<MetaCoreRenderSyncLight> Lights{};
std::vector<MetaCoreRenderSyncAnimationState> Animations{};
std::optional<MetaCoreRenderSyncCamera> PrimaryCamera{};
std::uint64_t FrameId = 0;
std::uint64_t SceneRevision = 0;

View File

@ -285,6 +285,9 @@ std::vector<MetaCoreId> MetaCoreScene::DuplicateGameObjects(const std::vector<Me
if (sourceObject.HasComponent<MetaCoreRotatorComponent>())
clonedObject.AddComponent<MetaCoreRotatorComponent>(sourceObject.GetComponent<MetaCoreRotatorComponent>());
if (sourceObject.HasComponent<MetaCoreAnimatorComponent>())
clonedObject.AddComponent<MetaCoreAnimatorComponent>(sourceObject.GetComponent<MetaCoreAnimatorComponent>());
if (sourceObject.HasComponent<MetaCoreScriptComponent>())
clonedObject.AddComponent<MetaCoreScriptComponent>(sourceObject.GetComponent<MetaCoreScriptComponent>());
@ -450,6 +453,8 @@ MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const {
data.Light = obj.GetComponent<MetaCoreLightComponent>();
if (obj.HasComponent<MetaCoreRotatorComponent>())
data.Rotator = obj.GetComponent<MetaCoreRotatorComponent>();
if (obj.HasComponent<MetaCoreAnimatorComponent>())
data.Animator = obj.GetComponent<MetaCoreAnimatorComponent>();
if (obj.HasComponent<MetaCoreScriptComponent>())
data.Script = obj.GetComponent<MetaCoreScriptComponent>();
if (obj.HasComponent<MetaCoreUiRendererComponent>())
@ -483,6 +488,8 @@ void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) {
obj.AddComponent<MetaCoreLightComponent>(data.Light.value());
if (data.Rotator.has_value())
obj.AddComponent<MetaCoreRotatorComponent>(data.Rotator.value());
if (data.Animator.has_value())
obj.AddComponent<MetaCoreAnimatorComponent>(data.Animator.value());
if (data.Script.has_value())
obj.AddComponent<MetaCoreScriptComponent>(data.Script.value());
if (data.UiRenderer.has_value())

View File

@ -146,6 +146,23 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo
}
objJson["Rotator"] = rotatorJson;
}
} else if (objField.Name == "Animator" && obj.Animator.has_value()) {
const MetaCoreStructDescriptor* animatorDesc = registry.FindStruct<MetaCoreAnimatorComponent>();
if (animatorDesc) {
json animatorJson = json::object();
const auto& animator = obj.Animator.value();
for (const auto& animatorField : animatorDesc->Fields) {
if (animatorField.Name == "Enabled") animatorJson["Enabled"] = animator.Enabled;
else if (animatorField.Name == "SourceModelAssetGuid") animatorJson["SourceModelAssetGuid"] = GuidToString(animator.SourceModelAssetGuid);
else if (animatorField.Name == "SourceModelPath") animatorJson["SourceModelPath"] = animator.SourceModelPath;
else if (animatorField.Name == "ClipIndex") animatorJson["ClipIndex"] = animator.ClipIndex;
else if (animatorField.Name == "ClipName") animatorJson["ClipName"] = animator.ClipName;
else if (animatorField.Name == "PlayOnStart") animatorJson["PlayOnStart"] = animator.PlayOnStart;
else if (animatorField.Name == "Loop") animatorJson["Loop"] = animator.Loop;
else if (animatorField.Name == "Speed") animatorJson["Speed"] = animator.Speed;
}
objJson["Animator"] = std::move(animatorJson);
}
} else if (objField.Name == "Script" && obj.Script.has_value()) {
json scriptJson = json::object();
json instancesJson = json::array();
@ -302,6 +319,23 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me
}
obj.Rotator = rotator;
}
} else if (objField.Name == "Animator" && objJson.contains("Animator")) {
const auto& animatorJson = objJson["Animator"];
const MetaCoreStructDescriptor* animatorDesc = registry.FindStruct<MetaCoreAnimatorComponent>();
if (animatorDesc) {
MetaCoreAnimatorComponent animator;
for (const auto& animatorField : animatorDesc->Fields) {
if (animatorField.Name == "Enabled" && animatorJson.contains("Enabled")) animator.Enabled = animatorJson["Enabled"].get<bool>();
else if (animatorField.Name == "SourceModelAssetGuid" && animatorJson.contains("SourceModelAssetGuid")) animator.SourceModelAssetGuid = StringToGuid(animatorJson["SourceModelAssetGuid"].get<std::string>());
else if (animatorField.Name == "SourceModelPath" && animatorJson.contains("SourceModelPath")) animator.SourceModelPath = animatorJson["SourceModelPath"].get<std::string>();
else if (animatorField.Name == "ClipIndex" && animatorJson.contains("ClipIndex")) animator.ClipIndex = animatorJson["ClipIndex"].get<std::int32_t>();
else if (animatorField.Name == "ClipName" && animatorJson.contains("ClipName")) animator.ClipName = animatorJson["ClipName"].get<std::string>();
else if (animatorField.Name == "PlayOnStart" && animatorJson.contains("PlayOnStart")) animator.PlayOnStart = animatorJson["PlayOnStart"].get<bool>();
else if (animatorField.Name == "Loop" && animatorJson.contains("Loop")) animator.Loop = animatorJson["Loop"].get<bool>();
else if (animatorField.Name == "Speed" && animatorJson.contains("Speed")) animator.Speed = animatorJson["Speed"].get<float>();
}
obj.Animator = animator;
}
} else if (objField.Name == "Script" && objJson.contains("Script")) {
const auto& scriptJson = objJson["Script"];
MetaCoreScriptComponent script;

View File

@ -176,6 +176,31 @@ struct MetaCoreRotatorComponent {
float DegreesPerSecond = 90.0F;
};
MC_STRUCT()
struct MetaCoreAnimatorComponent {
MC_GENERATED_BODY()
MC_PROPERTY()
bool Enabled = true;
MC_PROPERTY()
MetaCoreAssetGuid SourceModelAssetGuid{};
MC_PROPERTY()
std::string SourceModelPath{};
MC_PROPERTY()
std::int32_t ClipIndex = 0;
MC_PROPERTY()
std::string ClipName{};
MC_PROPERTY()
bool PlayOnStart = true;
MC_PROPERTY()
bool Loop = true;
MC_PROPERTY()
float Speed = 1.0F;
bool RuntimePlaying = false;
float RuntimeTimeSeconds = 0.0F;
};
MC_STRUCT()
struct MetaCoreScriptInstanceData {
MC_GENERATED_BODY()

View File

@ -132,6 +132,8 @@ struct MetaCoreGameObjectData {
MC_PROPERTY()
std::optional<MetaCoreRotatorComponent> Rotator;
MC_PROPERTY()
std::optional<MetaCoreAnimatorComponent> Animator;
MC_PROPERTY()
std::optional<MetaCoreScriptComponent> Script;
MC_PROPERTY()
std::optional<MetaCoreUiRendererComponent> UiRenderer;

View File

@ -24,9 +24,11 @@
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreScene/MetaCoreTransformUtils.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include "../Source/MetaCoreEditor/Private/MetaCoreGltfImporter.h"
#include <nlohmann/json.hpp>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <chrono>
#include <cstring>
@ -802,6 +804,7 @@ void MetaCoreTestComponentRegistryDescriptors() {
const auto* cameraDescriptor = componentRegistry->FindDescriptor("Camera");
const auto* lightDescriptor = componentRegistry->FindDescriptor("Light");
const auto* rotatorDescriptor = componentRegistry->FindDescriptor("Rotator");
const auto* animatorDescriptor = componentRegistry->FindDescriptor("Animator");
const auto* scriptDescriptor = componentRegistry->FindDescriptor("Script");
const auto* meshRendererDescriptor = componentRegistry->FindDescriptor("MeshRenderer");
@ -809,12 +812,14 @@ void MetaCoreTestComponentRegistryDescriptors() {
std::cout << "[CHECK] cameraDescriptor: " << cameraDescriptor << std::endl;
std::cout << "[CHECK] lightDescriptor: " << lightDescriptor << std::endl;
std::cout << "[CHECK] rotatorDescriptor: " << rotatorDescriptor << std::endl;
std::cout << "[CHECK] animatorDescriptor: " << animatorDescriptor << std::endl;
std::cout << "[CHECK] scriptDescriptor: " << scriptDescriptor << std::endl;
std::cout << "[CHECK] meshRendererDescriptor: " << meshRendererDescriptor << std::endl;
if (transformDescriptor) std::cout << "[CHECK] transform DrawInspector: " << static_cast<bool>(transformDescriptor->DrawInspector) << std::endl;
if (cameraDescriptor) std::cout << "[CHECK] camera DrawInspector: " << static_cast<bool>(cameraDescriptor->DrawInspector) << std::endl;
if (lightDescriptor) std::cout << "[CHECK] light DrawInspector: " << static_cast<bool>(lightDescriptor->DrawInspector) << std::endl;
if (rotatorDescriptor) std::cout << "[CHECK] rotator DrawInspector: " << static_cast<bool>(rotatorDescriptor->DrawInspector) << std::endl;
if (animatorDescriptor) std::cout << "[CHECK] animator DrawInspector: " << static_cast<bool>(animatorDescriptor->DrawInspector) << std::endl;
if (scriptDescriptor) std::cout << "[CHECK] script DrawInspector: " << static_cast<bool>(scriptDescriptor->DrawInspector) << std::endl;
if (meshRendererDescriptor) std::cout << "[CHECK] meshRenderer DrawInspector: " << static_cast<bool>(meshRendererDescriptor->DrawInspector) << std::endl;
@ -822,12 +827,14 @@ void MetaCoreTestComponentRegistryDescriptors() {
MetaCoreExpect(cameraDescriptor != nullptr, "Camera descriptor 应存在");
MetaCoreExpect(lightDescriptor != nullptr, "Light descriptor 应存在");
MetaCoreExpect(rotatorDescriptor != nullptr, "Rotator descriptor 应存在");
MetaCoreExpect(animatorDescriptor != nullptr, "Animator descriptor 应存在");
MetaCoreExpect(scriptDescriptor != nullptr, "Script descriptor 应存在");
MetaCoreExpect(meshRendererDescriptor != nullptr, "MeshRenderer descriptor 应存在");
MetaCoreExpect(!transformDescriptor->DrawInspector, "Transform 应继续由独立 InspectorDrawer 处理");
MetaCoreExpect(static_cast<bool>(cameraDescriptor->DrawInspector), "Camera descriptor 应提供 drawer");
MetaCoreExpect(static_cast<bool>(lightDescriptor->DrawInspector), "Light descriptor 应提供 drawer");
MetaCoreExpect(static_cast<bool>(rotatorDescriptor->DrawInspector), "Rotator descriptor 应提供 drawer");
MetaCoreExpect(static_cast<bool>(animatorDescriptor->DrawInspector), "Animator descriptor 应提供 drawer");
MetaCoreExpect(static_cast<bool>(scriptDescriptor->DrawInspector), "Script descriptor 应提供 drawer");
MetaCoreExpect(static_cast<bool>(meshRendererDescriptor->DrawInspector), "MeshRenderer descriptor 应提供 drawer");
@ -2123,21 +2130,39 @@ void MetaCoreTestProjectDescriptorAndGltfImporterSkeleton() {
MetaCoreExpect(gltfRecord->ImporterId == "GltfModelImporter", "glTF 应使用 GltfModelImporter");
MetaCoreExpect(gltfRecord->Type == "model", "glTF 资源类型应为 model");
// 【敏捷开发决策】:开发期不生成庞大的二进制 mcasset 包,仅做 Record 存在性及元数据 mcmeta 文件的校验
const std::filesystem::path valveMeta = tempProjectRoot / "Assets" / "Valve.gltf.mcmeta";
const std::filesystem::path valvePackage = tempProjectRoot / "Assets" / "Valve.gltf.mcasset";
MetaCoreExpect(std::filesystem::exists(valveMeta), "gltf 应生成 mcmeta 文件");
MetaCoreExpect(std::filesystem::exists(valvePackage), "gltf 应生成轻量 mcasset 元数据包");
{
std::ifstream input(valveMeta);
nlohmann::json metaJson;
input >> metaJson;
MetaCoreExpect(metaJson.value("source_size_bytes", 0ULL) > 0ULL, "gltf mcmeta 应记录源文件大小快速戳");
MetaCoreExpect(metaJson.contains("source_mtime_ns"), "gltf mcmeta 应记录源文件 mtime 快速戳");
}
const auto embeddedTextureRecord =
assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "EmbeddedTexture.gltf");
MetaCoreExpect(embeddedTextureRecord.has_value(), "应能找到内嵌贴图 glTF 资源记录");
const std::filesystem::path embeddedMeta = tempProjectRoot / "Assets" / "EmbeddedTexture.gltf.mcmeta";
const std::filesystem::path embeddedPackage = tempProjectRoot / "Assets" / "EmbeddedTexture.gltf.mcasset";
MetaCoreExpect(std::filesystem::exists(embeddedMeta), "内嵌贴图 glTF 应生成 mcmeta 文件");
MetaCoreExpect(std::filesystem::exists(embeddedPackage), "内嵌贴图 glTF 应生成轻量 mcasset 元数据包");
const auto glbRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Tank.glb");
MetaCoreExpect(glbRecord.has_value(), "应能找到 glb 资源记录");
MetaCoreExpect(glbRecord->ImporterId == "GltfModelImporter", "glb 应使用 GltfModelImporter");
const std::filesystem::path glbMeta = tempProjectRoot / "Assets" / "Tank.glb.mcmeta";
const std::filesystem::path glbPackage = tempProjectRoot / "Assets" / "Tank.glb.mcasset";
MetaCoreExpect(std::filesystem::exists(glbMeta), "glb 应生成 mcmeta 文件");
MetaCoreExpect(std::filesystem::exists(glbPackage), "glb 应生成轻量 mcasset 元数据包");
std::error_code removeError;
std::filesystem::remove(glbPackage, removeError);
MetaCoreExpect(!std::filesystem::exists(glbPackage), "测试前应能删除 glb mcasset");
MetaCoreExpect(assetDatabase->Refresh(), "缺失 glb mcasset 时刷新应成功");
MetaCoreExpect(std::filesystem::exists(glbPackage), "source hash 未变但 glb mcasset 缺失时应自动补包");
const auto objRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Crate.obj");
MetaCoreExpect(objRecord.has_value(), "应能找到 obj 资源记录");
@ -4386,6 +4411,165 @@ void MetaCoreTestUiRendererComponentSnapshotAndJson() {
MetaCoreExpectVec3Near(loadedRenderer.WorldSize, glm::vec3(3.0F, 2.0F, 0.0F), "JSON 往返后世界尺寸应一致");
}
void MetaCoreTestGltfAnimationMetadataImport() {
const std::filesystem::path tempDirectory =
std::filesystem::temp_directory_path() / "MetaCoreGltfAnimationImport";
std::filesystem::remove_all(tempDirectory);
std::filesystem::create_directories(tempDirectory);
const std::filesystem::path binPath = tempDirectory / "AnimatedMetadata.bin";
{
const std::vector<float> values{
0.0F, 0.5F, 1.0F,
0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, 2.0F, 0.0F, 0.0F,
0.0F, 2.5F,
0.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F
};
std::ofstream binFile(binPath, std::ios::binary | std::ios::trunc);
MetaCoreExpect(binFile.is_open(), "动画测试二进制缓冲应可写入");
binFile.write(reinterpret_cast<const char*>(values.data()), static_cast<std::streamsize>(values.size() * sizeof(float)));
}
const std::filesystem::path gltfPath = tempDirectory / "AnimatedMetadata.gltf";
{
std::ofstream gltfFile(gltfPath, std::ios::trunc);
MetaCoreExpect(gltfFile.is_open(), "动画测试 glTF 应可写入");
gltfFile
<< "{\n"
<< " \"asset\": {\"version\": \"2.0\"},\n"
<< " \"buffers\": [{\"uri\": \"AnimatedMetadata.bin\", \"byteLength\": 80}],\n"
<< " \"bufferViews\": [\n"
<< " {\"buffer\": 0, \"byteOffset\": 0, \"byteLength\": 12},\n"
<< " {\"buffer\": 0, \"byteOffset\": 12, \"byteLength\": 36},\n"
<< " {\"buffer\": 0, \"byteOffset\": 48, \"byteLength\": 8},\n"
<< " {\"buffer\": 0, \"byteOffset\": 56, \"byteLength\": 24}\n"
<< " ],\n"
<< " \"accessors\": [\n"
<< " {\"bufferView\": 0, \"componentType\": 5126, \"count\": 3, \"type\": \"SCALAR\"},\n"
<< " {\"bufferView\": 1, \"componentType\": 5126, \"count\": 3, \"type\": \"VEC3\"},\n"
<< " {\"bufferView\": 2, \"componentType\": 5126, \"count\": 2, \"type\": \"SCALAR\"},\n"
<< " {\"bufferView\": 3, \"componentType\": 5126, \"count\": 2, \"type\": \"VEC3\"}\n"
<< " ],\n"
<< " \"nodes\": [{\"name\": \"Root\", \"children\": [1]}, {\"name\": \"Joint\"}],\n"
<< " \"scenes\": [{\"nodes\": [0]}],\n"
<< " \"scene\": 0,\n"
<< " \"animations\": [\n"
<< " {\"name\": \"Walk\", \"samplers\": [{\"input\": 0, \"output\": 1, \"interpolation\": \"LINEAR\"}], \"channels\": [{\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"translation\"}}]},\n"
<< " {\"name\": \"Look\", \"samplers\": [{\"input\": 2, \"output\": 3, \"interpolation\": \"LINEAR\"}], \"channels\": [{\"sampler\": 0, \"target\": {\"node\": 0, \"path\": \"translation\"}}]}\n"
<< " ]\n"
<< "}\n";
}
std::vector<std::vector<std::byte>> meshPayloads;
const MetaCore::MetaCoreModelAssetDocument document = MetaCore::MetaCoreBuildImportedGltfAssetDocument(
gltfPath,
std::filesystem::path("Assets") / "AnimatedMetadata.gltf",
1234,
meshPayloads
);
MetaCoreExpect(document.Animations.size() == 2, "导入器应记录两个 glTF animation");
MetaCoreExpect(document.Animations[0].Name == "Walk", "第一个 animation 名称应保留");
MetaCoreExpect(document.Animations[0].StableImportKey == "animation:Walk:0", "第一个 animation stable key 应稳定");
MetaCoreExpect(std::abs(document.Animations[0].DurationSeconds - 1.0F) <= 0.0001F, "第一个 animation duration 应来自最大 key time");
MetaCoreExpect(document.Animations[0].ChannelCount == 1, "第一个 animation channel 数应正确");
MetaCoreExpect(document.Animations[0].SamplerCount == 1, "第一个 animation sampler 数应正确");
MetaCoreExpect(document.Animations[0].TargetNodeIndices.size() == 1 && document.Animations[0].TargetNodeIndices[0] == 1, "第一个 animation target node 应正确");
MetaCoreExpect(document.Animations[1].Name == "Look", "第二个 animation 名称应保留");
MetaCoreExpect(std::abs(document.Animations[1].DurationSeconds - 2.5F) <= 0.0001F, "第二个 animation duration 应正确");
MetaCoreExpect(document.Animations[1].TargetNodeIndices.size() == 1 && document.Animations[1].TargetNodeIndices[0] == 0, "第二个 animation target node 应正确");
std::filesystem::remove_all(tempDirectory);
}
void MetaCoreTestAnimatorComponentSerializationSnapshotJsonAndRenderSync() {
MetaCore::MetaCoreTypeRegistry registry;
MetaCoreRegisterFoundationGeneratedTypes(registry);
MetaCoreRegisterSceneGeneratedTypes(registry);
const MetaCore::MetaCoreAssetGuid modelGuid = MetaCore::MetaCoreAssetGuid::Generate();
MetaCore::MetaCoreAnimatorComponent animator;
animator.Enabled = true;
animator.SourceModelAssetGuid = modelGuid;
animator.SourceModelPath = "Assets/Animated.glb";
animator.ClipIndex = 1;
animator.ClipName = "Run";
animator.PlayOnStart = false;
animator.Loop = false;
animator.Speed = 1.75F;
animator.RuntimePlaying = true;
animator.RuntimeTimeSeconds = 3.25F;
const auto serializedComponent = MetaCore::MetaCoreSerializeToBytes(animator, registry);
MetaCoreExpect(serializedComponent.has_value(), "Animator 组件应可二进制序列化");
MetaCore::MetaCoreAnimatorComponent roundTripComponent;
MetaCoreExpect(
MetaCore::MetaCoreDeserializeFromBytes(*serializedComponent, roundTripComponent, registry),
"Animator 组件应可二进制反序列化"
);
MetaCoreExpect(roundTripComponent.SourceModelAssetGuid == modelGuid, "Animator Guid 应保留");
MetaCoreExpect(roundTripComponent.SourceModelPath == "Assets/Animated.glb", "Animator SourceModelPath 应保留");
MetaCoreExpect(roundTripComponent.ClipIndex == 1 && roundTripComponent.ClipName == "Run", "Animator clip 字段应保留");
MetaCoreExpect(!roundTripComponent.PlayOnStart && !roundTripComponent.Loop, "Animator bool 字段应保留");
MetaCoreExpect(std::abs(roundTripComponent.Speed - 1.75F) <= 0.0001F, "Animator speed 应保留");
MetaCoreExpect(!roundTripComponent.RuntimePlaying, "Animator RuntimePlaying 不应参与二进制序列化");
MetaCoreExpect(std::abs(roundTripComponent.RuntimeTimeSeconds) <= 0.0001F, "Animator RuntimeTimeSeconds 不应参与二进制序列化");
MetaCore::MetaCoreScene scene;
MetaCore::MetaCoreGameObject animatedObject = scene.CreateGameObject("Animated Root");
animatedObject.AddComponent<MetaCore::MetaCoreModelRootTag>(MetaCore::MetaCoreModelRootTag{"Assets/Animated.glb"});
animatedObject.AddComponent<MetaCore::MetaCoreAnimatorComponent>(animator);
const MetaCore::MetaCoreSceneSnapshot snapshot = scene.CaptureSnapshot();
MetaCoreExpect(snapshot.GameObjects.size() == 1, "Animator 对象应进入场景快照");
MetaCoreExpect(snapshot.GameObjects.front().Animator.has_value(), "场景快照应保存 Animator 组件");
MetaCoreExpect(snapshot.GameObjects.front().Animator->ClipName == "Run", "场景快照应保留 Animator clip 名称");
const std::vector<MetaCore::MetaCoreId> duplicateRoots = scene.DuplicateGameObjects({animatedObject.GetId()});
MetaCoreExpect(duplicateRoots.size() == 1, "复制 Animator 对象应返回一个根");
MetaCore::MetaCoreGameObject duplicatedObject = scene.FindGameObject(duplicateRoots.front());
MetaCoreExpect(duplicatedObject && duplicatedObject.HasComponent<MetaCore::MetaCoreAnimatorComponent>(), "复制后应保留 Animator 组件");
MetaCoreExpect(duplicatedObject.GetComponent<MetaCore::MetaCoreAnimatorComponent>().SourceModelAssetGuid == modelGuid, "复制后 Animator Guid 应保留");
MetaCore::MetaCoreSceneDocument sceneDocument;
sceneDocument.Name = "AnimatorScene";
sceneDocument.GameObjects = snapshot.GameObjects;
const std::filesystem::path scenePath = std::filesystem::temp_directory_path() / "MetaCoreAnimatorScene.mcscene.json";
MetaCoreExpect(MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(scenePath, sceneDocument, registry), "应能保存带 Animator 的场景 JSON");
const auto loadedDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(scenePath, registry);
std::error_code ec;
std::filesystem::remove(scenePath, ec);
MetaCoreExpect(loadedDocument.has_value(), "应能加载带 Animator 的场景 JSON");
MetaCoreExpect(loadedDocument->GameObjects.size() == 1, "加载后 Animator 场景对象数量应一致");
MetaCoreExpect(loadedDocument->GameObjects.front().Animator.has_value(), "JSON 往返后应保留 Animator 组件");
const auto& loadedAnimator = *loadedDocument->GameObjects.front().Animator;
MetaCoreExpect(loadedAnimator.SourceModelAssetGuid == modelGuid, "JSON 往返后 Animator Guid 应一致");
MetaCoreExpect(loadedAnimator.ClipIndex == 1 && loadedAnimator.ClipName == "Run", "JSON 往返后 Animator clip 应一致");
MetaCoreExpect(!loadedAnimator.PlayOnStart && !loadedAnimator.Loop, "JSON 往返后 Animator bool 字段应一致");
MetaCoreExpect(std::abs(loadedAnimator.Speed - 1.75F) <= 0.0001F, "JSON 往返后 Animator speed 应一致");
MetaCoreExpect(!loadedAnimator.RuntimePlaying, "JSON 往返后 RuntimePlaying 应保持默认值");
MetaCoreExpect(std::abs(loadedAnimator.RuntimeTimeSeconds) <= 0.0001F, "JSON 往返后 RuntimeTimeSeconds 应保持默认值");
MetaCore::MetaCoreSceneRenderSync renderSync;
const MetaCore::MetaCoreSceneRenderSyncSnapshot renderSnapshot = renderSync.BuildSnapshot(scene);
MetaCoreExpect(renderSnapshot.Animations.size() >= 1, "RenderSync 应输出 Animator 状态");
const auto animationStateIterator = std::find_if(
renderSnapshot.Animations.begin(),
renderSnapshot.Animations.end(),
[&](const MetaCore::MetaCoreRenderSyncAnimationState& state) {
return state.HostRootId == animatedObject.GetId();
}
);
MetaCoreExpect(animationStateIterator != renderSnapshot.Animations.end(), "RenderSync 应包含原 Animator 对象状态");
const auto& animationState = *animationStateIterator;
MetaCoreExpect(animationState.HostRootId == animatedObject.GetId(), "Animator 状态 HostRootId 应为组件所在模型根");
MetaCoreExpect(animationState.SourceModelAssetGuid == modelGuid, "Animator 状态 Guid 应同步");
MetaCoreExpect(animationState.SourceModelPath == "Assets/Animated.glb", "Animator 状态 SourceModelPath 应同步");
MetaCoreExpect(animationState.ClipIndex == 1, "Animator 状态 clip index 应同步");
MetaCoreExpect(animationState.Playing, "Animator 状态 playing 应同步运行态");
MetaCoreExpect(std::abs(animationState.TimeSeconds - 3.25F) <= 0.0001F, "Animator 状态 time 应同步运行态");
}
void MetaCoreTestDecoupledSnapshotSync() {
const std::filesystem::path projectRoot = MetaCoreTestRepositoryRoot() / "TestProject";
const std::filesystem::path modelRelativePath = std::filesystem::path("Assets") / "Models" / "Cube.glb";
@ -5245,8 +5429,12 @@ int main() {
MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream();
std::cout << "[RUN] MetaCoreTestUiDocumentSerialization..." << std::endl;
MetaCoreTestUiDocumentSerialization();
std::cout << "[RUN] MetaCoreTestGltfAnimationMetadataImport..." << std::endl;
MetaCoreTestGltfAnimationMetadataImport();
std::cout << "[RUN] MetaCoreTestUiRendererComponentSnapshotAndJson..." << std::endl;
MetaCoreTestUiRendererComponentSnapshotAndJson();
std::cout << "[RUN] MetaCoreTestAnimatorComponentSerializationSnapshotJsonAndRenderSync..." << std::endl;
MetaCoreTestAnimatorComponentSerializationSnapshotJsonAndRenderSync();
std::cout << "[RUN] MetaCoreTestDecoupledSnapshotSync..." << std::endl;
MetaCoreTestDecoupledSnapshotSync();