Unity
Joy-Con
Vtuber
NintendoSwitch
VRM

Nintendo SwitchのJoy-ConでVTuberっぽい動きを表現する

発端

下記の@fuloru169様の記事をみて、この記事に書いてあることを少し変更すればそのままVTuberに使えると思った。

参考にしたもの

参考:コガネブログ様
【Unity】Nintendo Switch の Joy-Con のジャイロ・加速度・傾きの値を取得したり、振動させたりすることができる「JoyconLib」紹介

参考:@fuloru169様
Nintendo SwitchのJoy-Conを使ってユニティちゃんの腕を動かす

参考:@decchi様
UniVRMのVRMBlendShapeProxyで表情アニメーション処理を共通化する

これらの方々を参考にしました。

実装方法

1,UnityとJoy-conの連携

コガネブログを参考にしながら、UnityとJoy-con(L)を連携する。

参考:コガネブログ様
【Unity】Nintendo Switch の Joy-Con のジャイロ・加速度・傾きの値を取得したり、振動させたりすることができる「JoyconLib」紹介

また、UniVRMのインポートを行う。

2,オブジェクトの配置

以下のものをsceneに配置します。

  • VRMモデル
  • 空のGameObject

3.スクリプトの記述

参考:コガネブログ様

【Unity】Nintendo Switch の Joy-Con のジャイロ・加速度・傾きの値を取得したり、振動させたりすることができる「JoyconLib」紹介

参考:@fuloru169様
Nintendo SwitchのJoy-Conを使ってユニティちゃんの腕を動かす

コガネブログ様が紹介している、Example.csに処理を書き加えた、
@fuloru169様が紹介しているExample_gyro.csに処理を書き加えた、
Example_gyro_V.csが下記になります。
このスクリプトとJoyConManager.csを空のGameObjectにアタッチします。
その後、Example_gyro_V.csのmodelに、UI上に青い立方体のマークがある3Dモデルのデータをタッチします。

Example_gyro_V.cs
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using VRM;
public class Example_gyro_V : MonoBehaviour
{
    private static readonly Joycon.Button[] m_buttons =
        Enum.GetValues( typeof( Joycon.Button ) ) as Joycon.Button[];

    private List<Joycon>    m_joycons;
    private Joycon          m_joyconL;
    //private Joycon          m_joyconR;
    private Joycon.Button?  m_pressedButtonL;
    private Joycon.Button?  m_pressedButtonR;
    private VRMBlendShapeProxy proxy;
    public GameObject model;

    private GameObject rCube,lCube;
    private Quaternion rciq,lciq,riq,liq,kosi;
    private Animator anim;
    private Transform RS_bone,LS_bone,KOSI;

    private void Start()
    {

        m_joycons = JoyconManager.Instance.j;

        if ( m_joycons == null || m_joycons.Count <= 0 ) return;

        m_joyconL = m_joycons.Find( c => c.isLeft );
        //m_joyconR = m_joycons.Find( c => !c.isLeft );

        anim = (Animator)FindObjectOfType (typeof(Animator));

        //rCube = GameObject.Find ("RightCube");
        //lCube = GameObject.Find ("LeftCube");

        RS_bone = anim.GetBoneTransform (HumanBodyBones.RightLowerArm);
        LS_bone = anim.GetBoneTransform (HumanBodyBones.Head);
        KOSI = anim.GetBoneTransform (HumanBodyBones.Spine);

        //rciq = rCube.transform.rotation;
        //lciq = lCube.transform.rotation;
        riq = RS_bone.rotation;
        liq = LS_bone.rotation;
        kosi = KOSI.rotation;
    }

