キャラクターを生き生きとさせたい
デモ
この内容をすべて入れたバージョン
ジェスチャーと揺れものだけとの比較
デモ + 動作付き
東京大学制作展 2022 の作品の一部です。
対話システムデモ
表情デモ
magica cloth デモ
目的
キャラクターを生き生きとさせて動かしたい!
必要技術一覧
リップシンク -> OVRLipSync
表情 -> BlendShape + Blender
まばたき -> RealsticEyeMovements
眼球運動 -> RealsticEyeMovements
頭・体の向き-> LookAtIK + RealsticEyeMovements
ボディランゲージ -> VIVEによるモーションキャプチャ(Final IK)
立体音響 -> Oculus Audio Spacializer
揺れモノ -> Magica Cloth
呼吸 -> BreathController
リップシンク
OVRLipSync
こちらを使います.
また、キャラクターの口はVRMLipSyncMorphTargetをつかって制御します。
表情
表情に関しては Facebook Research の有名な論文である empathetic dialogue にある32種類の感情を使っています.
これらの表情に対してひとつひとつ Blenderで表情を作成します。
各表情のサンプルに関してはデモをご覧ください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BlendShapeController : MonoBehaviour
{
[SerializeField] SkinnedMeshRenderer skinnedMeshRenderer;
Mesh mesh;
private string eyeIrisExpansion = "Eye_Iris_Expansion";
private Dictionary<string, int> name2Blendshape = new Dictionary<string, int>();
List<string> _emotion_list = new List<string>{
"surprised",
"excited",
"angry",
"proud",
"sad",
"annoyed",
"grateful",
"lonely",
"afraid",
"terrified",
"guilty",
"impressed",
"disgusted",
"hopeful",
"confident",
"furious",
"anxious",
"anticipating",
"joyful",
"nostalgic",
"disappointed",
"prepared",
"jealous",
"content",
"devastated",
"embarrassed",
"caring",
"sentimental",
"trusting",
"ashamed",
"apprehensive",
"faithful",
// "question",
// "assertion"
};
// Start is called before the first frame update
void Awake()
{
mesh = skinnedMeshRenderer.sharedMesh;
for (int i= 0; i < mesh.blendShapeCount; i++){
string blendShapeName = mesh.GetBlendShapeName(i);
name2Blendshape.Add(blendShapeName, i);
}
}
//指定した表情が存在する場合.
public void SetEmotionValue(string blendShapeName){
ResetEmotion();
if(name2Blendshape.ContainsKey(blendShapeName)){
skinnedMeshRenderer.SetBlendShapeWeight(name2Blendshape[blendShapeName], 100.0f);
}
}
void ResetEmotion(){
foreach(var _emotion in _emotion_list) {
skinnedMeshRenderer.SetBlendShapeWeight(name2Blendshape[_emotion], 0f);
}
}
//Iris Expansion show the interests to the user!
public void IrisExpansion(){
StartCoroutine(_IrisExpansion(1.0f));
}
private IEnumerator _IrisExpansion(float interval=1.0f){
skinnedMeshRenderer.SetBlendShapeWeight(name2Blendshape[eyeIrisExpansion], 100.0f);
yield return new WaitForSeconds(interval);
skinnedMeshRenderer.SetBlendShapeWeight(name2Blendshape[eyeIrisExpansion], 0.0f);
}
}
各Blendshapeのコントロールについては以上のスクリプトによって制御しています。
mesh = skinnedMeshRenderer.shareMesh
で顔のメッシュを取得し、それからブレンドシェイプの名前とインデックスをペアで取得し制御しています。
まばたき
RealEyeMovementsを使っています
公式ページの通りにあらかじめ、Look Target Controller と Eye And Head Animator を設定します.
これができたら、あとはいろいろな微調整の設定を以下のスクリプトで行います。
注意点としては眼球の上下左右の設定時には Boneの移動ではなく、回転を使って設定を行います。
そうしないと、うまく設定できません。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RealisticEyeMovements;
using Utils;
public class RealEyeMovementsSetter : MonoBehaviour
{
[SerializeField] GameObject CharacterAgent;
[SerializeField] Transform CenterEyeAnchor;
private LookTargetController lookTargetController;
private EyeAndHeadAnimator eyeAndHeadAnimator;
void Awake(){
lookTargetController = CharacterAgent.GetComponent<LookTargetController>();
lookTargetController.keepTargetEvenWhenLost = false;
// lookTargetController.pointsOfInterest.Add(CenterEyeAnchor); //default point of interest is MainCamera!
lookTargetController.lookAtPlayerRatio = 0.1f;
lookTargetController.minLookTime = 2.0f;
lookTargetController.maxLookTime = 4.0f;
eyeAndHeadAnimator = CharacterAgent.GetComponent<EyeAndHeadAnimator>();
eyeAndHeadAnimator.kMaxNextBlinkTime = 10.0f;
eyeAndHeadAnimator.kMaxNextBlinkTime = 2.0f;
eyeAndHeadAnimator.headBoneNonMecanim = FindTransform.FindDeep(CharacterAgent, "J_Bip_C_Head");
eyeAndHeadAnimator.neckBoneNonMecanim = FindTransform.FindDeep(CharacterAgent, "J_Bip_C_Neck");
eyeAndHeadAnimator.spineBoneNonMecanim = FindTransform.FindDeep(CharacterAgent, "J_Bip_C_Spine");
eyeAndHeadAnimator.resetHeadAtFrameStart = true;
}
public void SetEyeRate(float rate){
eyeAndHeadAnimator.eyesWeight = rate;
}
}
眼球運動
眼球運動の設定 + 瞳孔の設定を行います。
RealEyeMovementsのまばたきの設定と同時に眼球運動の設定が終わっているので、あとは瞳孔の設定を行います。
このように動くBlendShapeを用意しておき
public void IrisExpansion(){
StartCoroutine(_IrisExpansion(1.0f));
}
private IEnumerator _IrisExpansion(float interval=1.0f){
skinnedMeshRenderer.SetBlendShapeWeight(name2Blendshape[eyeIrisExpansion], 100.0f);
yield return new WaitForSeconds(interval);
skinnedMeshRenderer.SetBlendShapeWeight(name2Blendshape[eyeIrisExpansion], 0.0f);
}
キャラクターがこちらを向くときに IrisExpansionを呼び出すことで、キャラクターがこちらを向くときに興味があることを示しています。
頭・体の向き
LookAtIKの公式ページ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RootMotion.FinalIK;
using Utils;
[RequireComponent(typeof(Chat))]
[RequireComponent(typeof(RealEyeMovementsSetter))]
[RequireComponent(typeof(AudioSetter))]
public class LookAtUser : MonoBehaviour
{
private LookAtIK lookAt;
[SerializeField] Transform CenterEyeAnchor;
[SerializeField] GameObject Model;
private Chat chat;
[SerializeField] MicChecker micChecker;
private float outDuration = 3.0f;
private float inDuration = 1.0f;
private float fadeTime;
private bool isWaiting;
private RealEyeMovementsSetter realEyeMovementsSetter;
private AudioSetter audioSetter;
void Awake(){
lookAt = Model.AddComponent<LookAtIK>();
LookAtIKSetup();
chat = GetComponent<Chat>();
realEyeMovementsSetter = GetComponent<RealEyeMovementsSetter>();
audioSetter = GetComponent<AudioSetter>();
isWaiting = true;
}
void LookAtIKSetup(){
var spine = FindTransform.FindDeep(Model, "J_Bip_C_Spine");
var chest = FindTransform.FindDeep(Model, "J_Bip_C_Chest");
var upperChest = FindTransform.FindDeep(Model, "J_Bip_C_UpperChest");
var neck = FindTransform.FindDeep(Model, "J_Bip_C_Neck");
var head = FindTransform.FindDeep(Model, "J_Bip_C_Head");
lookAt.solver.head = new IKSolverLookAt.LookAtBone(head);
var lookAtSpine = new IKSolverLookAt.LookAtBone(spine);
var lookAtChest = new IKSolverLookAt.LookAtBone(upperChest);
var lookAtUpperChest = new IKSolverLookAt.LookAtBone(chest);
var lookAtNeck = new IKSolverLookAt.LookAtBone(neck);
lookAt.solver.spine = new IKSolverLookAt.LookAtBone[]{lookAtSpine, lookAtChest, lookAtUpperChest, lookAtNeck};
lookAt.solver.target = CenterEyeAnchor;
lookAt.solver.IKPositionWeight = 1f; // The master weight
// lookAt.solver.IKPosition = OVRCameraRig.position; // Changing the look at target
// Changing the weights of individual body parts
lookAt.solver.bodyWeight = 0.1f;
lookAt.solver.headWeight = 1f;
lookAt.solver.eyesWeight = 0.0f;
// Changing the clamp weight of individual body parts
lookAt.solver.clampWeight = 0.0f;
lookAt.solver.clampWeightHead = 0.0f;
lookAt.solver.clampWeightEyes = 1f;
lookAt.solver.IKPositionWeight = 0.0f;
lookAt.solver.IKPositionWeight = 0f;
}
void LateUpdate () {
if(isWaiting){ //自由にあちこちを向く
if(chat.isSpeaking || micChecker.isUserSpeaking || audioSetter.isCharacterSpeaking() || chat.isGenerating){
isWaiting = false;
// lookAt.solver.IKPositionWeight = 1f;
realEyeMovementsSetter.SetEyeRate(0.0f);
fadeTime = 0.0f;
return;
}
fadeTime += Time.deltaTime;
var t = fadeTime / outDuration;
if(Mathf.Abs(lookAt.solver.IKPositionWeight) < 0.001f){
return;
}
Debug.Log("waiting " + Mathf.SmoothStep(1.0f, 0.0f, t));
lookAt.solver.IKPositionWeight = Mathf.SmoothStep(1.0f, 0.0f, t);
//Real Eye Movements は 1.0f で自由に目が動く.
realEyeMovementsSetter.SetEyeRate(t);
}else{ //ユーザーの方を向く
if(!chat.isSpeaking && !micChecker.isUserSpeaking && !audioSetter.isCharacterSpeaking() && !chat.isGenerating){
isWaiting = true;
fadeTime = 0.0f;
realEyeMovementsSetter.SetEyeRate(1.0f);
return;
}
fadeTime += Time.deltaTime;
var t = fadeTime / inDuration;
if(Mathf.Abs(lookAt.solver.IKPositionWeight) > 0.999f){
return;
}
Debug.Log("not waiting " + Mathf.SmoothStep(0.0f, 1.0f, t));
lookAt.solver.IKPositionWeight = Mathf.SmoothStep(0.0f, 1.0f, t);
}
}
}
簡単な説明として、LookAtIKSetup()ではLookAtIKをモデルにアタッチしてパラメータの調節をしています。
LateUpdateでは、ユーザの発言中 or キャラクターの発言中に LookAtIKによりキャラクターがこちらを向くようにしています。
急に向くと怖いので、徐々にこちらを向いたり、元の方向に向いたりするように設定を行っています。
また、realEyeMovementsでは顔がこっちを向いていても目がいろいろな方向を向いてしまい、微妙なので、こちらを向いているときはeyeのウェイトを切っています。
ボディランゲージ
ボディランゲージに関しては、フルモーションキャプチャでとった動きを加工 + 下半身は固定アニメーション で対応しています。
感情の動作 32パターン × 動作意図の行動パターン 10 パターンで合計 320 パターンについて、制御を行っています。
参考リンク
ただし、これだけだと問題があり、アニメーション時に足が貧乏ゆすりのように動いてしまいます。
そのため、FullBodyBipedIKを使って足を固定します。
モデルに FullBodyBipedIKをアタッチして以下のスクリプトを設定します。
これは、つま先のIKのターゲットとなるオブジェクトを用意し、各下半身のアニメーションのつま先の位置を固定するものです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RootMotion.FinalIK;
public class ToeController : MonoBehaviour
{
[SerializeField] GameObject model;
private FullBodyBipedIK fullBodyBipedIK;
private GameObject leftToe;
private GameObject rightToe;
private static Dictionary<string, Vector3> toeTransform = new Dictionary<string, Vector3>{
{"ashikumiLeft", new Vector3(-0.1f, 0.42f, 0.315f)},
{"ashikumiRight", new Vector3(-0.375f, 0.516f, 0.0f)},
{"nagareLeft", new Vector3(-0.3f, 0.37f, 0.3f)},
{"nagareRight", new Vector3(-0.2f, 0.386f, 0.3f)},
{"uchimataLeft", new Vector3(0.134f, 0.387f, 0.29f)},
{"uchimataRight", new Vector3(-0.032f, 0.4f, 0.3f)}
};
void Awake(){
fullBodyBipedIK = model.GetComponent<FullBodyBipedIK>();
rightToe = new GameObject("RightToe");
leftToe = new GameObject("LeftToe");
leftToe.transform.SetParent(model.transform);
rightToe.transform.SetParent(model.transform);
fullBodyBipedIK.solver.leftFootEffector.target = leftToe.transform;
fullBodyBipedIK.solver.rightFootEffector.target = rightToe.transform;
fullBodyBipedIK.solver.leftFootEffector.positionWeight = 1.0f;
fullBodyBipedIK.solver.rightFootEffector.positionWeight = 1.0f;
}
public void SetToe(string name){
Debug.Log("Toe name " + name);
leftToe.transform.localPosition = toeTransform[name + "Left"];
rightToe.transform.localPosition = toeTransform[name + "Right"];
}
}
立体音響
Oculusの Audio Spacializerを使っています。
こちらを参考に設定をして...
AudioSetter.cs
こちらでいろいろと場所の調節をしています。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Utils;
public class AudioSetter : MonoBehaviour
{
[SerializeField] AudioSource characterAudio;
[SerializeField] AudioSource userAudio;
[SerializeField] GameObject CharacterAgent;
[SerializeField] GameObject UserAvatar;
void Awake(){
if(characterAudio != null && CharacterAgent != null){
characterAudio.playOnAwake = false;
characterAudio.loop = false;
characterAudio.Stop();
characterAudio.spatialBlend = 1.0f;
var ovrlipsynccontext = characterAudio.gameObject.GetComponent<OVRLipSyncContext>();
ovrlipsynccontext.audioSource = characterAudio;
ovrlipsynccontext.audioLoopback = true;
var vrmlipsyncmorphtarget = characterAudio.gameObject.GetComponent<VRMLipSyncMorphTarget>();
vrmlipsyncmorphtarget.lipsyncContext = ovrlipsynccontext;
vrmlipsyncmorphtarget.blendShapeProxy = CharacterAgent.GetComponent<VRM.VRMBlendShapeProxy>();
vrmlipsyncmorphtarget.smoothAmount = 35;
var characterHead = FindTransform.FindDeep(CharacterAgent, "J_Bip_C_Head");
characterAudio.gameObject.transform.SetParent(characterHead);
characterAudio.gameObject.transform.localPosition = new Vector3(0.0f, 0.0f, 0.08f);
}
if(userAudio != null && UserAvatar != null){
userAudio.playOnAwake = false;
userAudio.loop = false;
userAudio.Stop();
userAudio.spatialBlend = 1.0f;
var lipsyncmicinput = userAudio.gameObject.GetComponent<OVRLipSyncContext>();
lipsyncmicinput.audioSource = userAudio;
var ovrlipsynccontext = userAudio.gameObject.GetComponent<OVRLipSyncContext>();
ovrlipsynccontext.audioSource = userAudio;
var userHead = FindTransform.FindDeep(UserAvatar, "J_Bip_C_Head");
if(userHead == null){
userHead = UserAvatar.transform;
}
userAudio.gameObject.transform.SetParent(userHead);
userAudio.gameObject.transform.localPosition = new Vector3(0.0f, 0.0f, 0.08f);
}
}
public bool isCharacterSpeaking(){
return characterAudio.isPlaying;
}
public bool isUserSpeaking(){
return userAudio.isPlaying;
}
}
UserAudio
MicCheckerはユーザの音声の大きさにより、ユーザが話しているかを検知するスクリプトです。
CharacterAudio
揺れモノ
MagicaCloth を使っています。Magica Clothそのものの設定は譲るとして、
MagicaClothのコライダーに関する設定はSetColliders.cs, SetCollidersEditor.csの2つで既存のモデルに設定したコライダーを、新規のモデルに適用できるようにしています。
SetColliders.cs
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using UnityEditor;
using Utils;
using MagicaCloth;
public class SetColliders : MonoBehaviour
{
public GameObject Model;
public MagicaCloth.MagicaBoneCloth[] magicaCloths;
[HideInInspector]
public List<string> colliderBoneNames = new List<string>{
"J_Bip_C_Head",
"J_Bip_C_UpperChest",
"J_Bip_C_Chest",
"J_Bip_C_Spine",
"J_Bip_L_UpperArm",
"J_Bip_R_UpperArm",
"J_Bip_L_LowerArm",
"J_Bip_R_LowerArm",
"J_Bip_L_Hand",
"J_Bip_R_Hand",
"J_Bip_L_UpperLeg",
"J_Bip_R_UpperLeg",
"J_Bip_L_LowerLeg",
"J_Bip_R_LowerLeg",
"J_Bip_L_Shoulder",
"J_Bip_R_Shoulder"
};
[HideInInspector]
public List<Vector3> colliderBoneVecs = new List<Vector3>(){};
[HideInInspector]
public List<MagicaCapsuleCollider.Axis> colliderAxis = new List<MagicaCapsuleCollider.Axis>();
[HideInInspector]
public List<float> colliderLength = new List<float>();
[HideInInspector]
public List<float> colliderStartRadius = new List<float>();
[HideInInspector]
public List<float> colliderEndRadius = new List<float>();
}
SetCollidersEditor.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Utils;
using System;
using System.IO;
[System.Serializable]
public class MagicaCollider
{
public string boneName;
public Vector3 center;
public MagicaCloth.MagicaCapsuleCollider.Axis axis;
public float length;
public float startRadius;
public float endRadius;
}
[CustomEditor(typeof(SetColliders))]
[CanEditMultipleObjects]
public class SetCollidersEditor : Editor
{
SetColliders setColliders;
public override void OnInspectorGUI()
{
setColliders = target as SetColliders;
base.OnInspectorGUI();
serializedObject.Update ();
EditorGUILayout.Space();
string fileName = "MagicaRecord";
EditorGUILayout.LabelField("Recording Collider json file name");
fileName = EditorGUILayout.TextField(fileName);
EditorGUILayout.Space();
// if(GUILayout.Button("コライダーセットする!")){
// if(setColliders.Model == null){
// Debug.Log("モデルがセットされていません!");
// return;
// }
// for(int i = 0 ; i< setColliders.colliderBoneNames.Count; i++){
// if(setColliders.colliderStartRadius[i] < 0.01f && setColliders.colliderEndRadius[i] < 0.01f){
// continue;
// }
// var obj = Utils.FindTransform.FindDeep(setColliders.Model, setColliders.colliderBoneNames[i]);
// var collider = obj.gameObject.AddComponent<MagicaCloth.MagicaCapsuleCollider>();
// collider.Center = setColliders.colliderBoneVecs[i];
// collider.AxisMode = setColliders.colliderAxis[i];
// collider.Length = setColliders.colliderLength[i];
// collider.StartRadius = setColliders.colliderStartRadius[i];
// collider.EndRadius = setColliders.colliderEndRadius[i];
// foreach(var boneCloth in setColliders.magicaCloths){
// boneCloth.AddCollider(collider);
// }
// }
// }
if(GUILayout.Button("コライダーをすべて削除")){
if(setColliders.Model == null){
Debug.Log("モデルがセットされていません!");
return;
}
for(int i = 0 ; i< setColliders.colliderBoneNames.Count; i++){
var obj = Utils.FindTransform.FindDeep(setColliders.Model, setColliders.colliderBoneNames[i]);
var collider = obj.gameObject.GetComponent<MagicaCloth.MagicaCapsuleCollider>();
if(collider != null){
foreach(var boneCloth in setColliders.magicaCloths){
boneCloth.RemoveCollider(collider);
}
DestroyImmediate(collider);
}
}
Debug.Log("コライダーをすべて削除しました!");
}
if(GUILayout.Button("コライダーをjsonに記憶")){
if(setColliders.Model == null){
Debug.Log("モデルがセットされていません!");
return;
}
List<string> recordList = new List<string>();
for(int i=0; i<setColliders.colliderBoneNames.Count; i++){
var obj = Utils.FindTransform.FindDeep(setColliders.Model, setColliders.colliderBoneNames[i]);
var collider = obj.gameObject.GetComponent<MagicaCloth.MagicaCapsuleCollider>();
if(collider != null){
setColliders.colliderBoneVecs[i] = collider.Center;
setColliders.colliderAxis[i] = collider.AxisMode;
setColliders.colliderLength[i] = collider.Length;
setColliders.colliderStartRadius[i] = collider.StartRadius;
setColliders.colliderEndRadius[i] = collider.EndRadius;
var magicaCollier = new MagicaCollider();
magicaCollier.center = collider.Center;
magicaCollier.boneName = setColliders.colliderBoneNames[i];
magicaCollier.length = collider.Length;
magicaCollier.axis = collider.AxisMode;
magicaCollier.startRadius = collider.StartRadius;
magicaCollier.endRadius = collider.EndRadius;
recordList.Add(JsonUtility.ToJson(magicaCollier));
}
}
if(recordList.Count > 0){
string path = Application.dataPath + "/Resources/" + fileName + ".json";
using(StreamWriter sw = new StreamWriter(path)){
foreach(var record in recordList){
sw.WriteLine(record);
}
}
Debug.Log("コライダーをすべて記憶しました!");
}else{
Debug.Log("コライダーが存在しませんでした.");
}
}
if(GUILayout.Button("コライダーをjsonから読みこみ")){
if(setColliders.Model == null){
Debug.Log("モデルがセットされていません!");
return;
}
List<string> recordList = new List<string>();
string path = Application.dataPath + "/Resources/" + fileName + ".json";
using(StreamReader sr = new StreamReader(path)){
while(sr.Peek() != -1){
var line = sr.ReadLine();
recordList.Add(line);
}
}
foreach(var record in recordList){
var jsonCollider = JsonUtility.FromJson<MagicaCollider>(record);
var obj = Utils.FindTransform.FindDeep(setColliders.Model, jsonCollider.boneName);
if(obj.gameObject.GetComponent<MagicaCloth.MagicaCapsuleCollider>() == null){
var collider = obj.gameObject.AddComponent<MagicaCloth.MagicaCapsuleCollider>();
collider.Center = jsonCollider.center;
collider.AxisMode = jsonCollider.axis;
collider.Length = jsonCollider.length;
collider.EndRadius = jsonCollider.endRadius;
collider.StartRadius = jsonCollider.startRadius;
foreach(var boneCloth in setColliders.magicaCloths){
boneCloth.TeamData.ColliderList.Add(collider);
}
}
}
Debug.Log("コライダーをすべてjsonから読み取りました!");
}
EditorGUILayout.Space();
for(int i = 0; i< setColliders.colliderBoneNames.Count; i++){
setColliders.colliderEndRadius.Add(0.0f);
setColliders.colliderStartRadius.Add(0.0f);
setColliders.colliderLength.Add(0.0f);
setColliders.colliderBoneVecs.Add(Vector3.zero);
setColliders.colliderAxis.Add(MagicaCloth.MagicaCapsuleCollider.Axis.X);
// EditorGUILayout.LabelField(setColliders.colliderBoneNames[i]);
EditorGUILayout.Space();
}
// EditorGUILayout.BeginVertical();
// setColliders.colliderBoneVecs[i]= EditorGUILayout.Vector3Field("Center", setColliders.colliderBoneVecs[i]);
// EditorGUILayout.Space();
// setColliders.colliderAxis[i] = (MagicaCloth.MagicaCapsuleCollider.Axis)EditorGUILayout.EnumPopup((Enum)setColliders.colliderAxis[i]);
// EditorGUILayout.Space();
// setColliders.colliderLength[i] = EditorGUILayout.FloatField("Length", setColliders.colliderLength[i]);
// EditorGUILayout.Space();
// setColliders.colliderStartRadius[i] = EditorGUILayout.FloatField("Start Radius", setColliders.colliderStartRadius[i]);
// EditorGUILayout.Space();
// setColliders.colliderEndRadius[i] = EditorGUILayout.FloatField("End Radius", setColliders.colliderEndRadius[i]);
// EditorGUILayout.EndVertical();
// EditorGUILayout.Space();
// EditorGUILayout.Space();
// EditorGUILayout.Space();
// }
}
}
また、髪の毛の揺れものなどはVRMモデルについているデフォルトのVRMSpringBoneを利用しています。
Collierの調節は
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Utils;
using VRM;
public class ColliderSetter : MonoBehaviour
{
[SerializeField] GameObject Model;
// Start is called before the first frame update
void Awake()
{
var upperChestSpringColliders = FindTransform.FindDeep(Model, "J_Bip_C_UpperChest").GetComponent<VRMSpringBoneColliderGroup>();
upperChestSpringColliders.Colliders[0].Radius = 0.165f;
var headSpringBoneColliders = FindTransform.FindDeep(Model, "J_Bip_C_Head").GetComponent<VRMSpringBoneColliderGroup>();
headSpringBoneColliders.Colliders[0].Radius = 0.01f;
}
}
で大きさの変更を行っています.
呼吸
BreathControllerを使います
こちらで公開されているスクリプトを使っていています。
設定に関しては
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Utils;
using VRM;
using Mebiustos.BreathController;
public class BreathControllerSetter : MonoBehaviour
{
[SerializeField] GameObject Model;
private BreathController breathController;
// Start is called before the first frame update
void Awake()
{
breathController = Model.AddComponent<BreathController>();
}
}
のようにして単純にコンポーネントを追加します。