From 2972fa8797e26bd2940f68faf97f8945b555fc4c Mon Sep 17 00:00:00 2001 From: noa7 Date: Sun, 14 Sep 2025 19:33:41 -0700 Subject: [PATCH 01/20] Base Commit for strip hierarchy . 1. Changes to MeshConvertor and MeshAssetImporter to be able to import sub meshes in model as individual models in project assets --- .../ModelAssetImporter.cs | 30 +++++++ .../tools/Stride.Importer.3D/MeshConverter.cs | 84 ++++++++++++++++--- 2 files changed, 104 insertions(+), 10 deletions(-) diff --git a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs index bc6accd05b..0c60ceca41 100644 --- a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs +++ b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs @@ -111,6 +111,29 @@ public override IEnumerable Import(UFile localPath, AssetImporterPara modelAsset = ImportModel(rawAssetReferences, localPath, localPath, entityInfo, false, skeletonAsset); } + if (isImportingModel && modelAsset != null) + { + // How many meshes does the source have? (metadata lists ALL meshes now) + var meshCount = entityInfo.Models?.Count ?? 0; + + // Create one extra Model asset per additional mesh (Mesh 2..Mesh N) + // Note: base name (no suffix) = Mesh 1 (= index 0) + for (int meshIdx = 1; meshIdx < meshCount; meshIdx++) + { + var copy = CloneModelAsset(modelAsset); + var url = new UFile($"{localPath.GetFileNameWithoutExtension()} (Mesh {meshIdx + 1})"); + rawAssetReferences.Add(new AssetItem(url, copy)); + } + + // Create the combined "All" model (only useful if >1 mesh) + if (meshCount > 1) + { + var all = CloneModelAsset(modelAsset); + var urlAll = new UFile(localPath.GetFileNameWithoutExtension() + " (All)"); + rawAssetReferences.Add(new AssetItem(urlAll, all)); + } + } + // 5. Animation if (importParameters.IsTypeSelectedForOutput()) { @@ -326,5 +349,12 @@ private static void ImportTextures(IEnumerable textureDependencies, List assetReferences.Add(new AssetItem(texturePath.GetFileNameWithoutExtension(), texture)); } } + + private static ModelAsset CloneModelAsset(ModelAsset original) + { + var clone = AssetCloner.Clone(original); + clone.Id = AssetId.New(); // correct factory + return clone; + } } } diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index e7e64621ff..66b7589340 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -10,6 +10,7 @@ using Stride.Animations; using Stride.Assets.Materials; using Stride.Core; +using Stride.Core.Assets; using Stride.Core.Diagnostics; using Stride.Core.Extensions; using Stride.Core.IO; @@ -30,6 +31,9 @@ namespace Stride.Importer.ThreeD { public class MeshConverter { + private int keptMeshIndex = 0; + private bool IsKeptMeshIndex(int meshIndex) => keptMeshIndex < 0 || meshIndex == keptMeshIndex; + static MeshConverter() { if (Platform.Type == PlatformType.Windows) @@ -67,6 +71,53 @@ private void ResetConversionData() textureNameCount.Clear(); } + private void DecideKeptMeshIndexFromOutput() + { + keptMeshIndex = 0; // default + + try + { + var name = System.IO.Path.GetFileNameWithoutExtension(vfsOutputFilename); + if (string.IsNullOrEmpty(name)) + return; + + // "(All)" → keep every mesh + if (name.EndsWith(" (All)", StringComparison.OrdinalIgnoreCase)) + { + keptMeshIndex = -1; + return; + } + + // "(Mesh X)" → keep mesh index X-1 + const string tag = " (Mesh "; + var close = name.EndsWith(")", StringComparison.Ordinal); + var start = name.LastIndexOf(tag, StringComparison.OrdinalIgnoreCase); + if (close && start >= 0) + { + var numStr = name.Substring(start + tag.Length, name.Length - (start + tag.Length) - 1); + if (int.TryParse(numStr, out var oneBased) && oneBased >= 1) + { + keptMeshIndex = oneBased - 1; + return; + } + } + + // Back-compat: "(Copy)" == mesh 1 + if (name.EndsWith(" (Copy)", StringComparison.OrdinalIgnoreCase)) + { + keptMeshIndex = 1; + return; + } + + keptMeshIndex = 0; // base name = first mesh + } + catch + { + keptMeshIndex = 0; + } + } + + public unsafe EntityInfo ExtractEntity(string inputFilename, string outputFilename, bool extractTextureDependencies, bool deduplicateMaterials) { try @@ -160,6 +211,7 @@ public unsafe Rendering.Skeleton ConvertSkeleton(string inputFilename, string ou vfsInputFilename = inputFilename; vfsOutputFilename = outputFilename; vfsInputPath = VirtualFileSystem.GetParentFolder(inputFilename); + DecideKeptMeshIndexFromOutput(); var propStore = assimp.CreatePropertyStore(); assimp.SetImportPropertyInteger(propStore, "IMPORT_FBX_PRESERVE_PIVOTS", 0); // Trade some issues for others, see: https://github.com/assimp/assimp/issues/894, https://github.com/assimp/assimp/issues/1974 @@ -201,6 +253,8 @@ private unsafe Model ConvertAssimpScene(Scene* scene) // meshes for (var i = 0; i < scene->MNumMeshes; ++i) { + if (!IsKeptMeshIndex(i)) + continue; if (!meshIndexToNodeIndex.TryGetValue(i, out var value)) { continue; @@ -251,8 +305,10 @@ private unsafe Dictionary GenerateBoneToNodeMap(Scene* scene, Di // Get the all bones in the scene var allBones = new List<(IntPtr NodePointer, string BoneName)>(); var uniqueBoneNames = new HashSet(); - for (int meshIdx = 0; meshIdx < scene->MNumMeshes; meshIdx++) + for (int meshIdx = 0; meshIdx < (int)scene->MNumMeshes; meshIdx++) { + if (keptMeshIndex >= 0 && meshIdx != keptMeshIndex) + continue; var mesh = scene->MMeshes[meshIdx]; if (mesh->MNumBones == 0) { @@ -700,14 +756,20 @@ private unsafe void GenerateUniqueNames(Dictionary finalNames, L private unsafe void GenerateMeshNames(Scene* scene, Dictionary meshNames) { var baseNames = new List(); - for (uint i = 0; i < scene->MNumMeshes; i++) + var orderedMeshPtrs = new List(); + for (uint i = 0; i < scene->MNumMeshes; ++i) { var lMesh = scene->MMeshes[i]; baseNames.Add(lMesh->MName.AsString.CleanNodeName()); + orderedMeshPtrs.Add((IntPtr)lMesh); } - GenerateUniqueNames(meshNames, baseNames, i => (IntPtr)scene->MMeshes[i]); + + GenerateUniqueNames(meshNames, baseNames, idx => orderedMeshPtrs[idx]); } + + + private unsafe void GenerateAnimationNames(Scene* scene, Dictionary animationNames) { var baseNames = new List(); @@ -748,18 +810,20 @@ private unsafe void RegisterNodes( { var nodeIndex = nodes.Count; - // assign the index of the node to the index of the mesh + // assign the index of the node to the index of the mesh (keep only mesh 0) for (uint m = 0; m < fromNode->MNumMeshes; ++m) { - var meshIndex = fromNode->MMeshes[m]; + var meshIndex = (int)fromNode->MMeshes[m]; + if (!IsKeptMeshIndex(meshIndex)) + continue; - if (!meshIndexToNodeIndex.TryGetValue((int)meshIndex, out var nodeIndices)) + if (!meshIndexToNodeIndex.TryGetValue(meshIndex, out var nodeIndices)) { nodeIndices = new List(); - meshIndexToNodeIndex.Add((int)meshIndex, nodeIndices); + meshIndexToNodeIndex.Add(meshIndex, nodeIndices); } - nodeIndices.Add(nodeIndex); + nodeIndices.Add(nodeIndex); // or nodeIndex, depending on your existing code } // Create node @@ -772,7 +836,6 @@ private unsafe void RegisterNodes( // Extract scene scaling and rotation from the root node. // Bake scaling into all node's positions and rotation into the 1st-level nodes. - if (parentIndex == -1) { rootTransform = fromNode->MTransformation.ToStrideMatrix(); @@ -793,10 +856,12 @@ private unsafe void RegisterNodes( nodes.Add(modelNodeDefinition); + // Map this node pointer to its index exactly once, after we've added it if (nodePointerToNodeIndex is not null) { nodePointerToNodeIndex.Add((IntPtr)fromNode, nodeIndex); } + if (duplicateNodeNameToNodeIndices is not null) { string originalNodeName = fromNode->MName.AsString.CleanNodeName(); @@ -1515,7 +1580,6 @@ private unsafe List ExtractModels(Scene* scene, Dictionary Date: Sun, 14 Sep 2025 21:41:20 -0700 Subject: [PATCH 02/20] UI checkbox to split or not split Checkbox option for split at time of import --- .../Templates/ModelAssetTemplateWindow.xaml | 1 + .../ModelAssetTemplateWindow.xaml.cs | 3 ++ .../ModelFromFileTemplateGenerator.cs | 14 +++++- .../ModelAssetImporter.cs | 50 ++++++++++++------- 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelAssetTemplateWindow.xaml b/sources/editor/Stride.Assets.Presentation/Templates/ModelAssetTemplateWindow.xaml index 082f24bb67..d9602318a4 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelAssetTemplateWindow.xaml +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelAssetTemplateWindow.xaml @@ -28,6 +28,7 @@ + diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelAssetTemplateWindow.xaml.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelAssetTemplateWindow.xaml.cs index d690569eef..b27ded1ae4 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelAssetTemplateWindow.xaml.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelAssetTemplateWindow.xaml.cs @@ -43,6 +43,7 @@ public DummyReferenceContainer() private bool showDeduplicateMaterialsCheckBox = true; private bool showFbxDedupeNotSupportedWarning = false; private bool deduplicateMaterials = true; + private bool splitHierarchy = true; private bool importTextures = true; private bool importAnimations = true; private bool importSkeleton = true; @@ -65,6 +66,8 @@ public ImportModelFromFileViewModel(IViewModelServiceProvider serviceProvider) public bool ImportAnimations { get { return importAnimations; } set { SetValue(ref importAnimations, value); } } + public bool SplitHierarchy { get { return splitHierarchy; } set { SetValue(ref splitHierarchy, value); } } + public bool ImportSkeleton { get { return importSkeleton; } set { SetValue(ref importSkeleton, value); } } public bool DontImportSkeleton { get { return dontImportSkeleton; } set { SetValue(ref dontImportSkeleton, value); } } diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index 2a354fe8dc..f48f96d929 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -29,6 +29,7 @@ public static class ModelFromFileTemplateSettings public static SettingsKey ImportAnimations = new SettingsKey("Templates/ModelFromFile/ImportAnimations", PackageUserSettings.SettingsContainer, true); public static SettingsKey ImportSkeleton = new SettingsKey("Templates/ModelFromFile/ImportSkeleton", PackageUserSettings.SettingsContainer, true); public static SettingsKey DefaultSkeleton = new SettingsKey("Templates/ModelFromFile/DefaultSkeleton", PackageUserSettings.SettingsContainer, AssetId.Empty); + public static SettingsKey SplitHierarchy = new SettingsKey("Templates/ModelFromFile/SplitHierarchy", PackageUserSettings.SettingsContainer, false); } public class ModelFromFileTemplateGenerator : AssetFromFileTemplateGenerator @@ -43,6 +44,8 @@ public class ModelFromFileTemplateGenerator : AssetFromFileTemplateGenerator protected static readonly PropertyKey ImportAnimationsKey = new PropertyKey("ImportAnimations", typeof(ModelFromFileTemplateGenerator)); protected static readonly PropertyKey ImportSkeletonKey = new PropertyKey("ImportSkeleton", typeof(ModelFromFileTemplateGenerator)); protected static readonly PropertyKey SkeletonToUseKey = new PropertyKey("SkeletonToUse", typeof(ModelFromFileTemplateGenerator)); + protected static readonly PropertyKey SplitHierarchyKey = new PropertyKey("SplitHierarchy", typeof(ModelFromFileTemplateGenerator)); + public override bool IsSupportingTemplate(TemplateDescription templateDescription) { @@ -72,7 +75,8 @@ protected override async Task PrepareAssetCreation(AssetTemplateGeneratorP DeduplicateMaterials = ModelFromFileTemplateSettings.DeduplicateMaterials.GetValue(profile, true), ImportTextures = ModelFromFileTemplateSettings.ImportTextures.GetValue(profile, true), ImportAnimations = ModelFromFileTemplateSettings.ImportAnimations.GetValue(profile, true), - ImportSkeleton = ModelFromFileTemplateSettings.ImportSkeleton.GetValue(profile, true) + ImportSkeleton = ModelFromFileTemplateSettings.ImportSkeleton.GetValue(profile, true), + SplitHierarchy = ModelFromFileTemplateSettings.SplitHierarchy.GetValue(profile, false) } }; @@ -97,6 +101,7 @@ protected override async Task PrepareAssetCreation(AssetTemplateGeneratorP parameters.Tags.Set(ImportAnimationsKey, window.Parameters.ImportAnimations); parameters.Tags.Set(ImportSkeletonKey, window.Parameters.ImportSkeleton); parameters.Tags.Set(SkeletonToUseKey, skeletonToReuse); + parameters.Tags.Set(SplitHierarchyKey, window.Parameters.SplitHierarchy); // Save settings ModelFromFileTemplateSettings.ImportMaterials.SetValue(window.Parameters.ImportMaterials, profile); @@ -104,6 +109,7 @@ protected override async Task PrepareAssetCreation(AssetTemplateGeneratorP ModelFromFileTemplateSettings.ImportTextures.SetValue(window.Parameters.ImportTextures, profile); ModelFromFileTemplateSettings.ImportAnimations.SetValue(window.Parameters.ImportAnimations, profile); ModelFromFileTemplateSettings.ImportSkeleton.SetValue(window.Parameters.ImportSkeleton, profile); + ModelFromFileTemplateSettings.SplitHierarchy.SetValue(window.Parameters.SplitHierarchy, profile); skeletonId = AttachedReferenceManager.GetAttachedReference(skeletonToReuse)?.Id ?? AssetId.Empty; ModelFromFileTemplateSettings.DefaultSkeleton.SetValue(skeletonId, profile); parameters.Package.UserSettings.Save(); @@ -123,9 +129,11 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar var importAnimations = parameters.Tags.Get(ImportAnimationsKey); var importSkeleton = parameters.Tags.Get(ImportSkeletonKey); var skeletonToReuse = parameters.Tags.Get(SkeletonToUseKey); + var splitHierarchy = parameters.Tags.Get(SplitHierarchyKey); // <-- you read it here var importParameters = new AssetImporterParameters { Logger = parameters.Logger }; importParameters.InputParameters.Set(ModelAssetImporter.DeduplicateMaterialsKey, deduplicateMaterials); + importParameters.InputParameters.Set(ModelAssetImporter.SplitHierarchyKey, splitHierarchy); importParameters.SelectedOutputTypes.Add(typeof(ModelAsset), true); importParameters.SelectedOutputTypes.Add(typeof(MaterialAsset), importMaterials); importParameters.SelectedOutputTypes.Add(typeof(TextureAsset), importTextures); @@ -144,7 +152,9 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar continue; } - var assets = importer.Import(file, importParameters).Select(x => new AssetItem(UPath.Combine(parameters.TargetLocation, x.Location), x.Asset)).ToList(); + var assets = importer.Import(file, importParameters) + .Select(x => new AssetItem(UPath.Combine(parameters.TargetLocation, x.Location), x.Asset)) + .ToList(); foreach (var model in assets.Select(x => x.Asset).OfType()) { diff --git a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs index 0c60ceca41..a32ae910aa 100644 --- a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs +++ b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs @@ -23,6 +23,8 @@ namespace Stride.Assets.Models { public abstract class ModelAssetImporter : AssetImporterBase { + public static readonly PropertyKey SplitHierarchyKey = new PropertyKey("SplitHierarchy", typeof(ModelAssetImporter)); + public static readonly PropertyKey DeduplicateMaterialsKey = new PropertyKey("DeduplicateMaterials", typeof(ModelAssetImporter)); public override IEnumerable RootAssetTypes @@ -108,31 +110,43 @@ public override IEnumerable Import(UFile localPath, AssetImporterPara // 4. Model if (isImportingModel) { - modelAsset = ImportModel(rawAssetReferences, localPath, localPath, entityInfo, false, skeletonAsset); - } + // Read the checkbox from the import parameters (defaults to false if not provided) + bool splitHierarchy = false; + importParameters.InputParameters.TryGet(SplitHierarchyKey, out splitHierarchy); - if (isImportingModel && modelAsset != null) - { - // How many meshes does the source have? (metadata lists ALL meshes now) + // Ask the converter for how many meshes exist (we made ExtractModels return ALL meshes) var meshCount = entityInfo.Models?.Count ?? 0; - // Create one extra Model asset per additional mesh (Mesh 2..Mesh N) - // Note: base name (no suffix) = Mesh 1 (= index 0) - for (int meshIdx = 1; meshIdx < meshCount; meshIdx++) + if (splitHierarchy) { - var copy = CloneModelAsset(modelAsset); - var url = new UFile($"{localPath.GetFileNameWithoutExtension()} (Mesh {meshIdx + 1})"); - rawAssetReferences.Add(new AssetItem(url, copy)); - } + // Base = Mesh 1 + modelAsset = ImportModel(rawAssetReferences, localPath, localPath, entityInfo, false, skeletonAsset); + + // Mesh 2..N + for (int meshIdx = 1; meshIdx < meshCount; meshIdx++) + { + var perMeshCopy = AssetCloner.Clone(modelAsset); + perMeshCopy.Id = AssetId.New(); + var perMeshUrl = new UFile($"{localPath.GetFileNameWithoutExtension()} (Mesh {meshIdx + 1})"); + rawAssetReferences.Add(new AssetItem(perMeshUrl, perMeshCopy)); + } - // Create the combined "All" model (only useful if >1 mesh) - if (meshCount > 1) + // All meshes (only useful if more than one child mesh) + if (meshCount > 1) + { + var allCopy = AssetCloner.Clone(modelAsset); + allCopy.Id = AssetId.New(); + var allUrl = new UFile(localPath.GetFileNameWithoutExtension() + " (All)"); + rawAssetReferences.Add(new AssetItem(allUrl, allCopy)); + } + } + else { - var all = CloneModelAsset(modelAsset); - var urlAll = new UFile(localPath.GetFileNameWithoutExtension() + " (All)"); - rawAssetReferences.Add(new AssetItem(urlAll, all)); + // Only the combined "All" model (no per-mesh assets) + var allUrl = new UFile(localPath.GetFileNameWithoutExtension() + " (All)"); + modelAsset = ImportModel(rawAssetReferences, localPath, allUrl, entityInfo, false, skeletonAsset); } - } + } // 5. Animation if (importParameters.IsTypeSelectedForOutput()) From f32966481261d44dff16b560e80833cb52939caa Mon Sep 17 00:00:00 2001 From: noa7 Date: Tue, 16 Sep 2025 20:26:58 -0700 Subject: [PATCH 03/20] Introducing prefab --- .../ModelFromFileTemplateGenerator.cs | 164 +++++++++++++++++- .../ModelAssetImporter.cs | 4 + 2 files changed, 162 insertions(+), 6 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index f48f96d929..5caf11a893 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -4,20 +4,26 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Stride.Assets.Entities; +using Stride.Assets.Materials; +using Stride.Assets.Models; +using Stride.Assets.Textures; +using Stride.Core; using Stride.Core.Assets; using Stride.Core.Assets.Editor.Services; using Stride.Core.Assets.Editor.ViewModel; using Stride.Core.Assets.Templates; -using Stride.Core; using Stride.Core.IO; -using Stride.Core.Serialization; -using Stride.Core.Settings; using Stride.Core.Presentation.Services; using Stride.Core.Presentation.Windows; -using Stride.Assets.Materials; -using Stride.Assets.Models; -using Stride.Assets.Textures; +using Stride.Core.Serialization; +using Stride.Core.Settings; +using Stride.Engine; using Stride.Rendering; +using Stride.Importer.Common; + + + namespace Stride.Assets.Presentation.Templates { @@ -156,6 +162,56 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar .Select(x => new AssetItem(UPath.Combine(parameters.TargetLocation, x.Location), x.Asset)) .ToList(); + if (splitHierarchy) + { + var entityInfo = importer.GetEntityInfo(file, parameters.Logger, importParameters); + if (entityInfo != null) + { + // Build a mapping: (Mesh i) asset by index, plus the optional "(All)". + // We rely on your current naming convention created by the importer: + // Base model (Mesh 1, at base name), + // "(Mesh k)" for k >= 2, + // and "(All)" if there were multiple meshes. + var baseName = file.GetFileNameWithoutExtension(); + + // Collect model assets we just imported + var modelAssetsByName = assets + .Where(a => a.Asset is ModelAsset) + .ToDictionary(a => a.Location.GetFileNameWithoutExtension(), a => a); + + + // Build an array aligned with entityInfo.Models order: + // entityInfo.Models[0] -> base model (Mesh 1), + // entityInfo.Models[k] -> "(Mesh k+1)" + var perMeshModels = new List(); + for (int i = 0; i < (entityInfo.Models?.Count ?? 0); i++) + { + string name = i == 0 ? baseName : $"{baseName} (Mesh {i + 1})"; + if (modelAssetsByName.TryGetValue(name, out var item)) + perMeshModels.Add(item); + else + perMeshModels.Add(null); // defensive + } + + // Optional combined "(All)" model if it exists + modelAssetsByName.TryGetValue($"{baseName} (All)", out var allModelAsset); + + // Actually build the prefab + var prefabAssetItem = BuildPrefabForSplitHierarchy( + baseName, + entityInfo, + perMeshModels, + allModelAsset, + parameters.TargetLocation); + + if (prefabAssetItem != null) + { + assets.Add(prefabAssetItem); + } + + } + } + foreach (var model in assets.Select(x => x.Asset).OfType()) { if (skeletonToReuse != null) @@ -170,5 +226,101 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar return importedAssets; } + + private static AssetItem BuildPrefabForSplitHierarchy( + string baseName, + EntityInfo entityInfo, + IList perMeshModels, // index-aligned with entityInfo.Models + AssetItem allModelAsset, // currently unused (kept for future) + UDirectory targetLocation) + { + if (entityInfo?.Nodes == null || entityInfo.Nodes.Count == 0) + return null; + + // 1) Create entities in pre-order and rebuild parent/child relations using Depth + var entities = new List(entityInfo.Nodes.Count); + var stack = new Stack(); + Entity root = null; + + for (int i = 0; i < entityInfo.Nodes.Count; i++) + { + var node = entityInfo.Nodes[i]; + var e = new Entity(string.IsNullOrEmpty(node.Name) ? $"Node_{i}" : node.Name); + + // Keep the stack at (node.Depth) entries so parent is at depth-1 + while (stack.Count > 0 && (stack.Count - 1) > node.Depth) + stack.Pop(); + + if (stack.Count == 0) + { + // Depth 0 → root + root = e; + } + else + { + stack.Peek().AddChild(e); + } + + stack.Push(e); + entities.Add(e); + } + + // 2) Attach ModelComponent to nodes that host meshes (match by NodeName) + if (entityInfo.Models != null && entityInfo.Models.Count > 0) + { + var nodeNameToIndex = new Dictionary(StringComparer.Ordinal); + for (int i = 0; i < entityInfo.Nodes.Count; i++) + { + var n = entityInfo.Nodes[i].Name; + if (!string.IsNullOrEmpty(n) && !nodeNameToIndex.ContainsKey(n)) + nodeNameToIndex.Add(n, i); + } + + for (int m = 0; m < entityInfo.Models.Count; m++) + { + var meshInfo = entityInfo.Models[m]; + if (string.IsNullOrEmpty(meshInfo.NodeName)) + continue; + + if (!nodeNameToIndex.TryGetValue(meshInfo.NodeName, out var nodeIndex)) + continue; + + var modelItem = (m >= 0 && m < perMeshModels.Count) ? perMeshModels[m] : null; + if (modelItem?.Asset is ModelAsset) + { + var mc = new ModelComponent + { + // Reference the imported Model asset (no duplication) + Model = AttachedReferenceManager.CreateProxyObject(modelItem.Id, modelItem.Location) + }; + entities[nodeIndex].Components.Add(mc); + } + } + } + + root ??= entities[0]; + + // 3) Build Prefab: register ALL entities in Parts, add ROOT entity to RootParts + var prefab = new PrefabAsset(); + + // Parts: Guid -> EntityDesign (use the entity's Id as the key) + foreach (var e in entities) + { + var design = new EntityDesign(e); + prefab.Hierarchy.Parts.Add(e.Id, design); + } + + // RootParts: list of Entity (your API expects Entity here) + prefab.Hierarchy.RootParts.Add(root); + + // Done + var prefabUrl = new UFile($"{baseName} Prefab"); + return new AssetItem(UPath.Combine(targetLocation, prefabUrl), prefab); + } + + + + + } } diff --git a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs index a32ae910aa..017ffd847d 100644 --- a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs +++ b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs @@ -163,6 +163,10 @@ public override IEnumerable Import(UFile localPath, AssetImporterPara return rawAssetReferences; } + + + + private static AssetItem ImportSkeleton(List assetReferences, UFile assetSource, UFile localPath, EntityInfo entityInfo) { var asset = new SkeletonAsset { Source = assetSource }; From 3242841a185b0c3ee4ba922d2be21e82b554663c Mon Sep 17 00:00:00 2001 From: noa7 Date: Tue, 16 Sep 2025 21:18:32 -0700 Subject: [PATCH 04/20] Prefab + naming With all fixes suggested https://github.com/stride3d/stride/issues/2553#issuecomment-3296960004 --- .../ModelFromFileTemplateGenerator.cs | 215 ++++++++++++++---- .../tools/Stride.Importer.3D/MeshConverter.cs | 64 ++++-- 2 files changed, 208 insertions(+), 71 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index 5caf11a893..f707b40ec1 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -21,9 +21,7 @@ using Stride.Engine; using Stride.Rendering; using Stride.Importer.Common; - - - +using System.IO; namespace Stride.Assets.Presentation.Templates { @@ -162,53 +160,101 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar .Select(x => new AssetItem(UPath.Combine(parameters.TargetLocation, x.Location), x.Asset)) .ToList(); + var baseName = file.GetFileNameWithoutExtension(); + if (splitHierarchy) { var entityInfo = importer.GetEntityInfo(file, parameters.Logger, importParameters); - if (entityInfo != null) + if (entityInfo != null && (entityInfo.Models?.Count ?? 0) > 0) { - // Build a mapping: (Mesh i) asset by index, plus the optional "(All)". - // We rely on your current naming convention created by the importer: - // Base model (Mesh 1, at base name), - // "(Mesh k)" for k >= 2, - // and "(All)" if there were multiple meshes. - var baseName = file.GetFileNameWithoutExtension(); - - // Collect model assets we just imported - var modelAssetsByName = assets - .Where(a => a.Asset is ModelAsset) - .ToDictionary(a => a.Location.GetFileNameWithoutExtension(), a => a); - - - // Build an array aligned with entityInfo.Models order: - // entityInfo.Models[0] -> base model (Mesh 1), - // entityInfo.Models[k] -> "(Mesh k+1)" - var perMeshModels = new List(); - for (int i = 0; i < (entityInfo.Models?.Count ?? 0); i++) + // Collect the first imported model (we'll clone/rename it per-mesh) + var firstModelItem = assets.FirstOrDefault(a => a.Asset is ModelAsset); + if (firstModelItem != null) { - string name = i == 0 ? baseName : $"{baseName} (Mesh {i + 1})"; - if (modelAssetsByName.TryGetValue(name, out var item)) + // Remove any model assets we just imported; we'll re-add per-mesh ones with proper names + assets.RemoveAll(a => a.Asset is ModelAsset); + + var perMeshAssets = new List(); + for (int i = 0; i < entityInfo.Models.Count; i++) + { + var rawMeshName = entityInfo.Models[i].MeshName; + var meshPart = SanitizePart(rawMeshName) ?? $"Mesh-{i + 1}"; + var desiredNoExt = $"{baseName}-{meshPart}"; + + // Ensure uniqueness *within this import batch* + var uniqueFile = MakeUniqueFileName(desiredNoExt, assets); + + AssetItem itemForThisMesh; + + if (i == 0) + { + // Reuse the first imported model's ASSET, but give it a fresh Id before re-wrapping it + var baseModel = (ModelAsset)firstModelItem.Asset; + baseModel.Id = AssetId.New(); // <<—— ensure unique Id + itemForThisMesh = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), baseModel); + } + else + { + // Clone the first model's asset and give it a fresh Id + var clonedAsset = AssetCloner.Clone(firstModelItem.Asset); + ((ModelAsset)clonedAsset).Id = AssetId.New(); // <<—— ensure unique Id + itemForThisMesh = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), clonedAsset); + } + + // (Optional but recommended) Trim materials to only those used by this mesh + var wantedMaterialName = entityInfo.Models[i].MaterialName; + KeepOnlyMaterialByName((ModelAsset)itemForThisMesh.Asset, wantedMaterialName); + + perMeshAssets.Add(itemForThisMesh); + assets.Add(itemForThisMesh); // keep list current so MakeUniqueFileName sees it + } + + + // Build prefab using these per-mesh models + var perMeshByName = assets + .Where(a => a.Asset is ModelAsset) + .ToDictionary(a => a.Location.GetFileNameWithoutExtension(), a => a, StringComparer.OrdinalIgnoreCase); + + var perMeshModels = new List(entityInfo.Models.Count); + for (int i = 0; i < entityInfo.Models.Count; i++) + { + var rawMeshName = entityInfo.Models[i].MeshName; + var meshPart = SanitizePart(rawMeshName) ?? $"Mesh-{i + 1}"; + var expectedName = $"{baseName}-{meshPart}"; + perMeshByName.TryGetValue(expectedName, out var item); perMeshModels.Add(item); - else - perMeshModels.Add(null); // defensive - } + } - // Optional combined "(All)" model if it exists - modelAssetsByName.TryGetValue($"{baseName} (All)", out var allModelAsset); + // No combined "(All)" model when splitting; Prefab is the combined representation + AssetItem allModelAsset = null; - // Actually build the prefab - var prefabAssetItem = BuildPrefabForSplitHierarchy( - baseName, - entityInfo, - perMeshModels, - allModelAsset, - parameters.TargetLocation); + var prefabAssetItem = BuildPrefabForSplitHierarchy( + baseName, + entityInfo, + perMeshModels, + allModelAsset, + parameters.TargetLocation); - if (prefabAssetItem != null) - { - assets.Add(prefabAssetItem); + if (prefabAssetItem != null) + assets.Add(prefabAssetItem); } + } + } + else + { + // Split OFF: keep a single Model named exactly after the source file (no "(All)") + var idx = assets.FindIndex(a => a.Asset is ModelAsset); + if (idx >= 0) + { + var old = assets[idx]; + assets.RemoveAt(idx); + // Assign a fresh Id to the single model to avoid any Id collisions + ((ModelAsset)old.Asset).Id = AssetId.New(); // <<—— ensure unique Id + + var uniqueFile = MakeUniqueFileName(baseName, assets); + var renamed = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), old.Asset); + assets.Insert(idx, renamed); } } @@ -227,12 +273,67 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar return importedAssets; } - private static AssetItem BuildPrefabForSplitHierarchy( - string baseName, - EntityInfo entityInfo, - IList perMeshModels, // index-aligned with entityInfo.Models - AssetItem allModelAsset, // currently unused (kept for future) - UDirectory targetLocation) + // Filters the asset-level materials list so this ModelAsset only keeps the material that matches `wantedMaterialName` + // Keep only the material whose *asset name* (without extension) matches wantedMaterialName. + // Works across Stride branches where ModelMaterial may expose either "Material" or "MaterialInstance". + private static void KeepOnlyMaterialByName(ModelAsset modelAsset, string wantedMaterialName) + { + if (modelAsset?.Materials == null || modelAsset.Materials.Count == 0 || string.IsNullOrWhiteSpace(wantedMaterialName)) + return; + + var kept = new List(); + + foreach (var mm in modelAsset.Materials) + { + // Try to get a reference object we can resolve via AttachedReferenceManager: + // 1) ModelMaterial.Material (asset reference) + // 2) ModelMaterial.MaterialInstance (runtime instance that still carries a reference) + object materialRefObj = null; + var mmType = mm.GetType(); + + var propMaterial = mmType.GetProperty("Material"); + if (propMaterial != null) + materialRefObj = propMaterial.GetValue(mm); + + if (materialRefObj == null) + { + var propMaterialInstance = mmType.GetProperty("MaterialInstance"); + if (propMaterialInstance != null) + materialRefObj = propMaterialInstance.GetValue(mm); + } + + // Resolve the attached reference (if any) to get the asset URL + string assetUrl = null; + if (materialRefObj != null) + { + var aref = AttachedReferenceManager.GetAttachedReference(materialRefObj); + assetUrl = aref?.Url; // this is a string in your branch + } + + // Compare by asset name (no extension) + if (!string.IsNullOrEmpty(assetUrl)) + { + var nameNoExt = Path.GetFileNameWithoutExtension(assetUrl); + if (string.Equals(nameNoExt, wantedMaterialName, StringComparison.OrdinalIgnoreCase)) + { + kept.Add(mm); + } + } + } + + if (kept.Count > 0) + { + modelAsset.Materials.Clear(); + modelAsset.Materials.AddRange(kept); + } + else if (modelAsset.Materials.Count > 1) + { + // Fallback so the asset stays valid if we couldn't match by name + modelAsset.Materials.RemoveRange(1, modelAsset.Materials.Count - 1); + } + } + + private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInfo entityInfo, IList perMeshModels, AssetItem allModelAsset, UDirectory targetLocation) { if (entityInfo?.Nodes == null || entityInfo.Nodes.Count == 0) return null; @@ -309,18 +410,38 @@ private static AssetItem BuildPrefabForSplitHierarchy( var design = new EntityDesign(e); prefab.Hierarchy.Parts.Add(e.Id, design); } - // RootParts: list of Entity (your API expects Entity here) prefab.Hierarchy.RootParts.Add(root); - // Done + prefab.Id = AssetId.New(); // <<—— ensure unique Id var prefabUrl = new UFile($"{baseName} Prefab"); return new AssetItem(UPath.Combine(targetLocation, prefabUrl), prefab); } + private static string SanitizePart(string s) + { + if (string.IsNullOrWhiteSpace(s)) + return null; + var invalid = System.IO.Path.GetInvalidFileNameChars(); + var clean = new string(s.Where(ch => !invalid.Contains(ch)).ToArray()).Trim(); + return string.IsNullOrEmpty(clean) ? null : clean; + } + // Ensure a *file-name without extension* is unique within the current 'assets' list. + // It yields: name, name-1, name-2, ... + private static UFile MakeUniqueFileName(string desiredNameNoExt, List assets) + { + var existing = new HashSet( + assets.Select(a => a.Location.GetFileNameWithoutExtension()), + StringComparer.OrdinalIgnoreCase); + var name = desiredNameNoExt; + var i = 0; + while (existing.Contains(name)) + name = $"{desiredNameNoExt}-{++i}"; + return new UFile(name); // filename only; caller will UPath.Combine with target dir + } } } diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index 66b7589340..c05467568c 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -31,7 +31,11 @@ namespace Stride.Importer.ThreeD { public class MeshConverter { - private int keptMeshIndex = 0; + // keptMeshIndex < 0 => keep ALL meshes + // keptMeshIndex >= 0 => keep only that mesh index + private int keptMeshIndex = -1; // default to ALL unless name says otherwise + private string keptMeshNameHint = null; + private bool IsKeptMeshIndex(int meshIndex) => keptMeshIndex < 0 || meshIndex == keptMeshIndex; static MeshConverter() @@ -73,7 +77,8 @@ private void ResetConversionData() private void DecideKeptMeshIndexFromOutput() { - keptMeshIndex = 0; // default + keptMeshIndex = -1; + keptMeshNameHint = null; try { @@ -81,43 +86,36 @@ private void DecideKeptMeshIndexFromOutput() if (string.IsNullOrEmpty(name)) return; - // "(All)" → keep every mesh + // "(All)" => all meshes if (name.EndsWith(" (All)", StringComparison.OrdinalIgnoreCase)) - { - keptMeshIndex = -1; return; - } - // "(Mesh X)" → keep mesh index X-1 + // "(Mesh X)" => keep mesh X-1 (legacy) const string tag = " (Mesh "; - var close = name.EndsWith(")", StringComparison.Ordinal); - var start = name.LastIndexOf(tag, StringComparison.OrdinalIgnoreCase); - if (close && start >= 0) + if (name.EndsWith(")", StringComparison.Ordinal)) { - var numStr = name.Substring(start + tag.Length, name.Length - (start + tag.Length) - 1); - if (int.TryParse(numStr, out var oneBased) && oneBased >= 1) + var start = name.LastIndexOf(tag, StringComparison.OrdinalIgnoreCase); + if (start >= 0) { - keptMeshIndex = oneBased - 1; - return; + var numStr = name.Substring(start + tag.Length, name.Length - (start + tag.Length) - 1); + if (int.TryParse(numStr, out var oneBased) && oneBased >= 1) + { + keptMeshIndex = oneBased - 1; + return; + } } } - // Back-compat: "(Copy)" == mesh 1 - if (name.EndsWith(" (Copy)", StringComparison.OrdinalIgnoreCase)) - { - keptMeshIndex = 1; - return; - } - - keptMeshIndex = 0; // base name = first mesh + // Otherwise: remember the full output name; we will try to match a mesh by name later + keptMeshNameHint = name; } catch { - keptMeshIndex = 0; + keptMeshIndex = -1; + keptMeshNameHint = null; } } - public unsafe EntityInfo ExtractEntity(string inputFilename, string outputFilename, bool extractTextureDependencies, bool deduplicateMaterials) { try @@ -238,6 +236,24 @@ private unsafe Model ConvertAssimpScene(Scene* scene) var meshNames = new Dictionary(); GenerateMeshNames(scene, meshNames); + // If output asset name equals a mesh name, or ends with "-", select that mesh. + if (!string.IsNullOrEmpty(keptMeshNameHint)) + { + for (uint i = 0; i < scene->MNumMeshes; ++i) + { + var lMesh = scene->MMeshes[i]; + var meshName = meshNames[(IntPtr)lMesh]; + + if (string.Equals(keptMeshNameHint, meshName, StringComparison.OrdinalIgnoreCase) || + keptMeshNameHint.EndsWith("-" + meshName, StringComparison.OrdinalIgnoreCase)) + { + keptMeshIndex = (int)i; + break; + } + } + // If no match found, keptMeshIndex stays -1 → keep ALL meshes (Split OFF case) + } + var nodeNames = new Dictionary(); var duplicateNodeNameToNodePointers = new Dictionary>(); GenerateNodeNames(scene, nodeNames, duplicateNodeNameToNodePointers); From a89499710f9a9604973108e2992948ebc5bcb6c5 Mon Sep 17 00:00:00 2001 From: noa7 Date: Thu, 18 Sep 2025 22:36:12 -0700 Subject: [PATCH 05/20] Fix extra node "RootNode" Parent --- .../ModelFromFileTemplateGenerator.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index f707b40ec1..cade664810 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -349,7 +349,7 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf var e = new Entity(string.IsNullOrEmpty(node.Name) ? $"Node_{i}" : node.Name); // Keep the stack at (node.Depth) entries so parent is at depth-1 - while (stack.Count > 0 && (stack.Count - 1) > node.Depth) + while (stack.Count > node.Depth) stack.Pop(); if (stack.Count == 0) @@ -401,6 +401,22 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf root ??= entities[0]; + var firstNode = entityInfo.Nodes[0]; + var firstName = firstNode.Name ?? string.Empty; + bool looksLikeWrapper = + string.IsNullOrWhiteSpace(firstName) + || firstName.Equals(baseName, StringComparison.OrdinalIgnoreCase) + || firstName.Equals("RootNode", StringComparison.OrdinalIgnoreCase); + + // Use the constructed runtime tree to check children count + if (looksLikeWrapper && root.Transform.Children.Count == 1) + { + var onlyChild = root.Transform.Children[0].Entity; + // Detach so it becomes the actual root + onlyChild.Transform.Parent = null; + root = onlyChild; + } + // 3) Build Prefab: register ALL entities in Parts, add ROOT entity to RootParts var prefab = new PrefabAsset(); From cc374e3a171090b00188547381171b258af488cc Mon Sep 17 00:00:00 2001 From: noa7 Date: Fri, 19 Sep 2025 01:08:52 -0700 Subject: [PATCH 06/20] Material Fix --- .../ModelFromFileTemplateGenerator.cs | 6 +- .../ModelAssetImporter.cs | 61 +++++++++++++++---- .../tools/Stride.Importer.3D/MeshConverter.cs | 29 ++++++++- .../Stride.Importer.Common/EntityInfo.cs | 1 + 4 files changed, 76 insertions(+), 21 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index cade664810..4e68d3c092 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -200,11 +200,7 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar ((ModelAsset)clonedAsset).Id = AssetId.New(); // <<—— ensure unique Id itemForThisMesh = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), clonedAsset); } - - // (Optional but recommended) Trim materials to only those used by this mesh - var wantedMaterialName = entityInfo.Models[i].MaterialName; - KeepOnlyMaterialByName((ModelAsset)itemForThisMesh.Asset, wantedMaterialName); - + perMeshAssets.Add(itemForThisMesh); assets.Add(itemForThisMesh); // keep list current so MakeUniqueFileName sees it } diff --git a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs index 017ffd847d..bc6292c1dc 100644 --- a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs +++ b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs @@ -224,23 +224,59 @@ private static ModelAsset ImportModel(List assetReferences, UFile ass if (entityInfo.Models != null) { var loadedMaterials = assetReferences.Where(x => x.Asset is MaterialAsset).ToList(); - foreach (var material in entityInfo.Materials) + asset.Materials.Clear(); + + // Prefer the exact Assimp material order if present + if (entityInfo.MaterialOrder != null && entityInfo.MaterialOrder.Count > 0) { - var modelMaterial = new ModelMaterial + foreach (var matName in entityInfo.MaterialOrder) { - Name = material.Key, - MaterialInstance = new MaterialInstance() - }; - var foundMaterial = loadedMaterials.FirstOrDefault(x => x.Location == new UFile(material.Key)); - if (foundMaterial != null) + if (!entityInfo.Materials.TryGetValue(matName, out var _)) + continue; + + var modelMaterial = new ModelMaterial + { + Name = matName, + MaterialInstance = new MaterialInstance() + }; + + // Find the imported material asset by its location (same name convention) + var foundMaterial = loadedMaterials.FirstOrDefault(x => x.Location == new UFile(matName)); + if (foundMaterial != null) + { + var reference = AttachedReferenceManager.CreateProxyObject(foundMaterial.Id, foundMaterial.Location); + modelMaterial.MaterialInstance.Material = reference; + } + + asset.Materials.Add(modelMaterial); + } + } + else + { + // Fallback: deterministic by-name order if MaterialOrder wasn’t supplied + foreach (var kv in entityInfo.Materials.OrderBy(kv => kv.Key, StringComparer.Ordinal)) { - var reference = AttachedReferenceManager.CreateProxyObject(foundMaterial.Id, foundMaterial.Location); - modelMaterial.MaterialInstance.Material = reference; + var matName = kv.Key; + + var modelMaterial = new ModelMaterial + { + Name = matName, + MaterialInstance = new MaterialInstance() + }; + + var foundMaterial = loadedMaterials.FirstOrDefault(x => x.Location == new UFile(matName)); + if (foundMaterial != null) + { + var reference = AttachedReferenceManager.CreateProxyObject(foundMaterial.Id, foundMaterial.Location); + modelMaterial.MaterialInstance.Material = reference; + } + + asset.Materials.Add(modelMaterial); } - asset.Materials.Add(modelMaterial); } - //handle the case where during import we imported no materials at all - if (entityInfo.Materials.Count == 0) + + // If still none, keep a default slot + if (asset.Materials.Count == 0) { var modelMaterial = new ModelMaterial { Name = "Material", MaterialInstance = new MaterialInstance() }; asset.Materials.Add(modelMaterial); @@ -274,7 +310,6 @@ private static void ImportMaterials(List assetReferences, Dictionary< var materialAssetReference = materialAssetReferenceLink.Reference as IReference; if (materialAssetReference == null) continue; - // texture location is #nameOfTheModel_#nameOfTheTexture at this point in the material var foundTexture = loadedTextures.FirstOrDefault(x => x.Location == materialAssetReference.Location); if (foundTexture != null) diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index c05467568c..ee7ecc93c6 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -150,14 +150,18 @@ public unsafe EntityInfo ExtractEntity(string inputFilename, string outputFilena GenerateNodeNames(scene, nodeNames); + var materials = ExtractMaterials(scene, materialNames, out var materialOrder); + var entityInfo = new EntityInfo { - Materials = ExtractMaterials(scene, materialNames), + Materials = materials, + MaterialOrder = materialOrder, // NEW: preserve Assimp array order Models = ExtractModels(scene, meshNames, materialNames, nodeNames), Nodes = ExtractNodeHierarchy(scene, nodeNames), AnimationNodes = ExtractAnimations(scene, animationNames) }; + if (extractTextureDependencies) entityInfo.TextureDependencies = ExtractTextureDependencies(scene); @@ -1243,20 +1247,39 @@ private unsafe void CreateTextureFile(Silk.NET.Assimp.Texture* texture, string p System.IO.File.WriteAllBytes(path, buffer); } - private unsafe Dictionary ExtractMaterials(Scene* scene, Dictionary materialNames) + // MeshConverter.cs — inside class MeshConverter + private unsafe Dictionary ExtractMaterials( + Scene* scene, + Dictionary materialNames, + out List materialOrderOut) { + // Build the unique names first (already in Assimp array order) GenerateMaterialNames(scene, materialNames); var materials = new Dictionary(); + var materialOrder = new List(capacity: (int)scene->MNumMaterials); + for (uint i = 0; i < scene->MNumMaterials; i++) { var lMaterial = scene->MMaterials[i]; + + // Name in the same position as the Assimp index var materialName = materialNames[(IntPtr)lMaterial]; - materials.Add(materialName, ProcessMeshMaterial(scene, lMaterial)); + + // Build the MaterialAsset with your existing routine + var matAsset = ProcessMeshMaterial(scene, lMaterial); + + materials[materialName] = matAsset; // lookup by name + materialOrder.Add(materialName); // preserve exact Assimp order } + + materialOrderOut = materialOrder; return materials; } + + + private unsafe void GenerateMaterialNames(Scene* scene, Dictionary materialNames) { var baseNames = new List(); diff --git a/sources/tools/Stride.Importer.Common/EntityInfo.cs b/sources/tools/Stride.Importer.Common/EntityInfo.cs index 10e7e7e5f3..21d33ccef2 100644 --- a/sources/tools/Stride.Importer.Common/EntityInfo.cs +++ b/sources/tools/Stride.Importer.Common/EntityInfo.cs @@ -13,5 +13,6 @@ public class EntityInfo public List AnimationNodes; public List Models; public List Nodes; + public List MaterialOrder { get; set; } } } From 8303aafa6b38bec101e8dd1b3724cf5d42ef1b97 Mon Sep 17 00:00:00 2001 From: noa7 Date: Fri, 19 Sep 2025 04:05:45 -0700 Subject: [PATCH 07/20] Material + clean up --- .../ModelFromFileTemplateGenerator.cs | 61 ------------------- .../ModelAssetImporter.cs | 3 - .../tools/Stride.Importer.3D/MeshConverter.cs | 5 -- 3 files changed, 69 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index 4e68d3c092..ac88464753 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -50,7 +50,6 @@ public class ModelFromFileTemplateGenerator : AssetFromFileTemplateGenerator protected static readonly PropertyKey SkeletonToUseKey = new PropertyKey("SkeletonToUse", typeof(ModelFromFileTemplateGenerator)); protected static readonly PropertyKey SplitHierarchyKey = new PropertyKey("SplitHierarchy", typeof(ModelFromFileTemplateGenerator)); - public override bool IsSupportingTemplate(TemplateDescription templateDescription) { return templateDescription.Id == Id; @@ -269,66 +268,6 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar return importedAssets; } - // Filters the asset-level materials list so this ModelAsset only keeps the material that matches `wantedMaterialName` - // Keep only the material whose *asset name* (without extension) matches wantedMaterialName. - // Works across Stride branches where ModelMaterial may expose either "Material" or "MaterialInstance". - private static void KeepOnlyMaterialByName(ModelAsset modelAsset, string wantedMaterialName) - { - if (modelAsset?.Materials == null || modelAsset.Materials.Count == 0 || string.IsNullOrWhiteSpace(wantedMaterialName)) - return; - - var kept = new List(); - - foreach (var mm in modelAsset.Materials) - { - // Try to get a reference object we can resolve via AttachedReferenceManager: - // 1) ModelMaterial.Material (asset reference) - // 2) ModelMaterial.MaterialInstance (runtime instance that still carries a reference) - object materialRefObj = null; - var mmType = mm.GetType(); - - var propMaterial = mmType.GetProperty("Material"); - if (propMaterial != null) - materialRefObj = propMaterial.GetValue(mm); - - if (materialRefObj == null) - { - var propMaterialInstance = mmType.GetProperty("MaterialInstance"); - if (propMaterialInstance != null) - materialRefObj = propMaterialInstance.GetValue(mm); - } - - // Resolve the attached reference (if any) to get the asset URL - string assetUrl = null; - if (materialRefObj != null) - { - var aref = AttachedReferenceManager.GetAttachedReference(materialRefObj); - assetUrl = aref?.Url; // this is a string in your branch - } - - // Compare by asset name (no extension) - if (!string.IsNullOrEmpty(assetUrl)) - { - var nameNoExt = Path.GetFileNameWithoutExtension(assetUrl); - if (string.Equals(nameNoExt, wantedMaterialName, StringComparison.OrdinalIgnoreCase)) - { - kept.Add(mm); - } - } - } - - if (kept.Count > 0) - { - modelAsset.Materials.Clear(); - modelAsset.Materials.AddRange(kept); - } - else if (modelAsset.Materials.Count > 1) - { - // Fallback so the asset stays valid if we couldn't match by name - modelAsset.Materials.RemoveRange(1, modelAsset.Materials.Count - 1); - } - } - private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInfo entityInfo, IList perMeshModels, AssetItem allModelAsset, UDirectory targetLocation) { if (entityInfo?.Nodes == null || entityInfo.Nodes.Count == 0) diff --git a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs index bc6292c1dc..16185bf442 100644 --- a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs +++ b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs @@ -188,7 +188,6 @@ private static AssetItem ImportSkeleton(List assetReferences, UFile a assetReferences.Add(assetItem); return assetItem; } - private static void ImportAnimation(List assetReferences, UFile localPath, string animationNodeName, int animationNodeIndex, [MaybeNull]AssetItem skeletonAsset, [MaybeNull]ModelAsset modelAsset, TimeSpan animationStartTime, TimeSpan animationEndTime) { var assetSource = localPath; @@ -216,7 +215,6 @@ private static void ImportAnimation(List assetReferences, UFile local assetReferences.Add(new AssetItem(animUrl, asset)); } - private static ModelAsset ImportModel(List assetReferences, UFile assetSource, UFile localPath, EntityInfo entityInfo, bool shouldPostFixName, AssetItem skeletonAsset) { var asset = new ModelAsset { Source = assetSource }; @@ -291,7 +289,6 @@ private static ModelAsset ImportModel(List assetReferences, UFile ass assetReferences.Add(assetItem); return asset; } - private static void ImportMaterials(List assetReferences, Dictionary materials) { if (materials != null) diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index ee7ecc93c6..11c607af35 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -316,7 +316,6 @@ private unsafe Model ConvertAssimpScene(Scene* scene) } } - return modelData; } @@ -787,9 +786,6 @@ private unsafe void GenerateMeshNames(Scene* scene, Dictionary m GenerateUniqueNames(meshNames, baseNames, idx => orderedMeshPtrs[idx]); } - - - private unsafe void GenerateAnimationNames(Scene* scene, Dictionary animationNames) { var baseNames = new List(); @@ -1247,7 +1243,6 @@ private unsafe void CreateTextureFile(Silk.NET.Assimp.Texture* texture, string p System.IO.File.WriteAllBytes(path, buffer); } - // MeshConverter.cs — inside class MeshConverter private unsafe Dictionary ExtractMaterials( Scene* scene, Dictionary materialNames, From 6695a6bb19fd26c70a1bd1c44296f0d57d5d7ee3 Mon Sep 17 00:00:00 2001 From: noa7 Date: Fri, 19 Sep 2025 21:53:37 -0700 Subject: [PATCH 08/20] Material Fix --- .../ModelFromFileTemplateGenerator.cs | 22 +++++++++++++++++++ .../tools/Stride.Importer.3D/MeshConverter.cs | 8 +++++-- .../Stride.Importer.Common/MeshParameters.cs | 2 ++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index ac88464753..0faaf77441 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -220,6 +220,17 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar perMeshModels.Add(item); } + for (int i = 0; i < entityInfo.Models.Count; i++) + { + var item = perMeshModels[i]; + if (item?.Asset is ModelAsset modelAsset) + { + TrimModelAssetToSingleMaterialByIndex(modelAsset, + entityInfo.Models[i].OriginalMaterialIndex); // <- the index from step 1 + } + } + + // No combined "(All)" model when splitting; Prefab is the combined representation AssetItem allModelAsset = null; @@ -268,6 +279,17 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar return importedAssets; } + private static void TrimModelAssetToSingleMaterialByIndex(ModelAsset asset, int keepIndex) + { + if (asset?.Materials == null) return; + if (keepIndex < 0 || keepIndex >= asset.Materials.Count) return; + + var keep = asset.Materials[keepIndex]; + asset.Materials.Clear(); + asset.Materials.Add(keep); + } + + private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInfo entityInfo, IList perMeshModels, AssetItem allModelAsset, UDirectory targetLocation) { if (entityInfo?.Nodes == null || entityInfo.Nodes.Count == 0) diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index 11c607af35..0f66f9e4c4 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -293,7 +293,8 @@ private unsafe Model ConvertAssimpScene(Scene* scene) { Draw = meshInfo.Draw, Name = meshInfo.Name, - MaterialIndex = meshInfo.MaterialIndex, + //MaterialIndex = meshInfo.MaterialIndex, + MaterialIndex = (keptMeshIndex >= 0 ? 0 : meshInfo.MaterialIndex), NodeIndex = nodeIndex, }; @@ -316,6 +317,8 @@ private unsafe Model ConvertAssimpScene(Scene* scene) } } + + return modelData; } @@ -1609,7 +1612,8 @@ private unsafe List ExtractModels(Scene* scene, DictionaryMRootNode, i, nodeNames) + NodeName = SearchMeshNode(scene->MRootNode, i, nodeNames), + OriginalMaterialIndex = (int)mesh->MMaterialIndex }; meshList.Add(meshParams); diff --git a/sources/tools/Stride.Importer.Common/MeshParameters.cs b/sources/tools/Stride.Importer.Common/MeshParameters.cs index 0cf3763e11..40af1b5f97 100644 --- a/sources/tools/Stride.Importer.Common/MeshParameters.cs +++ b/sources/tools/Stride.Importer.Common/MeshParameters.cs @@ -11,5 +11,7 @@ public class MeshParameters public string MeshName; public string NodeName; public HashSet BoneNodes; + public int OriginalMaterialIndex { get; set; } + } } From fb77b7a46855086d01c2a201f84bfb5fac0a2d0e Mon Sep 17 00:00:00 2001 From: noa7 Date: Sat, 20 Sep 2025 20:07:40 -0700 Subject: [PATCH 09/20] TRS @ nodeinfo -> Entity --- .../ModelFromFileTemplateGenerator.cs | 17 +++++++++-- .../tools/Stride.Importer.3D/MeshConverter.cs | 28 +++++++++++++++++-- .../tools/Stride.Importer.Common/NodeInfo.cs | 5 ++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index 0faaf77441..160c1b7cb8 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -2,6 +2,7 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Stride.Assets.Entities; @@ -14,14 +15,14 @@ using Stride.Core.Assets.Editor.ViewModel; using Stride.Core.Assets.Templates; using Stride.Core.IO; +using Stride.Core.Mathematics; using Stride.Core.Presentation.Services; using Stride.Core.Presentation.Windows; using Stride.Core.Serialization; using Stride.Core.Settings; using Stride.Engine; -using Stride.Rendering; using Stride.Importer.Common; -using System.IO; +using Stride.Rendering; namespace Stride.Assets.Presentation.Templates { @@ -323,6 +324,18 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf entities.Add(e); } + + // AFTER the for(...) that pushes entities on the stack, BEFORE attaching ModelComponents + for (int i = 0; i < entityInfo.Nodes.Count; i++) + { + var n = entityInfo.Nodes[i]; + var e = entities[i]; + + e.Transform.Position = n.Position; + e.Transform.Rotation = n.Rotation; + e.Transform.Scale = n.Scale ; + } + // 2) Attach ModelComponent to nodes that host meshes (match by NodeName) if (entityInfo.Models != null && entityInfo.Models.Count > 0) { diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index 0f66f9e4c4..00577e078f 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -316,9 +316,28 @@ private unsafe Model ConvertAssimpScene(Scene* scene) modelData.Meshes.Add(nodeMeshData); } } + // Neutralize node binding for per-mesh export (split-hierarchy case) + if (keptMeshIndex >= 0) + { + // Replace node table with a single identity root + nodes.Clear(); + nodes.Add(new ModelNodeDefinition + { + ParentIndex = -1, + Name = "Root", + Flags = ModelNodeFlags.Default, + Transform = + { + Position = Vector3.Zero, + Rotation = Quaternion.Identity, + Scale = Vector3.One + } + }); - - + // Re-bind all meshes to that root (index 0) + for (int mi = 0; mi < modelData.Meshes.Count; mi++) + modelData.Meshes[mi].NodeIndex = 0; + } return modelData; } @@ -1655,7 +1674,12 @@ private unsafe void GetNodes(Node* node, int depth, Dictionary n Preserve = true }; + // read FBX node matrix and decompose + var m = node->MTransformation.ToStrideMatrix(); + m.Decompose(out newNodeInfo.Scale, out newNodeInfo.Rotation, out newNodeInfo.Position); + allNodes.Add(newNodeInfo); + for (uint i = 0; i < node->MNumChildren; ++i) GetNodes(node->MChildren[i], depth + 1, nodeNames, allNodes); } diff --git a/sources/tools/Stride.Importer.Common/NodeInfo.cs b/sources/tools/Stride.Importer.Common/NodeInfo.cs index 0bfb15b421..5e2c1ec4bd 100644 --- a/sources/tools/Stride.Importer.Common/NodeInfo.cs +++ b/sources/tools/Stride.Importer.Common/NodeInfo.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Stride.Core.Mathematics; + namespace Stride.Importer.Common { public class NodeInfo @@ -8,5 +10,8 @@ public class NodeInfo public string Name; public int Depth; public bool Preserve; + public Vector3 Position; + public Quaternion Rotation; + public Vector3 Scale; } } From bef4618462a99874168ec3a3c78a0999c836442d Mon Sep 17 00:00:00 2001 From: noa7 Date: Sun, 21 Sep 2025 04:47:32 -0700 Subject: [PATCH 10/20] duplicate name fix --- .../ModelFromFileTemplateGenerator.cs | 67 +++++++++++++------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index 160c1b7cb8..41d3936bba 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -175,11 +175,19 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar assets.RemoveAll(a => a.Asset is ModelAsset); var perMeshAssets = new List(); + var partCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < entityInfo.Models.Count; i++) { var rawMeshName = entityInfo.Models[i].MeshName; var meshPart = SanitizePart(rawMeshName) ?? $"Mesh-{i + 1}"; - var desiredNoExt = $"{baseName}-{meshPart}"; + + // Disambiguate *before* checking the filesystem list + if (!partCounts.TryGetValue(meshPart, out var dupCount)) + dupCount = 0; + partCounts[meshPart] = dupCount + 1; + + var disambiguated = (dupCount == 0) ? meshPart : $"{meshPart}-{dupCount + 1}"; + var desiredNoExt = $"{baseName}-{disambiguated}"; // Ensure uniqueness *within this import batch* var uniqueFile = MakeUniqueFileName(desiredNoExt, assets); @@ -206,20 +214,9 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar } - // Build prefab using these per-mesh models - var perMeshByName = assets - .Where(a => a.Asset is ModelAsset) - .ToDictionary(a => a.Location.GetFileNameWithoutExtension(), a => a, StringComparer.OrdinalIgnoreCase); + // Build prefab using the per-mesh models in the same order we created them + var perMeshModels = perMeshAssets; - var perMeshModels = new List(entityInfo.Models.Count); - for (int i = 0; i < entityInfo.Models.Count; i++) - { - var rawMeshName = entityInfo.Models[i].MeshName; - var meshPart = SanitizePart(rawMeshName) ?? $"Mesh-{i + 1}"; - var expectedName = $"{baseName}-{meshPart}"; - perMeshByName.TryGetValue(expectedName, out var item); - perMeshModels.Add(item); - } for (int i = 0; i < entityInfo.Models.Count; i++) { @@ -290,7 +287,6 @@ private static void TrimModelAssetToSingleMaterialByIndex(ModelAsset asset, int asset.Materials.Add(keep); } - private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInfo entityInfo, IList perMeshModels, AssetItem allModelAsset, UDirectory targetLocation) { if (entityInfo?.Nodes == null || entityInfo.Nodes.Count == 0) @@ -324,8 +320,7 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf entities.Add(e); } - - // AFTER the for(...) that pushes entities on the stack, BEFORE attaching ModelComponents + // 1b) Apply TRS from node info to the created entities (before attaching ModelComponents) for (int i = 0; i < entityInfo.Nodes.Count; i++) { var n = entityInfo.Nodes[i]; @@ -333,12 +328,13 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf e.Transform.Position = n.Position; e.Transform.Rotation = n.Rotation; - e.Transform.Scale = n.Scale ; + e.Transform.Scale = n.Scale; } // 2) Attach ModelComponent to nodes that host meshes (match by NodeName) if (entityInfo.Models != null && entityInfo.Models.Count > 0) { + // Map node name → entity index var nodeNameToIndex = new Dictionary(StringComparer.Ordinal); for (int i = 0; i < entityInfo.Nodes.Count; i++) { @@ -347,6 +343,10 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf nodeNameToIndex.Add(n, i); } + // Track extra mesh children so we never add 2+ ModelComponents to the same Entity + var extraChildCountByNode = new Dictionary(); + + // Iterate meshes in the same order as perMeshModels for (int m = 0; m < entityInfo.Models.Count; m++) { var meshInfo = entityInfo.Models[m]; @@ -364,13 +364,39 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf // Reference the imported Model asset (no duplication) Model = AttachedReferenceManager.CreateProxyObject(modelItem.Id, modelItem.Location) }; - entities[nodeIndex].Components.Add(mc); + + // Host entity is the node entity by default + var host = entities[nodeIndex]; + + // If host already has a ModelComponent, spawn a child entity for this additional mesh + if (host.Get() != null) + { + if (!extraChildCountByNode.TryGetValue(nodeIndex, out var k)) + k = 0; + k++; + extraChildCountByNode[nodeIndex] = k; + + var childName = string.IsNullOrEmpty(meshInfo.NodeName) + ? $"Mesh_{m}" + : $"{meshInfo.NodeName}_Mesh{k}"; + + var child = new Entity(childName); + host.AddChild(child); + + // IMPORTANT: also register this child so it ends up in prefab.Hierarchy.Parts + entities.Add(child); + + host = child; // attach the component to the child + } + + host.Components.Add(mc); // guaranteed first ModelComponent on 'host' } } } root ??= entities[0]; + // Heuristic: collapse trivial wrapper root (same name/base or "RootNode") with a single child var firstNode = entityInfo.Nodes[0]; var firstName = firstNode.Name ?? string.Empty; bool looksLikeWrapper = @@ -396,10 +422,11 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf var design = new EntityDesign(e); prefab.Hierarchy.Parts.Add(e.Id, design); } + // RootParts: list of Entity (your API expects Entity here) prefab.Hierarchy.RootParts.Add(root); - prefab.Id = AssetId.New(); // <<—— ensure unique Id + prefab.Id = AssetId.New(); // ensure unique Id var prefabUrl = new UFile($"{baseName} Prefab"); return new AssetItem(UPath.Combine(targetLocation, prefabUrl), prefab); } From 049d1322f3da5c46f4609e6aaf8ecb9de5be4483 Mon Sep 17 00:00:00 2001 From: noa7 Date: Tue, 23 Sep 2025 03:24:30 -0700 Subject: [PATCH 11/20] Fixing issues raised on PR https://github.com/stride3d/stride/pull/2898#pullrequestreview-3250065326 --- .../ModelFromFileTemplateGenerator.cs | 168 +++++++----------- .../tools/Stride.Importer.3D/MeshConverter.cs | 28 +++ .../Stride.Importer.Common/EntityInfo.cs | 2 + 3 files changed, 99 insertions(+), 99 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index 41d3936bba..0d66e5e9b7 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -148,7 +148,6 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar foreach (var file in files) { - // TODO: should we allow to select the importer? var importer = AssetRegistry.FindImporterForFile(file).OfType().FirstOrDefault(); if (importer == null) { @@ -162,16 +161,15 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar var baseName = file.GetFileNameWithoutExtension(); + //Find, unique name each item before building prefab if (splitHierarchy) { var entityInfo = importer.GetEntityInfo(file, parameters.Logger, importParameters); - if (entityInfo != null && (entityInfo.Models?.Count ?? 0) > 0) + if (entityInfo?.Models.Count > 0) { - // Collect the first imported model (we'll clone/rename it per-mesh) var firstModelItem = assets.FirstOrDefault(a => a.Asset is ModelAsset); if (firstModelItem != null) { - // Remove any model assets we just imported; we'll re-add per-mesh ones with proper names assets.RemoveAll(a => a.Asset is ModelAsset); var perMeshAssets = new List(); @@ -181,7 +179,6 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar var rawMeshName = entityInfo.Models[i].MeshName; var meshPart = SanitizePart(rawMeshName) ?? $"Mesh-{i + 1}"; - // Disambiguate *before* checking the filesystem list if (!partCounts.TryGetValue(meshPart, out var dupCount)) dupCount = 0; partCounts[meshPart] = dupCount + 1; @@ -189,78 +186,47 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar var disambiguated = (dupCount == 0) ? meshPart : $"{meshPart}-{dupCount + 1}"; var desiredNoExt = $"{baseName}-{disambiguated}"; - // Ensure uniqueness *within this import batch* var uniqueFile = MakeUniqueFileName(desiredNoExt, assets); AssetItem itemForThisMesh; if (i == 0) { - // Reuse the first imported model's ASSET, but give it a fresh Id before re-wrapping it + var baseModel = (ModelAsset)firstModelItem.Asset; - baseModel.Id = AssetId.New(); // <<—— ensure unique Id + baseModel.Id = AssetId.New(); itemForThisMesh = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), baseModel); } else { - // Clone the first model's asset and give it a fresh Id var clonedAsset = AssetCloner.Clone(firstModelItem.Asset); - ((ModelAsset)clonedAsset).Id = AssetId.New(); // <<—— ensure unique Id + ((ModelAsset)clonedAsset).Id = AssetId.New(); itemForThisMesh = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), clonedAsset); } perMeshAssets.Add(itemForThisMesh); - assets.Add(itemForThisMesh); // keep list current so MakeUniqueFileName sees it + assets.Add(itemForThisMesh); } - - - // Build prefab using the per-mesh models in the same order we created them - var perMeshModels = perMeshAssets; - - - for (int i = 0; i < entityInfo.Models.Count; i++) +; + //Assign materials + foreach (var item in perMeshAssets) { - var item = perMeshModels[i]; - if (item?.Asset is ModelAsset modelAsset) - { - TrimModelAssetToSingleMaterialByIndex(modelAsset, - entityInfo.Models[i].OriginalMaterialIndex); // <- the index from step 1 - } - } - - - // No combined "(All)" model when splitting; Prefab is the combined representation - AssetItem allModelAsset = null; + ResetMaterialsOnPrefabItems(item, entityInfo); + } var prefabAssetItem = BuildPrefabForSplitHierarchy( baseName, entityInfo, - perMeshModels, - allModelAsset, + perMeshAssets, parameters.TargetLocation); + if (prefabAssetItem != null) assets.Add(prefabAssetItem); } } } - else - { - // Split OFF: keep a single Model named exactly after the source file (no "(All)") - var idx = assets.FindIndex(a => a.Asset is ModelAsset); - if (idx >= 0) - { - var old = assets[idx]; - assets.RemoveAt(idx); - - // Assign a fresh Id to the single model to avoid any Id collisions - ((ModelAsset)old.Asset).Id = AssetId.New(); // <<—— ensure unique Id - - var uniqueFile = MakeUniqueFileName(baseName, assets); - var renamed = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), old.Asset); - assets.Insert(idx, renamed); - } - } + foreach (var model in assets.Select(x => x.Asset).OfType()) { @@ -270,29 +236,51 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar } } - // Create unique names amongst the list of assets importedAssets.AddRange(MakeUniqueNames(assets)); } return importedAssets; } - private static void TrimModelAssetToSingleMaterialByIndex(ModelAsset asset, int keepIndex) + private static void ResetMaterialsOnPrefabItems(AssetItem assetItem, EntityInfo entityInfo) { - if (asset?.Materials == null) return; - if (keepIndex < 0 || keepIndex >= asset.Materials.Count) return; + ModelAsset asset = assetItem?.Asset as ModelAsset; + if (asset == null || asset.Materials==null) + return; + + + string sourceName = Path.GetFileNameWithoutExtension(asset.Source); + string assetName = assetItem.Location.ToString(); + + string underlyingMeshName= assetName.Substring(sourceName.Length+1); + var underlyingModel=entityInfo.Models.Where(C=>C.MeshName==underlyingMeshName).FirstOrDefault(); + var nodeContainingMesh=entityInfo.Nodes.Where(c=>c.Name== underlyingModel.NodeName).FirstOrDefault(); + + var materialIndices=entityInfo.NodeNameToMaterialIndices?.Where(c=>c.Key== nodeContainingMesh.Name)?.FirstOrDefault().Value; + + if(materialIndices?.Count()< 1) + return; + + List materialsToApply = null; + for (int i = 0; i < asset.Materials.Count; i++) + { + if (materialIndices.Contains(i)) + { + (materialsToApply??=new List()).Add(asset.Materials[i]); + } + } - var keep = asset.Materials[keepIndex]; asset.Materials.Clear(); - asset.Materials.Add(keep); + materialsToApply?.ForEach(_mat => asset.Materials.Add(_mat)); + } - private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInfo entityInfo, IList perMeshModels, AssetItem allModelAsset, UDirectory targetLocation) + private static AssetItem? BuildPrefabForSplitHierarchy(string baseName, EntityInfo entityInfo, IList perMeshModels, UDirectory targetLocation) { if (entityInfo?.Nodes == null || entityInfo.Nodes.Count == 0) return null; - // 1) Create entities in pre-order and rebuild parent/child relations using Depth + // Step 1. Set up entites transversing the tree from imported entityinfo var entities = new List(entityInfo.Nodes.Count); var stack = new Stack(); Entity root = null; @@ -302,13 +290,11 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf var node = entityInfo.Nodes[i]; var e = new Entity(string.IsNullOrEmpty(node.Name) ? $"Node_{i}" : node.Name); - // Keep the stack at (node.Depth) entries so parent is at depth-1 while (stack.Count > node.Depth) stack.Pop(); if (stack.Count == 0) - { - // Depth 0 → root + { root = e; } else @@ -320,21 +306,18 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf entities.Add(e); } - // 1b) Apply TRS from node info to the created entities (before attaching ModelComponents) + + // Step 2. Apply TRS on each entity to that of imported source file for (int i = 0; i < entityInfo.Nodes.Count; i++) { - var n = entityInfo.Nodes[i]; - var e = entities[i]; - - e.Transform.Position = n.Position; - e.Transform.Rotation = n.Rotation; - e.Transform.Scale = n.Scale; + entities[i].Transform.Position = entityInfo.Nodes[i].Position; + entities[i].Transform.Rotation = entityInfo.Nodes[i].Rotation; + entities[i].Transform.Scale = entityInfo.Nodes[i].Scale; } - // 2) Attach ModelComponent to nodes that host meshes (match by NodeName) - if (entityInfo.Models != null && entityInfo.Models.Count > 0) + //Step 3. Attach ModelComponent and set up hierachical order + if (entityInfo?.Models?.Count > 0) { - // Map node name → entity index var nodeNameToIndex = new Dictionary(StringComparer.Ordinal); for (int i = 0; i < entityInfo.Nodes.Count; i++) { @@ -343,53 +326,46 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf nodeNameToIndex.Add(n, i); } - // Track extra mesh children so we never add 2+ ModelComponents to the same Entity var extraChildCountByNode = new Dictionary(); - // Iterate meshes in the same order as perMeshModels for (int m = 0; m < entityInfo.Models.Count; m++) { - var meshInfo = entityInfo.Models[m]; - if (string.IsNullOrEmpty(meshInfo.NodeName)) - continue; - if (!nodeNameToIndex.TryGetValue(meshInfo.NodeName, out var nodeIndex)) - continue; + var meshInfo = entityInfo.Models[m]; - var modelItem = (m >= 0 && m < perMeshModels.Count) ? perMeshModels[m] : null; + var nodeIndex = nodeNameToIndex[meshInfo.NodeName]; + + var modelItem = perMeshModels[m]; if (modelItem?.Asset is ModelAsset) { var mc = new ModelComponent { - // Reference the imported Model asset (no duplication) Model = AttachedReferenceManager.CreateProxyObject(modelItem.Id, modelItem.Location) }; - // Host entity is the node entity by default var host = entities[nodeIndex]; - // If host already has a ModelComponent, spawn a child entity for this additional mesh + if (host.Get() != null) { - if (!extraChildCountByNode.TryGetValue(nodeIndex, out var k)) - k = 0; - k++; - extraChildCountByNode[nodeIndex] = k; + if (!extraChildCountByNode.TryGetValue(nodeIndex, out var counter)) + counter = 0; + counter++; + extraChildCountByNode[nodeIndex] = counter; var childName = string.IsNullOrEmpty(meshInfo.NodeName) ? $"Mesh_{m}" - : $"{meshInfo.NodeName}_Mesh{k}"; + : $"{meshInfo.NodeName}_Mesh{counter}"; var child = new Entity(childName); host.AddChild(child); - // IMPORTANT: also register this child so it ends up in prefab.Hierarchy.Parts entities.Add(child); - host = child; // attach the component to the child + host = child; } - host.Components.Add(mc); // guaranteed first ModelComponent on 'host' + host.Components.Add(mc); } } } @@ -400,30 +376,26 @@ private static AssetItem BuildPrefabForSplitHierarchy(string baseName, EntityInf var firstNode = entityInfo.Nodes[0]; var firstName = firstNode.Name ?? string.Empty; bool looksLikeWrapper = - string.IsNullOrWhiteSpace(firstName) - || firstName.Equals(baseName, StringComparison.OrdinalIgnoreCase) - || firstName.Equals("RootNode", StringComparison.OrdinalIgnoreCase); + firstName.Equals(baseName, StringComparison.OrdinalIgnoreCase) + || + firstName.Equals("RootNode", StringComparison.OrdinalIgnoreCase); - // Use the constructed runtime tree to check children count + if (looksLikeWrapper && root.Transform.Children.Count == 1) { var onlyChild = root.Transform.Children[0].Entity; - // Detach so it becomes the actual root onlyChild.Transform.Parent = null; root = onlyChild; } - // 3) Build Prefab: register ALL entities in Parts, add ROOT entity to RootParts var prefab = new PrefabAsset(); - // Parts: Guid -> EntityDesign (use the entity's Id as the key) foreach (var e in entities) { var design = new EntityDesign(e); prefab.Hierarchy.Parts.Add(e.Id, design); } - // RootParts: list of Entity (your API expects Entity here) prefab.Hierarchy.RootParts.Add(root); prefab.Id = AssetId.New(); // ensure unique Id @@ -440,8 +412,6 @@ private static string SanitizePart(string s) return string.IsNullOrEmpty(clean) ? null : clean; } - // Ensure a *file-name without extension* is unique within the current 'assets' list. - // It yields: name, name-1, name-2, ... private static UFile MakeUniqueFileName(string desiredNameNoExt, List assets) { var existing = new HashSet( @@ -453,7 +423,7 @@ private static UFile MakeUniqueFileName(string desiredNameNoExt, List while (existing.Contains(name)) name = $"{desiredNameNoExt}-{++i}"; - return new UFile(name); // filename only; caller will UPath.Combine with target dir + return new UFile(name); } } diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index 00577e078f..a3e844b31c 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -165,6 +165,34 @@ public unsafe EntityInfo ExtractEntity(string inputFilename, string outputFilena if (extractTextureDependencies) entityInfo.TextureDependencies = ExtractTextureDependencies(scene); + // Aggregate materials per NODE not per mesh to account for multiple material limitation on assimp + if (entityInfo.Models != null && entityInfo.Models.Count > 0) + { + foreach (var m in entityInfo.Models) + { + var key = string.IsNullOrEmpty(m.NodeName) ? m.MeshName : m.NodeName; + if (string.IsNullOrEmpty(key)) + continue; + + int matIndex = -1; + var t = m.GetType(); + var prop = t.GetProperty("OriginalMaterialIndex") ?? t.GetProperty("MaterialIndex"); + if (prop != null) + matIndex = (int)prop.GetValue(m); + + if (matIndex < 0) continue; + + if (!entityInfo.NodeNameToMaterialIndices.TryGetValue(key, out var list)) + { + list = new List(); + entityInfo.NodeNameToMaterialIndices[key] = list; + } + + if (!list.Contains(matIndex)) + list.Add(matIndex); + } + } + return entityInfo; } catch (Exception ex) diff --git a/sources/tools/Stride.Importer.Common/EntityInfo.cs b/sources/tools/Stride.Importer.Common/EntityInfo.cs index 21d33ccef2..ad5db2ba82 100644 --- a/sources/tools/Stride.Importer.Common/EntityInfo.cs +++ b/sources/tools/Stride.Importer.Common/EntityInfo.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; using System.Collections.Generic; using Stride.Assets.Materials; @@ -14,5 +15,6 @@ public class EntityInfo public List Models; public List Nodes; public List MaterialOrder { get; set; } + public Dictionary> NodeNameToMaterialIndices { get; set; } = new Dictionary>(StringComparer.Ordinal); } } From 52775d206e78b1a02790e253e3c3d0b6f8fc6899 Mon Sep 17 00:00:00 2001 From: noa7 Date: Fri, 26 Sep 2025 09:56:42 -0700 Subject: [PATCH 12/20] Clean ResetMaterialsOnPrefabItems and applying TRS prefab items at MeshConvertor Level --- .../ModelFromFileTemplateGenerator.cs | 23 ++++++++----------- .../engine/Stride.Assets.Models/ModelAsset.cs | 3 +++ .../tools/Stride.Importer.3D/MeshConverter.cs | 9 ++++---- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index 0d66e5e9b7..3c331fd369 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -191,23 +191,24 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar AssetItem itemForThisMesh; if (i == 0) - { - + { var baseModel = (ModelAsset)firstModelItem.Asset; - baseModel.Id = AssetId.New(); + baseModel.Id = AssetId.New(); + baseModel.MeshName = rawMeshName; itemForThisMesh = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), baseModel); } else { var clonedAsset = AssetCloner.Clone(firstModelItem.Asset); - ((ModelAsset)clonedAsset).Id = AssetId.New(); + ((ModelAsset)clonedAsset).Id = AssetId.New(); + ((ModelAsset)clonedAsset).MeshName = rawMeshName; itemForThisMesh = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), clonedAsset); } - + perMeshAssets.Add(itemForThisMesh); assets.Add(itemForThisMesh); } -; + //Assign materials foreach (var item in perMeshAssets) { @@ -248,12 +249,7 @@ private static void ResetMaterialsOnPrefabItems(AssetItem assetItem, EntityInfo if (asset == null || asset.Materials==null) return; - - string sourceName = Path.GetFileNameWithoutExtension(asset.Source); - string assetName = assetItem.Location.ToString(); - - string underlyingMeshName= assetName.Substring(sourceName.Length+1); - var underlyingModel=entityInfo.Models.Where(C=>C.MeshName==underlyingMeshName).FirstOrDefault(); + var underlyingModel=entityInfo.Models.Where(C=>C.MeshName==asset.MeshName).FirstOrDefault(); var nodeContainingMesh=entityInfo.Nodes.Where(c=>c.Name== underlyingModel.NodeName).FirstOrDefault(); var materialIndices=entityInfo.NodeNameToMaterialIndices?.Where(c=>c.Key== nodeContainingMesh.Name)?.FirstOrDefault().Value; @@ -271,8 +267,7 @@ private static void ResetMaterialsOnPrefabItems(AssetItem assetItem, EntityInfo } asset.Materials.Clear(); - materialsToApply?.ForEach(_mat => asset.Materials.Add(_mat)); - + materialsToApply?.ForEach(_mat => asset.Materials.Add(_mat)); } private static AssetItem? BuildPrefabForSplitHierarchy(string baseName, EntityInfo entityInfo, IList perMeshModels, UDirectory targetLocation) diff --git a/sources/engine/Stride.Assets.Models/ModelAsset.cs b/sources/engine/Stride.Assets.Models/ModelAsset.cs index 123d70e6c9..a87aa58a1e 100644 --- a/sources/engine/Stride.Assets.Models/ModelAsset.cs +++ b/sources/engine/Stride.Assets.Models/ModelAsset.cs @@ -94,5 +94,8 @@ public sealed class ModelAsset : Asset, IAssetWithSource, IModelAsset /// [DataMemberIgnore] public override UFile MainSource => Source; + + [DataMemberIgnore] + public string MeshName { get; set; } = null; } } diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index a3e844b31c..2f7e4edde3 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -344,8 +344,9 @@ private unsafe Model ConvertAssimpScene(Scene* scene) modelData.Meshes.Add(nodeMeshData); } } - // Neutralize node binding for per-mesh export (split-hierarchy case) - if (keptMeshIndex >= 0) + // Incase split heirarchy, each mesh is positioned and rotated at entity level, scaled to account for assimp + // stride conversion factor of 0.01f + if (keptMeshIndex >= 0) { // Replace node table with a single identity root nodes.Clear(); @@ -358,11 +359,11 @@ private unsafe Model ConvertAssimpScene(Scene* scene) { Position = Vector3.Zero, Rotation = Quaternion.Identity, - Scale = Vector3.One + Scale = new Vector3(100,100,100) } }); - // Re-bind all meshes to that root (index 0) + for (int mi = 0; mi < modelData.Meshes.Count; mi++) modelData.Meshes[mi].NodeIndex = 0; } From 46e4edf9189b862122caa15190cad0bf4b02f4e5 Mon Sep 17 00:00:00 2001 From: noa7 Date: Fri, 26 Sep 2025 10:07:20 -0700 Subject: [PATCH 13/20] cleanup --- .../Templates/ModelFromFileTemplateGenerator.cs | 1 - sources/tools/Stride.Importer.3D/MeshConverter.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index 3c331fd369..0c3d3b23f7 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -204,7 +204,6 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar ((ModelAsset)clonedAsset).MeshName = rawMeshName; itemForThisMesh = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), clonedAsset); } - perMeshAssets.Add(itemForThisMesh); assets.Add(itemForThisMesh); } diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index 2f7e4edde3..e76ac3cae1 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -362,7 +362,6 @@ private unsafe Model ConvertAssimpScene(Scene* scene) Scale = new Vector3(100,100,100) } }); - for (int mi = 0; mi < modelData.Meshes.Count; mi++) modelData.Meshes[mi].NodeIndex = 0; From d1bc01a32079983581b39ffe87e337352804447a Mon Sep 17 00:00:00 2001 From: noa7 Date: Sat, 27 Sep 2025 06:59:51 -0700 Subject: [PATCH 14/20] Addressing PR issues Eideren #2898 --- .../ModelFromFileTemplateGenerator.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index 0c3d3b23f7..d445631fc2 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -133,7 +133,7 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar var importAnimations = parameters.Tags.Get(ImportAnimationsKey); var importSkeleton = parameters.Tags.Get(ImportSkeletonKey); var skeletonToReuse = parameters.Tags.Get(SkeletonToUseKey); - var splitHierarchy = parameters.Tags.Get(SplitHierarchyKey); // <-- you read it here + var splitHierarchy = parameters.Tags.Get(SplitHierarchyKey); var importParameters = new AssetImporterParameters { Logger = parameters.Logger }; importParameters.InputParameters.Set(ModelAssetImporter.DeduplicateMaterialsKey, deduplicateMaterials); @@ -148,6 +148,7 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar foreach (var file in files) { + // TODO: should we allow to select the importer? var importer = AssetRegistry.FindImporterForFile(file).OfType().FirstOrDefault(); if (importer == null) { @@ -251,22 +252,16 @@ private static void ResetMaterialsOnPrefabItems(AssetItem assetItem, EntityInfo var underlyingModel=entityInfo.Models.Where(C=>C.MeshName==asset.MeshName).FirstOrDefault(); var nodeContainingMesh=entityInfo.Nodes.Where(c=>c.Name== underlyingModel.NodeName).FirstOrDefault(); - var materialIndices=entityInfo.NodeNameToMaterialIndices?.Where(c=>c.Key== nodeContainingMesh.Name)?.FirstOrDefault().Value; - - if(materialIndices?.Count()< 1) - return; + entityInfo.NodeNameToMaterialIndices.TryGetValue(nodeContainingMesh.Name, out var materialIndices); - List materialsToApply = null; - for (int i = 0; i < asset.Materials.Count; i++) + if (materialIndices?.Count()< 1) + return; + + for (int i = asset.Materials.Count - 1; i >= 0; i--) { - if (materialIndices.Contains(i)) - { - (materialsToApply??=new List()).Add(asset.Materials[i]); - } + if (!materialIndices.Contains(i)) + asset.Materials.RemoveAt(i); } - - asset.Materials.Clear(); - materialsToApply?.ForEach(_mat => asset.Materials.Add(_mat)); } private static AssetItem? BuildPrefabForSplitHierarchy(string baseName, EntityInfo entityInfo, IList perMeshModels, UDirectory targetLocation) @@ -397,7 +392,7 @@ private static void ResetMaterialsOnPrefabItems(AssetItem assetItem, EntityInfo return new AssetItem(UPath.Combine(targetLocation, prefabUrl), prefab); } - private static string SanitizePart(string s) + private static string? SanitizePart(string? s) { if (string.IsNullOrWhiteSpace(s)) return null; From 93f5a3484d0ab911c7acad27a29f3ee7797bc51b Mon Sep 17 00:00:00 2001 From: noa7 Date: Sat, 27 Sep 2025 07:07:47 -0700 Subject: [PATCH 15/20] More Fixes #2898 --- .../Templates/ModelFromFileTemplateGenerator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index d445631fc2..f547aaaadb 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -249,12 +249,12 @@ private static void ResetMaterialsOnPrefabItems(AssetItem assetItem, EntityInfo if (asset == null || asset.Materials==null) return; - var underlyingModel=entityInfo.Models.Where(C=>C.MeshName==asset.MeshName).FirstOrDefault(); - var nodeContainingMesh=entityInfo.Nodes.Where(c=>c.Name== underlyingModel.NodeName).FirstOrDefault(); + var underlyingModel=entityInfo.Models.FirstOrDefault(c => c.MeshName == asset.MeshName); + var nodeContainingMesh=entityInfo.Nodes.FirstOrDefault(c => c.Name == underlyingModel.NodeName); entityInfo.NodeNameToMaterialIndices.TryGetValue(nodeContainingMesh.Name, out var materialIndices); - if (materialIndices?.Count()< 1) + if (materialIndices?.Count< 1) return; for (int i = asset.Materials.Count - 1; i >= 0; i--) From 34112cd4211fff1ec891cda836b4400ac33640c1 Mon Sep 17 00:00:00 2001 From: noa7 Date: Sun, 12 Oct 2025 02:28:07 +0000 Subject: [PATCH 16/20] Remove Filename based import determination --- .../ImportModelCommand.Animation.cs | 1 + .../ImportThreeDCommand.cs | 3 + .../ModelAssetCompiler.cs | 1 + .../tools/Stride.Importer.3D/MeshConverter.cs | 56 +++++-------------- 4 files changed, 18 insertions(+), 43 deletions(-) diff --git a/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs b/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs index 299d2cf132..190827b8f9 100644 --- a/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs +++ b/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs @@ -26,6 +26,7 @@ public partial class ImportModelCommand public bool ImportCustomAttributes { get; set; } public int AnimationStack { get; set; } + public string KeepOnlyMeshName { get; set; } private unsafe object ExportAnimation(ICommandContext commandContext, ContentManager contentManager, bool failOnEmptyAnimation) { diff --git a/sources/engine/Stride.Assets.Models/ImportThreeDCommand.cs b/sources/engine/Stride.Assets.Models/ImportThreeDCommand.cs index 339764ed1c..f2f805372b 100644 --- a/sources/engine/Stride.Assets.Models/ImportThreeDCommand.cs +++ b/sources/engine/Stride.Assets.Models/ImportThreeDCommand.cs @@ -41,6 +41,9 @@ protected override Model LoadModel(ICommandContext commandContext, ContentManage { var converter = CreateMeshConverter(commandContext); + if (!string.IsNullOrWhiteSpace(KeepOnlyMeshName)) + converter.KeepOnlyMeshByName(KeepOnlyMeshName); + // Note: FBX exporter uses Materials for the mapping, but Assimp already uses indices so we can reuse them // We should still unify the behavior to be more consistent at some point (i.e. if model was changed on the HDD but not in the asset). // This should probably be better done during a large-scale FBX/Assimp refactoring. diff --git a/sources/engine/Stride.Assets.Models/ModelAssetCompiler.cs b/sources/engine/Stride.Assets.Models/ModelAssetCompiler.cs index c3baaaa0b9..4b005858b1 100644 --- a/sources/engine/Stride.Assets.Models/ModelAssetCompiler.cs +++ b/sources/engine/Stride.Assets.Models/ModelAssetCompiler.cs @@ -67,6 +67,7 @@ protected override void Prepare(AssetCompilerContext context, AssetItem assetIte importModelCommand.MergeMeshes = asset.MergeMeshes; importModelCommand.DeduplicateMaterials = asset.DeduplicateMaterials; importModelCommand.ModelModifiers = asset.Modifiers; + importModelCommand.KeepOnlyMeshName = asset.MeshName; if (skeleton != null) { diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index e76ac3cae1..c67ed40db1 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -74,48 +74,7 @@ private void ResetConversionData() { textureNameCount.Clear(); } - - private void DecideKeptMeshIndexFromOutput() - { - keptMeshIndex = -1; - keptMeshNameHint = null; - - try - { - var name = System.IO.Path.GetFileNameWithoutExtension(vfsOutputFilename); - if (string.IsNullOrEmpty(name)) - return; - - // "(All)" => all meshes - if (name.EndsWith(" (All)", StringComparison.OrdinalIgnoreCase)) - return; - - // "(Mesh X)" => keep mesh X-1 (legacy) - const string tag = " (Mesh "; - if (name.EndsWith(")", StringComparison.Ordinal)) - { - var start = name.LastIndexOf(tag, StringComparison.OrdinalIgnoreCase); - if (start >= 0) - { - var numStr = name.Substring(start + tag.Length, name.Length - (start + tag.Length) - 1); - if (int.TryParse(numStr, out var oneBased) && oneBased >= 1) - { - keptMeshIndex = oneBased - 1; - return; - } - } - } - - // Otherwise: remember the full output name; we will try to match a mesh by name later - keptMeshNameHint = name; - } - catch - { - keptMeshIndex = -1; - keptMeshNameHint = null; - } - } - + public unsafe EntityInfo ExtractEntity(string inputFilename, string outputFilename, bool extractTextureDependencies, bool deduplicateMaterials) { try @@ -234,6 +193,18 @@ public unsafe Rendering.Skeleton ConvertSkeleton(string inputFilename, string ou return ProcessSkeleton(scene); } + public void KeepOnlyMeshByName(string name) + { + keptMeshIndex = -1; + keptMeshNameHint = string.IsNullOrWhiteSpace(name) ? null : name; + } + + public void KeepOnlyMeshByIndex(int index) + { + keptMeshNameHint = null; + keptMeshIndex = index >= 0 ? index : -1; + } + private unsafe Scene* Initialize(string inputFilename, string outputFilename, uint importFlags, aiPostProcessSteps postProcessFlags) { ResetConversionData(); @@ -241,7 +212,6 @@ public unsafe Rendering.Skeleton ConvertSkeleton(string inputFilename, string ou vfsInputFilename = inputFilename; vfsOutputFilename = outputFilename; vfsInputPath = VirtualFileSystem.GetParentFolder(inputFilename); - DecideKeptMeshIndexFromOutput(); var propStore = assimp.CreatePropertyStore(); assimp.SetImportPropertyInteger(propStore, "IMPORT_FBX_PRESERVE_PIVOTS", 0); // Trade some issues for others, see: https://github.com/assimp/assimp/issues/894, https://github.com/assimp/assimp/issues/1974 From 0bf7e425af750e01427baae65f67ea90fc0b01cc Mon Sep 17 00:00:00 2001 From: noa7 Date: Mon, 13 Oct 2025 14:21:34 +0000 Subject: [PATCH 17/20] Fixing naming issues, storing mesh id as key --- .../ModelFromFileTemplateGenerator.cs | 2 ++ .../ImportModelCommand.Animation.cs | 3 +-- .../ImportModelCommand.Model.cs | 2 ++ .../ImportThreeDCommand.cs | 6 ++--- .../engine/Stride.Assets.Models/ModelAsset.cs | 3 +++ .../ModelAssetCompiler.cs | 2 +- .../tools/Stride.Importer.3D/MeshConverter.cs | 26 +++++++++---------- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs index f547aaaadb..f561cf37bd 100644 --- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs +++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs @@ -196,6 +196,7 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar var baseModel = (ModelAsset)firstModelItem.Asset; baseModel.Id = AssetId.New(); baseModel.MeshName = rawMeshName; + baseModel.KepMeshIndex = i; itemForThisMesh = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), baseModel); } else @@ -203,6 +204,7 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar var clonedAsset = AssetCloner.Clone(firstModelItem.Asset); ((ModelAsset)clonedAsset).Id = AssetId.New(); ((ModelAsset)clonedAsset).MeshName = rawMeshName; + ((ModelAsset)clonedAsset).KepMeshIndex = i; itemForThisMesh = new AssetItem(UPath.Combine(parameters.TargetLocation, uniqueFile), clonedAsset); } perMeshAssets.Add(itemForThisMesh); diff --git a/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs b/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs index 190827b8f9..dcda93483f 100644 --- a/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs +++ b/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs @@ -26,8 +26,7 @@ public partial class ImportModelCommand public bool ImportCustomAttributes { get; set; } public int AnimationStack { get; set; } - public string KeepOnlyMeshName { get; set; } - + private unsafe object ExportAnimation(ICommandContext commandContext, ContentManager contentManager, bool failOnEmptyAnimation) { // Read from model file diff --git a/sources/engine/Stride.Assets.Models/ImportModelCommand.Model.cs b/sources/engine/Stride.Assets.Models/ImportModelCommand.Model.cs index c63f463e9e..9f72e64180 100644 --- a/sources/engine/Stride.Assets.Models/ImportModelCommand.Model.cs +++ b/sources/engine/Stride.Assets.Models/ImportModelCommand.Model.cs @@ -31,6 +31,8 @@ public partial class ImportModelCommand public List Materials { get; set; } public string EffectName { get; set; } + public int KeptMeshIndex { get; set; } = -1; + public List ModelModifiers { get; set; } /// diff --git a/sources/engine/Stride.Assets.Models/ImportThreeDCommand.cs b/sources/engine/Stride.Assets.Models/ImportThreeDCommand.cs index f2f805372b..ae8aab2569 100644 --- a/sources/engine/Stride.Assets.Models/ImportThreeDCommand.cs +++ b/sources/engine/Stride.Assets.Models/ImportThreeDCommand.cs @@ -40,10 +40,8 @@ private Stride.Importer.ThreeD.MeshConverter CreateMeshConverter(ICommandContext protected override Model LoadModel(ICommandContext commandContext, ContentManager contentManager) { var converter = CreateMeshConverter(commandContext); - - if (!string.IsNullOrWhiteSpace(KeepOnlyMeshName)) - converter.KeepOnlyMeshByName(KeepOnlyMeshName); - + converter.KeepOnlyMeshByIndex(((ImportThreeDCommand)commandContext.CurrentCommand).KeptMeshIndex); + // Note: FBX exporter uses Materials for the mapping, but Assimp already uses indices so we can reuse them // We should still unify the behavior to be more consistent at some point (i.e. if model was changed on the HDD but not in the asset). // This should probably be better done during a large-scale FBX/Assimp refactoring. diff --git a/sources/engine/Stride.Assets.Models/ModelAsset.cs b/sources/engine/Stride.Assets.Models/ModelAsset.cs index a87aa58a1e..59efc9ebdc 100644 --- a/sources/engine/Stride.Assets.Models/ModelAsset.cs +++ b/sources/engine/Stride.Assets.Models/ModelAsset.cs @@ -91,6 +91,9 @@ public sealed class ModelAsset : Asset, IAssetWithSource, IModelAsset [Category] public List Modifiers { get; } = new List(); + [DataMember(55)] + public int KepMeshIndex { get; set; } = -1; + /// [DataMemberIgnore] public override UFile MainSource => Source; diff --git a/sources/engine/Stride.Assets.Models/ModelAssetCompiler.cs b/sources/engine/Stride.Assets.Models/ModelAssetCompiler.cs index 4b005858b1..a84ffa2bdd 100644 --- a/sources/engine/Stride.Assets.Models/ModelAssetCompiler.cs +++ b/sources/engine/Stride.Assets.Models/ModelAssetCompiler.cs @@ -67,7 +67,7 @@ protected override void Prepare(AssetCompilerContext context, AssetItem assetIte importModelCommand.MergeMeshes = asset.MergeMeshes; importModelCommand.DeduplicateMaterials = asset.DeduplicateMaterials; importModelCommand.ModelModifiers = asset.Modifiers; - importModelCommand.KeepOnlyMeshName = asset.MeshName; + importModelCommand.KeptMeshIndex = asset.KepMeshIndex; if (skeleton != null) { diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index c67ed40db1..0f729a856f 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -74,7 +74,19 @@ private void ResetConversionData() { textureNameCount.Clear(); } - + + public void KeepOnlyMeshByName(string name) + { + keptMeshIndex = -1; + keptMeshNameHint = string.IsNullOrWhiteSpace(name) ? null : name; + } + + public void KeepOnlyMeshByIndex(int index) + { + keptMeshNameHint = null; + keptMeshIndex = index >= 0 ? index : -1; + } + public unsafe EntityInfo ExtractEntity(string inputFilename, string outputFilename, bool extractTextureDependencies, bool deduplicateMaterials) { try @@ -193,18 +205,6 @@ public unsafe Rendering.Skeleton ConvertSkeleton(string inputFilename, string ou return ProcessSkeleton(scene); } - public void KeepOnlyMeshByName(string name) - { - keptMeshIndex = -1; - keptMeshNameHint = string.IsNullOrWhiteSpace(name) ? null : name; - } - - public void KeepOnlyMeshByIndex(int index) - { - keptMeshNameHint = null; - keptMeshIndex = index >= 0 ? index : -1; - } - private unsafe Scene* Initialize(string inputFilename, string outputFilename, uint importFlags, aiPostProcessSteps postProcessFlags) { ResetConversionData(); From 02ae3cc76f34253a3335496901f131828a73040a Mon Sep 17 00:00:00 2001 From: noa7 Date: Tue, 14 Oct 2025 03:40:53 +0000 Subject: [PATCH 18/20] comm --- .../Stride.Assets.Presentation/ViewModel/ModelViewModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs index 7c48da0745..88b73cb12f 100644 --- a/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs @@ -53,6 +53,9 @@ protected override void PrepareImporterInputParametersForUpdateFromSource(Proper protected override void UpdateAssetFromSource(ModelAsset assetToMerge) { + if (Asset.KepMeshIndex > -1) + return; + // Create a dictionary containing all new and old materials, favoring old ones to maintain existing references var dictionary = assetToMerge.Materials.ToDictionary(x => x.Name, x => x); Asset.Materials.ForEach(x => dictionary[x.Name] = x); From 3a4075687806f4ef00dce50a06afc372b5be72ea Mon Sep 17 00:00:00 2001 From: noa7 Date: Tue, 14 Oct 2025 03:44:55 +0000 Subject: [PATCH 19/20] Mesh ModelAsset reimport --- .../Stride.Assets.Presentation/ViewModel/ModelViewModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs index 88b73cb12f..ad3377f047 100644 --- a/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs @@ -53,6 +53,7 @@ protected override void PrepareImporterInputParametersForUpdateFromSource(Proper protected override void UpdateAssetFromSource(ModelAsset assetToMerge) { + //Keep the material assignment unchanged for sub mesh model asset if (Asset.KepMeshIndex > -1) return; From cd779bf8085f763ef60ed1114b9dda104da0d325 Mon Sep 17 00:00:00 2001 From: noa7 Date: Fri, 17 Oct 2025 08:28:00 +0000 Subject: [PATCH 20/20] Fixing multiple material issue --- .../tools/Stride.Importer.3D/MeshConverter.cs | 124 ++++++++++++++---- .../Stride.Importer.Common/MeshParameters.cs | 6 +- 2 files changed, 98 insertions(+), 32 deletions(-) diff --git a/sources/tools/Stride.Importer.3D/MeshConverter.cs b/sources/tools/Stride.Importer.3D/MeshConverter.cs index 0f729a856f..d8ea7628ce 100644 --- a/sources/tools/Stride.Importer.3D/MeshConverter.cs +++ b/sources/tools/Stride.Importer.3D/MeshConverter.cs @@ -36,8 +36,6 @@ public class MeshConverter private int keptMeshIndex = -1; // default to ALL unless name says otherwise private string keptMeshNameHint = null; - private bool IsKeptMeshIndex(int meshIndex) => keptMeshIndex < 0 || meshIndex == keptMeshIndex; - static MeshConverter() { if (Platform.Type == PlatformType.Windows) @@ -145,22 +143,20 @@ public unsafe EntityInfo ExtractEntity(string inputFilename, string outputFilena if (string.IsNullOrEmpty(key)) continue; - int matIndex = -1; - var t = m.GetType(); - var prop = t.GetProperty("OriginalMaterialIndex") ?? t.GetProperty("MaterialIndex"); - if (prop != null) - matIndex = (int)prop.GetValue(m); - - if (matIndex < 0) continue; - if (!entityInfo.NodeNameToMaterialIndices.TryGetValue(key, out var list)) { list = new List(); entityInfo.NodeNameToMaterialIndices[key] = list; } - if (!list.Contains(matIndex)) - list.Add(matIndex); + if (m.MaterialIndices != null) + { + foreach (var idx in m.MaterialIndices) + { + if (idx >= 0 && !list.Contains(idx)) + list.Add(idx); + } + } } } @@ -231,6 +227,27 @@ public unsafe Rendering.Skeleton ConvertSkeleton(string inputFilename, string ou return scene; } + private unsafe bool FindOwningNodeAndCollectSiblingMeshes(Node* node, uint targetMesh, HashSet outSet) + { + for (uint m = 0; m < node->MNumMeshes; ++m) + { + if (node->MMeshes[m] == targetMesh) + { + // Collect ALL meshes attached to this node + for (uint k = 0; k < node->MNumMeshes; ++k) + outSet.Add((int)node->MMeshes[k]); + return true; + } + } + + // Recurse + for (uint c = 0; c < node->MNumChildren; ++c) + if (FindOwningNodeAndCollectSiblingMeshes(node->MChildren[c], targetMesh, outSet)) + return true; + + return false; + } + private unsafe Model ConvertAssimpScene(Scene* scene) { modelData = new Model(); @@ -238,6 +255,39 @@ private unsafe Model ConvertAssimpScene(Scene* scene) var meshNames = new Dictionary(); GenerateMeshNames(scene, meshNames); + HashSet keptSet = null; + if (keptMeshIndex >= 0) + { + keptSet = new HashSet(); + FindOwningNodeAndCollectSiblingMeshes(scene->MRootNode, (uint)keptMeshIndex, keptSet); + } + + bool IsKept(int meshIndex, out int index) + { + index = -1; + + if (keptSet == null) + { + index = 0; + return true; + } + + int i = 0; + foreach (var item in keptSet) + { + if (item == meshIndex) + { + index = i; + return true; + } + i++; + } + + return false; + } + + bool MeshFilter(int meshIndex) => IsKept(meshIndex, out _); + // If output asset name equals a mesh name, or ends with "-", select that mesh. if (!string.IsNullOrEmpty(keptMeshNameHint)) { @@ -263,16 +313,17 @@ private unsafe Model ConvertAssimpScene(Scene* scene) // register the nodes and fill hierarchy var meshIndexToNodeIndex = new Dictionary>(); var nodePointerToNodeIndex = new Dictionary(); - RegisterNodes(scene->MRootNode, -1, nodeNames, meshIndexToNodeIndex, nodePointerToNodeIndex); + RegisterNodes(scene->MRootNode, -1, nodeNames, meshIndexToNodeIndex, nodePointerToNodeIndex,null, MeshFilter); // Map the Bone pointers to their corresponding Node pointer var bonePointerToNodePointerMap = GenerateBoneToNodeMap(scene, nodePointerToNodeIndex, duplicateNodeNameToNodePointers); // meshes for (var i = 0; i < scene->MNumMeshes; ++i) - { - if (!IsKeptMeshIndex(i)) + { + if (!IsKept(i, out int matIndex)) continue; + if (!meshIndexToNodeIndex.TryGetValue(i, out var value)) { continue; @@ -291,9 +342,8 @@ private unsafe Model ConvertAssimpScene(Scene* scene) { Draw = meshInfo.Draw, Name = meshInfo.Name, - //MaterialIndex = meshInfo.MaterialIndex, - MaterialIndex = (keptMeshIndex >= 0 ? 0 : meshInfo.MaterialIndex), NodeIndex = nodeIndex, + MaterialIndex = keptMeshIndex > 0 ? matIndex : meshInfo.MaterialIndex }; if (meshInfo.Bones != null) @@ -842,7 +892,7 @@ private unsafe void GetNodeNames(Node* node, List nodeNames, List nodeNames, Dictionary> meshIndexToNodeIndex, - Dictionary nodePointerToNodeIndex = null, Dictionary> duplicateNodeNameToNodeIndices = null) + Dictionary nodePointerToNodeIndex = null, Dictionary> duplicateNodeNameToNodeIndices = null, Func isKept = null) { var nodeIndex = nodes.Count; @@ -850,7 +900,7 @@ private unsafe void RegisterNodes( for (uint m = 0; m < fromNode->MNumMeshes; ++m) { var meshIndex = (int)fromNode->MMeshes[m]; - if (!IsKeptMeshIndex(meshIndex)) + if (isKept!=null && !isKept(meshIndex)) continue; if (!meshIndexToNodeIndex.TryGetValue(meshIndex, out var nodeIndices)) @@ -916,7 +966,7 @@ private unsafe void RegisterNodes( // register the children for (uint child = 0; child < fromNode->MNumChildren; ++child) { - RegisterNodes(fromNode->MChildren[child], nodeIndex, nodeNames, meshIndexToNodeIndex, nodePointerToNodeIndex, duplicateNodeNameToNodeIndices); + RegisterNodes(fromNode->MChildren[child], nodeIndex, nodeNames, meshIndexToNodeIndex, nodePointerToNodeIndex, duplicateNodeNameToNodeIndices, isKept); } } @@ -1619,25 +1669,41 @@ private ComputeTextureColor GetTextureReferenceNode(string vfsOutputPath, string private unsafe List ExtractModels(Scene* scene, Dictionary meshNames, Dictionary materialNames, Dictionary nodeNames) { GenerateMeshNames(scene, meshNames); - var meshList = new List(); + + var aggregated = new Dictionary(StringComparer.Ordinal); + for (uint i = 0; i < scene->MNumMeshes; ++i) { var mesh = scene->MMeshes[i]; var lMaterial = scene->MMaterials[mesh->MMaterialIndex]; - var meshParams = new MeshParameters + var meshName = meshNames[(IntPtr)mesh]; + var nodeName = SearchMeshNode(scene->MRootNode, i, nodeNames); + var matName = materialNames[(IntPtr)lMaterial]; + var matIndex = (int)mesh->MMaterialIndex; + + var groupKey = string.IsNullOrEmpty(nodeName) ? meshName : nodeName; + + if (!aggregated.TryGetValue(groupKey, out var mp)) { - MeshName = meshNames[(IntPtr)mesh], - MaterialName = materialNames[(IntPtr)lMaterial], - NodeName = SearchMeshNode(scene->MRootNode, i, nodeNames), - OriginalMaterialIndex = (int)mesh->MMaterialIndex - }; + mp = new MeshParameters + { + MeshName = meshName, + NodeName = nodeName, + }; + aggregated[groupKey] = mp; + } - meshList.Add(meshParams); + if (!mp.MaterialNames.Contains(matName)) + mp.MaterialNames.Add(matName); + if (!mp.MaterialIndices.Contains(matIndex)) + mp.MaterialIndices.Add(matIndex); } - return meshList; + + return aggregated.Values.ToList(); } + private unsafe string SearchMeshNode(Node* node, uint meshIndex, Dictionary nodeNames) { for (uint i = 0; i < node->MNumMeshes; ++i) diff --git a/sources/tools/Stride.Importer.Common/MeshParameters.cs b/sources/tools/Stride.Importer.Common/MeshParameters.cs index 40af1b5f97..493345d307 100644 --- a/sources/tools/Stride.Importer.Common/MeshParameters.cs +++ b/sources/tools/Stride.Importer.Common/MeshParameters.cs @@ -7,11 +7,11 @@ namespace Stride.Importer.Common { public class MeshParameters { - public string MaterialName; + public List MaterialNames { get; set; } = new(); + public List MaterialIndices { get; set; } = new(); + public string MeshName; public string NodeName; public HashSet BoneNodes; - public int OriginalMaterialIndex { get; set; } - } }