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..f561cf37bd 100644
--- a/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs
+++ b/sources/editor/Stride.Assets.Presentation/Templates/ModelFromFileTemplateGenerator.cs
@@ -2,21 +2,26 @@
// 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;
+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.Mathematics;
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.Importer.Common;
using Stride.Rendering;
namespace Stride.Assets.Presentation.Templates
@@ -29,6 +34,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 +49,7 @@ 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 +79,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 +105,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 +113,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 +133,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);
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 +156,80 @@ 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();
+
+ var baseName = file.GetFileNameWithoutExtension();
+
+ //Find, unique name each item before building prefab
+ if (splitHierarchy)
+ {
+ var entityInfo = importer.GetEntityInfo(file, parameters.Logger, importParameters);
+ if (entityInfo?.Models.Count > 0)
+ {
+ var firstModelItem = assets.FirstOrDefault(a => a.Asset is ModelAsset);
+ if (firstModelItem != null)
+ {
+ 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}";
+
+ 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}";
+
+ var uniqueFile = MakeUniqueFileName(desiredNoExt, assets);
+
+ AssetItem itemForThisMesh;
+
+ if (i == 0)
+ {
+ 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
+ {
+ 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);
+ assets.Add(itemForThisMesh);
+ }
+
+ //Assign materials
+ foreach (var item in perMeshAssets)
+ {
+ ResetMaterialsOnPrefabItems(item, entityInfo);
+ }
+
+ var prefabAssetItem = BuildPrefabForSplitHierarchy(
+ baseName,
+ entityInfo,
+ perMeshAssets,
+ parameters.TargetLocation);
+
+
+ if (prefabAssetItem != null)
+ assets.Add(prefabAssetItem);
+ }
+ }
+ }
+
foreach (var model in assets.Select(x => x.Asset).OfType())
{
@@ -154,11 +239,183 @@ protected override IEnumerable CreateAssets(AssetTemplateGeneratorPar
}
}
- // Create unique names amongst the list of assets
importedAssets.AddRange(MakeUniqueNames(assets));
}
return importedAssets;
}
+
+ private static void ResetMaterialsOnPrefabItems(AssetItem assetItem, EntityInfo entityInfo)
+ {
+ ModelAsset asset = assetItem?.Asset as ModelAsset;
+ if (asset == null || asset.Materials==null)
+ return;
+
+ 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)
+ return;
+
+ for (int i = asset.Materials.Count - 1; i >= 0; i--)
+ {
+ if (!materialIndices.Contains(i))
+ asset.Materials.RemoveAt(i);
+ }
+ }
+
+ private static AssetItem? BuildPrefabForSplitHierarchy(string baseName, EntityInfo entityInfo, IList perMeshModels, UDirectory targetLocation)
+ {
+ if (entityInfo?.Nodes == null || entityInfo.Nodes.Count == 0)
+ return null;
+
+ // 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;
+
+ 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);
+
+ while (stack.Count > node.Depth)
+ stack.Pop();
+
+ if (stack.Count == 0)
+ {
+ root = e;
+ }
+ else
+ {
+ stack.Peek().AddChild(e);
+ }
+
+ stack.Push(e);
+ entities.Add(e);
+ }
+
+
+ // Step 2. Apply TRS on each entity to that of imported source file
+ for (int i = 0; i < entityInfo.Nodes.Count; i++)
+ {
+ entities[i].Transform.Position = entityInfo.Nodes[i].Position;
+ entities[i].Transform.Rotation = entityInfo.Nodes[i].Rotation;
+ entities[i].Transform.Scale = entityInfo.Nodes[i].Scale;
+ }
+
+ //Step 3. Attach ModelComponent and set up hierachical order
+ if (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);
+ }
+
+ var extraChildCountByNode = new Dictionary();
+
+ for (int m = 0; m < entityInfo.Models.Count; m++)
+ {
+
+ var meshInfo = entityInfo.Models[m];
+
+ var nodeIndex = nodeNameToIndex[meshInfo.NodeName];
+
+ var modelItem = perMeshModels[m];
+ if (modelItem?.Asset is ModelAsset)
+ {
+ var mc = new ModelComponent
+ {
+ Model = AttachedReferenceManager.CreateProxyObject(modelItem.Id, modelItem.Location)
+ };
+
+ var host = entities[nodeIndex];
+
+
+ if (host.Get() != null)
+ {
+ if (!extraChildCountByNode.TryGetValue(nodeIndex, out var counter))
+ counter = 0;
+ counter++;
+ extraChildCountByNode[nodeIndex] = counter;
+
+ var childName = string.IsNullOrEmpty(meshInfo.NodeName)
+ ? $"Mesh_{m}"
+ : $"{meshInfo.NodeName}_Mesh{counter}";
+
+ var child = new Entity(childName);
+ host.AddChild(child);
+
+ entities.Add(child);
+
+ host = child;
+ }
+
+ host.Components.Add(mc);
+ }
+ }
+ }
+
+ 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 =
+ firstName.Equals(baseName, StringComparison.OrdinalIgnoreCase)
+ ||
+ firstName.Equals("RootNode", StringComparison.OrdinalIgnoreCase);
+
+
+ if (looksLikeWrapper && root.Transform.Children.Count == 1)
+ {
+ var onlyChild = root.Transform.Children[0].Entity;
+ onlyChild.Transform.Parent = null;
+ root = onlyChild;
+ }
+
+ var prefab = new PrefabAsset();
+
+ foreach (var e in entities)
+ {
+ var design = new EntityDesign(e);
+ prefab.Hierarchy.Parts.Add(e.Id, design);
+ }
+
+ prefab.Hierarchy.RootParts.Add(root);
+
+ 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;
+ }
+
+ 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);
+ }
+
}
}
diff --git a/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs
index 7c48da0745..ad3377f047 100644
--- a/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs
+++ b/sources/editor/Stride.Assets.Presentation/ViewModel/ModelViewModel.cs
@@ -53,6 +53,10 @@ 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;
+
// 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);
diff --git a/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs b/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs
index 299d2cf132..dcda93483f 100644
--- a/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs
+++ b/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs
@@ -26,7 +26,7 @@ public partial class ImportModelCommand
public bool ImportCustomAttributes { get; set; }
public int AnimationStack { 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 339764ed1c..ae8aab2569 100644
--- a/sources/engine/Stride.Assets.Models/ImportThreeDCommand.cs
+++ b/sources/engine/Stride.Assets.Models/ImportThreeDCommand.cs
@@ -40,7 +40,8 @@ private Stride.Importer.ThreeD.MeshConverter CreateMeshConverter(ICommandContext
protected override Model LoadModel(ICommandContext commandContext, ContentManager contentManager)
{
var converter = CreateMeshConverter(commandContext);
-
+ 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 123d70e6c9..59efc9ebdc 100644
--- a/sources/engine/Stride.Assets.Models/ModelAsset.cs
+++ b/sources/engine/Stride.Assets.Models/ModelAsset.cs
@@ -91,8 +91,14 @@ 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;
+
+ [DataMemberIgnore]
+ public string MeshName { get; set; } = null;
}
}
diff --git a/sources/engine/Stride.Assets.Models/ModelAssetCompiler.cs b/sources/engine/Stride.Assets.Models/ModelAssetCompiler.cs
index c3baaaa0b9..a84ffa2bdd 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.KeptMeshIndex = asset.KepMeshIndex;
if (skeleton != null)
{
diff --git a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs
index bc6accd05b..16185bf442 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,8 +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);
+
+ // Ask the converter for how many meshes exist (we made ExtractModels return ALL meshes)
+ var meshCount = entityInfo.Models?.Count ?? 0;
+
+ if (splitHierarchy)
+ {
+ // 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));
+ }
+
+ // 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
+ {
+ // 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())
@@ -126,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 };
@@ -147,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;
@@ -175,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 };
@@ -183,23 +222,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);
@@ -214,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)
@@ -233,7 +307,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)
@@ -326,5 +399,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..d8ea7628ce 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,11 @@ namespace Stride.Importer.ThreeD
{
public class MeshConverter
{
+ // 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;
+
static MeshConverter()
{
if (Platform.Type == PlatformType.Windows)
@@ -67,6 +73,18 @@ 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
@@ -101,17 +119,47 @@ 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);
+ // 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;
+
+ if (!entityInfo.NodeNameToMaterialIndices.TryGetValue(key, out var list))
+ {
+ list = new List();
+ entityInfo.NodeNameToMaterialIndices[key] = list;
+ }
+
+ if (m.MaterialIndices != null)
+ {
+ foreach (var idx in m.MaterialIndices)
+ {
+ if (idx >= 0 && !list.Contains(idx))
+ list.Add(idx);
+ }
+ }
+ }
+ }
+
return entityInfo;
}
catch (Exception ex)
@@ -179,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();
@@ -186,6 +255,57 @@ 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))
+ {
+ 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);
@@ -193,14 +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 (!IsKept(i, out int matIndex))
+ continue;
+
if (!meshIndexToNodeIndex.TryGetValue(i, out var value))
{
continue;
@@ -219,8 +342,8 @@ private unsafe Model ConvertAssimpScene(Scene* scene)
{
Draw = meshInfo.Draw,
Name = meshInfo.Name,
- MaterialIndex = meshInfo.MaterialIndex,
NodeIndex = nodeIndex,
+ MaterialIndex = keptMeshIndex > 0 ? matIndex : meshInfo.MaterialIndex
};
if (meshInfo.Bones != null)
@@ -241,8 +364,28 @@ private unsafe Model ConvertAssimpScene(Scene* scene)
modelData.Meshes.Add(nodeMeshData);
}
}
-
-
+ // 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();
+ nodes.Add(new ModelNodeDefinition
+ {
+ ParentIndex = -1,
+ Name = "Root",
+ Flags = ModelNodeFlags.Default,
+ Transform =
+ {
+ Position = Vector3.Zero,
+ Rotation = Quaternion.Identity,
+ Scale = new Vector3(100,100,100)
+ }
+ });
+
+ for (int mi = 0; mi < modelData.Meshes.Count; mi++)
+ modelData.Meshes[mi].NodeIndex = 0;
+ }
return modelData;
}
@@ -251,8 +394,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,12 +845,15 @@ 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)
@@ -744,22 +892,24 @@ 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;
- // 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 (isKept!=null && !isKept(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 +922,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 +942,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();
@@ -815,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);
}
}
@@ -1162,20 +1313,38 @@ 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)
+ 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();
@@ -1500,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)
- };
+ 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)
@@ -1553,7 +1738,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/EntityInfo.cs b/sources/tools/Stride.Importer.Common/EntityInfo.cs
index 10e7e7e5f3..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;
@@ -13,5 +14,7 @@ public class EntityInfo
public List AnimationNodes;
public List Models;
public List Nodes;
+ public List MaterialOrder { get; set; }
+ public Dictionary> NodeNameToMaterialIndices { get; set; } = new Dictionary>(StringComparer.Ordinal);
}
}
diff --git a/sources/tools/Stride.Importer.Common/MeshParameters.cs b/sources/tools/Stride.Importer.Common/MeshParameters.cs
index 0cf3763e11..493345d307 100644
--- a/sources/tools/Stride.Importer.Common/MeshParameters.cs
+++ b/sources/tools/Stride.Importer.Common/MeshParameters.cs
@@ -7,7 +7,9 @@ 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;
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;
}
}