    private void Update()
    {
        m_pressedButtonL = null;
        //m_pressedButtonR = null;
        foreach ( var button in m_buttons )
        {
            if ( m_joyconL.GetButton( button ) )
            {
                m_pressedButtonL = button;
            }
            //if ( m_joyconR.GetButton( button ) )
            //{
            //    m_pressedButtonR = button;
            //}
        }

        if ( m_joycons == null || m_joycons.Count <= 0 ) return;

        if ( Input.GetKeyDown( KeyCode.Z ) )
        {
            //m_joyconL.SetRumble( 160, 320, 0.6f, 200 );
        }
        if ( Input.GetKeyDown( KeyCode.X ) )
        {
            //m_joyconR.SetRumble( 160, 320, 0.6f, 200 );
        }


        const float MOVE_PER_CLOCK = 0.0033f;
        Vector3 joyconGyro;

        // 右の箱
        //joyconGyro = m_joycons[1].GetGyro();
        //Quaternion rcqt = rCube.transform.rotation;
        //rcqt.x += -joyconGyro[1] * MOVE_PER_CLOCK*3;
        //rcqt.y += -joyconGyro[0] * MOVE_PER_CLOCK*3;
        //rcqt.z += -joyconGyro[2] * MOVE_PER_CLOCK*3;

        // 右肩
        //Quaternion rb = RS_bone.transform.rotation * Quaternion.Inverse(riq);
        //rb.x += -joyconGyro [1] * MOVE_PER_CLOCK*3;
        //rb.y += -joyconGyro [0] * MOVE_PER_CLOCK*3;
        //rb.z += -joyconGyro [2] * MOVE_PER_CLOCK*3;

        // 箱
        joyconGyro = m_joycons[0].GetGyro();

        //Quaternion lcqt = lCube.transform.rotation;
        //lcqt.x += -joyconGyro[1] * MOVE_PER_CLOCK*3;
        //lcqt.y += -joyconGyro[0] * MOVE_PER_CLOCK*3;
        //lcqt.z += -joyconGyro[2] * MOVE_PER_CLOCK*3;
        //lCube.transform.rotation = lcqt;

        // 首
        Quaternion lb = LS_bone.transform.rotation * Quaternion.Inverse(liq);
        lb.x += -joyconGyro [1] * MOVE_PER_CLOCK;
        lb.y += -joyconGyro [0] * MOVE_PER_CLOCK*3;
        lb.z += -joyconGyro [2] * MOVE_PER_CLOCK*3;
        LS_bone.rotation = lb * liq;

        // 腰
        Quaternion Ap = KOSI.transform.rotation * Quaternion.Inverse(kosi);
        Ap.x += -joyconGyro [1] * MOVE_PER_CLOCK;
        Ap.y += -joyconGyro [0] * MOVE_PER_CLOCK;
        Ap.z += -joyconGyro [2] * MOVE_PER_CLOCK;
        KOSI.rotation = Ap * kosi;

        // 左のジョイコンのYボタンが押されたら、首と腰は初期ポジションに戻る
        if (m_joyconL.GetButtonDown(m_buttons[2])) {
            //lCube.transform.rotation = lciq;
            LS_bone.rotation = liq;
            KOSI.rotation = kosi;
        }
if (proxy == null)
        {
            proxy = model.GetComponent<VRMBlendShapeProxy>();
        }
        else
        {

            //  くち
            if (m_joyconL.GetButtonDown(m_buttons[3]))
            {
                proxy.SetValue(BlendShapePreset.A, 1f);

            }
            if (m_joyconL.GetButtonUp(m_buttons[3]))
            {
                proxy.SetValue(BlendShapePreset.A, 0f);
            }

            // 瞬き
            if (m_joyconL.GetButtonDown(m_buttons[0]))
            {
                proxy.SetValue(BlendShapePreset.Blink, 1f);
            }
            if (m_joyconL.GetButtonUp(m_buttons[0]))
            {
                proxy.SetValue(BlendShapePreset.Blink, 0f);
            }

            // 怒り
            if (m_joyconL.GetButtonDown(m_buttons[1]))
            {
                proxy.SetValue(BlendShapePreset.Angry, 1f);
            }
            if (m_joyconL.GetButtonUp(m_buttons[1]))
            {
                proxy.SetValue(BlendShapePreset.Angry, 0f);
            }

        }

    }

    private void OnGUI()
    {
        var style = GUI.skin.GetStyle( "label" );
        style.fontSize = 24;

        if ( m_joycons == null || m_joycons.Count <= 0 )
        {
        //    GUILayout.Label( "Joy-Con が接続されていません" );
            return;
        }

        if ( !m_joycons.Any( c => c.isLeft ) )
        {
        //    GUILayout.Label( "Joy-Con (L) が接続されていません" );
            return;
        }

        if ( !m_joycons.Any( c => !c.isLeft ) )
        {
        //    GUILayout.Label( "Joy-Con (R) が接続されていません" );
            return;
        }

        GUILayout.BeginHorizontal( GUILayout.Width( 960 ) );

        foreach ( var joycon in m_joycons )
        {
            var isLeft      = joycon.isLeft;
            var name        = isLeft ? "Joy-Con (L)" : "Joy-Con (R)";
            var key         = isLeft ? "Z キー" : "X キー";
            var button      = isLeft ? m_pressedButtonL : m_pressedButtonR;
            var stick       = joycon.GetStick();
            var gyro        = joycon.GetGyro();
            var accel       = joycon.GetAccel();
            var orientation = joycon.GetVector();

//          GUILayout.BeginVertical( GUILayout.Width( 480 ) );
//          GUILayout.Label( name );
//          GUILayout.Label( key + ":振動" );
//          GUILayout.Label( "押されているボタン:" + button );
//          GUILayout.Label( string.Format( "スティック:({0}, {1})", stick[ 0 ], stick[ 1 ] ) );
//          GUILayout.Label( "ジャイロ:" + gyro );
//          GUILayout.Label( "加速度:" + accel );
//          GUILayout.Label( "傾き:" + orientation );
//          GUILayout.EndVertical();
        }

        GUILayout.EndHorizontal();
    }
}

Example_gyro.csからの主な追記点

  • Joy-conを(L)のみ読み込むようにした。
  • Cube関連が必要ないためコメントアウトした。
  • Joy-con(L)が動かす部位を頭(Head)と腰(Spine)に変更した。
    • 自然に見せるため、動きの激しさを方向ごとに変更した。
  • Joy-con(L)の左ボタンでキャラを初期位置に戻し、上ボタン・下ボタン・右ボタンでそれぞれVRMのBlendShapeのA・Blink・Angryが操作できるようにした。

以上をした後はUnity上で再生した時に体や表情が動くようになるので、それを確認した後、自然に見えるように腕や手などのボーンの傾きを変更したり、背景になるオブジェクトを配置したり、カメラをうまく映る位置に配置したりしたのち、Buildしましょう。

おわりに

安価で3DでVTuberやりたいけどカメラ使うの怖いっていう人とかぬるぬる動きたい人におすすめ。
(カメラで顔認識でVRM動かすやつは私の環境ではカクツキが目立った)
あとねこますさんが投稿したこれも採用したほうがいいと思った。