Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<TextBlock Text="{sd:Localize Warning\: Deduplicate materials is currently not supported for FBX files}" Margin="5" Visibility="{Binding ShowFbxDedupeNotSupportedWarning, Converter={sd:VisibleOrCollapsed}}"/>
<CheckBox Content="{sd:Localize Import textures, Context=Button}" IsChecked="{Binding ImportTextures}" Margin="5"/>
<CheckBox Content="{sd:Localize Import Animations, Context=Button}" IsChecked="{Binding ImportAnimations}" Margin="5"/>
<CheckBox Content="{sd:Localize Split hierarchy, Context=Button}" IsChecked="{Binding SplitHierarchy}" Margin="5"/>
</StackPanel>
</Border>
<Border Background="{StaticResource EmphasisColorBrush}" Margin="20,0">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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); } }
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public partial class ImportModelCommand
public List<ModelMaterial> Materials { get; set; }
public string EffectName { get; set; }

public int KeptMeshIndex { get; set; } = -1;

public List<IModelModifier> ModelModifiers { get; set; }

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion sources/engine/Stride.Assets.Models/ImportThreeDCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions sources/engine/Stride.Assets.Models/ModelAsset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,14 @@ public sealed class ModelAsset : Asset, IAssetWithSource, IModelAsset
[Category]
public List<IModelModifier> Modifiers { get; } = new List<IModelModifier>();

[DataMember(55)]
public int KepMeshIndex { get; set; } = -1;

/// <inheritdoc/>
[DataMemberIgnore]
public override UFile MainSource => Source;

[DataMemberIgnore]
public string MeshName { get; set; } = null;
}
}
1 change: 1 addition & 0 deletions sources/engine/Stride.Assets.Models/ModelAssetCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
116 changes: 98 additions & 18 deletions sources/engine/Stride.Assets.Models/ModelAssetImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ namespace Stride.Assets.Models
{
public abstract class ModelAssetImporter : AssetImporterBase
{
public static readonly PropertyKey<bool> SplitHierarchyKey = new PropertyKey<bool>("SplitHierarchy", typeof(ModelAssetImporter));

public static readonly PropertyKey<bool> DeduplicateMaterialsKey = new PropertyKey<bool>("DeduplicateMaterials", typeof(ModelAssetImporter));

public override IEnumerable<Type> RootAssetTypes
Expand Down Expand Up @@ -108,8 +110,43 @@ public override IEnumerable<AssetItem> 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<AnimationAsset>())
Expand All @@ -126,6 +163,10 @@ public override IEnumerable<AssetItem> Import(UFile localPath, AssetImporterPara
return rawAssetReferences;
}





private static AssetItem ImportSkeleton(List<AssetItem> assetReferences, UFile assetSource, UFile localPath, EntityInfo entityInfo)
{
var asset = new SkeletonAsset { Source = assetSource };
Expand All @@ -147,7 +188,6 @@ private static AssetItem ImportSkeleton(List<AssetItem> assetReferences, UFile a
assetReferences.Add(assetItem);
return assetItem;
}

private static void ImportAnimation(List<AssetItem> assetReferences, UFile localPath, string animationNodeName, int animationNodeIndex, [MaybeNull]AssetItem skeletonAsset, [MaybeNull]ModelAsset modelAsset, TimeSpan animationStartTime, TimeSpan animationEndTime)
{
var assetSource = localPath;
Expand Down Expand Up @@ -175,31 +215,66 @@ private static void ImportAnimation(List<AssetItem> assetReferences, UFile local

assetReferences.Add(new AssetItem(animUrl, asset));
}

private static ModelAsset ImportModel(List<AssetItem> assetReferences, UFile assetSource, UFile localPath, EntityInfo entityInfo, bool shouldPostFixName, AssetItem skeletonAsset)
{
var asset = new ModelAsset { Source = assetSource };

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<Material>(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<Material>(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<Material>(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);
Expand All @@ -214,7 +289,6 @@ private static ModelAsset ImportModel(List<AssetItem> assetReferences, UFile ass
assetReferences.Add(assetItem);
return asset;
}

private static void ImportMaterials(List<AssetItem> assetReferences, Dictionary<string, MaterialAsset> materials)
{
if (materials != null)
Expand All @@ -233,7 +307,6 @@ private static void ImportMaterials(List<AssetItem> 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)
Expand Down Expand Up @@ -326,5 +399,12 @@ private static void ImportTextures(IEnumerable<string> 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;
}
}
}
Loading