diff --git a/Assets/UniGLTF/Runtime/Extensions/glTFExtensions.cs b/Assets/UniGLTF/Runtime/Extensions/glTFExtensions.cs index ba1966d22..29a9ec703 100644 --- a/Assets/UniGLTF/Runtime/Extensions/glTFExtensions.cs +++ b/Assets/UniGLTF/Runtime/Extensions/glTFExtensions.cs @@ -26,6 +26,7 @@ namespace UniGLTF { typeof(Vector2), new ComponentVec(glComponentType.FLOAT, 2) }, { typeof(Vector3), new ComponentVec(glComponentType.FLOAT, 3) }, { typeof(Vector4), new ComponentVec(glComponentType.FLOAT, 4) }, + { typeof(Quaternion), new ComponentVec(glComponentType.FLOAT, 4) }, { typeof(UShort4), new ComponentVec(glComponentType.UNSIGNED_SHORT, 4) }, { typeof(Matrix4x4), new ComponentVec(glComponentType.FLOAT, 16) }, { typeof(Color), new ComponentVec(glComponentType.FLOAT, 4) }, diff --git a/Assets/VRM10/Editor/Vrm10TopMenu.cs b/Assets/VRM10/Editor/Vrm10TopMenu.cs index cfc99cba7..91e144bff 100644 --- a/Assets/VRM10/Editor/Vrm10TopMenu.cs +++ b/Assets/VRM10/Editor/Vrm10TopMenu.cs @@ -6,10 +6,14 @@ namespace UniVRM10 { private const string UserMenuPrefix = VRMVersion.MENU; private const string DevelopmentMenuPrefix = VRMVersion.MENU + "/Development"; + private const string ExperimentalMenuPrefix = VRMVersion.MENU + "/Experimental"; [MenuItem(UserMenuPrefix + "/Export VRM-1.0", priority = 1)] private static void OpenExportDialog() => VRM10ExportDialog.Open(); + [MenuItem(ExperimentalMenuPrefix + "/Convert BVH to VRM-Animation", priority = 100)] + private static void ConvertVrmAnimation() => VrmAnimationMenu.BvhToVrmAnimationMenu(); + #if VRM_DEVELOP [MenuItem(UserMenuPrefix + "/VRM1 Window", false, 2)] private static void OpenWindow() => VRM10Window.Open(); diff --git a/Assets/VRM10/Editor/VrmAnimationMenu.cs b/Assets/VRM10/Editor/VrmAnimationMenu.cs new file mode 100644 index 000000000..d8a8230d9 --- /dev/null +++ b/Assets/VRM10/Editor/VrmAnimationMenu.cs @@ -0,0 +1,25 @@ +using System.IO; +using UnityEditor; + +namespace UniVRM10 +{ + internal static class VrmAnimationMenu + { + public static void BvhToVrmAnimationMenu() + { + var path = EditorUtility.OpenFilePanel("select bvh", null, "bvh"); + if (!string.IsNullOrEmpty(path)) + { + var bytes = VrmAnimationExporter.BvhToVrmAnimation(path); + var dst = EditorUtility.SaveFilePanel("write vrma", + Path.GetDirectoryName(path), + Path.GetFileNameWithoutExtension(path), + "vrma"); + if (!string.IsNullOrEmpty(dst)) + { + File.WriteAllBytes(dst, bytes); + } + } + } + } +} diff --git a/Assets/VRM10/Editor/VrmAnimationMenu.cs.meta b/Assets/VRM10/Editor/VrmAnimationMenu.cs.meta new file mode 100644 index 000000000..efae9abb9 --- /dev/null +++ b/Assets/VRM10/Editor/VrmAnimationMenu.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7cd670e43db7e7f42a210eb10f5da2c7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VRM10/Runtime/IO/VrmAnimationExporter.cs b/Assets/VRM10/Runtime/IO/VrmAnimationExporter.cs new file mode 100644 index 000000000..65416602e --- /dev/null +++ b/Assets/VRM10/Runtime/IO/VrmAnimationExporter.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.IO; +using UniGLTF; +using UniHumanoid; +using UnityEngine; +using VRMShaders; + +namespace UniVRM10 +{ + public class VrmAnimationExporter : gltfExporter + { + public VrmAnimationExporter( + ExportingGltfData data, + GltfExportSettings settings) + : base(data, settings) { } + + // private (INormalizedPoseProvider, ITPoseProvider) m_controlRig; + readonly List m_times = new(); + + class PositionExporter + { + public List Values = new(); + public Transform Node; + readonly Transform m_root; + + public PositionExporter(Transform bone, Transform root) + { + Node = bone; + m_root = root; + } + + public void Add() + { + Values.Add(m_root.worldToLocalMatrix.MultiplyPoint(Node.position)); + } + } + PositionExporter m_position; + + class RotationExporter + { + public List Values = new(); + readonly Transform Node; + public Transform m_parent; + + public RotationExporter(Transform bone, Transform parent) + { + Node = bone; + m_parent = parent; + } + + public void Add() + { + Values.Add(Quaternion.Inverse(m_parent.rotation) * Node.rotation); + } + } + readonly Dictionary m_rotations = new(); + + static Transform GetParentBone(Dictionary map, Vrm10HumanoidBones bone) + { + while (true) + { + if (bone == Vrm10HumanoidBones.Hips) + { + break; + } + var parentBone = Vrm10HumanoidBoneSpecification.GetDefine(bone).ParentBone.Value; + var unityParentBone = Vrm10HumanoidBoneSpecification.ConvertToUnityBone(parentBone); + if (map.TryGetValue(unityParentBone, out var found)) + { + return found; + } + bone = parentBone; + } + + // hips has no parent + return null; + } + + private void AddFrame(TimeSpan time) + { + m_times.Add((float)time.TotalSeconds); + m_position.Add(); + foreach (var kv in m_rotations) + { + kv.Value.Add(); + } + } + + public void Export(BvhImporterContext bvh) + { + base.Export(new RuntimeTextureSerializer()); + + // + // setup + // + var map = new Dictionary(); + var animator = bvh.Root.GetComponent(); + foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) + { + if (bone == HumanBodyBones.LastBone) + { + continue; + } + var t = animator.GetBoneTransform(bone); + if (t == null) + { + continue; + } + map.Add(bone, t); + } + + m_position = new PositionExporter(map[HumanBodyBones.Hips], + bvh.Root.transform); + + foreach (var kv in map) + { + var vrmBone = Vrm10HumanoidBoneSpecification.ConvertFromUnityBone(kv.Key); + var parent = GetParentBone(map, vrmBone) ?? bvh.Root.transform; + m_rotations.Add(kv.Key, new RotationExporter(kv.Value, parent)); + } + + // + // get data + // + var animation = bvh.Root.gameObject.GetComponent(); + var clip = animation.clip; + var state = animation[clip.name]; + + var time = default(TimeSpan); + for (int i = 0; i < bvh.Bvh.FrameCount; ++i, time += bvh.Bvh.FrameTime) + { + state.time = (float)time.TotalSeconds; + animation.Sample(); + AddFrame(time); + } + + // + // export + // + var gltfAnimation = new glTFAnimation + { + }; + _data.Gltf.animations.Add(gltfAnimation); + + // time values + var input = _data.ExtendBufferAndGetAccessorIndex(m_times.ToArray()); + + { + var output = _data.ExtendBufferAndGetAccessorIndex(m_position.Values.ToArray()); + var sampler = gltfAnimation.samplers.Count; + gltfAnimation.samplers.Add(new glTFAnimationSampler + { + input = input, + output = output, + interpolation = "LINEAR", + }); + + gltfAnimation.channels.Add(new glTFAnimationChannel + { + sampler = sampler, + target = new glTFAnimationTarget + { + node = Nodes.IndexOf(m_position.Node), + path = "translation", + }, + }); + } + + foreach (var kv in m_rotations) + { + var output = _data.ExtendBufferAndGetAccessorIndex(kv.Value.Values.ToArray()); + var sampler = gltfAnimation.samplers.Count; + gltfAnimation.samplers.Add(new glTFAnimationSampler + { + input = input, + output = output, + interpolation = "LINEAR", + }); + + gltfAnimation.channels.Add(new glTFAnimationChannel + { + sampler = sampler, + target = new glTFAnimationTarget + { + node = Nodes.IndexOf(m_position.Node), + path = "rotation", + }, + }); + } + } + + public static byte[] BvhToVrmAnimation(string path) + { + var bvh = new BvhImporterContext(); + bvh.Parse(path, File.ReadAllText(path)); + bvh.Load(); + + var data = new ExportingGltfData(); + using (var exporter = new VrmAnimationExporter( + data, new GltfExportSettings())) + { + exporter.Prepare(bvh.Root.gameObject); + exporter.Export(bvh); + return data.ToGlbBytes(); + } + } + } +} diff --git a/Assets/VRM10/Runtime/IO/VrmAnimationExporter.cs.meta b/Assets/VRM10/Runtime/IO/VrmAnimationExporter.cs.meta new file mode 100644 index 000000000..56ed3cc88 --- /dev/null +++ b/Assets/VRM10/Runtime/IO/VrmAnimationExporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9ec9e478ed511fa478a5935aa08b36c8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: