さわぐちFMオキーステーション

幸せの鐘が鳴り響き僕はただ嬉しいふりをする

Oculus Questのハンドトラッキングでボーンを視覚的に表示する

Oculus Questのハンドトラッキングに割り当てられているボーンを視覚的に表示してみましょう。デバッグやテストをする際に少しわかりやすくなるかも...?

割り当てられているボーンIDを確認する

OVRSkeletonにボーン関連の情報が集約されており、以下のプロパティからIList型でボーンの情報にアクセスできるようです。

skeleteon = GetComponent<OVRSkeleton>();

skeleteon.Bones  ← IList型で取得できる

型はOVRBoneで自身のID、親のインデックス、Transform型を持つ以下のようなクラスとして宣言されています。このクラスを使用すればIDや親子関係も取得できそうです。

public class OVRBone
{
    public OVRSkeleton.BoneId Id { get; private set; }
    public short ParentBoneIndex { get; private set; }
    public Transform Transform { get; private set; }

    public OVRBone(OVRSkeleton.BoneId id, short parentBoneIndex, Transform trans)
    {
        Id = id;
        ParentBoneIndex = parentBoneIndex;
        Transform = trans;
    }
}

で、ボーンIDは以下のように宣言されており、

public enum BoneId
    {
        Invalid                 = -1,

        Hand_Start              = 0,
        Hand_WristRoot          = Hand_Start + 0, // root frame of the hand, where the wrist is located
        Hand_ForearmStub        = Hand_Start + 1, // frame for user's forearm
        Hand_Thumb0             = Hand_Start + 2, // thumb trapezium bone
        Hand_Thumb1             = Hand_Start + 3, // thumb metacarpal bone
        Hand_Thumb2             = Hand_Start + 4, // thumb proximal phalange bone
        Hand_Thumb3             = Hand_Start + 5, // thumb distal phalange bone
        Hand_Index1             = Hand_Start + 6, // index proximal phalange bone
        Hand_Index2             = Hand_Start + 7, // index intermediate phalange bone
        Hand_Index3             = Hand_Start + 8, // index distal phalange bone
        Hand_Middle1            = Hand_Start + 9, // middle proximal phalange bone
        Hand_Middle2            = Hand_Start + 10, // middle intermediate phalange bone
        Hand_Middle3            = Hand_Start + 11, // middle distal phalange bone
        Hand_Ring1              = Hand_Start + 12, // ring proximal phalange bone
        Hand_Ring2              = Hand_Start + 13, // ring intermediate phalange bone
        Hand_Ring3              = Hand_Start + 14, // ring distal phalange bone
        Hand_Pinky0             = Hand_Start + 15, // pinky metacarpal bone
        Hand_Pinky1             = Hand_Start + 16, // pinky proximal phalange bone
        Hand_Pinky2             = Hand_Start + 17, // pinky intermediate phalange bone
        Hand_Pinky3             = Hand_Start + 18, // pinky distal phalange bone
        Hand_MaxSkinnable       = Hand_Start + 19,
        // Bone tips are position only. They are not used for skinning but are useful for hit-testing.
        // NOTE: Hand_ThumbTip == Hand_MaxSkinnable since the extended tips need to be contiguous
        Hand_ThumbTip           = Hand_Start + Hand_MaxSkinnable + 0, // tip of the thumb
        Hand_IndexTip           = Hand_Start + Hand_MaxSkinnable + 1, // tip of the index finger
        Hand_MiddleTip          = Hand_Start + Hand_MaxSkinnable + 2, // tip of the middle finger
        Hand_RingTip            = Hand_Start + Hand_MaxSkinnable + 3, // tip of the ring finger
        Hand_PinkyTip           = Hand_Start + Hand_MaxSkinnable + 4, // tip of the pinky
        Hand_End                = Hand_Start + Hand_MaxSkinnable + 5,

        // add new bones here

        Max = Hand_End + 0,
    }

個々のボーンのIDは以下の図のように割り当てられています。

f:id:amidaMangrove:20200103141132j:plain

ボーンの表示 & 指ごとに色分けしてみる

ボーンを視覚的に表示し指単位で色分けするスクリプトを作ってみました。以下のスクリプトをOVRHandPrefabにアタッチすれば、確認できます。実行時はOVR Mesh Rendererを無効にしておきましょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class BoneInfoSample : MonoBehaviour
{
    OVRHand _hand;
    OVRSkeleton _skeleton;
    List<GameObject> _spheres = new List<GameObject>();

    void Start()
    {
        _hand = GetComponent<OVRHand>();
        _skeleton = GetComponent<OVRSkeleton>();

        var boneColor = new Dictionary<string, Color>()
        {
            { "Start",Color.black},  //  スタート位置
            { "Thumb",Color.red},       //  親指
            { "Index",Color.green},     //  人差し指    
            { "Middle",Color.blue},     //  中指
            { "Ring", Color.cyan},      //  薬指
            { "Pinky",Color.magenta},   //  小指
            { "Forearm",Color.yellow},  //  前腕部
        };

        foreach (var bone in _skeleton.Bones) {

            //  Sphereを生成しボーンに割り当てる
            var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            sphere.transform.position = bone.Transform.position;
            sphere.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f);
            sphere.transform.parent = bone.Transform;

            //  指単位で色を変える
            var color = boneColor.FirstOrDefault(x => bone.Id.ToString().Contains(x.Key));
            sphere.GetComponent<Renderer>().material.color = color.Value;

            _spheres.Add(sphere);
        }
    }

    void Update()
    {
        //  トラックが外れたらSphereを消す
        foreach (var sphere in _spheres) {
            sphere.SetActive(_hand.IsTracked);
        }
            
    }
}

