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 + + + + + +