diff --git a/Assets/VRM/Runtime/SpringBone/FastSpringBoneDisposer.cs b/Assets/VRM/Runtime/SpringBone/FastSpringBoneDisposer.cs
new file mode 100644
index 000000000..ce0a464d1
--- /dev/null
+++ b/Assets/VRM/Runtime/SpringBone/FastSpringBoneDisposer.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace VRM
+{
+ ///
+ /// FastSpringBoneに関連して、特定のGameObjectと紐付いたIDisposableの破棄を担当するクラス
+ ///
+ public sealed class FastSpringBoneDisposer : MonoBehaviour
+ {
+ private readonly List _disposables = new List();
+
+ public void Add(IDisposable disposable)
+ {
+ _disposables.Add(disposable);
+ }
+
+ private void OnDestroy()
+ {
+ foreach (var disposable in _disposables)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/VRM/Runtime/SpringBone/FastSpringBoneDisposer.cs.meta b/Assets/VRM/Runtime/SpringBone/FastSpringBoneDisposer.cs.meta
new file mode 100644
index 000000000..7f55e4b79
--- /dev/null
+++ b/Assets/VRM/Runtime/SpringBone/FastSpringBoneDisposer.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0b96d696de06439e97ac33ed2d890c88
+timeCreated: 1632893490
\ No newline at end of file
diff --git a/Assets/VRM/Runtime/SpringBone/FastSpringBoneReplacer.cs b/Assets/VRM/Runtime/SpringBone/FastSpringBoneReplacer.cs
new file mode 100644
index 000000000..356ab345c
--- /dev/null
+++ b/Assets/VRM/Runtime/SpringBone/FastSpringBoneReplacer.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using UnityEngine;
+using VRM.FastSpringBones.Blittables;
+using VRM.FastSpringBones.Components;
+using VRMShaders;
+using Object = UnityEngine.Object;
+
+namespace VRM
+{
+ ///
+ /// 指定されたGameObject内にあるSpringBoneをFastSpringBoneに差し替えるユーティリティ
+ ///
+ public static class FastSpringBoneReplacer
+ {
+ public static async Task ReplaceAsync(GameObject gameObject, IAwaitCaller awaitCaller = null, CancellationToken token = default)
+ {
+ var service = FastSpringBoneService.Instance;
+ var springBones = gameObject.GetComponentsInChildren();
+ var disposer = gameObject.AddComponent();
+
+ // VRMSpringBoneで動いた後の状態がFastSpringBoneの初期状態にならないようにするためawait UniTask.Yield()する前にVRMSpringBoneをdisableにしておく
+ foreach (var springBone in springBones)
+ {
+ springBone.enabled = false;
+ };
+
+ if (awaitCaller != null)
+ {
+ await awaitCaller.NextFrame();
+ token.ThrowIfCancellationRequested();
+ }
+
+ var vrmColliderGroups = gameObject.GetComponentsInChildren();
+ var colliderGroupDictionary = new Dictionary();
+
+ // Colliderを差し替える
+ foreach (var vrmColliderGroup in vrmColliderGroups)
+ {
+ if (awaitCaller != null)
+ {
+ await awaitCaller.NextFrame();
+ token.ThrowIfCancellationRequested();
+ }
+
+ var fastSpringBoneCollider = vrmColliderGroup.gameObject.AddComponent();
+ fastSpringBoneCollider.Initialize(
+ service.TransformRegistry,
+ vrmColliderGroup.Colliders
+ .Select(data => new BlittableCollider(data.Offset, data.Radius))
+ .ToArray()
+ );
+ colliderGroupDictionary[vrmColliderGroup] = fastSpringBoneCollider;
+ }
+
+ var springRootBones =
+ (
+ from springBone in springBones
+ from rootBone in springBone.RootBones
+ select (springBone, rootBone)
+ ).ToList();
+
+ for (var i = 0; i < springRootBones.Count; i++)
+ {
+ var current = springRootBones[i];
+
+ // 他のRootBoneのどれかが、自分の親(もしくは同じTransform)なら自分自身を削除する
+ if (springRootBones
+ .Where(other => other != current)
+ .Any(other => current.rootBone.IsChildOf(other.rootBone)))
+ {
+ springRootBones.RemoveAt(i);
+ --i;
+ }
+ }
+
+ if (awaitCaller != null)
+ {
+ await awaitCaller.NextFrame();
+ token.ThrowIfCancellationRequested();
+ }
+ token.ThrowIfCancellationRequested();
+
+ foreach (var (vrmSpringBone, rootBoneTransform) in springRootBones)
+ {
+ // FastSpringRootBoneに差し替える
+ var fastSpringRootBone =
+ new FastSpringRootBone(
+ service.TransformRegistry,
+ rootBoneTransform,
+ service.RootBoneRegistry,
+ service.ColliderGroupRegistry);
+ disposer.Add(fastSpringRootBone);
+
+ var colliderGroups =
+ vrmSpringBone.ColliderGroups != null
+ ? vrmSpringBone.ColliderGroups.Select(group => colliderGroupDictionary[@group]).ToArray()
+ : Array.Empty();
+
+ fastSpringRootBone.Initialize(
+ vrmSpringBone.m_gravityPower,
+ vrmSpringBone.m_gravityDir,
+ vrmSpringBone.m_dragForce,
+ vrmSpringBone.m_stiffnessForce,
+ colliderGroups,
+ vrmSpringBone.m_hitRadius,
+ vrmSpringBone.m_center
+ );
+
+ Object.Destroy(vrmSpringBone);
+
+ if (awaitCaller != null)
+ {
+ await awaitCaller.NextFrame();
+ token.ThrowIfCancellationRequested();
+ }
+ token.ThrowIfCancellationRequested();
+ }
+
+ // Colliderを削除
+ foreach (var vrmSpringBoneColliderGroup in vrmColliderGroups)
+ {
+ Object.Destroy(vrmSpringBoneColliderGroup);
+ }
+ }
+ }
+}
diff --git a/Assets/VRM/Runtime/SpringBone/FastSpringBoneReplacer.cs.meta b/Assets/VRM/Runtime/SpringBone/FastSpringBoneReplacer.cs.meta
new file mode 100644
index 000000000..76ca1edb5
--- /dev/null
+++ b/Assets/VRM/Runtime/SpringBone/FastSpringBoneReplacer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a344a05a3634bac419be8a218d8b85d6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/VRM/Runtime/SpringBone/FastSpringBoneService.cs b/Assets/VRM/Runtime/SpringBone/FastSpringBoneService.cs
new file mode 100644
index 000000000..86fab4112
--- /dev/null
+++ b/Assets/VRM/Runtime/SpringBone/FastSpringBoneService.cs
@@ -0,0 +1,54 @@
+using UnityEngine;
+using VRM.FastSpringBones.Components;
+using VRM.FastSpringBones.Registries;
+
+namespace VRM
+{
+ ///
+ /// Scene 上に単一で存在する、FastSpringBone の ServiceLocator 兼 EntryPoint
+ ///
+ public class FastSpringBoneService : MonoBehaviour
+ {
+ public RootBoneRegistry RootBoneRegistry { get; private set; }
+ public TransformRegistry TransformRegistry { get; private set; }
+ public ColliderGroupRegistry ColliderGroupRegistry { get; private set; }
+ public FastSpringBoneScheduler FastSpringBoneScheduler { get; private set; }
+
+ private static FastSpringBoneService _instance;
+
+ public static FastSpringBoneService Instance
+ {
+ get
+ {
+ if (!_instance)
+ {
+ var gameObject = new GameObject("FastSpringBone Service");
+ DontDestroyOnLoad(gameObject);
+ _instance = gameObject.AddComponent();
+ }
+ return _instance;
+ }
+ }
+
+ ///
+ /// 専有しているインスタンスを破棄する
+ ///
+ public static void Free()
+ {
+ Destroy(_instance.gameObject);
+ _instance = null;
+ }
+
+ private void Awake()
+ {
+ RootBoneRegistry = new RootBoneRegistry();
+ TransformRegistry = new TransformRegistry();
+ ColliderGroupRegistry = new ColliderGroupRegistry();
+ FastSpringBoneScheduler = gameObject.AddComponent();
+ FastSpringBoneScheduler.Initialize(
+ RootBoneRegistry,
+ TransformRegistry,
+ ColliderGroupRegistry);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/VRM/Runtime/SpringBone/FastSpringBoneService.cs.meta b/Assets/VRM/Runtime/SpringBone/FastSpringBoneService.cs.meta
new file mode 100644
index 000000000..8db972822
--- /dev/null
+++ b/Assets/VRM/Runtime/SpringBone/FastSpringBoneService.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 7c3edad580b54158b37169025a1e207d
+timeCreated: 1632894654
\ No newline at end of file
diff --git a/Assets/VRM/Runtime/VRM.asmdef b/Assets/VRM/Runtime/VRM.asmdef
index 053d4f4d5..c89f8bc23 100644
--- a/Assets/VRM/Runtime/VRM.asmdef
+++ b/Assets/VRM/Runtime/VRM.asmdef
@@ -5,7 +5,8 @@
"GUID:8d76e605759c3f64a957d63ef96ada7c",
"GUID:da3e51d19d51a544fa14d43fee843098",
"GUID:301b251fd9834274c9228e0532f444f7",
- "GUID:a9bc101fb0471f94a8f99fd242fdd934"
+ "GUID:a9bc101fb0471f94a8f99fd242fdd934",
+ "GUID:ac229b552c3025545b074203f857547c"
],
"includePlatforms": [],
"excludePlatforms": [],