f:id:amidaMangrove:20200103161109p:plain

できました!

OVRSkeletonには「Capsules」が定義されていてここに衝突判定用のCapsule Colliderが入っている予感。次回はこの辺りを見ていこうと思います。

Oculus Questのハンドトラッキングでポインティング方向を取得する

ハンドトラッキング時、手がどの方向を向いているかを取得する方法についての紹介。

独自に座標や方向を設定しても良さそうだけど、公式が用意している機能を使って実装。公式なので向きや座標の共通化が図れるかなーと思うので自作アプリではなるべくこの方法を使っていこうと思います。

Pointer Poseの取得

以下の実装で手が向いている方向諸々をTransform型で取得することができます。

var hand = GetComponent<OVRHand>();

hand.PointerPose ←これでOK

Transformなのでpositionや、forwardが使えるので簡単なポインティング機能はこれで実装できそうです。また、PointPoseが取得できているかどうかは以下のbool型で取得できます。

hand.IsPointerPoseValid

これがfalseの場合、レイやポインティングを描画しないとすれば良さそうです。

簡単なサンプル

まずはレイを画面に表示するプログラムを作りましょう。初期化処理の手順に従ってOVRCameraRig階下にOVRHandPrefabを追加します。

f:id:amidaMangrove:20200101131438p:plain

以下のスクリプトを作成しOVRHandPrefabにアタッチします。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PointerPoseSample : MonoBehaviour
{
    [SerializeField] float _rayDistance = 100;
    OVRHand _hand;
    LineRenderer _lineRenderer;

    // Start is called before the first frame update
    void Start()
    {
        _hand = GetComponent<OVRHand>();
        _lineRenderer = GetComponent<LineRenderer>();
    }

    // Update is called once per frame
    void Update()
    {
        //  レイをLineRendererで描画
        var positions = new Vector3[]{
            _hand.PointerPose.position,
            _hand.PointerPose.position + _hand.PointerPose.forward * _rayDistance
        };

        _lineRenderer.SetPositions(positions);

        //  PointerPoseが有効な時のみLineRendererを表示
        _lineRenderer.enabled = _hand.IsPointerPoseValid;

    }
}

LineRendererもOVRHandPrefabにそれぞれ追加します。ラインのサイズは適当の小さめに調整

f:id:amidaMangrove:20200101131911p:plain

実行すると以下の動画のようになります。

www.youtube.com

ホームで表示されている手と同じ位置 & 方向にレイが伸びているのが確認できました。

ピンチを検出してオブジェクトを手元に引っ張ってくる

レイにオブジェクトが衝突している状態でピンチを検出したらオブジェクトを手元に引っ張ってくるサンプルを作ってみます。

ピンチ検出に関してはこちら参照。

amidamangrove.hatenablog.com

先ほどのスクリプトにRaycastを追加します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PointerPoseSample : MonoBehaviour
{
    [SerializeField] float _rayDistance = 100;
    [SerializeField] float _speed = 10;
    OVRHand _hand;
    LineRenderer _lineRenderer;
    
    //[SerializeField]GameObject _cube;
    // Start is called before the first frame update
    void Start()
    {
        _hand = GetComponent<OVRHand>();
        _lineRenderer = GetComponent<LineRenderer>();
    }

    // Update is called once per frame
    void Update()
    {
        //  レイをLineRendererで描画
        var positions = new Vector3[]{
            _hand.PointerPose.position,
            _hand.PointerPose.position + _hand.PointerPose.forward * _rayDistance
        };

        _lineRenderer.SetPositions(positions);

        //  PointerPoseが有効な時のみLineRendererを表示
        _lineRenderer.enabled = _hand.IsPointerPoseValid;

        
        //  ピンチされているか判定
        if (_hand.GetFingerPinchStrength(OVRHand.HandFinger.Index) >= 1f) { 

            //  レイの衝突判定
            var ray = new Ray(_hand.PointerPose.position, _hand.PointerPose.forward * _rayDistance);

            if (Physics.Raycast(ray, out var hitinfo, _rayDistance)){
                var hitObject = hitinfo.collider.gameObject;
                var distance = _hand.PointerPose.position - hitObject.transform.position;
                var velocity = distance.normalized;

                hitObject.GetComponent<Rigidbody>().AddForce(velocity * _speed);
                
            }

        }      
    }
}

