diff --git a/.gitignore b/.gitignore
index 5adc34ed40..7b62c07e94 100644
--- a/.gitignore
+++ b/.gitignore
@@ -154,3 +154,5 @@ fastlane/report.xml
fastlane/screenshots
*.user
project.lock.json
+/hooks
+/lfs/objects
diff --git a/build/Stride.sln b/build/Stride.sln
index a31a911137..6d9753b3af 100644
--- a/build/Stride.sln
+++ b/build/Stride.sln
@@ -335,6 +335,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.VisualStudio.Command
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Engine.NoAssets.Tests.Windows", "..\sources\engine\Stride.Engine.NoAssets.Tests\Stride.Engine.NoAssets.Tests.Windows.csproj", "{1C94168A-3C0D-4C6B-883B-91627D2EF3A1}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Importer.Gltf", "..\sources\tools\Stride.Importer.Gltf\Stride.Importer.Gltf.csproj", "{69CA1F59-5735-403C-8009-2CE282C72DE6}"
+EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
..\sources\shared\Stride.NuGetResolver\Stride.NuGetResolver.projitems*{00b72ed7-00e9-47f7-868d-8162027cd068}*SharedItemsImports = 13
@@ -1527,6 +1529,18 @@ Global
{1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|Win32.ActiveCfg = Release|Any CPU
{1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|Win32.Build.0 = Release|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Debug|Win32.ActiveCfg = Debug|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Debug|Win32.Build.0 = Debug|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Release|Win32.ActiveCfg = Release|Any CPU
+ {69CA1F59-5735-403C-8009-2CE282C72DE6}.Release|Win32.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1654,6 +1668,7 @@ Global
{7FA381C8-8EBE-4515-969D-01369CC47D0E} = {1AE1AC60-5D2F-4CA7-AE20-888F44551185}
{83E4E436-659A-4D82-90DD-0DC67BE38D17} = {E4508D15-6503-4A29-ADC4-27B3A5E99545}
{1C94168A-3C0D-4C6B-883B-91627D2EF3A1} = {A7ED9F01-7D78-4381-90A6-D50E51C17250}
+ {69CA1F59-5735-403C-8009-2CE282C72DE6} = {6F473FA6-4F8B-4FBA-AE33-EE5AF997D50C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FF877973-604D-4EA7-B5F5-A129961F9EF2}
diff --git a/sources/engine/Stride.Assets.Models/AssimpAssetImporter.cs b/sources/engine/Stride.Assets.Models/AssimpAssetImporter.cs
index a4925202c0..d4fd6b3dbe 100644
--- a/sources/engine/Stride.Assets.Models/AssimpAssetImporter.cs
+++ b/sources/engine/Stride.Assets.Models/AssimpAssetImporter.cs
@@ -20,7 +20,7 @@ static AssimpAssetImporter()
}
// Supported file extensions for this importer
- internal const string FileExtensions = ".dae;.3ds;.gltf;.glb;.obj;.blend;.x;.md2;.md3;.dxf;.ply;.stl;.stp";
+ internal const string FileExtensions = ".dae;.3ds;.obj;.blend;.x;.md2;.md3;.dxf;.ply;.stl;.stp";
private static readonly Guid Uid = new Guid("30243FC0-CEC7-4433-977E-95DCA29D846E");
diff --git a/sources/engine/Stride.Assets.Models/GltfAssetImporter.cs b/sources/engine/Stride.Assets.Models/GltfAssetImporter.cs
new file mode 100644
index 0000000000..7da0dfa170
--- /dev/null
+++ b/sources/engine/Stride.Assets.Models/GltfAssetImporter.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Stride contributors (https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// 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.Core.Assets;
+using Stride.Core;
+using Stride.Core.Diagnostics;
+using Stride.Core.IO;
+using Stride.Animations;
+using Stride.Assets.Textures;
+using Stride.Importer.Common;
+using Stride.Importer.Gltf;
+using System.Linq;
+
+namespace Stride.Assets.Models
+{
+ public class GltfAssetImporter : ModelAssetImporter
+ {
+
+ // Supported file extensions for this importer
+ internal const string FileExtensions = ".gltf;.glb;";
+
+ private static readonly Guid Uid = new Guid("A5A08530-4856-4EB7-91F8-B0A148B3A627");
+
+ public override Guid Id => Uid;
+
+ public override string Description => "Gltf importer used for creating entities, 3D Models or animations assets";
+
+ public override string SupportedFileExtensions => FileExtensions;
+
+ ///
+ public override EntityInfo GetEntityInfo(UFile localPath, Logger logger, AssetImporterParameters importParameters)
+ {
+
+ return GltfMeshParser.ExtractEntityInfo(SharpGLTF.Schema2.ModelRoot.Load(localPath.FullPath), localPath);
+ }
+
+ ///
+ public override void GetAnimationDuration(UFile localPath, Logger logger, AssetImporterParameters importParameters, out TimeSpan startTime, out TimeSpan endTime)
+ {
+ var gltfFile = SharpGLTF.Schema2.ModelRoot.Load(localPath);
+ endTime = GltfMeshParser.GetAnimationDuration(gltfFile);
+ startTime = CompressedTimeSpan.Zero;
+ }
+
+ }
+}
diff --git a/sources/engine/Stride.Assets.Models/ImportGltfCommand.cs b/sources/engine/Stride.Assets.Models/ImportGltfCommand.cs
new file mode 100644
index 0000000000..ae4f17162f
--- /dev/null
+++ b/sources/engine/Stride.Assets.Models/ImportGltfCommand.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Stride contributors (https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// 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.ComponentModel;
+using System.IO;
+using System.Linq;
+using Stride.Core.BuildEngine;
+using Stride.Core.Serialization.Contents;
+using Stride.Animations;
+using Stride.Rendering;
+using Stride.Importer.Gltf;
+
+namespace Stride.Assets.Models
+{
+ [Description("Import GLTF")]
+ public class ImportGltfCommand : ImportModelCommand
+ {
+ private static string[] supportedExtensions = GltfAssetImporter.FileExtensions.Split(';');
+
+ ///
+ public override string Title { get { string title = "Import GLTF "; try { title += Path.GetFileName(SourcePath) ?? "[File]"; } catch { title += "[INVALID PATH]"; } return title; } }
+
+ public static bool IsSupportingExtensions(string ext)
+ {
+ if (string.IsNullOrEmpty(ext))
+ return false;
+
+ var extToLower = ext.ToLowerInvariant();
+
+ return supportedExtensions.Any(supExt => supExt.Equals(extToLower));
+ }
+
+ protected override Model LoadModel(ICommandContext commandContext, ContentManager contentManager)
+ {
+ var model = GltfMeshParser.LoadGltf(SourcePath);
+ var sceneData = GltfMeshParser.LoadFirstModel(model);
+ return sceneData;
+ }
+
+ protected override Dictionary LoadAnimation(ICommandContext commandContext, ContentManager contentManager, out TimeSpan duration)
+ {
+ var file = SharpGLTF.Schema2.ModelRoot.Load(SourcePath);
+ var sceneData = GltfMeshParser.ConvertAnimations(file,SourcePath.GetFileNameWithoutExtension());
+ duration = GltfMeshParser.GetAnimationDuration(file);
+ return sceneData;
+ }
+
+ protected override Skeleton LoadSkeleton(ICommandContext commandContext, ContentManager contentManager)
+ {
+ var file = SharpGLTF.Schema2.ModelRoot.Load(SourcePath);
+ var sceneData = GltfAnimationParser.ConvertSkeleton(file);
+ return sceneData;
+ }
+
+ public override bool ShouldSpawnNewProcess()
+ {
+ return true;
+ }
+
+ public override string ToString()
+ {
+ return "Import GLTF " + base.ToString();
+ }
+ }
+}
diff --git a/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs b/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs
index 2e6bbfbb5f..d354aa8d78 100644
--- a/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs
+++ b/sources/engine/Stride.Assets.Models/ImportModelCommand.Animation.cs
@@ -26,6 +26,13 @@ public partial class ImportModelCommand
private unsafe object ExportAnimation(ICommandContext commandContext, ContentManager contentManager, bool failOnEmptyAnimation)
{
+ // Gltf case
+ if (commandContext.CurrentCommand.Title.ToLower().Contains("gltf"))
+ {
+ var ts = TimeSpan.FromSeconds(1);
+ return LoadAnimation(commandContext, contentManager, out ts);
+ }
+
// Read from model file
var modelSkeleton = LoadSkeleton(commandContext, contentManager); // we get model skeleton to compare it to real skeleton we need to map to
AdjustSkeleton(modelSkeleton);
@@ -42,7 +49,7 @@ private unsafe object ExportAnimation(ICommandContext commandContext, ContentMan
{
foreach (var animationCurve in clip.Value.Curves)
{
- animationCurve.ShiftKeys(startTime);
+ animationCurve?.ShiftKeys(startTime);
}
}
@@ -76,6 +83,10 @@ private unsafe object ExportAnimation(ICommandContext commandContext, ContentMan
{
animationClip.AddCurve($"[TransformComponent.Key]." + channelName.Replace("Transform.", string.Empty), curve);
}
+ else if(channelName.StartsWith("[TransformComponent.Key]"))
+ {
+ animationClip.AddCurve(channelName, curve);
+ }
// Also apply Camera curves
// TODO: Add some other curves?
@@ -140,13 +151,13 @@ private unsafe object ExportAnimation(ICommandContext commandContext, ContentMan
foreach (var node in nodesToMerge)
{
if (node.Item3 != null)
- foreach (var curve in node.Item3.Clip.Curves)
- {
- foreach (CompressedTimeSpan time in curve.Keys)
+ foreach (var curve in node.Item3.Clip.Curves)
{
- animationKeysSet.Add(time);
+ foreach (CompressedTimeSpan time in curve.Keys)
+ {
+ animationKeysSet.Add(time);
+ }
}
- }
}
// Sort key times
@@ -170,7 +181,7 @@ private unsafe object ExportAnimation(ICommandContext commandContext, ContentMan
foreach (var node in nodesToMerge)
{
// Needs to be an array in order for it to be modified by the UpdateEngine, otherwise it would get passed by value
- var modelNodeDefinitions = new ModelNodeDefinition[1] {node.Item1};
+ var modelNodeDefinitions = new ModelNodeDefinition[1] { node.Item1 };
if (node.Item2 != null && node.Item3 != null)
{
diff --git a/sources/engine/Stride.Assets.Models/ImportModelCommand.cs b/sources/engine/Stride.Assets.Models/ImportModelCommand.cs
index e00667f3d3..6d222e20b4 100644
--- a/sources/engine/Stride.Assets.Models/ImportModelCommand.cs
+++ b/sources/engine/Stride.Assets.Models/ImportModelCommand.cs
@@ -28,6 +28,8 @@ public abstract partial class ImportModelCommand : SingleFileImportCommand
public static ImportModelCommand Create(string extension)
{
+ if (ImportGltfCommand.IsSupportingExtensions(extension))
+ return new ImportGltfCommand();
if (ImportFbxCommand.IsSupportingExtensions(extension))
return new ImportFbxCommand();
if (ImportAssimpCommand.IsSupportingExtensions(extension))
@@ -90,8 +92,17 @@ protected override async Task DoCommandOverride(ICommandContext co
commandContext.Logger.Error($"Failed to import file {ContextAsString}.");
return ResultStatus.Failed;
}
+ if (exportedObject is Dictionary)
+ {
+ var exportedObjects = exportedObject as Dictionary;
+ foreach (var obj in exportedObjects)
+ assetManager.Save(obj.Key, obj.Value);
+ }
+ else
+ {
+ assetManager.Save(Location, exportedObject);
+ }
- assetManager.Save(Location, exportedObject);
commandContext.Logger.Verbose($"The {ContextAsString} has been successfully imported.");
@@ -130,7 +141,7 @@ private Matrix CombineMatricesFromNodeIndices(ModelNodeTransformation[] nodes, i
return result;
}
-
+
protected abstract Model LoadModel(ICommandContext commandContext, ContentManager contentManager);
protected abstract Dictionary LoadAnimation(ICommandContext commandContext, ContentManager contentManager, out TimeSpan duration);
@@ -172,7 +183,7 @@ private static bool CompareParameters(Model model, Mesh baseMesh, Mesh newMesh)
return false;
return IsSubsetOf(localParams, newMesh.Parameters) && IsSubsetOf(newMesh.Parameters, localParams);
}
-
+
///
/// Compares the shadow options between the two meshes.
///
diff --git a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs
index 09e8e9f0d2..a892a0e06f 100644
--- a/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs
+++ b/sources/engine/Stride.Assets.Models/ModelAssetImporter.cs
@@ -100,8 +100,8 @@ public override IEnumerable Import(UFile localPath, AssetImporterPara
{
TimeSpan startTime, endTime;
GetAnimationDuration(localPath, importParameters.Logger, importParameters, out startTime, out endTime);
-
- ImportAnimation(rawAssetReferences, localPath, entityInfo.AnimationNodes, isImportingModel, skeletonAsset, startTime, endTime);
+ bool isGltfAsset = localPath.GetFileExtension().ToLower().Contains("glb") || localPath.GetFileExtension().ToLower().Contains("gltf");
+ ImportAnimation(rawAssetReferences, localPath, isGltfAsset ? entityInfo.AnimationNames : entityInfo.AnimationNodes, isImportingModel, isGltfAsset, skeletonAsset, startTime, endTime);
}
// 4. Materials
@@ -141,20 +141,44 @@ private static AssetItem ImportSkeleton(List assetReferences, UFile a
return assetItem;
}
- private static void ImportAnimation(List assetReferences, UFile localPath, List animationNodes, bool shouldPostFixName, AssetItem skeletonAsset, TimeSpan animationStartTime, TimeSpan animationEndTime)
+ private static void ImportAnimation(List assetReferences, UFile localPath, List animationNodes, bool shouldPostFixName, bool isGltfAsset, AssetItem skeletonAsset, TimeSpan animationStartTime, TimeSpan animationEndTime)
{
if (animationNodes != null && animationNodes.Count > 0)
{
- var assetSource = localPath;
+ if (isGltfAsset)
+ {
+ // Animation nodes in GLTF correspond to animations inside the gltf files, not the bone names in fbx files.
+ foreach (var anim in animationNodes)
+ {
+ var assetSource = localPath;
+
+ var asset = new AnimationAsset { Source = assetSource, AnimationTimeMaximum = animationEndTime, AnimationTimeMinimum = animationStartTime };
+ string animUrl = anim + "_Animation";
+
+ if (skeletonAsset != null)
+ asset.Skeleton = AttachedReferenceManager.CreateProxyObject(skeletonAsset.Id, skeletonAsset.Location);
- var asset = new AnimationAsset { Source = assetSource, AnimationTimeMaximum = animationEndTime, AnimationTimeMinimum = animationStartTime };
- var animUrl = localPath.GetFileNameWithoutExtension() + (shouldPostFixName ? " Animation" : "");
+ assetReferences.Add(new AssetItem(animUrl, asset));
+ }
+
+ }
+ else
+ {
- if (skeletonAsset != null)
- asset.Skeleton = AttachedReferenceManager.CreateProxyObject(skeletonAsset.Id, skeletonAsset.Location);
+ var assetSource = localPath;
- assetReferences.Add(new AssetItem(animUrl, asset));
+ var asset = new AnimationAsset { Source = assetSource, AnimationTimeMaximum = animationEndTime, AnimationTimeMinimum = animationStartTime };
+ var animUrl = localPath.GetFileNameWithoutExtension() + (shouldPostFixName ? " Animation" : "");
+
+ if (skeletonAsset != null)
+ asset.Skeleton = AttachedReferenceManager.CreateProxyObject(skeletonAsset.Id, skeletonAsset.Location);
+
+ assetReferences.Add(new AssetItem(animUrl, asset));
+
+ }
}
+
+
}
private static void ImportModel(List assetReferences, UFile assetSource, UFile localPath, EntityInfo entityInfo, bool shouldPostFixName, AssetItem skeletonAsset)
@@ -190,7 +214,7 @@ private static void ImportModel(List assetReferences, UFile assetSour
if (skeletonAsset != null)
asset.Skeleton = AttachedReferenceManager.CreateProxyObject(skeletonAsset.Id, skeletonAsset.Location);
- var modelUrl = new UFile(localPath.GetFileNameWithoutExtension() + (shouldPostFixName?" Model": ""));
+ var modelUrl = new UFile(localPath.GetFileNameWithoutExtension() + (shouldPostFixName ? " Model" : ""));
var assetItem = new AssetItem(modelUrl, asset);
assetReferences.Add(assetItem);
}
@@ -269,7 +293,7 @@ private static void AdjustForTransparency(MaterialAsset material)
// var isTransparent = false;
// if (material.Parameters.ContainsKey(MaterialParameters.HasTransparency))
// isTransparent = (bool)material.Parameters[MaterialParameters.HasTransparency];
-
+
// if (!isTransparent)
// {
// // remove the diffuse node
diff --git a/sources/engine/Stride.Assets.Models/Stride.Assets.Models.csproj b/sources/engine/Stride.Assets.Models/Stride.Assets.Models.csproj
index 25f8229118..0d6fc9e49a 100644
--- a/sources/engine/Stride.Assets.Models/Stride.Assets.Models.csproj
+++ b/sources/engine/Stride.Assets.Models/Stride.Assets.Models.csproj
@@ -18,6 +18,7 @@
+
diff --git a/sources/tools/Stride.Importer.Common/Stride.Importer.Common.cpp b/sources/tools/Stride.Importer.Common/Stride.Importer.Common.cpp
index 9fe4ea6ccc..6e3c4cd6ff 100644
--- a/sources/tools/Stride.Importer.Common/Stride.Importer.Common.cpp
+++ b/sources/tools/Stride.Importer.Common/Stride.Importer.Common.cpp
@@ -61,6 +61,7 @@ public ref class EntityInfo
List^ AnimationNodes;
List^ Models;
List^ Nodes;
+ List^ AnimationNames;
};
public ref class MeshMaterials
diff --git a/sources/tools/Stride.Importer.Gltf/GltfAnimationParser.cs b/sources/tools/Stride.Importer.Gltf/GltfAnimationParser.cs
new file mode 100644
index 0000000000..6748ed9ad7
--- /dev/null
+++ b/sources/tools/Stride.Importer.Gltf/GltfAnimationParser.cs
@@ -0,0 +1,331 @@
+// Copyright (c) Stride contributors (https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// 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.Linq;
+using Stride.Animations;
+using Stride.Core.Collections;
+using Stride.Core.Mathematics;
+using Stride.Rendering;
+using static Stride.Importer.Gltf.GltfUtils;
+
+namespace Stride.Importer.Gltf
+{
+ public static class GltfAnimationParser
+ {
+ ///
+ /// Converts GLTF Skeleton into a Stride Skeleton
+ ///
+ ///
+ /// skeleton
+ public static Skeleton ConvertSkeleton(SharpGLTF.Schema2.ModelRoot root)
+ {
+ Skeleton result = new Skeleton();
+ var nodes = new List();
+ var skins = root.LogicalSkins;
+ //var skin = root.LogicalNodes.First(x => x.Mesh == root.LogicalMeshes.First()).Skin;
+ // If there is no corresponding skins return a skeleton with 2 bones (an empty skeleton would make the editor crash)
+ foreach (var skin in skins)
+ {
+ var jointList = Enumerable.Range(0, skin.JointsCount).Select(x => skin.GetJoint(x).Joint).ToList();
+ nodes.AddRange(
+ jointList
+ .Select(
+ x =>
+ new ModelNodeDefinition
+ {
+ Name = x.Name ?? "Joint_" + x.LogicalIndex,
+ Flags = ModelNodeFlags.Default,
+ ParentIndex = jointList.IndexOf(x.VisualParent) + 1,
+ Transform = new TransformTRS
+ {
+ Position = ConvertNumerics(x.LocalTransform.Translation),
+ Rotation = ConvertNumerics(x.LocalTransform.Rotation),
+ Scale = ConvertNumerics(x.LocalTransform.Scale)
+ }
+
+ }
+ )
+ );
+ // And insert a parent one not caught by the above function (GLTF does not consider the parent bone as a bone)
+
+ }
+ nodes.Insert(
+ 0,
+ new ModelNodeDefinition
+ {
+ Name = "Armature",
+ Flags = ModelNodeFlags.EnableRender,
+ ParentIndex = -1,
+ Transform = new TransformTRS
+ {
+ Position = Vector3.Zero,
+ Rotation = Quaternion.Identity,
+ Scale = Vector3.Zero
+ }
+ });
+ result.Nodes = nodes.ToArray();
+ return result;
+ }
+
+ ///
+ /// Helper function to create a keyframe from values
+ ///
+ ///
+ ///
+ ///
+ /// keyframe
+ public static KeyFrameData CreateKeyFrame(float keyTime, T value)
+ {
+ return new KeyFrameData((CompressedTimeSpan)TimeSpan.FromSeconds(keyTime), value);
+ }
+
+ ///
+ /// Convert a GLTF animation channel into a Stride AnimationCurve.
+ /// If the model has no skin, root motion should be enabled
+ ///
+ ///
+ ///
+ /// animationCurves
+ public static Dictionary ConvertCurves(IReadOnlyList channels, SharpGLTF.Schema2.ModelRoot root)
+ {
+ var result = new Dictionary();
+ if (root.LogicalAnimations.Count == 0) return result;
+ var skins = root.LogicalSkins;
+ var skNodes = ConvertSkeleton(root).Nodes.ToList();
+ //var skin = root.LogicalNodes.First(x => x.Mesh == root.LogicalMeshes.First()).Skin;
+
+ // In case there is no skin joints/bones, animate transform component
+ if (skins.Count() == 0)
+ {
+ string basestring = "[TransformComponent.Key].type";
+ foreach (var chan in channels)
+ {
+ switch (chan.TargetNodePath)
+ {
+ case SharpGLTF.Schema2.PropertyPath.translation:
+ result.Add(basestring.Replace("type", "Position"), ConvertCurve(chan.GetTranslationSampler()));
+ break;
+ case SharpGLTF.Schema2.PropertyPath.rotation:
+ result.Add(basestring.Replace("type", "Rotation"), ConvertCurve(chan.GetRotationSampler()));
+ break;
+ case SharpGLTF.Schema2.PropertyPath.scale:
+ result.Add(basestring.Replace("type", "Scale"), ConvertCurve(chan.GetScaleSampler()));
+ break;
+ };
+ }
+ return result;
+ }
+
+
+ foreach (var skin in skins)
+ {
+ var jointList = Enumerable.Range(0, skin.JointsCount).Select(x => skin.GetJoint(x).Joint).ToList();
+ foreach (var chan in channels)
+ {
+ //var index0 = jointList.IndexOf(chan.TargetNode) + 1;
+ var index = skNodes.IndexOf(skNodes.First(x => x.Name == chan.TargetNode.Name));
+ switch (chan.TargetNodePath)
+ {
+ case SharpGLTF.Schema2.PropertyPath.translation:
+ result.Add(
+ $"[ModelComponent.Key].Skeleton.NodeTransformations[{index}].Transform.Position",
+ ConvertCurve(chan.GetTranslationSampler())
+ );
+ break;
+ case SharpGLTF.Schema2.PropertyPath.rotation:
+ result.Add(
+ $"[ModelComponent.Key].Skeleton.NodeTransformations[{index}].Transform.Rotation",
+ ConvertCurve(chan.GetRotationSampler())
+ );
+ break;
+ case SharpGLTF.Schema2.PropertyPath.scale:
+ result.Add(
+ $"[ModelComponent.Key].Skeleton.NodeTransformations[{index}].Transform.Scale",
+ ConvertCurve(chan.GetScaleSampler())
+ );
+ break;
+ };
+
+ }
+ }
+ return result;
+
+ }
+
+ ///
+ /// Converts a GLTF AnimationSampler into a Stride AnimationCurve
+ ///
+ ///
+ ///
+ public static AnimationCurve ConvertCurve(SharpGLTF.Schema2.IAnimationSampler sampler)
+ {
+ var interpolationType =
+ sampler.InterpolationMode switch
+ {
+ SharpGLTF.Schema2.AnimationInterpolationMode.LINEAR => AnimationCurveInterpolationType.Linear,
+ SharpGLTF.Schema2.AnimationInterpolationMode.STEP => AnimationCurveInterpolationType.Constant,
+ SharpGLTF.Schema2.AnimationInterpolationMode.CUBICSPLINE => AnimationCurveInterpolationType.Cubic,
+ _ => throw new NotImplementedException(),
+ };
+
+ var keyframes =
+ interpolationType switch
+ {
+ AnimationCurveInterpolationType.Constant =>
+ sampler.GetLinearKeys().Select(x => CreateKeyFrame(x.Key, ConvertNumerics(x.Value))),
+ AnimationCurveInterpolationType.Linear =>
+ sampler.GetLinearKeys().Select(x => CreateKeyFrame(x.Key, ConvertNumerics(x.Value))),
+ // Cubic might be broken
+ AnimationCurveInterpolationType.Cubic =>
+ sampler.GetCubicKeys().Select(x => ConvertNumerics(x.Value).Select(y => CreateKeyFrame(x.Key, y))).SelectMany(x => x),
+ _ => throw new NotImplementedException()
+ };
+
+ return new AnimationCurve
+ {
+ InterpolationType = interpolationType,
+ KeyFrames = new FastList>(keyframes)
+ };
+ }
+
+ ///
+ /// Converts a GLTF AnimationSampler into a Stride AnimationCurve
+ ///
+ ///
+ ///
+ public static AnimationCurve ConvertCurve(SharpGLTF.Schema2.IAnimationSampler sampler)
+ {
+ var interpolationType =
+ sampler.InterpolationMode switch
+ {
+ SharpGLTF.Schema2.AnimationInterpolationMode.LINEAR => AnimationCurveInterpolationType.Linear,
+ SharpGLTF.Schema2.AnimationInterpolationMode.STEP => AnimationCurveInterpolationType.Constant,
+ SharpGLTF.Schema2.AnimationInterpolationMode.CUBICSPLINE => AnimationCurveInterpolationType.Cubic,
+ _ => throw new NotImplementedException(),
+ };
+
+ var keyframes =
+ interpolationType switch
+ {
+ AnimationCurveInterpolationType.Constant =>
+ sampler.GetLinearKeys().Select(x => CreateKeyFrame(x.Key, ConvertNumerics(x.Value))),
+ AnimationCurveInterpolationType.Linear =>
+ sampler.GetLinearKeys().Select(x => CreateKeyFrame(x.Key, ConvertNumerics(x.Value))),
+ // TODO : Cubic can be broken
+ AnimationCurveInterpolationType.Cubic =>
+ sampler.GetCubicKeys().Select(x => ConvertNumerics(x.Value).Select(y => CreateKeyFrame(x.Key, y))).SelectMany(x => x),
+ _ => throw new NotImplementedException()
+ };
+
+ return new AnimationCurve
+ {
+ InterpolationType = interpolationType,
+ KeyFrames = new FastList>(keyframes)
+ };
+ }
+ ///
+ /// Converts a GLTF AnimationSampler into a Stride AnimationCurve
+ ///
+ ///
+ ///
+ public static AnimationCurve ConvertCurve(SharpGLTF.Schema2.IAnimationSampler sampler)
+ {
+ var interpolationType =
+ sampler.InterpolationMode switch
+ {
+ SharpGLTF.Schema2.AnimationInterpolationMode.LINEAR => AnimationCurveInterpolationType.Linear,
+ SharpGLTF.Schema2.AnimationInterpolationMode.STEP => AnimationCurveInterpolationType.Constant,
+ SharpGLTF.Schema2.AnimationInterpolationMode.CUBICSPLINE => AnimationCurveInterpolationType.Cubic,
+ _ => throw new NotImplementedException(),
+ };
+
+ var keyframes =
+ interpolationType switch
+ {
+ AnimationCurveInterpolationType.Constant =>
+ sampler.GetLinearKeys().Select(x => CreateKeyFrame(x.Key, ConvertNumerics(x.Value))),
+ AnimationCurveInterpolationType.Linear =>
+ sampler.GetLinearKeys().Select(x => CreateKeyFrame(x.Key, ConvertNumerics(x.Value))),
+ // TODO : Cubic can be broken
+ AnimationCurveInterpolationType.Cubic =>
+ sampler.GetCubicKeys().Select(x => ConvertNumerics(x.Value).Select(y => CreateKeyFrame(x.Key, y))).SelectMany(x => x),
+ _ => throw new NotImplementedException()
+ };
+
+ return new AnimationCurve
+ {
+ InterpolationType = interpolationType,
+ KeyFrames = new FastList>(keyframes)
+ };
+ }
+ ///
+ /// Converts a GLTF AnimationSampler into a Stride AnimationCurve
+ ///
+ ///
+ ///
+ public static AnimationCurve ConvertCurve(SharpGLTF.Schema2.IAnimationSampler sampler)
+ {
+ var interpolationType =
+ sampler.InterpolationMode switch
+ {
+ SharpGLTF.Schema2.AnimationInterpolationMode.LINEAR => AnimationCurveInterpolationType.Linear,
+ SharpGLTF.Schema2.AnimationInterpolationMode.STEP => AnimationCurveInterpolationType.Constant,
+ SharpGLTF.Schema2.AnimationInterpolationMode.CUBICSPLINE => AnimationCurveInterpolationType.Cubic,
+ _ => throw new NotImplementedException(),
+ };
+
+ var keyframes =
+ interpolationType switch
+ {
+ AnimationCurveInterpolationType.Constant =>
+ sampler.GetLinearKeys().Select(x => CreateKeyFrame(x.Key, ConvertNumerics(x.Value))),
+ AnimationCurveInterpolationType.Linear =>
+ sampler.GetLinearKeys().Select(x => CreateKeyFrame(x.Key, ConvertNumerics(x.Value))),
+ // TODO : Cubic can be broken
+ AnimationCurveInterpolationType.Cubic =>
+ sampler.GetCubicKeys().Select(x => ConvertNumerics(x.Value).Select(y => CreateKeyFrame(x.Key, y))).SelectMany(x => x),
+ _ => throw new NotImplementedException()
+ };
+
+ return new AnimationCurve
+ {
+ InterpolationType = interpolationType,
+ KeyFrames = new FastList>(keyframes)
+ };
+ }
+ ///
+ /// Converts a GLTF AnimationSampler into a Stride AnimationCurve
+ ///
+ ///
+ ///
+ public static AnimationCurve ConvertCurve(SharpGLTF.Schema2.IAnimationSampler sampler)
+ {
+ var interpolationType =
+ sampler.InterpolationMode switch
+ {
+ SharpGLTF.Schema2.AnimationInterpolationMode.LINEAR => AnimationCurveInterpolationType.Linear,
+ SharpGLTF.Schema2.AnimationInterpolationMode.STEP => AnimationCurveInterpolationType.Constant,
+ SharpGLTF.Schema2.AnimationInterpolationMode.CUBICSPLINE => AnimationCurveInterpolationType.Cubic,
+ _ => throw new NotImplementedException(),
+ };
+
+ var keyframes =
+ interpolationType switch
+ {
+ AnimationCurveInterpolationType.Constant =>
+ sampler.GetLinearKeys().Select(x => CreateKeyFrame(x.Key, x.Value)),
+ AnimationCurveInterpolationType.Linear =>
+ sampler.GetLinearKeys().Select(x => CreateKeyFrame(x.Key, x.Value)),
+ _ => throw new NotImplementedException()
+ };
+
+ return new AnimationCurve
+ {
+ InterpolationType = interpolationType,
+ KeyFrames = new FastList>(keyframes)
+ };
+ }
+ }
+}
diff --git a/sources/tools/Stride.Importer.Gltf/GltfMeshParser.cs b/sources/tools/Stride.Importer.Gltf/GltfMeshParser.cs
new file mode 100644
index 0000000000..ac6c3170a5
--- /dev/null
+++ b/sources/tools/Stride.Importer.Gltf/GltfMeshParser.cs
@@ -0,0 +1,432 @@
+// Copyright (c) Stride contributors (https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// 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 Stride.Animations;
+using Stride.Assets.Materials;
+using Stride.Core.IO;
+using Stride.Core.Mathematics;
+using Stride.Extensions;
+using Stride.Graphics;
+using Stride.Graphics.Data;
+using Stride.Importer.Common;
+using Stride.Rendering;
+using Stride.Rendering.Materials;
+using Stride.Rendering.Materials.ComputeColors;
+using static Stride.Importer.Gltf.GltfAnimationParser;
+using static Stride.Importer.Gltf.GltfUtils;
+
+namespace Stride.Importer.Gltf
+{
+ public static class GltfMeshParser
+ {
+ ///
+ /// Loads the gltf file depending its extension.
+ ///
+ /// path of the gltf file
+ /// ModelRoot
+ public static SharpGLTF.Schema2.ModelRoot LoadGltf(Stride.Core.IO.UFile sourcePath)
+ {
+
+ switch (sourcePath.GetFileExtension())
+ {
+ case ".gltf":
+ return SharpGLTF.Schema2.ModelRoot.Load(sourcePath);
+ case null:
+ return SharpGLTF.Schema2.ModelRoot.Load(sourcePath);
+ case ".glb":
+ var fs = new FileStream(sourcePath.FullPath, FileMode.Open);
+ return SharpGLTF.Schema2.ModelRoot.ReadGLB(fs);
+ default:
+ return null;
+ }
+
+ }
+
+ ///
+ /// Converts the first mesh in the GLTF file into a stride Model
+ ///
+ ///
+ ///
+ public static Model LoadFirstModel(SharpGLTF.Schema2.ModelRoot root)
+ {
+ // We load every primitives of the first mesh
+ var sk = ConvertSkeleton(root);
+ var draws =
+ root.LogicalMeshes
+ .Select(x => (x.Primitives.Select(x => LoadMesh(x, sk)).ToList(), ConvertNumerics(x.VisualParents.First().WorldMatrix))).ToList();
+ for (int i = 0; i < draws.Count; i++)
+ {
+ var mat = draws[i].Item2;
+ for (int j = 0; j < draws[i].Item1.Count; j++)
+ {
+ for (int k = 0; k < draws[i].Item1[j].Draw.VertexBuffers.Count(); k++)
+ {
+ draws[i].Item1[j].Draw.VertexBuffers[k].TransformBuffer(ref mat);
+ }
+ }
+ }
+ var result = new Model()
+ {
+ Meshes = draws.SelectMany(x => x.Item1).ToList()
+ };
+ result.Skeleton = sk;
+ return result;
+ }
+
+ ///
+ /// Gets the sum of all animation duration
+ ///
+ ///
+ ///
+ public static TimeSpan GetAnimationDuration(SharpGLTF.Schema2.ModelRoot root)
+ {
+ var time = root.LogicalAnimations.Select(x => x.Duration).Sum();
+ return TimeSpan.FromSeconds(time);
+ }
+
+ public static List GetAnimatedNodes(SharpGLTF.Schema2.ModelRoot root)
+ {
+ return root.LogicalAnimations.SelectMany(x => x.Channels.SelectMany(y => x.Channels.Select(z => z.TargetNode.Name))).ToList();
+ }
+
+ ///
+ /// Extract the entity info. This function tells the editor informations about any assets.
+ /// If any info is missing/wrong the assests won't be correctly imported.
+ ///
+ ///
+ ///
+ ///
+ public static EntityInfo ExtractEntityInfo(SharpGLTF.Schema2.ModelRoot modelRoot, UFile sourcePath)
+ {
+ SharpGLTF.Schema2.Skin skin = null;
+ HashSet boneNames = new HashSet();
+ List nodes = new List();
+
+ var meshName = sourcePath.GetFileNameWithoutExtension();
+
+
+ if (modelRoot.LogicalSkins.Where(x => x.VisualParents.First()?.Mesh == modelRoot.LogicalMeshes[0]).Count() > 0)
+ skin =
+ modelRoot.LogicalSkins
+ .Where(x => x.VisualParents.First().Mesh == modelRoot.LogicalMeshes[0])
+ .First();
+
+ // If there is a skin, we can load the bone names and instantiate the node informations.
+ if (skin != null)
+ {
+ boneNames =
+ Enumerable.Range(0, skin.JointsCount)
+ .Select(x => skin.GetJoint(x).Joint.Name ?? "Joint_" + skin.GetJoint(x).Joint.LogicalIndex)
+ .ToHashSet();
+ nodes = Enumerable.Range(0, skin.JointsCount)
+ .Select(x => new NodeInfo() { Name = skin.GetJoint(x).Joint.Name ?? "Joint_" + skin.GetJoint(x).Joint.LogicalIndex, Depth = skin.GetJoint(x).Joint.LogicalIndex, Preserve = true })
+ .ToList();
+ }
+
+ // Loading Mesh parameters, this will link the materials with the meshes
+ var meshes =
+ modelRoot
+ .LogicalMeshes[0].Primitives
+ .Select(
+ x =>
+ {
+ var materialName = x.Material != null ? sourcePath.GetFileNameWithoutExtension() + "_" + (x.Material.Name ?? "Material" + x.Material.LogicalIndex) : "";
+ return (meshName + "_" + x.LogicalIndex, materialName);
+ }
+ )
+ .Select(
+ x =>
+ new MeshParameters()
+ {
+ MeshName = x.Item1,
+ BoneNodes = boneNames,
+ MaterialName = x.materialName,
+ NodeName = ""
+ }
+ )
+ .ToList();
+
+ // Loading the animation names (should be the same as the keys used in animations
+ List animNodes = GetAnimatedNodes(modelRoot);
+ List animNames = ConvertAnimations(modelRoot, sourcePath.GetFileNameWithoutExtension()).Keys.ToList();
+
+ return new EntityInfo
+ {
+ Models = meshes,
+ AnimationNodes = animNodes,
+ AnimationNames = animNames,
+ Materials = LoadMaterials(modelRoot, sourcePath),
+ Nodes = nodes,
+ TextureDependencies = GenerateTextureFullPaths(modelRoot, sourcePath)
+ };
+ }
+
+
+
+ ///
+ /// Converts GLTF joints into MeshSkinningDefinition, defining the Mesh to World matrix useful for skinning and animations.
+ ///
+ ///
+ ///
+ public static MeshSkinningDefinition ConvertInverseBindMatrices(SharpGLTF.Schema2.ModelRoot root, Skeleton sk)
+ {
+ var skin = root.LogicalNodes.First(x => x.Mesh == root.LogicalMeshes[0]).Skin;
+ if (skin == null) return null;
+ var jointList = Enumerable.Range(0, skin.JointsCount).Select(skin.GetJoint);
+ var nodeList = sk.Nodes.ToList();
+ var mnt =
+ new MeshSkinningDefinition
+ {
+ Bones =
+ jointList
+ .Select(x =>
+ new MeshBoneDefinition
+ {
+ NodeIndex = nodeList.IndexOf(nodeList.First(n => n.Name == x.Joint.Name)),
+ LinkToMeshMatrix = ConvertNumerics(x.InverseBindMatrix)
+ }
+ )
+ .ToArray()
+ };
+ return mnt;
+ }
+
+ ///
+ /// Converts GLTF animations into Stride AnimationClips.
+ ///
+ ///
+ ///
+ public static Dictionary ConvertAnimations(SharpGLTF.Schema2.ModelRoot root, string filename)
+ {
+ var animations = root.LogicalAnimations;
+ var meshName = filename;
+ var sk = ConvertSkeleton(root).Nodes.Select(x => x.Name);
+
+ var clips =
+ animations
+ .Select(x =>
+ {
+ //Create animation clip with
+ var clip = new AnimationClip { Duration = TimeSpan.FromSeconds(x.Duration) };
+ clip.RepeatMode = AnimationRepeatMode.LoopInfinite;
+ // Add Curve
+ foreach (var c in ConvertCurves(x.Channels, root))
+ {
+ clip.AddCurve(c.Key, c.Value);
+ }
+
+ if (clip.Curves.Count > 1) clip.Optimize();
+ return (x.Name ?? "Animation_" + animations.ToList().IndexOf(x), clip);
+ }
+ )
+ .ToList()
+ .ToDictionary(x => x.Item1, x => x.clip);
+ return clips;
+ }
+
+ ///
+ /// Convert GLTF materials to Stride materials
+ ///
+ ///
+ ///
+ ///
+ public static Dictionary LoadMaterials(SharpGLTF.Schema2.ModelRoot root, UFile sourcePath)
+ {
+ // TODO : Handle official ClearCoat extension
+ var result = new Dictionary();
+ foreach (var mat in root.LogicalMaterials)
+ {
+ var material = new MaterialAsset
+ {
+ Attributes = new MaterialAttributes()
+ };
+ foreach (var chan in mat.Channels)
+ {
+
+ if (chan.Texture != null && !chan.HasDefaultContent)
+ {
+
+ var gltfImg = chan.Texture.PrimaryImage;
+ var textureName = gltfImg.Name ?? sourcePath.GetFileNameWithoutExtension() + "_" + (mat.Name ?? mat.LogicalIndex.ToString()) + "_" + chan.Key;
+
+ string imgPath;
+ if (gltfImg.Content.SourcePath == null)
+ {
+ imgPath = Path.Join(sourcePath.GetFullDirectory(), textureName + "." + gltfImg.Content.FileExtension);
+ if (!File.Exists(imgPath))
+ gltfImg.Content.SaveToFile(imgPath);
+ }
+ else
+ {
+ imgPath = gltfImg.Content.SourcePath;
+ }
+
+ switch (chan.Key)
+ {
+ case "BaseColor":
+ material.Attributes.Diffuse = new MaterialDiffuseMapFeature(GenerateTextureColor(imgPath, (TextureCoordinate)chan.TextureCoordinate, Vector2.One));
+ material.Attributes.DiffuseModel = new MaterialDiffuseLambertModelFeature();
+ break;
+ case "MetallicRoughness":
+ material.Attributes.MicroSurface = new MaterialGlossinessMapFeature(GenerateTextureScalar(imgPath, (TextureCoordinate)chan.TextureCoordinate, Vector2.One));
+ break;
+ case "Normal":
+ material.Attributes.Surface = new MaterialNormalMapFeature(GenerateTextureColor(imgPath, (TextureCoordinate)chan.TextureCoordinate, Vector2.One));
+ break;
+ case "Occlusion":
+ material.Attributes.Occlusion = new MaterialOcclusionMapFeature();
+ break;
+ case "Emissive":
+ material.Attributes.Emissive = new MaterialEmissiveMapFeature(GenerateTextureColor(imgPath, (TextureCoordinate)chan.TextureCoordinate, Vector2.One));
+ break;
+ }
+ }
+ else if (chan.Texture == null && !chan.HasDefaultContent)
+ {
+ var vt = new ComputeColor(new Color(ConvertNumerics(chan.Parameter)));
+ var x = new ComputeFloat(chan.Parameter.X);
+
+ switch (chan.Key)
+ {
+ case "BaseColor":
+ material.Attributes.Diffuse = new MaterialDiffuseMapFeature(vt);
+ material.Attributes.DiffuseModel = new MaterialDiffuseLambertModelFeature();
+ //material.Attributes.Transparency = new MaterialTransparencyBlendFeature();
+ break;
+ case "MetallicRoughness":
+ material.Attributes.MicroSurface = new MaterialGlossinessMapFeature(x) { Invert = true };
+ break;
+ case "Normal":
+ material.Attributes.Surface = new MaterialNormalMapFeature(vt) { IsXYNormal = true };
+ break;
+ case "Occlusion":
+ material.Attributes.Occlusion = new MaterialOcclusionMapFeature() { CavityMap = vt as IComputeScalar };
+ break;
+ case "Emissive":
+ material.Attributes.Emissive = new MaterialEmissiveMapFeature(vt);
+ break;
+ }
+ }
+
+ }
+ material.Attributes.CullMode = CullMode.Back;
+ var materialName = sourcePath.GetFileNameWithoutExtension() + "_" + (mat.Name ?? "Material") + "_" + mat.LogicalIndex;
+
+ result.TryAdd(materialName, material);
+ }
+ return result;
+ }
+
+ ///
+ /// Convert a GLTF Primitive into a Stride Mesh
+ ///
+ ///
+ ///
+ public static Mesh LoadMesh(SharpGLTF.Schema2.MeshPrimitive mesh, Skeleton sk)
+ {
+
+ var draw = new MeshDraw
+ {
+ PrimitiveType = ConvertPrimitiveType(mesh.DrawPrimitiveType),
+ IndexBuffer = ConvertSerializedIndexBufferBinding(mesh),
+ VertexBuffers = ConvertSerializedVertexBufferBinding(mesh),
+ DrawCount = GetDrawCount(mesh)
+ };
+
+
+
+ var result = new Mesh(draw, new ParameterCollection())
+ {
+ Skinning = ConvertInverseBindMatrices(mesh.LogicalParent.LogicalParent, sk),
+ Name = mesh.LogicalParent.Name,
+ MaterialIndex = mesh.LogicalParent.LogicalParent.LogicalMaterials.ToList().IndexOf(mesh.Material)
+ };
+
+
+ // TODO : Add parameter collection only after checking if it has
+ result.Parameters.Set(MaterialKeys.HasSkinningPosition, true);
+ result.Parameters.Set(MaterialKeys.HasSkinningNormal, true);
+ return result;
+ }
+
+ ///
+ /// Gets the number of triangle indices.
+ ///
+ ///
+ ///
+ private static int GetDrawCount(SharpGLTF.Schema2.MeshPrimitive mesh)
+ {
+ // TODO : Check if every meshes has triangle indices
+ return mesh.GetTriangleIndices().Select(x => new int[] { x.A, x.B, x.C }).SelectMany(x => x).Select(x => (uint)x).ToArray().Length;
+ }
+
+ ///
+ /// Converts an index buffer into a serialized index buffer binding for asset creation.
+ ///
+ ///
+ ///
+ public static IndexBufferBinding ConvertSerializedIndexBufferBinding(SharpGLTF.Schema2.MeshPrimitive mesh)
+ {
+ var indices =
+ mesh.GetTriangleIndices()
+ .Select(x => new int[] { x.A, x.C, x.B })
+ .SelectMany(x => x).Select(x => (uint)x)
+ .Select(BitConverter.GetBytes)
+ .SelectMany(x => x).ToArray();
+ var buf = GraphicsSerializerExtensions.ToSerializableVersion(new BufferData(BufferFlags.IndexBuffer, indices));
+ return new IndexBufferBinding(buf, true, indices.Length);
+ }
+
+ ///
+ /// Converts a vertex buffer into a serialized vertex buffer for asset creation.
+ ///
+ ///
+ ///
+ public static VertexBufferBinding[] ConvertSerializedVertexBufferBinding(SharpGLTF.Schema2.MeshPrimitive mesh)
+ {
+ var offset = 0;
+ var vertElem =
+ mesh.VertexAccessors
+ .Select(
+ x =>
+ {
+ var y = ConvertVertexElement(x, offset);
+ offset += y.Item2;
+ return y.Item1;
+ })
+ .ToList();
+ var declaration =
+ new VertexDeclaration(
+ vertElem.ToArray()
+ );
+
+ var size = mesh.VertexAccessors.First().Value.Count;
+
+ List bytelst = new();
+
+ for (int i = 0; i < size; i++)
+ {
+ foreach (var e in mesh.VertexAccessors.Keys)
+ {
+ var bytes = mesh.GetVertexAccessor(e).TryGetVertexBytes(i).ToArray();
+ bytelst.AddRange(bytes);
+ }
+ }
+ var byteBuffer = bytelst.ToArray();
+
+ var buffer =
+ GraphicsSerializerExtensions.ToSerializableVersion(
+ new BufferData(BufferFlags.VertexBuffer, byteBuffer)
+ );
+ var binding = new VertexBufferBinding(buffer, declaration, size);
+
+ return new List() { binding }.ToArray();
+ }
+
+
+ }
+}
diff --git a/sources/tools/Stride.Importer.Gltf/GltfUtils.cs b/sources/tools/Stride.Importer.Gltf/GltfUtils.cs
new file mode 100644
index 0000000000..388b5073cf
--- /dev/null
+++ b/sources/tools/Stride.Importer.Gltf/GltfUtils.cs
@@ -0,0 +1,230 @@
+// Copyright (c) Stride contributors (https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// 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 Stride.Core.Assets;
+using Stride.Core.IO;
+using Stride.Core.Mathematics;
+using Stride.Core.Serialization;
+using Stride.Graphics;
+using Stride.Rendering.Materials;
+using Stride.Rendering.Materials.ComputeColors;
+
+namespace Stride.Importer.Gltf
+{
+ public static class GltfUtils
+ {
+ ///
+ /// Converts a System.Numerics value into a Stride value
+ ///
+ ///
+ ///
+ public static Matrix ConvertNumerics(System.Numerics.Matrix4x4 mat)
+ {
+ return new Matrix(
+ mat.M11, mat.M12, mat.M13, mat.M14,
+ mat.M21, mat.M22, mat.M23, mat.M24,
+ mat.M31, mat.M32, mat.M33, mat.M34,
+ mat.M41, mat.M42, mat.M43, mat.M44
+ );
+ }
+ ///
+ /// Converts a System.Numerics value into a Stride value
+ ///
+ ///
+ ///
+ public static Quaternion ConvertNumerics(System.Numerics.Quaternion v) => new Quaternion(v.X, v.Y, v.Z, v.W);
+
+ ///
+ /// Converts a System.Numerics value into a Stride value
+ ///
+ ///
+ ///
+ public static Vector4 ConvertNumerics(System.Numerics.Vector4 v) => new Vector4(v.X, v.Y, v.Z, v.W);
+
+ ///
+ /// Converts a System.Numerics value into a Stride value
+ ///
+ ///
+ ///
+ public static Vector3 ConvertNumerics(System.Numerics.Vector3 v) => new Vector3(v.X, v.Y, v.Z);
+
+ ///
+ /// Converts a System.Numerics value into a Stride value
+ ///
+ ///
+ ///
+ public static Vector2 ConvertNumerics(System.Numerics.Vector2 v) => new Vector2(v.X, v.Y);
+
+
+ public static List ConvertNumerics((System.Numerics.Vector2, System.Numerics.Vector2, System.Numerics.Vector2) value)
+ {
+ return new List { ConvertNumerics(value.Item1), ConvertNumerics(value.Item2), ConvertNumerics(value.Item3) };
+ }
+
+ public static List ConvertNumerics((System.Numerics.Vector3, System.Numerics.Vector3, System.Numerics.Vector3) value)
+ {
+ return new List { ConvertNumerics(value.Item1), ConvertNumerics(value.Item2), ConvertNumerics(value.Item3) };
+ }
+
+ public static List ConvertNumerics((System.Numerics.Vector4, System.Numerics.Vector4, System.Numerics.Vector4) value)
+ {
+ return new List { ConvertNumerics(value.Item1), ConvertNumerics(value.Item2), ConvertNumerics(value.Item3) };
+ }
+
+ public static List ConvertNumerics((System.Numerics.Quaternion, System.Numerics.Quaternion, System.Numerics.Quaternion) value)
+ {
+ return new List { ConvertNumerics(value.Item1), ConvertNumerics(value.Item2), ConvertNumerics(value.Item3) };
+ }
+
+ ///
+ /// Gets the Stride VertexElement equivalent of a GLTF vertex
+ ///
+ ///
+ ///
+ ///
+ public static (VertexElement, int) ConvertVertexElement(KeyValuePair accessor, int offset)
+ {
+ // TODO : Can be simplified but this gives better control over what's implemented or not
+ return (accessor.Key, accessor.Value.Format.ByteSize) switch
+ {
+ ("POSITION", 12) => (VertexElement.Position(0, offset), 12),
+ ("NORMAL", 12) => (VertexElement.Normal(0, offset), 12),
+ ("TANGENT", 16) => (VertexElement.Tangent(0, offset), 16),
+ ("COLOR", 16) => (VertexElement.Color(0, offset), 16),
+ ("COLOR_0", 8) => (new VertexElement(VertexElementUsage.Color, 0, PixelFormat.R16G16B16A16_Float, offset), 8),
+ ("COLOR_0", 16) => (VertexElement.Color(0, offset), 16),
+ ("TEXCOORD_0", 8) => (VertexElement.TextureCoordinate(0, offset), 8),
+ ("TEXCOORD_1", 8) => (VertexElement.TextureCoordinate(1, offset), 8),
+ ("TEXCOORD_2", 8) => (VertexElement.TextureCoordinate(2, offset), 8),
+ ("TEXCOORD_3", 8) => (VertexElement.TextureCoordinate(3, offset), 8),
+ ("TEXCOORD_4", 8) => (VertexElement.TextureCoordinate(4, offset), 8),
+ ("TEXCOORD_5", 8) => (VertexElement.TextureCoordinate(5, offset), 8),
+ ("TEXCOORD_6", 8) => (VertexElement.TextureCoordinate(6, offset), 8),
+ ("TEXCOORD_7", 8) => (VertexElement.TextureCoordinate(7, offset), 8),
+ ("TEXCOORD_8", 8) => (VertexElement.TextureCoordinate(8, offset), 8),
+ ("TEXCOORD_9", 8) => (VertexElement.TextureCoordinate(9, offset), 8),
+ ("JOINTS_0", 8) => (new VertexElement(VertexElementUsage.BlendIndices, 0, PixelFormat.R16G16B16A16_UInt, offset), 8),
+ ("JOINTS_0", 4) => (new VertexElement(VertexElementUsage.BlendIndices, 0, PixelFormat.R8G8B8A8_UInt, offset), 4),
+ ("WEIGHTS_0", 4) => (new VertexElement(VertexElementUsage.BlendWeight, 0, PixelFormat.R8G8B8A8_UInt, offset), 4),
+ ("WEIGHTS_0", 8) => (new VertexElement(VertexElementUsage.BlendWeight, 0, PixelFormat.R16G16B16A16_Float, offset), 8),
+ ("WEIGHTS_0", 16) => (new VertexElement(VertexElementUsage.BlendWeight, 0, PixelFormat.R32G32B32A32_Float, offset), 16),
+ _ => throw new NotImplementedException(),
+ };
+ }
+ ///
+ /// Gets the GLTF primitive's PrimitiveType
+ ///
+ ///
+ ///
+ public static PrimitiveType ConvertPrimitiveType(SharpGLTF.Schema2.PrimitiveType gltfType)
+ {
+ return gltfType switch
+ {
+ SharpGLTF.Schema2.PrimitiveType.LINES => PrimitiveType.LineList,
+ SharpGLTF.Schema2.PrimitiveType.POINTS => PrimitiveType.PointList,
+ SharpGLTF.Schema2.PrimitiveType.LINE_LOOP => PrimitiveType.Undefined,
+ SharpGLTF.Schema2.PrimitiveType.LINE_STRIP => PrimitiveType.LineStrip,
+ SharpGLTF.Schema2.PrimitiveType.TRIANGLES => PrimitiveType.TriangleList,
+ SharpGLTF.Schema2.PrimitiveType.TRIANGLE_STRIP => PrimitiveType.TriangleStrip,
+ _ => PrimitiveType.Undefined
+ };
+ }
+
+ ///
+ /// Generates the texture files paths.
+ ///
+ ///
+ ///
+ ///
+ public static List GenerateTextureFullPaths(SharpGLTF.Schema2.ModelRoot root, UFile sourcePath)
+ {
+ var result = new List();
+ foreach (var mat in root.LogicalMaterials)
+ {
+ foreach (var chan in mat.Channels)
+ {
+
+ if (chan.Texture != null && !chan.HasDefaultContent)
+ {
+
+ var gltfImg = chan.Texture.PrimaryImage;
+ if (gltfImg.Content.SourcePath == null)
+ {
+ var textureName = gltfImg.Name ?? sourcePath.GetFileNameWithoutExtension() + "_" + (mat.Name ?? mat.LogicalIndex.ToString()) + "_" + chan.Key;
+ result.Add(Path.Join(sourcePath.GetFullDirectory(), textureName + "." + gltfImg.Content.FileExtension));
+
+ }
+ else
+ {
+ result.Add(gltfImg.Content.SourcePath);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ ///
+ /// Generate a ComputeTextureColor for asset creation
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static ComputeTextureColor GenerateTextureColor(string sourceTextureFile, TextureCoordinate textureUVSetIndex, Vector2 textureUVscaling, TextureAddressMode addressModeU = TextureAddressMode.Wrap, TextureAddressMode addressModeV = TextureAddressMode.Wrap, string vfsOutputPath = "")
+ {
+ var textureFileName = Path.GetFileNameWithoutExtension(sourceTextureFile);
+
+ var uvScaling = textureUVscaling;
+ var textureName = textureFileName;
+
+ var texture = AttachedReferenceManager.CreateProxyObject(AssetId.Empty, textureName);
+
+
+ var currentTexture =
+ new ComputeTextureColor(texture, textureUVSetIndex, uvScaling, Vector2.Zero)
+ {
+ AddressModeU = addressModeU,
+ AddressModeV = addressModeV
+ };
+
+ return currentTexture;
+ }
+
+ ///
+ /// Generate a ComputeTextureScalar for asset creation.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static ComputeTextureScalar GenerateTextureScalar(string sourceTextureFile, TextureCoordinate textureUVSetIndex, Vector2 textureUVscaling, TextureAddressMode addressModeU = TextureAddressMode.Wrap, TextureAddressMode addressModeV = TextureAddressMode.Wrap, string vfsOutputPath = "")
+ {
+ var textureFileName = Path.GetFileNameWithoutExtension(sourceTextureFile);
+
+ var uvScaling = textureUVscaling;
+ var textureName = textureFileName;
+
+ var texture = AttachedReferenceManager.CreateProxyObject(AssetId.Empty, textureName);
+
+ var currentTexture =
+ new ComputeTextureScalar(texture, textureUVSetIndex, uvScaling, Vector2.Zero)
+ {
+ AddressModeU = addressModeU,
+ AddressModeV = addressModeV
+ };
+
+ return currentTexture;
+ }
+
+ }
+}
diff --git a/sources/tools/Stride.Importer.Gltf/Stride.Importer.Gltf.csproj b/sources/tools/Stride.Importer.Gltf/Stride.Importer.Gltf.csproj
new file mode 100644
index 0000000000..c625ff15fe
--- /dev/null
+++ b/sources/tools/Stride.Importer.Gltf/Stride.Importer.Gltf.csproj
@@ -0,0 +1,30 @@
+
+
+
+
+ $(StrideEditorTargetFramework)
+ 9.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\Stride.Importer.Common\bin\net6.0-windows7.0\x64\Debug\Stride.Importer.Common.dll
+
+
+
+
+
+