UnityでVRMモデルの胸を揺らす知見
通常のVRMモデルを用いるアプリにおいて、このような知見は不要かもしれないが、特定の条件下でVRMモデルの胸を揺らしたいケースは必ず出てくると思う。衝突法がVRM憑依ちゃんの開発で使っている手法。囲い型は組み込み検討中。
必読
手法
VRアプリで胸を触る場合
VRアプリの場合は、手に追従するGameObjectに対しVRMSpringBoneColliderGroup.cs
をアタッチしてパラメータを調整、胸を揺らしたいVRMモデルのsecondary
にあるVRMSpringBone
(胸のRoot Bonesが設定された箇所)のCollider Groups
に手のGameObject
を指定。髪も触りたい場合は全てにColliderを設定すれば良い。半径を大きくしすぎると現実離れした胸揺れになるので、0.05~0.1あたりが最適と思われる。
オブジェクトの衝突
自分で触らずに、Animation等と同期処理させたいケースで用いる。Animationだけでは揺れをダイナミックに表現できない為、VRMSpringBoneColliderGroup
をオブジェクトに設定し、胸に衝突させるという手法。欠点として、Animationにより対象の胸の位置が変化した場合、都度オブジェクトの位置や、衝突させる角度を変更する必要がある。
参考画像(センシティブツイート) :
せすれりあ on Twitter: "私が考えたVRMの乳揺らしシステム(ある意味物理)… "
//コルーチンでこういうのを回すだけ for(int j=0; j<10; j++){ Shake.transform.position -= MainCharactor.Player.transform.forward / 100; yield return new WaitForSeconds(0.01f); }
囲い型
胸の周りにVRMSpringBoneColliderGroup
を設定したオブジェクトを配置、状況によりRadiusをコルーチンで調整し、胸を揺らすという手法。揺れを細かく制御でき、親子関係を設定することで体に追従するので、位置を考慮する必要はない。しかし、VRMモデルの胸サイズや体格によっては個別に位置を調整する必要がある為、動的に読み込んだVRMに対しては使いにくい弱点も。VRoidの場合は正確なポジションを設定しやすい(詳細は後述)。UpperChest
は設定されていないモデルが多いので、そういった場合は他のボーンから位置を取得して補正しなければならない。
左右から衝撃を与えるケースにおいて、Radius差による揺れ表現の違い
事前準備
Resourcesフォルダに以下のPrefabを作成。
コード
//VRMモデルにアタッチする using System.Collections; using System.Collections.Generic; using UnityEngine; using System; using VRM; public class ShakeBust : MonoBehaviour { [SerializeField] public float defineTransition = 0.01f; [SerializeField] public float defineSize = 0.1f; private Vector3[] colliderPosition = new Vector3[9]; private GameObject[] shakingCollider = new GameObject[9]; private VRMSpringBoneColliderGroup[] springBone = new VRMSpringBoneColliderGroup[9]; // Start is called before the first frame update void Start() { Animator anime = this.GetComponent<Animator>(); Transform leftUpperArm = anime.GetBoneTransform(HumanBodyBones.LeftUpperArm); Transform rightUpperArm = anime.GetBoneTransform(HumanBodyBones.RightUpperArm); Transform parentChest = anime.GetBoneTransform(HumanBodyBones.UpperChest); colliderPosition[0] = new Vector3(parentChest.position.x, parentChest.position.y - 0.03f, parentChest.position.z + 0.10f); colliderPosition[1] = new Vector3(parentChest.position.x + leftUpperArm.localPosition.x / 1.05f, parentChest.position.y - 0.03f, parentChest.position.z + 0.15f); colliderPosition[2] = new Vector3(parentChest.position.x + leftUpperArm.localPosition.x / 1.05f, parentChest.position.y + 0.03f, parentChest.position.z + 0.11f); colliderPosition[3] = new Vector3(parentChest.position.x + leftUpperArm.localPosition.x / 1.05f, parentChest.position.y - 0.09f, parentChest.position.z + 0.11f); colliderPosition[4] = new Vector3(parentChest.position.x + leftUpperArm.localPosition.x - 0.05f, parentChest.position.y - 0.03f, parentChest.position.z + 0.09f); colliderPosition[5] = new Vector3(parentChest.position.x + rightUpperArm.localPosition.x / 1.05f, parentChest.position.y - 0.03f, parentChest.position.z + 0.15f); colliderPosition[6] = new Vector3(parentChest.position.x + rightUpperArm.localPosition.x / 1.05f, parentChest.position.y + 0.03f, parentChest.position.z + 0.11f); colliderPosition[7] = new Vector3(parentChest.position.x + rightUpperArm.localPosition.x / 1.05f, parentChest.position.y - 0.09f, parentChest.position.z + 0.11f); colliderPosition[8] = new Vector3(parentChest.position.x + rightUpperArm.localPosition.x + 0.05f, parentChest.position.y - 0.03f, parentChest.position.z + 0.09f); GameObject shakinbBall = (GameObject)Resources.Load("ShakingBall"); for(int i = 0; i <= 8; i++){ shakingCollider[i] = Instantiate(shakinbBall, colliderPosition[i], Quaternion.identity); shakingCollider[i].name = "ShakingBall_" + i; shakingCollider[i].transform.parent = parentChest.transform; springBone[i] = shakingCollider[i].GetComponent<VRMSpringBoneColliderGroup>(); } GameObject secondary = this.transform.Find("secondary").gameObject; VRMSpringBone[] Bones = secondary.GetComponents<VRMSpringBone>(); foreach (VRMSpringBone Bone in Bones) { if(Bone.ColliderGroups == null){ Bone.ColliderGroups = new VRMSpringBoneColliderGroup[9]; for(int i = 0; i <= 8; i++){ Bone.ColliderGroups[i] = springBone[i]; } }else{ Array.Resize(ref Bone.ColliderGroups , Bone.ColliderGroups.Length + 9); for(int i = 1; i <= 9; i++){ Bone.ColliderGroups[Bone.ColliderGroups.Length - i] = springBone[i - 1]; } } } } // Update is called once per frame void Update() { if(Input.GetKeyDown(KeyCode.Alpha1)){ StartCoroutine(ShakeBoobs(0, defineTransition, defineSize)); } if(Input.GetKeyDown(KeyCode.Alpha2)){ StartCoroutine(ShakeBoobs(1, defineTransition, defineSize)); } if(Input.GetKeyDown(KeyCode.Alpha3)){ StartCoroutine(ShakeBoobs(2, defineTransition, defineSize)); } if(Input.GetKeyDown(KeyCode.Alpha4)){ StartCoroutine(ShakeBoobs(3, defineTransition, defineSize)); } if(Input.GetKeyDown(KeyCode.Alpha5)){ StartCoroutine(ShakeBoobs(4, defineTransition, defineSize)); } if(Input.GetKeyDown(KeyCode.Alpha6)){ StartCoroutine(ShakeBoobs(5, defineTransition, defineSize)); } if(Input.GetKeyDown(KeyCode.Alpha7)){ StartCoroutine(ShakeBoobs(6, defineTransition, defineSize)); } if(Input.GetKeyDown(KeyCode.Alpha8)){ StartCoroutine(ShakeBoobs(7, defineTransition, defineSize)); } if(Input.GetKeyDown(KeyCode.Alpha9)){ StartCoroutine(ShakeBoobs(8, defineTransition, defineSize)); } } private IEnumerator ShakeBoobs(int colliderNo, float transition, float size){ for(int i = 10; i > 0; i--){ springBone[colliderNo].Colliders[0].Radius = size / i; yield return new WaitForSeconds(transition); } springBone[colliderNo].Colliders[0].Radius = 0; } }
以下の部分において全てのVRMSpringBoneにColliderを設定しているのは、胸がどこに設定されているのか判別するのが困難な為。コメント等で判別可能なケースもある。その為、Radiusを極端に大きな値にしてしまうと、髪の毛等と衝突してしまうので注意が必要。
GameObject secondary = this.transform.Find("secondary").gameObject; VRMSpringBone[] Bones = secondary.GetComponents<VRMSpringBone>(); foreach (VRMSpringBone Bone in Bones) { if(Bone.ColliderGroups == null){ Bone.ColliderGroups = new VRMSpringBoneColliderGroup[9]; for(int i = 0; i <= 8; i++){ Bone.ColliderGroups[i] = springBone[i]; } }else{ Array.Resize(ref Bone.ColliderGroups , Bone.ColliderGroups.Length + 9); for(int i = 1; i <= 9; i++){ Bone.ColliderGroups[Bone.ColliderGroups.Length - i] = springBone[i - 1]; } } }
特殊ケース(VRoidモデル)
VRoidモデル限定ではあるが、transform.find
等で以下オブジェクトのposition
を取得してやると、確実に胸の付け根(Bust1)や、乳輪の位置(Bust2)を取得出来る。このパラメータを元にすれば、囲い型でも、ほぼ正確なポジションにオブジェクトを配置可能。
J_Sec_L_Bust1
J_Sec_L_Bust2
J_Sec_R_Bust1
J_Sec_R_Bust2
いつもの
僕が一番上手にVRoidの胸を触れるんだ・・! #VRM憑依ちゃん pic.twitter.com/IQzu9rorxo
— せすれりあ (@sesleria) 2019年4月23日