あとはRigidBodyを持ったオブジェクトを適当にフィールドに置けば完成です。

実際に動作させた動画がこちらになります。

www.youtube.com

もうちょっと全体に手を加えればテレキネシス的なものが作れそうです

www.youtube.com

Oculus Questのハンドトラッキングでピンチ判定を行う

f:id:amidaMangrove:20191230171620p:plain

この形を自作アプリ内で取得する方法。

ピンチ判定の方法

OVRHandに以下のメソッドが定義されているのでそれを使用する

メソッド名 戻り値
GetFingerIsPinching 指がピンチ状態かをTrue / Falseで取得
GetFingerPinchStrength ピンチ強度を0~1fで取得
GetFingerConfidence 指のポーズの信頼度をLow / Highで取得

引数にはそれぞれHandFinger型を指定する。

HandFinger.Thumb 親指
HandFinger.Index 人差し指
HandFinger.Middle 中指
HandFinger.Ring 薬指
HandFinger.Pinky 小指

因みに公式のサンプルではこんな感じで使用例がかかれていた

var hand = GetComponent<OVRHand>();
bool isIndexFingerPinching = hand.GetFingerIsPinching(HandFinger.Index);
float ringFingerPinchStrength = hand.GetFingerPinchStrength(HandFinger.Ring);

画面上でピンチ判定を視覚化したいので以下のようなスクリプトを作成し親指~小指までの値を表示してみる。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class HandInfo : MonoBehaviour
{
    [SerializeField]OVRHand _hand;
    Text _info;

    // Start is called before the first frame update
    void Start()
    {
        _info = GetComponent<Text>();
    }

    // Update is called once per frame
    void Update()
    {
        _info.text = "";
        _info.text += $"IsTracked : {_hand.IsTracked}\n";
        _info.text += $"HandConfidence : {_hand.HandConfidence}\n\n";

        _info.text += $"ThumbFingerPinch : {_hand.GetFingerIsPinching(OVRHand.HandFinger.Thumb)}\n";
        _info.text += $"ThumbFingerPinchStrength : {_hand.GetFingerPinchStrength(OVRHand.HandFinger.Thumb)}\n";

        _info.text += $"IndexFingerPinch : {_hand.GetFingerIsPinching(OVRHand.HandFinger.Index)}\n";
        _info.text += $"IndexFingerPinchStrength : {_hand.GetFingerPinchStrength(OVRHand.HandFinger.Index)}\n";

        _info.text += $"MiddleFingerPinch : {_hand.GetFingerIsPinching(OVRHand.HandFinger.Middle)}\n";
        _info.text += $"MiddleFingerPinchStrength : {_hand.GetFingerPinchStrength(OVRHand.HandFinger.Middle)}\n";

        _info.text += $"RingFingerPinch : {_hand.GetFingerIsPinching(OVRHand.HandFinger.Ring)}\n";
        _info.text += $"RingFingerPinchStrength : {_hand.GetFingerPinchStrength(OVRHand.HandFinger.Ring)}\n";

        _info.text += $"PinkyFingerPinch : {_hand.GetFingerIsPinching(OVRHand.HandFinger.Pinky)}\n";
        _info.text += $"PinkyFingerPinchStrength : {_hand.GetFingerPinchStrength(OVRHand.HandFinger.Pinky)}\n";
       
    }
}

f:id:amidaMangrove:20191230173828j:plain

使ってみた個人的な所感だと「親指 + 人差し指」は無理なく判定できるけど、他の指だと結構意識してやらないと微妙な感じ...。ちょっとお遊びでGetFingerPinchStrengthから取得できる値をCubeの拡大率に反映してみた。動作しているコードは以下。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class HandInfo : MonoBehaviour
{
    [SerializeField]OVRHand _hand;
    [SerializeField] GameObject _cube;
    Text _info;
    Vector3 _orignscale;

    // Start is called before the first frame update
    void Start()
    {
        _info = GetComponent<Text>();
        _orignscale = _cube.transform.localScale;
    }

    // Update is called once per frame
    void Update()
    {
        _info.text = "";
        _info.text += $"IsTracked : {_hand.IsTracked}\n";
        _info.text += $"HandConfidence : {_hand.HandConfidence}\n\n";

        _info.text += $"IndexFingerPinch : {_hand.GetFingerIsPinching(OVRHand.HandFinger.Index)}\n";
        _info.text += $"IndexFingerPinchStrength : {_hand.GetFingerPinchStrength(OVRHand.HandFinger.Index)}\n";

        var scale = Mathf.Abs(_hand.GetFingerPinchStrength(OVRHand.HandFinger.Index) - 1);
        _cube.transform.localScale = _orignscale * scale;
        
    }
}

f:id:amidaMangrove:20191231100835g:plain

なるほど。なんか面白い事できそうな気がするかも??

