diff --git a/Assets/VGltfExamples/VRMExample/Scripts/VRMLoader.cs b/Assets/VGltfExamples/VRMExample/Scripts/VRMLoader.cs index a9f6adb..ab6c546 100644 --- a/Assets/VGltfExamples/VRMExample/Scripts/VRMLoader.cs +++ b/Assets/VGltfExamples/VRMExample/Scripts/VRMLoader.cs @@ -8,6 +8,7 @@ using UnityEngine.UI; using VGltf; using VGltf.Unity; +using System.Linq; namespace VGltfExamples.VRMExample { @@ -39,6 +40,21 @@ public void Dispose() readonly List _vrmResources = new List(); + [Serializable] + sealed class ClipResource : IDisposable + { + public IImporterContext Context; + [SerializeField] public AnimationClip[] ClipRefs; + + public void Dispose() + { + Context?.Dispose(); + } + } + + // デバッグ用 + [SerializeField] List _clipResources = new List(); + void Start() { loadButton.onClick.AddListener(UIOnLoadButtonClick); @@ -58,6 +74,11 @@ void OnDestroy() { disposable.Dispose(); } + + foreach(var disposable in _clipResources) + { + disposable.Dispose(); + } } async UniTask LoadVRM() @@ -120,6 +141,11 @@ void UIOnLoadButtonClick() async UniTaskVoid UIOnLoadButtonClickAsync() { +#if false + var clipRes = await ImportClip("anim.glb"); + _clipResources.Insert(0, clipRes); +#endif + var p0 = Common.MemoryProfile.Now; DebugLogProfile(p0); @@ -168,6 +194,10 @@ async UniTaskVoid UIOnExportButtonClickedAsync() var head = _vrmResources[0]; var anim = head.Go.GetComponentInChildren(); + + var info = anim.GetCurrentAnimatorClipInfo(0); + var clip = info[0].clip; + var animCtrl = anim.runtimeAnimatorController; anim.runtimeAnimatorController = null; // Make the model to the rest pose @@ -211,6 +241,69 @@ await Task.Run(() => Debug.Log("exported"); }); + +#if false + await ExportClip(clip, "anim.glb"); +#endif + } + + async Task ImportClip(string path) + { + // Read the glTF container (unity-independent) + var gltfContainer = await Task.Run(() => + { + using (var fs = new FileStream(path, FileMode.Open)) + { + return GltfContainer.FromGlb(fs); + } + }); + + var clipRefs = new List(); + + var timeSlicer = new Common.TimeSlicer(); + using (var gltfImporter = new Importer(gltfContainer, timeSlicer)) + { + gltfImporter.AddHook(new VGltf.Unity.Ext.Helper.AnimationClipImporter( + hooks: new VGltf.Unity.Ext.Helper.AnimationClipImporterHook[] { + new VGltf.Unity.Ext.Helper.HumanoidAnimationImporter(), + }, + clipRefs: clipRefs + )); + + var context = await gltfImporter.ImportEmpty(System.Threading.CancellationToken.None); + return new ClipResource + { + Context = context, + ClipRefs = clipRefs.ToArray(), + }; + } + } + + async Task ExportClip(AnimationClip clip, string path) + { + GltfContainer gltfContainer = null; + using (var gltfExporter = new Exporter()) + { + gltfExporter.AddHook(new VGltf.Unity.Ext.Helper.AnimationClipExporter( + clips: new AnimationClip[] { clip }, + hooks: new VGltf.Unity.Ext.Helper.AnimationClipExporterHook[] { + new VGltf.Unity.Ext.Helper.HumanoidAnimationExporter(), + } + )); + + gltfExporter.ExportEmpty(); + gltfContainer = gltfExporter.IntoGlbContainer(); + } + + await Task.Run(() => + { + using (var fs = new FileStream(path, FileMode.Create)) + { + GltfContainer.ToGlb(fs, gltfContainer); + } + + Debug.Log("clip exported"); + }); } void DebugLogProfile(Common.MemoryProfile now, Common.MemoryProfile prev = null) diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Exporter.cs b/Packages/net.yutopp.vgltf.unity/Runtime/Exporter.cs index 97850de..3b2901d 100644 --- a/Packages/net.yutopp.vgltf.unity/Runtime/Exporter.cs +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Exporter.cs @@ -121,6 +121,14 @@ public void ExportGameObjectAsScene(GameObject go) } } + public void ExportEmpty() + { + foreach (var hook in Hooks) + { + hook.PostHook(this, null); + } + } + void ExportGameObjectAsSceneWithoutNormalize(GameObject go) { Func[]> generator = () => diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/ExporterRuntimeResources.cs b/Packages/net.yutopp.vgltf.unity/Runtime/ExporterRuntimeResources.cs index 7497587..b33d10d 100644 --- a/Packages/net.yutopp.vgltf.unity/Runtime/ExporterRuntimeResources.cs +++ b/Packages/net.yutopp.vgltf.unity/Runtime/ExporterRuntimeResources.cs @@ -20,6 +20,7 @@ public sealed class ExporterRuntimeResources : IDisposable public IndexedResourceDict Materials = new IndexedResourceDict(); public IndexedResourceDict<(Mesh, Material[]), Mesh> Meshes = new IndexedResourceDict<(Mesh, Material[]), Mesh>(new MeshEqualityComparer()); public IndexedResourceDict Skins = new IndexedResourceDict(); + public IndexedResourceDict Animations = new IndexedResourceDict(); public void Dispose() { diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper.meta b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper.meta new file mode 100644 index 0000000..bc51072 --- /dev/null +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ee35cfd37da454db6bd687370998633f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/AnimationClipExporter.cs b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/AnimationClipExporter.cs new file mode 100644 index 0000000..c2c20cc --- /dev/null +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/AnimationClipExporter.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) 2022- yutopp (yutopp@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using VGltf.Types.Extensions; + +namespace VGltf.Unity.Ext.Helper +{ + public abstract class AnimationClipExporterHook + { + public abstract IndexedResource Export(IExporterContext context, AnimationClip clip); + } + + public sealed class AnimationClipExporter : ExporterHookBase + { + readonly AnimationClip[] _clips; + readonly AnimationClipExporterHook[] _hooks; + + public AnimationClipExporter(AnimationClip clip, AnimationClipExporterHook[] hooks) + : this(new AnimationClip[] { clip }, hooks) + { + } + + public AnimationClipExporter(AnimationClip[] clips, AnimationClipExporterHook[] hooks) + { + _clips = clips; + _hooks = hooks; + } + + public override void PostHook(Exporter exporter, GameObject go) + { + foreach (var clip in _clips) + { + foreach (var hook in _hooks) + { + var r = hook.Export(exporter.Context, clip); + if (r != null) + { + // If the clip was processed, ignore hooks subsequently (per clips). + break; + } + } + } + } + } +} diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/AnimationClipExporter.cs.meta b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/AnimationClipExporter.cs.meta new file mode 100644 index 0000000..e1648b3 --- /dev/null +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/AnimationClipExporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ffc50ae30f40e46b584a695d0993ad03 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/AnimationClipImporter.cs b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/AnimationClipImporter.cs new file mode 100644 index 0000000..871b51a --- /dev/null +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/AnimationClipImporter.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) 2022- yutopp (yutopp@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +namespace VGltf.Unity.Ext.Helper +{ + public abstract class AnimationClipImporterHook + { + public abstract Task> Import(IImporterContext context, int animIndex, CancellationToken ct); + } + + public sealed class AnimationClipImporter : ImporterHookBase + { + readonly AnimationClipImporterHook[] _hooks; + readonly List _clipRefs; + + public AnimationClipImporter(AnimationClipImporterHook[] hooks, List clipRefs) + { + _hooks = hooks; + _clipRefs = clipRefs; + } + + public override async Task PostHook(IImporterContext context, CancellationToken ct) + { + var gltf = context.Container.Gltf; + + for(var i=0; i Export(IExporterContext context, AnimationClip clip) + { + // If clip is not HumanMotion, Do nothing. + if (!clip.isHumanMotion) + { + return null; + } + + var curveBindings = AnimationUtility.GetCurveBindings(clip); + + var channels = new List(); + var samplers = new List(); + + foreach (var binding in curveBindings) + { + var curve = AnimationUtility.GetEditorCurve(clip, binding); + + var timestamps = new float[curve.keys.Length]; + var values = new float[curve.keys.Length]; + + var inTangents = new float[curve.keys.Length]; + var inWeights = new float[curve.keys.Length]; + var outTangents = new float[curve.keys.Length]; + var outWeights = new float[curve.keys.Length]; + + foreach (var (keyframe, index) in curve.keys.Select((v, i) => (v, i))) + { + timestamps[index] = keyframe.time; + values[index] = keyframe.value; + + inTangents[index] = keyframe.inTangent; + inWeights[index] = keyframe.inWeight; + outTangents[index] = keyframe.outTangent; + outWeights[index] = keyframe.outWeight; + // keyframe.weightedMode + } + + var inputAccessorId = ExportTimestamp(context, timestamps); + var outputAccessorId = ExportHumanoidFloatScalarValue(context, values); + + var inTangentAccessorId = ExportHumanoidFloatScalarValue(context, inTangents); + var inWeightAccessorId = ExportHumanoidFloatScalarValue(context, inWeights); + var outTangentAccessorId = ExportHumanoidFloatScalarValue(context, outTangents); + var outWeightAccessorId = ExportHumanoidFloatScalarValue(context, outWeights); + + // sampler + var samplerId = samplers.Count; + + var sampler = new VGltf.Types.Animation.SamplerType + { + Input = inputAccessorId, + // Interpolation will not be used when "VGLTF_unity_humanoid_animation_sampler" specified + Output = outputAccessorId, + }; + sampler.AddExtra(Types.HumanoidAnimationType.SamplerType.ExtraName, new Types.HumanoidAnimationType.SamplerType + { + InTangent = inTangentAccessorId, + InWeight = inWeightAccessorId, + OutTangent = outTangentAccessorId, + OutWeight = outWeightAccessorId, + }); + samplers.Add(sampler); + + // node (dummy) + var dummyNode = context.Gltf.AddNode(new VGltf.Types.Node + { + Matrix = null, + Translation = null, + Rotation = null, + Scale = null, + }); + + // channel + var channel = new VGltf.Types.Animation.ChannelType + { + Sampler = samplerId, + // NOTE: Target will not be used when "VGLTF_unity_humanoid_animation_channel" specified + Target = new VGltf.Types.Animation.ChannelType.TargetType + { + Node = dummyNode, // Specify the dummy node to suppress validation errors... + Path = "", // Never used for Unity humanoid + }, + }; + channel.AddExtra(Types.HumanoidAnimationType.ChannelType.ExtraName, new Types.HumanoidAnimationType.ChannelType + { + RelativePath = binding.path, + PropertyName = binding.propertyName + }); + channels.Add(channel); + } + + // TODO: Append standard animations if needed. + + var gltfAnim = new VGltf.Types.Animation + { + Name = clip.name, + Channels = channels, + Samplers = samplers, + }; + gltfAnim.AddExtra(Types.HumanoidAnimationType.ExtraName, new Types.HumanoidAnimationType{}); + var animIndex = context.Gltf.AddAnimation(gltfAnim); + + var res = context.Resources.Animations.Add(clip, animIndex, clip.name, clip); + + return res; + } +#else + public override IndexedResource Export(IExporterContext context, AnimationClip clip) + { + throw new NotImplementedException("Could not export AnimationClips without Editor"); + } +#endif + + public static int ExportTimestamp(IExporterContext context, float[] timestamps) + { + // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#_animation_sampler_input + + // Scalar | FLOAT + + byte[] buffer = PrimitiveExporter.Marshal(timestamps); + var viewIndex = context.BufferBuilder.AddView(new ArraySegment(buffer)); + + var viewComponentType = VGltf.Types.Accessor.ComponentTypeEnum.FLOAT; + + var accessor = new VGltf.Types.Accessor + { + BufferView = viewIndex, + ByteOffset = 0, + ComponentType = viewComponentType, + Count = timestamps.Length, + Min = new float[]{ Mathf.Min(timestamps) }, + Max = new float[]{ Mathf.Max(timestamps) }, + Type = VGltf.Types.Accessor.TypeEnum.Scalar, + }; + return context.Gltf.AddAccessor(accessor); + } + + public static int ExportHumanoidFloatScalarValue(IExporterContext context, float[] values) + { + // Scalar | FLOAT + + byte[] buffer = PrimitiveExporter.Marshal(values); + var viewIndex = context.BufferBuilder.AddView(new ArraySegment(buffer)); + + var viewComponentType = VGltf.Types.Accessor.ComponentTypeEnum.FLOAT; + + var accessor = new VGltf.Types.Accessor + { + BufferView = viewIndex, + ByteOffset = 0, + ComponentType = viewComponentType, + Count = values.Length, + Min = new float[]{ Mathf.Min(values) }, + Max = new float[]{ Mathf.Max(values) }, + Type = VGltf.Types.Accessor.TypeEnum.Scalar, + }; + return context.Gltf.AddAccessor(accessor); + } + } +} diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/HumanoidAnimationExporter.cs.meta b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/HumanoidAnimationExporter.cs.meta new file mode 100644 index 0000000..c77c612 --- /dev/null +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/HumanoidAnimationExporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 60059d8a805bc4bc4abd68b5d3df0467 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/HumanoidAnimationImporter.cs b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/HumanoidAnimationImporter.cs new file mode 100644 index 0000000..bd0a640 --- /dev/null +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/HumanoidAnimationImporter.cs @@ -0,0 +1,119 @@ +// +// Copyright (c) 2022- yutopp (yutopp@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +namespace VGltf.Unity.Ext.Helper +{ + public sealed class HumanoidAnimationImporter : AnimationClipImporterHook + { + public override Task> Import(IImporterContext context, int animIndex, CancellationToken ct) + { + var gltf = context.Container.Gltf; + var gltfAnim = gltf.Animations[animIndex]; + + var animExtra = default(Types.HumanoidAnimationType); + if (!gltfAnim.TryGetExtra(Types.HumanoidAnimationType.ExtraName, context.Container.JsonSchemas, out animExtra)) + { + // This importer aims to importing only HumanoidAnimation extras, thus skip process. + return null; + } + + var animClip = new AnimationClip(); + animClip.name = gltfAnim.Name; + + var resource = context.Resources.Animations.Add(animIndex, animIndex, animClip.name, new Utils.DestroyOnDispose(animClip)); + // Ownership is already held by "context.Resources". This is just a reference. + var resourceTyped = new IndexedResource(resource.Index, animClip); + + foreach (var channel in gltfAnim.Channels) + { + var channelExtra = default(Types.HumanoidAnimationType.ChannelType); + if (!channel.TryGetExtra(Types.HumanoidAnimationType.ChannelType.ExtraName, context.Container.JsonSchemas, out channelExtra)) + { + throw new NotImplementedException($"Extras {Types.HumanoidAnimationType.ChannelType.ExtraName} is not given"); + } + + var sampler = gltfAnim.Samplers[channel.Sampler]; + + var samplerExtra = default(Types.HumanoidAnimationType.SamplerType); + if (!sampler.TryGetExtra(Types.HumanoidAnimationType.SamplerType.ExtraName, context.Container.JsonSchemas, out samplerExtra)) + { + throw new NotImplementedException($"Extras {Types.HumanoidAnimationType.SamplerType.ExtraName} is not given"); + } + + var timestamps = ImportTimestamp(context, sampler.Input); + var values = ImportHumanoidFloatScalarValue(context, sampler.Output); + Debug.Assert(timestamps.Length == values.Length); + + var inTangents = ImportHumanoidFloatScalarValue(context, samplerExtra.InTangent); + var inWeights = ImportHumanoidFloatScalarValue(context, samplerExtra.InWeight); + var outTangents = ImportHumanoidFloatScalarValue(context, samplerExtra.OutTangent); + var outWeights = ImportHumanoidFloatScalarValue(context, samplerExtra.OutWeight); + + // TODO: support interpolation + var keyframes = new Keyframe[timestamps.Length]; + for (var i = 0; i < timestamps.Length; ++i) + { + var keyframe = new Keyframe( + time: timestamps[i], + value: values[i], + inTangent: inTangents[i], + outTangent: outTangents[i], + inWeight: inWeights[i], + outWeight: outWeights[i]); + keyframes[i] = keyframe; + } + + var curve = new AnimationCurve(keyframes); + animClip.SetCurve(channelExtra.RelativePath, typeof(Animator), channelExtra.PropertyName, curve); + } + + return Task.FromResult(resourceTyped); + } + + public static float[] ImportTimestamp(IImporterContext context, int index) + { + // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#_animation_sampler_input + + // SCALAR | FLOAT + var buf = context.GltfResources.GetOrLoadTypedBufferByAccessorIndex(index); + var acc = buf.Accessor; + if (acc.Type == VGltf.Types.Accessor.TypeEnum.Scalar) + { + if (acc.ComponentType == VGltf.Types.Accessor.ComponentTypeEnum.FLOAT) + { + return buf.GetEntity((xs, i) => xs[i]).AsArray(); + } + } + + throw new NotImplementedException(); // TODO + } + + public static float[] ImportHumanoidFloatScalarValue(IImporterContext context, int index) + { + // https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#_animation_sampler_input + + // SCALAR | FLOAT + var buf = context.GltfResources.GetOrLoadTypedBufferByAccessorIndex(index); + var acc = buf.Accessor; + if (acc.Type == VGltf.Types.Accessor.TypeEnum.Scalar) + { + if (acc.ComponentType == VGltf.Types.Accessor.ComponentTypeEnum.FLOAT) + { + return buf.GetEntity((xs, i) => xs[i]).AsArray(); + } + } + + throw new NotImplementedException(); // TODO + } + } +} diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/HumanoidAnimationImporter.cs.meta b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/HumanoidAnimationImporter.cs.meta new file mode 100644 index 0000000..f9ebf3e --- /dev/null +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/HumanoidAnimationImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 05fbba6ec765849f688b36d3c6ace5e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/Types.meta b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/Types.meta new file mode 100644 index 0000000..0b005a9 --- /dev/null +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/Types.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fcca6173f054240ca9eb70c89744f0a7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/Types/HumanoidAnimationType.cs b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/Types/HumanoidAnimationType.cs new file mode 100644 index 0000000..c644dd9 --- /dev/null +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/Types/HumanoidAnimationType.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2022- yutopp (yutopp@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using VGltf.Types; +using VJson; +using VJson.Schema; + +namespace VGltf.Unity.Ext.Helper.Types +{ + /// + /// + [Json] + public sealed class HumanoidAnimationType + { + public static readonly string ExtraName = "VGLTF_unity_humanoid_animation"; + + [Json] + public sealed class SamplerType + { + public static readonly string ExtraName = "VGLTF_unity_humanoid_animation_sampler"; + + [JsonField(Name = "inTangent")] + [JsonSchemaRequired, JsonSchemaRef(typeof(GltfID))] + public int InTangent; // Accessor (Scalar, FLOAT) + + [JsonField(Name = "inWeight")] + [JsonSchemaRequired, JsonSchemaRef(typeof(GltfID))] + public int InWeight; // Accessor (Scalar, FLOAT) + + [JsonField(Name = "outTangent")] + [JsonSchemaRequired, JsonSchemaRef(typeof(GltfID))] + public int OutTangent; // Accessor (Scalar, FLOAT) + + [JsonField(Name = "outWeight")] + [JsonSchemaRequired, JsonSchemaRef(typeof(GltfID))] + public int OutWeight; // Accessor (Scalar, FLOAT) + } + + [Json] + public sealed class ChannelType + { + public static readonly string ExtraName = "VGLTF_unity_humanoid_animation_channel"; + + [JsonField(Name = "relativePath")] + public string RelativePath; + + [JsonField(Name = "propertyName")] + public string PropertyName; + } + } +} diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/Types/HumanoidAnimationType.cs.meta b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/Types/HumanoidAnimationType.cs.meta new file mode 100644 index 0000000..e6a4c86 --- /dev/null +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Extra/Helper/Types/HumanoidAnimationType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5a4fd875c047d41ad93fe6de702f031e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/Importer.cs b/Packages/net.yutopp.vgltf.unity/Runtime/Importer.cs index 286810d..4548140 100644 --- a/Packages/net.yutopp.vgltf.unity/Runtime/Importer.cs +++ b/Packages/net.yutopp.vgltf.unity/Runtime/Importer.cs @@ -184,6 +184,17 @@ public Task ImportSceneNodes(CancellationToken ct = default) return ImportSceneNodes(null, ct); } + public async Task ImportEmpty(CancellationToken ct) + { + foreach (var hook in Hooks) + { + await hook.PostHook(Context, ct); + await _context.TimeSlicer.Slice(ct); + } + + return TakeContext(); + } + // Take ownership of Context from importer. public IImporterContext TakeContext() { diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/ImporterRuntimeResources.cs b/Packages/net.yutopp.vgltf.unity/Runtime/ImporterRuntimeResources.cs index 9a35cbc..6a7369e 100644 --- a/Packages/net.yutopp.vgltf.unity/Runtime/ImporterRuntimeResources.cs +++ b/Packages/net.yutopp.vgltf.unity/Runtime/ImporterRuntimeResources.cs @@ -17,10 +17,12 @@ public sealed class ImporterRuntimeResources : IDisposable public IndexedResourceDict Textures = new IndexedResourceDict(); public IndexedResourceDict Materials = new IndexedResourceDict(); public IndexedResourceDict Meshes = new IndexedResourceDict(); + public IndexedDisposableResourceDict Animations = new IndexedDisposableResourceDict(); public Dictionary AuxResources { get; } = new Dictionary(); public void Dispose() { + Animations.Dispose(); Nodes.Dispose(); Meshes.Dispose(); Materials.Dispose(); diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/IndexedResource.cs b/Packages/net.yutopp.vgltf.unity/Runtime/IndexedResource.cs index 68e99d3..5b41c5a 100644 --- a/Packages/net.yutopp.vgltf.unity/Runtime/IndexedResource.cs +++ b/Packages/net.yutopp.vgltf.unity/Runtime/IndexedResource.cs @@ -7,9 +7,15 @@ namespace VGltf.Unity { - public class IndexedResource + public sealed class IndexedResource { - public int Index; - public T Value; + public readonly int Index; + public readonly T Value; + + public IndexedResource(int index, T value) + { + Index = index; + Value = value; + } } -} \ No newline at end of file +} diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/IndexedResourceDict.cs b/Packages/net.yutopp.vgltf.unity/Runtime/IndexedResourceDict.cs index add552a..7cbda31 100644 --- a/Packages/net.yutopp.vgltf.unity/Runtime/IndexedResourceDict.cs +++ b/Packages/net.yutopp.vgltf.unity/Runtime/IndexedResourceDict.cs @@ -13,22 +13,106 @@ namespace VGltf.Unity { public sealed class IndexedResourceDict : IDisposable where V : UnityEngine.Object + { + readonly IndexedDisposableResourceDict> _internal; + + public IndexedResourceDict(IEqualityComparer equalityComparer = default) + { + _internal = new IndexedDisposableResourceDict>(equalityComparer); + } + + public IndexedResource Add(K k, int index, string name, V v) + { + var res = _internal.Add(k, index, name, new Utils.DestroyOnDispose(v)); + return Unwrap(res); + } + + public IndexedResource this[K k] + { + get => Unwrap(_internal[k]); + } + + public IndexedResource GetOrCall(K k, Func> generator) + { + return Unwrap(_internal.GetOrCall(k, () => Wrap(generator()))); + } + + public async Task> GetOrCallAsync(K k, Func>> generator) + { + return Unwrap(await _internal.GetOrCallAsync(k, async () => Wrap(await generator()))); + } + + public IEnumerable Map(Func, T> f) + { + return _internal.Map(wres => f(Unwrap(wres))); + } + + public bool Contains(K k) + { + return _internal.Contains(k); + } + + public bool TryGetValue(K k, out IndexedResource res) + { + var found = _internal.TryGetValue(k, out var wres); + if (found) + { + res = Unwrap(wres); + return true; + } + + res = default; + return false; + } + + public bool TryGetValueByName(string k, out IndexedResource res) + { + var found = _internal.TryGetValueByName(k, out var wres); + if (found) + { + res = Unwrap(wres); + return true; + } + + res = default; + return false; + } + + public void Dispose() + { + _internal.Dispose(); + } + + static IndexedResource> Wrap(IndexedResource wres) + { + return new IndexedResource>( + index: wres.Index, + value: new Utils.DestroyOnDispose(wres.Value) + ); + } + + static IndexedResource Unwrap(IndexedResource> wres) + { + return new IndexedResource( + index: wres.Index, + value: wres.Value.Value + ); + } + } + + public sealed class IndexedDisposableResourceDict : IDisposable where V : IDisposable { readonly Dictionary> _dict; readonly MultiMap> _nameDict = new MultiMap>(); - public IndexedResourceDict(IEqualityComparer equalityComparer = default) + public IndexedDisposableResourceDict(IEqualityComparer equalityComparer = default) { _dict = new Dictionary>(equalityComparer); } public IndexedResource Add(K k, int index, string name, V v) { - var resource = new IndexedResource - { - Index = index, - Value = v, - }; + var resource = new IndexedResource(index, v); _dict.Add(k, resource); if (!string.IsNullOrEmpty(name)) @@ -104,8 +188,9 @@ public void Dispose() { foreach (var v in _dict.Values) { - Utils.Destroy(v.Value); + v.Value.Dispose(); } + _dict.Clear(); _nameDict.Clear(); } diff --git a/Packages/net.yutopp.vgltf.unity/Runtime/PrimitiveExporter.cs b/Packages/net.yutopp.vgltf.unity/Runtime/PrimitiveExporter.cs index 7f04d8b..edd8278 100644 --- a/Packages/net.yutopp.vgltf.unity/Runtime/PrimitiveExporter.cs +++ b/Packages/net.yutopp.vgltf.unity/Runtime/PrimitiveExporter.cs @@ -64,6 +64,21 @@ public static byte[] Marshal(int[] arr) return buffer; } + public static byte[] Marshal(float[] arr) + { + // TODO: optimize + using (var ms = new MemoryStream()) + using (var w = new BinaryWriter(ms)) + { + foreach (var v in arr) + { + w.Write(v); + } + w.Flush(); + return ms.ToArray(); + } + } + public static byte[] Marshal(Vector2[] vec2) { // TODO: optimize diff --git a/Packages/net.yutopp.vgltf/Runtime/Types/Animation.cs b/Packages/net.yutopp.vgltf/Runtime/Types/Animation.cs index d0a23b8..eba71f9 100644 --- a/Packages/net.yutopp.vgltf/Runtime/Types/Animation.cs +++ b/Packages/net.yutopp.vgltf/Runtime/Types/Animation.cs @@ -26,7 +26,7 @@ public sealed class Animation : GltfChildOfRootProperty // [JsonSchema(Id = "animation.channel.schema.json")] - public class ChannelType : GltfProperty + public sealed class ChannelType : GltfProperty { [JsonField(Name = "sampler")] [JsonSchemaRequired, JsonSchemaRef(typeof(GltfID))] @@ -39,35 +39,25 @@ public class ChannelType : GltfProperty // [JsonSchema(Id = "animation.channel.target.schema.json")] - public class TargetType : GltfProperty + public sealed class TargetType : GltfProperty { - [JsonField(Name = "node")] + public const string PathEnumTranslation = "translation"; + public const string PathEnumRotation = "rotation"; + public const string PathEnumScale = "scale"; + public const string PathEnumWeights = "weights"; + + [JsonField(Name = "node"), JsonFieldIgnorable] [JsonSchemaRef(typeof(GltfID))] - public int Node; + public int? Node; [JsonField(Name = "path")] [JsonSchemaRequired] - public PathEnum Path; - - // - - [Json(EnumConversion = EnumConversionType.AsString)] - public enum PathEnum - { - [JsonField(Name = "translation")] - Translation, - [JsonField(Name = "rotation")] - Rotation, - [JsonField(Name = "scale")] - Scale, - [JsonField(Name = "weights")] - Weights, - } + public string Path; } } [JsonSchema(Id = "animation.sampler.schema.json")] - public class SamplerType + public sealed class SamplerType : GltfProperty { [JsonField(Name = "input")] [JsonSchemaRequired, JsonSchemaRef(typeof(GltfID))] diff --git a/Packages/net.yutopp.vgltf/Runtime/Types/Gltf.cs b/Packages/net.yutopp.vgltf/Runtime/Types/Gltf.cs index 594bace..1308899 100644 --- a/Packages/net.yutopp.vgltf/Runtime/Types/Gltf.cs +++ b/Packages/net.yutopp.vgltf/Runtime/Types/Gltf.cs @@ -271,6 +271,18 @@ public static int AddSkin(this Gltf gltf, Skin item) return n; } + public static int AddAnimation(this Gltf gltf, Animation item) + { + if (gltf.Animations == null) + { + gltf.Animations = new List(); + } + + var n = gltf.Animations.Count; + gltf.Animations.Add(item); + return n; + } + public static void AddExtensionUsed(this Gltf gltf, string name) { if (gltf.ExtensionsUsed == null)