Iwate Developers Meetupに参加した話

Iwate Developers Meetupとは?

おかもんさんたくちゃんさんchii.mさんが中心となって主催している岩手県内のIT関連の勉強会です。

グループ発足のきっかけは

  • エンジニア人口が少ないと言われている地方で、技術の質を磨き合える仲間を見つける
  • 地元を応援し、ITで岩手県を盛り上げる

というもので僕自身も同じ思いを持っていたので活動内容非常に共感し今回LT枠で参加を申し込みました。

iwadev.connpass.com

当日のLTについて

f:id:amidaMangrove:20191230151657j:plain

LTの内容はIT関連なら特に定めないということだったのでWeb、セキュリティ、デバッガ、サイト運営と多岐に渡り普通では得られない知見も数多く得られました。当日のLTを簡単に列挙していきます(ガバガバですいません)

iwddのサイトをなんとかしたい件(@suzuryo)

  • Webを軸に、関連数話題を扱う岩手県のIT勉強会
  • 合計158回開催で13年続いている(!!)
  • サイトはこちら→https://www.iwdd.net/

岩手県でこんなに続いていた勉強会があったとは...!話題も一部掲載されていましたその時代時代にあったテーマで開催しているようでした。

動画配信の話(@daidai)

  • 動画配信技術に関する話
  • WebRTCは聞いた事がある程度だったので用語含め勉強になった
  • みんな!動画の沼に引きずりこむ

動画配信技術に関して普段は意識した事がなかったのでなかなか勉強になった。WebRTCちょっとかじろうと思った。

ハニポの話(@fatsheep)

ハニーポットも初めて聞く内容。(どんだけ俺は知見がないのか...)不正アクセスを受ける事を前提としたシステムがあるというのもこれまた発見。確かにこれ観測するのは面白そう。

Spotify Web APIについて(@tkpea)

  • Spotify Web APIを使用して色々なアルバムを分析した
  • 分析内容はエネルギーやらテンポやら
  • electron-vueを使用してAPIを使用したデスクトップアプリを作った

Spotifyからこのようなデータが取れるのは良い発見。このAPIを使って曲のテイストにあったヴィジュアライザとか作れないかなーと思ったり。

自宅の開発環境のご紹介(oga_baku)

  • 自宅の環境、VR、お掃除ロボ、オシロスコープ等の紹介
  • Oculus Quest買うか悩んでいる←買ったほうがいい!
  • 今後は机の上をドローンが掃除するシステムを作りたい

僕の自宅も似たような事をやっていたので非常に親近感が湧きました。お家ハック的なのって結構楽しいんですよねー。

ゲームだけじゃないUnity()

  • Unite Tokyo 2019に行って感じた事の感想
  • Unity初めてみよう!っていう勧誘(?)
  • スタートしたIwate.Unityの紹介

岩手県内のUnityユーザーを少しでも増やしたいと思い、Unityの紹介をメインとしたLTを行いました。これがきっかけで一人でも初めて貰えればー!

デバッガから低レイヤを学ぶそして自作デバッガ(Strader)の紹介(@NaokiYoshida)

  • 自作デバッガ(!)の紹介
  • デバッガとして必要な機能や低レイヤのお話
  • 制作したのは若干10代の学生!

いやー凄いですねー....。デバッガを自作するって考え自体が僕にはなかったですし何よりそれを作ってアウトプットする姿勢が素晴らしいです。応援しますよ!

iOSAirDropで遊んで見ました(@KumagaiY)

  • 名刺管理アプリケーションの紹介
  • 名刺の情報を読みアウトプットするアプリケーション
  • アプリはFilemaker Proで作成されているみたい

名刺一枚で全ての情報にアプリからアクセスできるっていう機能紹介!要望ですが顔写真かアバターとかと同期できれば...!(名刺だけで顔忘れることあるので...

ETL現場のスクリプト構成(@yousken900)

  • ETLは抽出、変換、格納の頭文字
  • 散らばったデータを整理し集約する?
  • ヤバイ、マジで初めて聞くワードだ

ETLというワード自体が初耳だったので手元のPCで調べながら聞いていました。イメージ的に社内に散らばったデータを成型しなおし、一か所に集約する?内容が面白そうなだけに知識不足が悔やまれる...

ビッグデータとセキュリティとログ解析と(@7GHz)

  • ビッグデータ = ゴミ漁り(妙に納得した!)
  • Splunkを使用することでログの傾向等の分析ができるみたい?
  • なんとこれらの解析を仕事ではなく趣味でやっている

こちらの記事「Splunk Stream を使わずにパケット解析したい」も参照。ビッグデータの説明に納得。これらも普段触ることのない領域だったので新鮮な気持ちで聞けました

イベントを振り返って

どの地域にも「一定の熱いヤツ」が必ずいてそれらが繋がり自走できる環境が作れれば物事は勝手に進む...と楽観的に考えていたのですが、今回のイベントでその第一歩が作れたのでは!?と一参加者ながら感じています。

岩手というと決してITに明るくない土地ですが、実は多くの勉強会が立ち上がってます。(勉強会は大将さんのブログにまとまってますのでそちらご確認いただければ)

t-takeda.hatenablog.com

良い流れが来ていますし僕の周りでも「岩手熱い」というワードが少し聞こえるようになってきました。思うに地方を盛り上げるのを他人任せ(企業誘致やら行政まかせ)にするんじゃなく、ひとりひとりが当事者意識を持ち無理の無い範囲でその価値を高める行動をしていくのが正しい地方創生なんじゃないかなーっていうポエムっぽいことを書いてみたり。

運営の皆様、本当にお疲れ様でした!素敵なイベントをありがとうございます!!

余談:ちなみに二次会でLTが始まりました

Oculus Questのハンドトラッキングを雑にまとめる

ハンドトラッキングSDKで使用できるようになったので自分用メモということで簡単にまとめていこう。7割程度、自分用メモ

まずどこを見ればよいのか?

とりあえずこのページを読んでおけば一通りの設定はできそう developer.oculus.com

最短の設定

AndroidにSwitch Platform、Oculus Integrationをインポート済み前提

  • OVRCameraRigをHierarchyに入れる
  • OVR Managerの Hand Tracking Supportを「Controllers and Hands」か「Hands Only」に変更する
  • LeftHandAnchor、RightHandAnchor直下にOVRHandPrefabを追加
  • RightHandに設定したOVR Hand、OVR Skelton、OVR Meshをそれぞれ「Hand Right」に設定する
  • ビルド

OVRHandPrefabの構成

OVR Skeleton

ボーン情報やジェスチャの検出、衝突判定まわりが含まれている。

f:id:amidaMangrove:20191226004707p:plain

  • Skelton Type:右手か左手か
  • Update Root Pose:OVR Camera Rigの下層に配置する場合チェックを付けない。OVRHandPrefab単体で使用する場合チェックをつける。
  • Update Root Scale:トラッキングしている手のサイズを反映するかどうか(あんまり差がわからなかった)
  • Enable Physics Capsules:物理挙動、衝突を有効にする

OVR Mesh

描画に使用するメッシュの情報。中身を見るとOVRPluginからメッシュを引っ張ってきてるみたい。

f:id:amidaMangrove:20191226010449p:plain

OVR Mesh Renderer

OVR SkeletonとOVR Meshから返されたデータを組み合わせて、手のアニメーション化された3Dモデルを生成します。(原文翻訳まま)

f:id:amidaMangrove:20191226010547p:plain

Inspectorに出ている値、特に設定しない場合同一オブジェクトからGetComponentして取得している。

   if (_ovrMesh == null)
    {
        _ovrMesh = GetComponent<OVRMesh>();
    }

    if (_ovrSkeleton == null)
    {
        _ovrSkeleton = GetComponent<OVRSkeleton>();
    }

OVR Skeleton Renderer

ボーンを描画する。使用する際にはOVR Mesh Rendererのチェックを外しておいた方が良くみえる。下図は左がOVR Skeleton Rendererを有効にした状態。 f:id:amidaMangrove:20191227175729j:plain

ボーンID

OVRSkelton.cs内でBoneIDが以下のように定義されている。

public enum BoneId
{
    Invalid                 = OVRPlugin.BoneId.Invalid,

    Hand_Start              = OVRPlugin.BoneId.Hand_Start,
    Hand_WristRoot          = OVRPlugin.BoneId.Hand_WristRoot,          // root frame of the hand, where the wrist is located
    Hand_ForearmStub        = OVRPlugin.BoneId.Hand_ForearmStub,        // frame for user's forearm
    Hand_Thumb0             = OVRPlugin.BoneId.Hand_Thumb0,             // thumb trapezium bone
    Hand_Thumb1             = OVRPlugin.BoneId.Hand_Thumb1,             // thumb metacarpal bone
    Hand_Thumb2             = OVRPlugin.BoneId.Hand_Thumb2,             // thumb proximal phalange bone
    Hand_Thumb3             = OVRPlugin.BoneId.Hand_Thumb3,             // thumb distal phalange bone
    Hand_Index1             = OVRPlugin.BoneId.Hand_Index1,             // index proximal phalange bone
    Hand_Index2             = OVRPlugin.BoneId.Hand_Index2,             // index intermediate phalange bone
    Hand_Index3             = OVRPlugin.BoneId.Hand_Index3,             // index distal phalange bone
    Hand_Middle1            = OVRPlugin.BoneId.Hand_Middle1,            // middle proximal phalange bone
    Hand_Middle2            = OVRPlugin.BoneId.Hand_Middle2,            // middle intermediate phalange bone
    Hand_Middle3            = OVRPlugin.BoneId.Hand_Middle3,            // middle distal phalange bone
    Hand_Ring1              = OVRPlugin.BoneId.Hand_Ring1,              // ring proximal phalange bone
    Hand_Ring2              = OVRPlugin.BoneId.Hand_Ring2,              // ring intermediate phalange bone
    Hand_Ring3              = OVRPlugin.BoneId.Hand_Ring3,              // ring distal phalange bone
    Hand_Pinky0             = OVRPlugin.BoneId.Hand_Pinky0,             // pinky metacarpal bone
    Hand_Pinky1             = OVRPlugin.BoneId.Hand_Pinky1,             // pinky proximal phalange bone
    Hand_Pinky2             = OVRPlugin.BoneId.Hand_Pinky2,             // pinky intermediate phalange bone
    Hand_Pinky3             = OVRPlugin.BoneId.Hand_Pinky3,             // pinky distal phalange bone
    Hand_MaxSkinnable       = OVRPlugin.BoneId.Hand_MaxSkinnable,
    // Bone tips are position only. They are not used for skinning but are useful for hit-testing.
    // NOTE: Hand_ThumbTip == Hand_MaxSkinnable since the extended tips need to be contiguous
    Hand_ThumbTip           = OVRPlugin.BoneId.Hand_ThumbTip,           // tip of the thumb
    Hand_IndexTip           = OVRPlugin.BoneId.Hand_IndexTip,           // tip of the index finger
    Hand_MiddleTip          = OVRPlugin.BoneId.Hand_MiddleTip,          // tip of the middle finger
    Hand_RingTip            = OVRPlugin.BoneId.Hand_RingTip,            // tip of the ring finger
    Hand_PinkyTip           = OVRPlugin.BoneId.Hand_PinkyTip,           // tip of the pinky
    Hand_End                = OVRPlugin.BoneId.Hand_End,

    // add new bones here

    Max                     = OVRPlugin.BoneId.Max
}

OVRSkelton.csではボーンに関するメソッドが用意されている。試しに以下のようなスクリプトを作ってGetCurrentStartBoneID、_skeleton.GetCurrentEndBoneId、GetCurrentNumBones、GetCurrentNumSkinnableBonesを表示してみる。

using UnityEngine;
using UnityEngine.UI;

public class SkeletonInfo : MonoBehaviour
{
    [SerializeField]OVRSkeleton _skeleton;

    Text _info;
    // Start is called before the first frame update
    void Start()
    {
        _info = GetComponent<Text>();

        _info.text += $"StartBoneId : {_skeleton.GetCurrentStartBoneId()}\n";
        _info.text += $"EndBoneId : {_skeleton.GetCurrentEndBoneId()}\n";
        _info.text += $"CurrentNumBones : {_skeleton.GetCurrentNumBones()}\n";
        _info.text += $"CurrentNumSkinnableBones : {_skeleton.GetCurrentNumSkinnableBones()}\n";
    }

}

GetCurrentStartBoneID、_skeleton.GetCurrentEndBoneIdはどちらもBone IDのHand_StartとHand_Endが取得できるよう。GetCurrentNumBonesではスケルトン内のボーン数が、GetCurrentNumSkinnableBonesでは指先の~Tipを除外した数が取得できる

f:id:amidaMangrove:20191227182931p:plain

手がトラッキングできてるかどうか

OVRHandのIsTrackedで手がトラッキングできているかをBool値で取得できる。HandConfidenceはトラッキングされた手の信頼度をTrackingConfidence型で取得できる。値はLowかHighのどちらか。 以下のようなスクリプトを作成し、動かしながら挙動を確認してみる

using UnityEngine;
using UnityEngine.UI;

public class HandInfo : MonoBehaviour
{
    [SerializeField]OVRHand _hand;
    Text _info;

    // Start is called before the first frame update
    void Start()
    {
        _info = GetComponent<Text>();
    }

    // Update is called once per frame
    void Update()
    {
        _info.text = "";
        _info.text += $"IsTracked : {_hand.IsTracked}\n";
        _info.text += $"HandConfidence : {_hand.HandConfidence}";
    }
}

f:id:amidaMangrove:20191227190645j:plain f:id:amidaMangrove:20191227190654j:plain

両手が近くても描画したい!

どうやら「IsTrackedがtrueでもTrackingConfidenceがfalseだと手が描画されない」っていうのがデフォルトっぽい。これを修正したい場合、OVRHand.csの中でHandConfidenceを判定している部分を書き換えるとIsTrackedのみでも手が描画されるようになった。

    OVRMeshRenderer.MeshRendererData OVRMeshRenderer.IOVRMeshRendererDataProvider.GetMeshRendererData()
    {
        var data = new OVRMeshRenderer.MeshRendererData();

        data.IsDataValid = _isInitialized;
        if (_isInitialized)
        {
       // ↓ここの判定を書き換える
            data.IsDataHighConfidence = IsTracked;// && HandConfidence == TrackingConfidence.High;
        }

        return data;
    }

f:id:amidaMangrove:20191230163327j:plain

とりあえずここまで。

Oculus Questで俺のギターをコントローラーにする

この記事はUnity #3 Advent Calendar 2019の25日目の記事です。

僕が書くのは完全に趣味と自己満の記事です。

あぁーVRでもギターが弾きてぇよー

僕はギターが大好きだ。中一でベンチャーズ(パイプラインとか....)をコピーし、今でも歪ませた音ばかりを出している。30後半になったけども必死にスウィープ奏法を練習している。そんな大好きなギターをなんとかVR上でコントローラとして使えないかなーと思ったのがこの記事のきっかけです。

ということで手元にあるギターをOculus Questのコントローラーとして使用できないかを考えてみよー

f:id:amidaMangrove:20191225011133p:plain

これだ!多分これでいけるんじゃあないかと思うのでやってみる。おそらくコーディング量はほぼ0です。

用意するもの

Keijiroさんが公開している「Lasp」と「OscJack」を使用します。Unityは2019.3.0fを使用。

github.com

github.com

他必要なものはどのご家庭にもある

となります。さらにOculus Questがケーブルレスなのを考慮し

  • ワイヤレスシステム(あれば)
  • ワイヤレスヘッドフォン(あれば)

この辺まで揃えられればなんか良さそう

まぁぶっちゃけオーディオインターフェース経由ならギターである必要はないのです。もっといえば、PCにマイクが内臓できてればとりあえず入力は取れます。

Oculus Questのipアドレスを調べる

Oscを使用するためOculus QuestとPCをUSBで繋ぎ以下のコマンドをcmdに入力してipアドレスを調べます。(事前にadbがインストールされている前提)

adb shell ip addr show wlan0

「inet xxx.xxx.xxx.xxx brd ~」とipアドレスが表示されるので覚えておきましょう。 このipアドレスを送信アプリに埋め込みます。

Lasp → OSC送信用のプロジェクトを作成する

変換とOSC送信するPCアプリと、受信側のOculus Questアプリの2つを作ります。

まずは送信側のプロジェクトを作りそれぞれ「Lasp」と「OscJack」をインポートしておきます。

Laspを使用してオーディオ入力を受け取る

低・中・高の音域を取り出したいのでSphereヒエラルキーに3つ追加しそれぞれ「AudioLevelTracker」をアタッチして「Filter Type」を「Low Pass」「Band Pass」「High Pass」に設定しておきます。

f:id:amidaMangrove:20191221184302p:plain とりあえず左から「Low」「Band」「High」。きちんとオーディオ入力が取れているか確認するため簡単なスクリプトを作りアタッチします。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NormalizedLevelView : MonoBehaviour
{
    public float Level { get;  set; }
    
    void Update()
    {
        transform.localScale = new Vector3(Level, Level, Level);
    }
}

アタッチしたら「AudioLevelTracker」の「Normalized Level Event」にプロパティの「Level」を設定します。 f:id:amidaMangrove:20191221221638p:plain

この段階で実行しオーディオ入力が取れているか確認しましょう。 f:id:amidaMangrove:20191221221900g:plain

※注意:Laspには「デフォルトデバイスを使用する」と「モノラル入力」の制限があるのでデバイス側で少し設定いじらないと入力取れないケースもあるかもしれません。その場合はサウンド設定を見直すといいかと。

OscJackを使用し入力結果を送信する

先ほど作成した3つのSphereに「OscPropertySender」もアタッチし、とりあえず以下の値を設定します

  • IP Address:調べたOculus Questのipアドレス
  • OSC Address:それぞれ「/low」「/band」「/high」
  • Data Source:自身のオブジェクト
  • Component:AudioLevelTracker
  • Property:normalizedLevel

f:id:amidaMangrove:20191221222758p:plain

ひとまずこのプロジェクトで送信できているか確認するため、適当なGameObjectを置き「OscEventReceiver」をアタッチ。Window→OSC Monitorを開き確認します。

f:id:amidaMangrove:20191221223222g:plain とりあえず送れてるっぽい。

Oculus Quest向け受信用のプロジェクトを作成する

Oculus Quest用のアプリの作り方は先人の方々がまとめてくださっているので「Oculus Quest Unity ビルド」でググってもらえれば色々ヒットします。

ひとまず先ほど作成した送信アプリから値を受け取り、Oculus Quest上でもSphereが動作するアプリを作ってみましょう。

新しく作成したプロジェクトに「OscJack」と先ほど作成した「NormalizedLevelView.cs」を追加します。

で、先ほどと全く同様にSphereで「Low」「Band」「High」を作っておき....(↓の画像は使いまわし)

f:id:amidaMangrove:20191221184302p:plain

それぞれに「NormalizedLevelView」と「OscEventReceiver」をアタッチします。

f:id:amidaMangrove:20191221231648p:plain イベントの取得先をNormalizedLevelViewのLevelに設定するのを忘れないように。

これでビルドを行いOculus Questで実行します。

結果

www.youtube.com

やりたかったことはだいたいできた!!

ギターの音自体はオーディオインターフェースから出力。入力はPCを通してOsc送信→Oculus Questで受信→ビューに反映って感じで。遅延どうかなーと思ったけど自宅環境でやる分には個人的に気にならなかったです。そしてやっぱりkeijiroさんは神でした。

まとめ

今回の内容で何ができそうか考えてみると

  • リアル楽器をコントローラーとして使ったインタラクティブな遊び
  • VFXGraphとかを使用して音のビジュアライズ化(QuestではVFXGraphうまく表示されないけど..)
  • Hapbeatとかと組み合わせるのも良い?
  • タブ譜とかVR内で見ながら弾けたら良いかも

まぁ実際やってて思ったのは機材の準備が面倒。あと手元がよく見えない自宅以外の環境でちゃんとできるのか?とか

まぁ....というわけで、Unityも楽器も....楽しんだもん勝ちだぜ!!

f:id:amidaMangrove:20191225010051j:plain

(おしまい)

Oculus QuestでUniversal Render Pipelineを使用してみる

Oculus QuestでURPを使うまでの簡単な軌跡です。Oculus Questのビルド手順はこりんさん(@korinVR)が完璧にまとめてくれているのでそちらを参照して頂ければと思います。

framesynthesis.jp

Universal Render Pipelineについて

詳しくは「凹みさん」が素晴らしくまとめてくれているのでこちらをご覧頂ければ。 tips.hecomi.com

どうやらこれまでのパイプラインはレガシーになりそうなので今のうちに色々調べておいた方が良さそう...というわけでOculus QuestをURPで使用してみたいと思います。

環境に関しては「Unity 2019.3.0f1」を使用してAndroidのビルド環境が既に構築済み前提で進めます。

プロジェクトの作成

テンプレートで「Universal RP」が用意されているのですが余計なアセットも同梱されてしまうので、「3D」で作ります。 f:id:amidaMangrove:20191215131633p:plain

Universal Render Pipeline関連のパッケージを追加

Package Managerを開き、以下のパッケージをがしがしと入れていきます。

  • Core RP Library
  • Shader Graph
  • Universal RP

プロジェクトの設定

Switch PlatformでAndroidを選択します。Texture Compressionは「ASTC」を指定しておくと良いです。 f:id:amidaMangrove:20191215132006p:plain

次にProject Settings→Player→Other Settingsを開き、「Color Space」を「Linear」に変更します。Oculusの「Build and Upload Android Applications」のページを見るとわかるのですが「ガンマじゃなくてリニア使え、あとあんまポストプロセス使うなよ」的なことが書いてあるので「あっ、はい」と声に出しておきましょう。

f:id:amidaMangrove:20191215132428p:plain

そのすぐに下にある「Graphics APIs」から「Vulkan」を削除して、Minimum API Levelを6.0に変更します。 f:id:amidaMangrove:20191215133319p:plain

f:id:amidaMangrove:20191215133627p:plain

最後にProject Settings→XR Settingを開き下図のように設定します。 f:id:amidaMangrove:20191215133735p:plain

Oculus Integrationのインポート

Asset StoreからOculus Integrationをインポートしておきます。インポート結構時間がかかるのでお湯を沸かしてコーヒーでも飲んでゆっくりしましょう。もし奥さんがいるなら話しかけてみてはどうでしょうか(テクスチャの圧縮形式を変更しているのが原因っぽい...?) f:id:amidaMangrove:20191215134320p:plain

完了するとUnity再起動促されるので再起動しておく。今再起動したらこうなった。 f:id:amidaMangrove:20191215140118p:plain

負けない。

ビルド時間短縮のためOculus Integrationから使わないアセットは予め削除しておくと良いです。因みに僕は以下を消しています。

Universal Render Pipelineの設定を行う

Assets→Create→Rendering→Universal Render Pipeline→Pipeline Assetsを選択します。名前は適当で良いですがここではOculusQuestURPとします。

f:id:amidaMangrove:20191215140727p:plain

再度Project Settingsを開き、GraphicsのScriptable Render Pipeline Settingsの欄に先ほど作成したURPのアセットを放り込みます。 f:id:amidaMangrove:20191215152821p:plain

適当にビューを作る

Main Cameraを削除しOVRCameraRigをヒエラルキーに追加し以下の設定を行います

  • TargetDevice : Quest
  • Tracking Origin Type : Floor Level

とりあえず床になるようにPlaneを設定します f:id:amidaMangrove:20191215143122p:plain

ビルドしてみる

段階でビルドしてみる。URPを使用している関係か初回は長いので少し我慢。 f:id:amidaMangrove:20191215141945p:plain

これで正常に表示されれば完了です。

まとめ

URP適用自体は非常に簡単。確認したところポストプロセス = Global Volumeも一部使用できた(パフォーマンスは勿論下がるが....)。Shader Graphも勿論使用できる。

気になった & 気づいた点

  • Multi Passにすると右目側が描画されない
  • VFXGraphを使用すると描画がおかしくなる