20191222のUnityに関する記事は17件です。

クソゲーづくりで学んだこと ~ミスしてもいいから動いて学ぼう~

はじめに

この記事はNitKit コンピュータ研究部 Advent Calendar 2019 22日目の記事です。
Cpawの記事に隠れてひたすらこの記事を書いてました。
今回は私がゲームを始めて作ったときのメイキング・軌跡みたいなものを思い出しながら、時々コードやスクショも載せながらゆるく解説していきます。それと共に私がこのゲーム作りで学んだことを少し書いてみたいと思います。よろしくお願いいたします。

早速ですが

私はゲームが本当に好きです。今現在も就活をしているくらいにはゲーム企業での就業を夢みて日々ESを出しては落とされています。
でも、実際に学校生活の中でゲーム開発を行ってきてはおらず、いざ就活を始めるときにふとこう思いました。

「ゲーム作ったことなくったって学校のプログラミング経験でどうにかなるもんなのかな?」

これはまごうことなく偽です。プログラミング学習で学べるのはあくまでプログラミングのやり方であり、ゲームの作り方ではないからです。
この至極まっとうなことに気づくのがあまりにも遅すぎたのは本当に今でも悔やんでいるのですがこの事実に気づいた私はその時こう思いました。

「よし、それじゃあ実際に簡単なゲームを作って会社側に意欲を見せてみよう!」

そこから私の初めてのゲーム作りが始まりました。

ずぶの素人のゲーム作り

~その1:仕様書づくり~

実はこの時までにDXlibやSiv3D、Xenkoなどで色々とゲーム作りにトライしていたことはあるんです。
あるんですが、その当時使っていたパソコンが本当にクソスペックだったので、何をどうしても何も動かない状態が続き、気づけばやる気を失っていました。
「じゃあコマンドライン上で動くものを作ればいいじゃない!」と普通のゲーム開発者の方々は至極当然のように思うはずですが、この時の私は3Dゲーム、フルグラフィックこそがゲームたるものだ、と信じて疑わず、コマンドライン上で動くゲーム開発には見向きもしませんでした。(この時の私のカスみたいな判断にも死ぬほど後悔してます、ゲームだったら何でも作ってみればよかったのに...)

まあそんなこんなで、しょうもないながらも何回か挫折を味わっていた私は「あてずっぽうで作るより、きちんと企画書・仕様書から作成して作るほうが挫折しにくいのでは?」と思い、実際に三時間くらい使って仕様書を完成させ、その仕様書の通りに作ろうと決心しました。
これは後から見ても成功でした。大枠での進捗管理が容易になったのもそうですが、自分が何をすべきなのかを明確化できたところも後々響いてきます。
結果、今回作るゲームのコンセプト・期間・としては以下のようになりました。(仕様書があったはずなんですが捨ててました

コンセプト...一分間でやる鬼ごっこ
製作期間...一ヶ月
使用ツール...Unity

~その2:Unityの操作方法を覚える~

さてとにかく自分が何をすべきか、どれくらいの期間で作るかは決まりました。
ただこの時点ではUnityの使い方を何も知らない状況です。技術選定だけして後は野となれ山となれ方式ではいずれ計画は破綻します。
ということでこのチュートリアルをやりました。Unityは偉大。でもチュートリアルに何十日もかける余裕はないので、とりあえずこのチュートリアルは3日で終わらせました。
はじめてのUnity

~その3:ゲームの大枠を作ろうとするも想像以上にコードが書けない~

さて、なんとかUnityのチュートリアルは終了したので、さっそくゲーム制作に取り掛かりました。

ゲームに必要な要素として、

  • 3Dゲームである
  • 一分間でゲームを終了させる
  • 鬼はフィールドのランダム座標に一定の間隔でポップする
  • フィールドの外に落ちてもゲームを終了させる
  • 鬼はプレイヤーの座標を毎秒取得し、追いかける

大体こんな感じにリストアップしていたので、あとはこれに沿ってプロジェクトを進めていけばよいのですが。
しかし、私はここで拗らせてしまいます。

「どうせならできる限り自分で作りたい!」

まだUnityのチュートリアルを終えたばかりのひよっこがそんなことを思うもんですから、速攻でプロジェクトが頓挫しかけました。
どのように作ればいいのかという経験・知識もない状態の人間にはそこまで自作で作りこむことなんてできません。できるわけがありません。その時の私はうんうん唸った結果、こうすることにしました。

「出来合いのモデルは嫌だし自分でモデル作るか、、、」

~その4:もっともらしい理由を付けてモデリングに逃げた~

一見良さそうなこの考えも、期間が決まっているプロジェクトに扱ったことのないソフトをもう一つ扱うことが確定するモデリングの要素を導入するという点ではまごうことなき愚策です。しかし一度決まったことを曲げるのも自分の成長につながらないのでは、と思い自分なりに様々なサイトを閲覧しまくることによってblenderを使って大体二週間かけてモデルを完成させました。

human.PNG
とりあえず人っぽいのを成型して、

asderftghj.PNG

足っぽいのをミラーで生成して、

ajjet.PNG
何となく人っぽい形を作りました。でも手があまりリアリティがなかったので、

ude.PNG
こんな感じで腕を弄って、
aaaa.PNG

最終的にこんな感じのモデルを作成しました。
uv.PNG

最後にUV展開をしてテクスチャを作成しました。ただこれに関してはホントにガバガバなのでクソみたいなテクスチャしか作れませんでした。
そのテクスチャを作ったモデルに貼り付けて、

asdg.PNG

なんとかUnity上にインポートすることができました。万歳。

ちなみにモデルの頂点数とかはこんな感じです。ほんとに初心者のモデリングだったので、パラメータは軒並みクソです。
rdff.PNG

モデリングは自分が思っている以上に意外と楽しかったです。ただ、
death.PNG
手の細かい成型ができなかったため、手がデスフェイサーみたいになってしまったり、

tigau.PNG

モデルがこんな感じの形になってからしばらく戻せなかったり、

tubasa.PNG
変形を間違えて恐ろしい形になった挙句この状態で保存をかけてしまい泣く泣く作り直す羽目になってしまったりとかなり時間をかけてはしまいました。しばらくモデリングはやりたくないです。

~その5:さあ動かすぞと思ったもののやっぱりコードが書けないので、他力本願で行こう!~

まあ何とかモデルは完成しましたが、モデルの作成にここまでで二週間もかけてしまっていました。残りの一週間前後でやるのは残ったコードの実装です。さすがにモデル自作の時に思い知ったのか、ここらへんで私はスクリプト全て自作することを諦め、ほかの人のコードをガンガン流用することに決めました。でもほかの方が作ったソースを読みとって、どういう意味かを理解する、自分の環境に落とし込む、そんな作業をすると決めた時点でどうなるかは明白でした。

~その6:技術力不足で毎日徹夜のデスマーチ突入~

はい、その通りです。

「デスマーチ」の時間です。

学校から帰ってきたらすぐにパソコンの前に座ってうんうん苦しみながらほかの人のソースを読む。そのソースを自分のプロジェクトの環境に落とし込む。デバッグも忘れないでやる。
自分でアルゴリズムを考えて実装するというよりほかの人の考えをうまく自分と同じ領域に移しているだけであったため、かなり無為でつらい作業でした。でも時間がないとできない作業でしたので、生活リズムが毎日三時寝は当たり前みたいな、完全にリリース前のIT企業勤めのサラリーマンになってしまいました。

そんな感じで必死こいて作ったソースがこれらです。ほんとにリファクタリングのこととか何一つ考えていないため、正直クソコードの羅列にしかなりませんが、皆様の反面教師となるのなら本望ということで、後悔公開します。

作成したコード集

UI上に残り時間を表示するコードです。

TimerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class TimerController : MonoBehaviour
{

       // GameObject TimerTex;
        public Text timerText;

        public float totalTime;
        float seconds;

        // Use this for initialization
        IEnumerator Start()
        {
             enabled = false;
             yield return new WaitForSeconds(3); //三秒待ってUpdate()を有効化
             enabled = true;
        }

        // Update is called once per frame
        void Update()
        {
            totalTime -= Time.deltaTime;
            seconds = totalTime;
            timerText.text = "残り時間:" + seconds.ToString();

            if(totalTime <= 0)
            {
            SceneManager.LoadScene("GameClear");
            }
        }
 }

カウントダウン表示を行うソースです。

CowntDown.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class CountDown : MonoBehaviour {

     Text text;
     void Start()
    {
        text = GameObject.Find("CountDown").GetComponent<Text>();
        StartCoroutine(Count());
    }

    IEnumerator Count()
    {

        yield return new WaitForSeconds(1f);
        text.text = ("2");
        yield return new WaitForSeconds(1f);
        text.text = ("1");
        yield return new WaitForSeconds(1f);
        text.text = ("Start!");

        yield return new WaitForSeconds(1.0f);
        text.gameObject.SetActive(false);
    }
}

フィールドのランダム座標に敵を出現させるコードです。

Pop.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Pop : MonoBehaviour {

    // 出現させる敵を入れておく
    [SerializeField] GameObject Enemy;


    // 次に敵が出現するまでの時間
    [SerializeField] float appearNextTime;
    // この場所から出現する敵の数
    [SerializeField] int maxNumOfEnemys;
    // 今何人の敵を出現させたか
    private int numberOfEnemys;
    // 待ち時間計測フィールド
    private float elapsedTime;


    IEnumerator Start()
    {
        numberOfEnemys = 0;
        elapsedTime = 0f;

        enabled = false;
        yield return new WaitForSeconds(3); 
        enabled = true;
    }

    void Update () {
        // この場所から出現する最大数を超えてたら何もしない
        if (numberOfEnemys >= maxNumOfEnemys)
        {
            return;
        }
        // 経過時間を足す
        elapsedTime += Time.deltaTime;

        // 経過時間が経ったら
        if (elapsedTime > appearNextTime)
        {
            elapsedTime = 0f;

            AppearEnemy();
        }
        Resources.UnloadUnusedAssets();
    }

    // 敵出現メソッド
    void AppearEnemy()
    {

        float x = Random.Range(10f, 450f);
        float y = 116;
        float z = Random.Range(10f, 450f);
        Vector3 position = new Vector3(x,y, z);
        Instantiate(Enemy, new Vector3(x,y,z),Quaternion.identity);

        numberOfEnemys++;
        elapsedTime = 0f;
    }

}


敵の挙動について記述したコードです。NavMeshAgentを用いて簡単にプレイヤー追跡を行っています。

Enemy.cs
using UnityEngine;
using UnityEngine.AI;
using System.Collections;
using UnityEngine.SceneManagement;

public class Enemy : MonoBehaviour
{

    [SerializeField] public GameObject Player;

    private NavMeshAgent navAgent = null;

    IEnumerator Start()
    {
        GetComponent<NavMeshAgent>().enabled = true;
        Player = GameObject.Find("Player"); 
        navAgent = GetComponent<NavMeshAgent>();

        enabled = false;
        yield return new WaitForSeconds(3); //三秒待ってUpdate()を有効化
        enabled = true;

    }
    private void Update()
    {
            navAgent.destination = Player.transform.position;//navMeshAgentの操作
    }
}

ゲームオーバー時の挙動を記述したものです。

GameOverSceneChanger.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameOverSceneChanger : MonoBehaviour
{
    GameObject player;

    private ParticleSystem particle;
    //Exploder Exploder;

    void Start()
    {

        particle = GetComponent<ParticleSystem>();
        //Exploder = player.GetComponent<Exploder>();
    }
    // Use this for initialization
    IEnumerator OnControllerColliderHit (ControllerColliderHit other)
    {
        Debug.Log("Hit");
        if (other.gameObject.CompareTag("Enemy"))
        {

            yield return new WaitForSeconds(0.3f); 
            SceneManager.LoadScene("GameOver");
        }
        if (other.gameObject.CompareTag("DeathZone"))
        {

            yield return new WaitForSeconds(0.01f); 
            SceneManager.LoadScene("GameOver");
        }
    }
}

プレイヤーの挙動について記述したコードです。このコードが一番他力本願です。当時は本当にどんなふうに実装したらいいかわからず、ほとんどがコピペみたいなソースになっています。

PlayerController.cs
using UnityEngine;
using System.Collections;


public class PlayerController : MonoBehaviour   
{



    private CharacterController charaCon;       
    private Vector3 moveDirection = Vector3.zero;   


    public float MoveSpeed = 5.0f;        
    public float RotateSpeed = 3.0F;     
    public float RollSpeed = 1200.0f;   
    public float gravity = 20.0F;   
    public float jumpPower = 6.0F;  



   IEnumerator Start()
    {
        charaCon = GetComponent<CharacterController>(); 
        animCon = GetComponent<Animator>(); 
        enabled = false;
        yield return new WaitForSeconds(3); 
        enabled = true;
    }



    void LateUpdate()
    {


        var cameraForward = Vector3.Scale(Camera.main.transform.forward, new Vector3(1, 0, 1)).normalized; 
        Vector3 direction = cameraForward * Input.GetAxis("Vertical") + Camera.main.transform.right * Input.GetAxis("Horizontal"); 



        charaCon.Move(moveDirection * Time.deltaTime);  

        if (Input.GetAxis("Vertical") == 0 && Input.GetAxis("Horizontal") == 0)  
        {
            animCon.SetBool("Running", false);  
        }

        else
        {
            Rotate(direction);  
            animCon.SetBool("Running", true);  
        }



        if (charaCon.isGrounded)    
        {
            animCon.SetBool("Jumping", Input.GetKey(KeyCode.Space)); 
            moveDirection.y = 0f;  
            moveDirection = direction * MoveSpeed;  

            if (Input.GetKey(KeyCode.Space) ) 
            {
                moveDirection.y = jumpPower; 
            }
            else 
            {
                moveDirection.y -= gravity * Time.deltaTime; 
            }

        }
        else 
        {
            moveDirection.y -= gravity * Time.deltaTime;  
        }


    }



    void Rotate(Vector3 Hope_Rotate)
    {
        Quaternion q = Quaternion.LookRotation(Hope_Rotate);
        transform.rotation = Quaternion.RotateTowards(transform.rotation, q, RollSpeed * Time.deltaTime);
    }
}

そんな感じでコードを書いて、あとはこれを適応させるだけだったんですがまだいろいろと作業が終わってなかったのを思い出し、コードの詳細な適応と共に以下の作業を行いました。

rfrr.PNG
インポートしたモデルにSAColliderBuilderというアセットを適応したり、

gasgga.PNG
フィールドを野生溢れる地形にしたり、

enemy.PNG
敵のテクスチャを作成したりしました。

そういう作業を必死に行ったり、残りの数日でUIの部分を作成したり、実際にテストプレイしながらストレスのたまらないようなゲーム性を追求し、

aaaaaa.PNG
何とか完成しました。今見るとボタンの所とか本当にUnity臭がすごいですね。
デモプレイの様子はこちらです。

完成したゲームのリンクはこちらです。クソゲーですが、もし気になったら触ってみてほしいです。(Windowsのみ)
1 Minutes Tag!!! ver1.01 ~for Windows - Google ドライブ

当時を振り返って・自分が得たもの

その当時はわからなかったんですが、結果的にサウンドの実装を全くしないまま終わってしまっていました。この時はとにかく動くものを作りあげる、ってところしか頭になかったため、今では臨場感に欠けるものができてしまったと反省しています。
対して、得たものですが、これは単純に自信がついたのと、様々な機会が増えたことです。
一ヶ月という期間で自分なりの考えをアウトプットできた、というやりがいは本当に何物にも代えられないものでしたし、拙くてもそれらを遂行することができた自分の実行力に自信が付きました。
また、逆求人のイベントに行く際の明確な提示材料ができ、様々な企業の方とお話しする機会をいただいたり、自分が幼少期熱中していたゲームを製作していた会社の方とお話しする機会をいただいたり、最終選考の機会をいただいたりと、本当に様々なものを得ることができました。やっぱり自分から行動するのは大事です。

最後に

今この記事を読んでるゲームを開発しようか迷ってる皆さん。

「今すぐゲームを作りましょう」

拙くてもいいです。クソゲーでもいいです。いくらくだらなくてもいいです。作りましょう

「ゲームのアイデアはあるのに迷ってる?迷うくらいなら作りましょう」

私みたいにこんな恥さらしゲームを作って平然としてる人間だっています。違いは行動するかしないかです。

ゲーム作りのノウハウは学校で本当に学べますか?

学べませんよね?確かに基礎的なことは様々な部分で応用ができます。企画、計算、マネージメントなどには様々な教科で学んだことが活きたりします。そういう観点からすると学校の学習も本当に有意義なはずのものです。
ですが、ゲーム作りのノウハウはゲーム専門学校でもない限り実際に自分で作ってみないことにはつかないのです。ゲーム作りが上手くなりたければゲームを作るしかないのです。そこを行動しないままにして丸く収まっているのは非常にもったいないです。

いつかはビッグタイトルを作ってみたい!

と思っているならなおのこと自分で行動し、それを様々な場所にアウトプットしましょう。早いうちに恥をかいたり、失敗することこそが将来の大成功にきっとつながります。重ね重ね言いますが、今現在様々なシーンで活躍されているゲーム開発者とあなたの違いは
「目的のために行動しているか」
これ一つのみです。

全力で取り組むのであれば、私もそうですが、あなたの周りの方々も、他のゲーム開発者の方々もきっと全力で応援します。するはずです。ゲームを作れるようになりたければ

とにかく自分で、自分の意思で行動しましょう

自分で簡単なゲームを最後まで作り上げること、それこそがあなたのゲーム作りの第一歩です。

ここまでの長文を読んで下さり、誠にありがとうございます。
これからのゲーム作り人生を、皆様がより有意義に過ごせますように。


Writer:ゆゆゆうた
Twitter はてブ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity VisualEffectGraph -シンプルスォームパーティクル解説-

この記事はUnity Advent Calender 2019 23日目の記事です。

はじめに

 VisualEffectGraphのシンプルスォームパーティクルの項目について記事にしました。
 以前にUnity VisualEffectGraph -インストール方法-と、Unity VisualEffectGraph -シンプルパーティクル解説-を記事にしているので、初めて触る方はこちらを参考にしていただければと思います。

シンプルスォームパーティクル

 まずVEGウィンドウ上のコンテキスト類を全て消しましょう。マウス左ドラッグで全選択して右クリック[Delete]ですべて消えます。その後に再度右クリックして[CreateNode]→[System]→[Simple Swarm Particle System]を選択し生成します。(図1)
 生成されたコンテキストは図2の様なボックスを持っています。また、生成されたVFXは図3のような火の玉のようなVFXです。

image.png
 図1 Simple Swarm Particle Systemのボタン

image.png
 図2 Simple Swarm Particle Systemのコンテキスト

image.png
 図3 Simple Swarm Particle SystemのVFX

ブロック部

 それではブロック部の解説をしていきましょう。

Spawn内のブロック

 この部分は「ConstantSpawnRate」のみが有ります。
・ConstantSpawnRate
 このブロックには「Rate」というプロパティがあり、毎秒いくつのパーティクルを生成するかを設定できます。
 時間等で消失していないパーティクルの数とInitializeのCapacityが同じ数になった時はパーティクルが消えるまで生成が止まります。

Initialize内のブロック

 ここでは、「Set Lifetime Random」ブロックがあり、生存時間をコントロールしています。
・Position(Sphere)
 生成場所を設定した球体の表面にパーティクルを配置するブロックです。
・Set Lifetime Random
 パーティクルの生存時間(秒)を設定するブロックです。最大値と最小値の2つを決めるとその間のランダムな時間で消失します。

Update内のブロック

 「Vector Field Force」ブロックのみがあります。
 ・Vector Field Force
  このブロックはベクターフィールドを用いてパーティクルに速度を与えます。
 ベクターフィールドとは空間上に力場を作るイメージで、特定の場所に行くと設定された方向に力が掛る空間の設定です。

Output内のブロック

 ブロックの数が多いですが、複雑な物ではないので1つ1つ見ていきましょう。
・Orient:Along Velocity
 このブロックはパーティクルの向く方向を設定することができます。「AlonVelocity」設定ではパーティクルが進行方向を向きます。
・Set Size
 パーティクルのXYZ全ての大きさを変更するブロックです。1を中心に拡大縮小します。
・Set ScaleX
 パーティクルのX方向の大きさを変更するブロックです。1を中心に拡大縮小します。
・Seet ScaleY over Life
 パーティクルのY方向の大きさをLifeTimeを参考に変更するブロックです。スケールの大きさはCurveによって変更されます。
・Color/Alpha over Life
 LifeTimeによってα値(透明度)と色を変えるブロックです。横軸がLifeTimeで左端が生成、右端が消失の時間を示します。ウィンドウの操作はUnityマニュアルを参考にしてください。

実際の動き

 それでは上記を踏まえてどう動くのか、最初からSpawn,Initialize,Update,QuadOutputの順に追っていきましょう。

Spawn

 まずSpawnコンテキストの[Rate]で毎秒25000個のパーティクルが生成されることが決まります。
image.png
図4 Spawnコンテキスト

Initialize

 Initializeコンテキストでパーティクルの個数上限65536個と定められます。
 「Position(Sphere)」ブロックでは[Position Mode]より、生成場所が球体表面(Surface)に設定されています。また、[Spawn Mode]より、ランダムに生成されます。生成場所の球体の詳細はすぐ下の項目から設定できます。
 「SetLifetimeRandom」ブロックで[Lifetime]を1秒~3秒の間と定められ、この時間内で消失することを設定されます。
image.png

図5 Initializeコンテキスト

Update

 Updateコンテキストではブロックが無い場合もありますが、今回は「Vector Field Force」ブロックが追加されています。
 「Vector Field Force」ブロックでは、ベクターフィールドを読み込んで適応させています。このおかげでパーティクルがベクターフィールドの影響を受けて動いています。
image.png
図6 Updateコンテキスト

QuadOutput

 QuadOutputコンテキストです。MainTextureにDefaultParticleが設定されており、このTextureが生成されます。
 「Orient」ブロックでは、「Along Velocity」モードとなっており、このモードはパーティクルが進行方向を向いて帯のようになるモードです。
 「SetSize」ブロックはその名の通りサイズを設定しています。
 「SetScaleX」ブロックではスケールを設定しています。[Channels]の項目でx,y,z,xy,xz,yz,xyzの7つから選択します。ブロック名の最後は選択した項目で名前が変わります。その下の[Scale]では前の項目での軸のスケールを設定できます。
 次の「SetScaleY overLife」では、Y軸のOverLifeに応じたスケールを設定しています。軸の設定は[SetScale]と同じです。
 最後の「Color/AlphaoverLife」ブロックではRGB255の白と生成時、消失時付近のα値を0に近くする設定がされています。これによって生成時は透明な状態から始まり、少し経つとα値が255になり姿がハッキリとし、消失時近くになるとまたα値が減って消えるように消失していきます。

image.png
図7 Outputコンテキスト

出力結果

image.png
図8 シンプルスォームパーティクル出力

色々値を触ってみよう

 一通りシンプルスォームパーティクルに触れたので、ある程度どこを触ればどうなるか分かったと思います。
 後は自由にどんどん値を変えたりして触ってみて下さい!
 最初に戻したくなったら画面に置いたノードを全て消し、右クリックして「Create Node」→「System」→「Simple Swarm Particle System」と押せば最初のシンプルパーティクルのノードが配置されます。

まとめ

 今回はシンプルスォームパーティクルのSpawn,Initialize,Update,Outputといったコンテキストに始まり、コンテキスト内の機能としてのブロックを解説しました。
 記事を読んでコンテキストとブロックについて多少なりとも分かって頂けたら嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NitendoSwich Joycon + FinalIKで簡単にVTuberを作ってみる

本記事はVTuber Tech #1 Advent Calendar 2019の23日目の記事です。
Vtuberハッカソンでやったことを書こうと思っていたのですが、プレミアム大会がまだ終わってないので、別のことを書きます。

準備

準備するもの

  • Unity
  • UniVRM
  • FinalIK
  • JoyconLib
  • 任意のVRMファイル
  • NintendoSwitchのJoy-con

Unity

Qiitaを見るような人に説明は不要
今回使ったバージョンは2019.2.9f1

UniVRM

VRMをUnityで動かすためのあれこれ

FinalIK

Humanoidをいいかんじに制御するAsset

JoyconLib

UnityでJoyCon使うためのやつ

任意のVRMファイル

VroidHubでかわいいやつを見つけたのでそれを使います
守りたいこの笑顔

使ったモデル

必要なものをダウンロードする

Unityのインストールは飛ばします

UniVRM

ReleaseページからUnityPackageをダウンロードします。

FinalIK

FinalIK
UnityAssetStoreにあります。有料なので買ってください。

JoyconLib

1.JoycobLibのリポジトリに行きます
2.「Clone or download」ボタンを押して「Download ZIP」を選択します
3.ダウンロードした「JoyconLib-master.zip」を展開しておきます
4.必要な別のファイルを持ってくるためここに行きます
5.「Clone or download」ボタンを押して「Download ZIP」を選択します
6.ダウンロードした「Unity-Wiimote-master.zip」を展開しておきます

使うAsset、オブジェクトを配置する

VRM,UniVRM

1.UniVRMのUnityPackageをUnityEditor上に放り込みます。
2.VRMファイルをUnityのプロジェクトに追加すると、勝手にPrefabファイルが生成されます。

FinalIK

FinalIKをAssetStoreからImportしておきます

JoyconLib

「JoyconLib-master/Assets」フォルダ内の「JoyconLib_scripts」フォルダをUnityプロジェクトに追加します
「Unity-Wiimote-master/Assets/Wiimote/Plugins/win64フォルダ内の「hidapi.dll」を Unity プロジェクトに追加します

JoyCon

JoyconをBluetoothでパソコンにつないでおきます

FinalIKで頭を動かす

1.VRMモデルのPrefabをSceneに追加します。
2.Create→3DObject→CubeでCubeをSceneに追加します
3.頭の位置にCubeを持っていきます
Cube
4.モデルのInspectorからAddComponent→VRIKをアタッチ
5.さっき作ったCubeをVRIKのSolver→Spine→HeadTargetに投げ入れます
VRIK
6.CubeのMeshRendererのチェックを外しておきます
7.実行して、Scene ViewでCubeの角度を変えてみると頭がそれに追従して動きます!
首が動くよ

JoyconRを使って頭を制御する

1.Create→CreateEmptyでJoycon制御用のObjectを作ります。
2.JoyConLib_scriptsの中のJoyConManagerをアタッチします。
3.AddComponent→NewScriptから新しいScript「JoyConController」を作ります。

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

public class JoyConController : MonoBehaviour
{
    [SerializeField]
    private GameObject targetObject;

    private Joycon m_joyconR;

    void Start()
    {
        SetControllers();
    }

    void Update()
    {
        if (m_joyconR == null) return;

        rightProcess();
    }

    private void rightProcess()
    {
        Vector3 gyro = m_joyconR.GetGyro();
        Vector3 angle = targetObject.transform.rotation.eulerAngles + new Vector3(-gyro.y, gyro.z, -gyro.x) * 0.5f;
        targetObject.transform.rotation = Quaternion.Euler(angle);
    }

    private void SetControllers()
    {
        List<Joycon> joycons = JoyconManager.Instance.j;
        if (joycons == null || joycons.Count <= 0) return;
        m_joyconR = joycons.Find(c => !c.isLeft);
    }
}

JoyConControllerのTargetObjectにさっき作ったCubeを入れます。

頭にJoyconRをどうにか固定して、動かしてみましょう

JoyconLを使ってズレを補正する

Joyconのgyroで角度をとると、ずれてくるので、手動でまっすぐにできるようにしておきます

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

public class JoyConController : MonoBehaviour
{
    [SerializeField]
    private GameObject targetObject;

    //staticとして定義しておく
    static Quaternion ZeroQuaternion = Quaternion.Euler(Vector3.zero);

    private Joycon m_joyconL;
    private Joycon m_joyconR;

    private bool isLerpControl = false;
    private float lerpTime;
    private Quaternion beforeQuaternion;


    void Start()
    {
        SetControllers();
    }

    void Update()
    {
        if (m_joyconL == null || m_joyconR == null) return;

        //JoyconLの処理は常に動かしておく
        leftProcess();

        //Lerpコントロール中はJoyconRでの制御はしない
        if (isLerpControl)
        {
            lerpProcess();
        }
        else
        {
            rightProcess();
        }
    }

    private void lerpProcess()
    {
        lerpTime += Time.deltaTime * 2.0f;

        if(lerpTime >= 1.0f)
        {
            lerpTime = 1.0f;
            isLerpControl = false;
        }

        targetObject.transform.rotation = Quaternion.Lerp(beforeQuaternion, ZeroQuaternion, lerpTime);
    }

    private void leftProcess()
    {
        if (m_joyconL.GetButtonDown(Joycon.Button.SHOULDER_1)) //L1が押されたとき
        {
            beforeQuaternion = targetObject.transform.rotation;
            isLerpControl = true;
            lerpTime = 0.0f;
        }
    }

    private void rightProcess()
    {
        //省略
    }

    private void SetControllers()
    {
        List<Joycon> joycons = JoyconManager.Instance.j;
        if (joycons == null || joycons.Count <= 0) return;
        m_joyconL = joycons.Find(c => c.isLeft);
        m_joyconR = joycons.Find(c => !c.isLeft);
    }
}

完成

完成したものがこちら

最終的に完成したスクリプト

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

public class JoyConController : MonoBehaviour
{
    [SerializeField]
    private GameObject targetObject;

    static Quaternion ZeroQuaternion = Quaternion.Euler(Vector3.zero);

    private Joycon m_joyconL;
    private Joycon m_joyconR;

    private bool isLerpControl = false;
    private float lerpTime;
    private Quaternion beforeQuaternion;


    void Start()
    {
        SetControllers();
    }

    void Update()
    {
        if (m_joyconL == null || m_joyconR == null) return;

        leftProcess();

        if (isLerpControl)
        {
            lerpProcess();
        }
        else
        {
            rightProcess();
        }
    }

    private void lerpProcess()
    {
        lerpTime += Time.deltaTime * 2.0f;

        if(lerpTime >= 1.0f)
        {
            lerpTime = 1.0f;
            isLerpControl = false;
        }

        targetObject.transform.rotation = Quaternion.Lerp(beforeQuaternion, ZeroQuaternion, lerpTime);
    }

    private void leftProcess()
    {
        if (m_joyconL.GetButtonDown(Joycon.Button.SHOULDER_1))
        {
            beforeQuaternion = targetObject.transform.rotation;
            isLerpControl = true;
            lerpTime = 0.0f;
        }
    }

    private void rightProcess()
    {
        Vector3 gyro = m_joyconR.GetGyro();

        Vector3 angle = targetObject.transform.rotation.eulerAngles + new Vector3(-gyro.y, gyro.z, -gyro.x) * 0.5f;

        targetObject.transform.rotation = Quaternion.Euler(angle);
    }

    private void SetControllers()
    {
        List<Joycon> joycons = JoyconManager.Instance.j;
        if (joycons == null || joycons.Count <= 0) return;
        m_joyconL = joycons.Find(c => c.isLeft);
        m_joyconR = joycons.Find(c => !c.isLeft);
    }
}

おわりに

適当に作った割にはいいものができた。嬉しい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

toioとLEGOを使って生き物を作ってみた(ヘビ編)

toioに命を吹き込んでいきたいと思います

Hiroshi Takagiと申します。普段は組み込みエンジニアをやっています。

この記事は、以前作成した、Unityでtoioを動かすスクリプトを用いて、身の回りの生物(ヘビ)を作ってみた記事です。
開発の環境等は前回の記事を参考にしてみてください。

工作生物 ゲズンロイドとか、ウロチョロスってすごく愛嬌のある動きをしますよね。
何か愛嬌のあるものを作ってみたい!と思い、身の回りのものをtoioに模倣させてみようと決めました。

さて、何を作ろう

最初はR2-D2とか作れたらいいなぁと思ってLEGOを組み立ててみたんですが、

なかなか思い通りに行きませんね。。
思ったよりもロボットの見た目とか動きとかを真似るのって難しいです。

そこで、ふと身の回りの動物たちを作ってみようと思い立ち、
なんとなぁく簡単にできそうかなヘビを作ろうと決めました。
要はくねくねすればいいんですよねw

ヘビの動きを作ってみた

とは言ってみたものの、動きをイメージして作るのは難しかったので、
まずはネット検索から始めたところ、こんな動画を見つけることができました。
https://www.youtube.com/watch?v=58aZnl9V2L0

東工大の教授さんが発見したヘビの動きで、サーペノイド曲線っていうらしいです。
向きをサイン波で動かしながら、進むのがポイントのようです。

公式とか調べるのは大変だったので、そのまま実装に入りました。
それで作成したコードがこちら

    void Update()
    {
        timeout += Time.deltaTime;
        if (timeout >= 0.05f)
        {
            timeout = 0f;
            if(isMoving){
                int speed = (int)Speed.GetComponent<Slider>().value;
                int gain = (int)Gain.GetComponent<Slider>().value;
                int times = (int)Times.GetComponent<Slider>().value * 5;
                float direction = Mathf.Sin( 2.0f * Mathf.PI * (float)count/(float)(times) + 0.5f * Mathf.PI);
                int diff = (int)((float)gain * direction);
                int left = speed - diff;
                int right = speed + diff;

                Cube.GetComponent<CubeController>().Move(left, right, 100);

                count++;
                if(count >= times){
                    count = 0;
                }
            }
        }
    }

Unityの記述になっていますが、50msec間隔でsin関数で計算した向きからモーター指示を出しています。
speedとgainとtimesは後で調整できるように以下のようにUI上のデータから引っ張ってきています。
スクリーンショット 2019-12-22 9.29.35.png
*ヘビのアイコンはイラストエイトさんのものを使わせていただきました。

動作させてみた結果がこちら

いい感じですかね。あとは見た目を作成できれば完了です。

あとはLEGOで見た目を作るだけ

LEGO初心者の私には、LEGOでくねくねさせるイメージが全く湧かなかったのですが、
周りの方に聞いてみたところ、意外と基本的なパーツで作成することができました。
IMG_0914.jpg

この白いフック形状のものと、黄色い穴あきのものを組み合わせて、お花で蓋をすれば完成です。

完成品がこちら

どうですか?結構様になっていませんかね?
Unityとtoio、LEGOがあるおかげで、こんなにもすぐに動くヘビが作れてしまったことに驚きです。

おわり

これからもtoioとLEGOの生き物シリーズを作っていきたいと思いますので、お楽しみに。

それではまた!(クリスマスまであと三日!)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サークルでVRコンテンツ開発をしてみた

1. はじめに

本記事は三重大学 計算研 Advent Calendar 2019 最終日です。

大学4年の@inamasu、M1の@kimarianです。

計算研はプログラミングや数学が好きな人が集まって情報共有したり競プロに参加したりといった活動をしており、公認サークルになってから今年で4年目にあたります。

最初はメンバーも情報工学科生ばかりで、こじんまりと活動していました。
ですが、近年は工学部だけでなく文理関係なくメンバーに加わり、少しづつですがサークルの規模も大きくなってきました。

計算研では、学園祭にてプログラミング関連で学術展示を行っています。
最初はLTをしてみたりと試行錯誤してきましたが、昨年度、数人で制作したVRコンテンツを展示したところ評判がよく、想定以上の多くの人にVRを体験してもらえました。

そこで、今年はより大規模に、サークルメンバーの多くが参加して合計10人以上(内半分は1年生)でVRコンテンツ開発をしました。

その際の開発の流れやツールなどの振り返り、来年にむけての反省を行っていきます。

チームメンバーの雰囲気

10人以上で共同で開発するということで、バージョン管理システムを用いた開発は必須でしたが、1年生や他学科などgitを使ったことのない人も多いので(というか情報工学科でもgitの使い方は基本学ばない)、学習をして足並みを揃える必要がありました。
そこで、学習用の資料や手順を作成して「学習タスク」とし、必要な人には取り組んでもらうようにしました。
(gitを用いたことについて詳しくは、大学のサークルでGitbucketとGitを使ってゲームコンテンツをチーム開発をしてみた)

作業の分担としては、1・2・3年生に、内容の企画、仕様など「VRコンテンツの内容」の決定をお願いし、4年、M1は開発の進行や技術的なサポートをする、といった方針で分担しました。

  • 1・2・3年生

    • 企画案作成
    • 仕様書作成
    • 使用BGM選定
    • イラスト、テクスチャ制作
    • モデリング
    • プログラミング
  • 4年・M1

    • 学習用資料の整備
    • 設計
    • プログラミングのサポート
    • モデリング
    • プログラミング
    • gitリポジトリ壊れたときのお助け係 (@mouri111さんありがとう)

といった形です。
もちろん、企画や仕様、開発ロードマップを考える際はみんなで相談して決めていきました。アンケートでみんなの意見を取り入れたりして、なるべく学年関係なくフラットに意見してもらえるよう心がけました。

2. 作ったもの

今回、最初にコンペを行い、2つの企画を採用しVRコンテンツを制作しました。

学園祭に展示するということで、3分程度で説明(チュートリアル)を終え、5分程度のプレイ時間で終わるような内容に設定しています。

また、今回機材にHTC ViveとOculus Rift Sを使えたので、空間を動き回って楽しめるコンテンツにしました。

2.1. 草刈りVR

草刈り.png

辺りに草が多い茂っており、それを右手に持ったカマでガシガシ刈り取ります。
刈り取っても左手のじょうろで水をかけてやればぐんぐん草が育っていくので、育ったらまたガシガシ刈り取ります。

制限時間内にどれだけの量の草を刈り取れるかで競います。刈り取った草の量に応じて途中でステージが変わっていき、どんどんステージがスケールアップしていきます。

体を動かしながらどんどん草を刈り取っていく爽快感が特徴です。

2.2. 脱出VR

脱出.png

ダンジョン風の部屋に閉じ込められ、その中を自由に探索しながら脱出を試みます。
様々なギミックを解いて石版を入手し、正しい位置に配置することでステージを攻略していきます。

なわを切ったり、松明を使ってツタを燃やしたりジェスチャーしたり....と、トリガーボタン1つで様々な動作が盛り込まれているところが特徴です。

3. 使用ツールと機材

3.1. Discord

週に2回ほど集まって進捗報告をしたり相談する時間を設けましたが、それ以外のオンラインでのコミュニケーションする場としてDiscordを使いました。

<反省>
様々な相談、会議をしたり、決まったことを共有したり。かなり活発に使われていました。
しかし、活発に使われすぎてチャンネルが無限に増え続けてしまったので、後から見返すものはwiki、テキストでの相談や連絡はSlack、音声通話やチャットはdiscord、といったように用途に分けてツールを使い分けたほうがいいと思います。

複数のツールを使うことによる情報の分散が問題になるようでしたらRedmine + Discord とかもありかもしれません。

3.2. Unity & C#

VRゲームコンテンツを作るにあたって、ゲームエンジンとしてUnity & C# を採用しました。

サークルでVR Ready PCを購入することは予算的に難しいです。VR機材はとてもありがたいことに先生方の協力によりお借りする事ができましたが、PCはそういうわけにもいかないので、個人がそれぞれ所有しているPCで開発しました。

その際、(生協で販売されてるような)普通のノートパソコンでも動作するUnityはありがたかったです。

開発中、VRのコントローラー入力とマウス入力を切り替えれるような設計することで、
強めのデスクトップPCを使ってVR部分を開発し、ほかはノートパソコンとマウスで開発が進められるようにできました。

少し具体的に言いますと、開発では「ものをつかむ・はなす」といった動作が実装されているUnityのアセットを使うことになりました。しかし、全員が全員VRデバイスを開発を行うときに持っているわけではありません。そこで、マウス操作でもある程度実装したスクリプトがうまく動くかを確かめられるようにしました。これは脱出VR班の一部メンバーの発案によって組み込まれたシステムで開発序盤中盤でかなり有用に働いたようにおもいます。

3.3. Git & Gitbucket , Trello

サークルで借りているサーバーでGitBucketを立ち上げ、プロジェクトをGit管理しました。
これがなかったらグループ開発は全く成り立っていなかったと思います。

タスク管理にはTrelloを使いました。これもグループ全体の把握には不可欠でした。

詳しくは
大学のサークルでGitbucketとGitを使ってゲームコンテンツをチーム開発をしてみた
へ。

3.4. Blender

草刈りVRのカマやじょうろはBlenderを用いてモデリングしました。

VR用ということでポリゴン数やマテリアル数は最小限にし、なるべく負荷をかけないようなモデルを心がけました。

Unityの素晴らしい点の一つは充実したAsset Storeであり、これによりモデリングしなくても3Dゲームを作る事ができますが、自分でモデリング出来るようになると見た目のオリジナリティが出せますし、なにより作れるゲームの幅が広がると思うので、ぜひ無料で始められるBlenderをおすすめします。
(Blender入門の参考にどうぞ : Blender2.8で超簡単なモデルを作ろう! )

また、今回は
モデル製作 → 修正依頼 → モデル修正
といったサイクルを回して行きたかったため、BlenderファイルもGitでバージョン管理しました。

これについては
Git Hookにより自動でBlenderのレンダリング・FBXエクスポートを行う
に紹介しています。

3.5. Hackmd.io

開発中盤で「あれ?ここってどうするんだっけな...」とならないように、開発者全員がゲーム内容を把握できるよう、企画書・仕様書・設計書はしっかり書くよう心がけました。

仕様書は複数人で時間をかけて作成、修正していく必要があったのですが、その際に共同編集が行えるHackmdはかなり便利でした。

Markdownで書け、画像も添付でき、同期のレスポンスも良く、議事録をリアルタイムに共有する際にも使いました。

3.6. VR機材(Oculus Rift/Rift S & HTC Vive)

使用機材については、機材をお借りする関係上少々複雑で、

開発初期 : Oculus Rift
開発後半 : Oculus Rift + Oculus Rift S ( +時々 HTC Vive)
展示当日 : Oculus Rift S + HTC Vive

といった感じになりました。

Oculus Rift S は外部センサ無しで、ケーブルも少なく、動ける範囲が広い上に設置の難易度が低くとても良かったです。

開発ではUnityにOpenVRという仕組みがあり、RiftとViveでどちらでも動くアプリケーションがつくれるので利用しました。これがどうやらInputManagerで扱う形式に対応しているようでした。
開発上文字列で入力インターフェースを指定するのはまずい気がしたので列挙体やButtonDown、ButtonUpを取れるように仲介するクラスを一つ用意しました。(参考: UnityでViveやWindowsMRのモーションコントローラーの入力を統合する方法 - Qiita)

4. 開発の流れ

4.1. 企画

夏休み前に学祭でなにか展示しようという話が上がり、話し合いを経てVRコンテンツを2つ展示することに決定しました。

メンバーで集まって、企画についてあーだこーだ考える「企画を練る会」を行い、
固まってきた企画に一人担当者を決めてその人に「企画書」を書いてもらい、検討会の後、コンペを行いました。

誰か一人に企画を考えてもらうのではなく、企画を練る会を開き複数人で企画を考えたことで一人の負担が減り、さらにみんなのモチベーションを高めることができました。
また、みんなで企画を考えることで、開発後半で「あれ?これってほんまに面白いんかな...」という嫌な疑問を抱かずに、「完成さえすれば面白いはず!」という気持ちで開発をすすめることができました。

また、企画書の文章を書くのは一人担当者を決めることで、企画決定までスピーディーに進めることができました。

4.2. 学習タスク

1年生や、今回初めてGit・Unityを触る人用に、先輩が学習資料をまとめて、後輩に学習してもらう「学習タスク」を設置しました。
Trelloでタスク化し、進捗が確認できるようにしました。

学習タスクの項目としては、

項目 説明
Unityの学習 シーン遷移、オブジェクトの出現制御(Instantiate)、スクリプト連携(GetComponent)周りを扱えるようにする。
C#の学習 クラスの定義や基礎的な命令、制御をかけるようにする。
Gitの学習 Gitの基本的なコマンドを学習する。fork,pull request,mergeの流れを学ぶ。
VRをUnityで扱うための学習 OVRを利用したUnityゲーム開発の環境構築およびスクリプティング方法を解説する文書を作成する。

といったものを用意しました。

<反省>
gitの学習については、Remoteやプルリクなどの概念の学習や、誤ったコミットを行った際の復旧方法についてもう少し学習が必要でした。メンバーが自分で調べてみたり「ここわかんないんですけど!」とわかってる人に聞くなどその場の機転に助けられた部分が多くあります。
これについては実際に小さなプロジェクト開発をしながら学ぶのが一番だと思うので、学祭前になにか試しに小さなプロジェクト活動をしてみるのが良いと思いました。

4.3. 仕様書作成

決定した2つの企画に対して1・2・3年生を割り振り、仕様書の作成を行いました。

今回、きっちり仕様書にまとめるよう心がけましたが、これにより開発中に「あれってどうするんだっけ?」という相談が減り、かなりスムーズに進められたと思います。

また、VR機材をお借りしている先生方に、どんな内容のものをどれくらいの規模感で開発しているかを伝えることができ、意見をもらうこともできたので、きっちり書き上げるようにしてよかったと思います。

4.4. 大まかに設計

仕様書をもとに @kimarian@koske1814 が中心となり設計を行いました。来年度はこのメンバーが参加できるかどうかわからないので大体どんな流れで設計したのかというのを示そうと思います。

流れとしては

  1. 機能列挙
  2. オブジェクト間で呼び出し合うインターフェースを大体決める
  3. クラス構成
  4. シーンに存在するオブジェクトの構成

を上から順にやり、その後適宜追加仕様と照らし合わせて修正やクラスの再分割等を行いました。

行った工夫として

  • できるだけ分担作業やその場でテストがしやすいようにあるスクリプトが依存するモジュールを減らした
  • 依存する場合はできるだけメソッドの呼び方などをわかりやすくした

を意識しました。そのほかにも依存するモジュールをインターフェースとして切り離しておいてそのインターフェースを実装したテスト用のクラスを定義してあげることでテストしやすくしました。

設計序盤まではある程度詳細な部分(メソッド内のデータの扱い方)等も書いていましたが、コア部分が終わった後はフィールドやメソッドはpublicなもののみ指定し、それらの大体の機能を書くというスタンスにしました。

設計書がなければ「ここのこんなモジュールでこんな風にいい感じにしてー」という風に口頭で説明したり、チャットツールで情報が分散して存在する状態になってしまいます。そうすると説明の齟齬があったり実装上の不明点が発生したときにほしい情報をとってくることが難しくなります。そういう点を考えると設計書をきちんと容易したのは正解だったと思います。

4.4. 開発

大体の開発では「設計書に記載した1クラスを実装してきてね」、「このリソース用意してね」「このシーン構成してね」といった形態でTrelloでタスクを発行しGitbucketからGitコマンドで対応リポジトリをクローン or プルして作業、出来上がったらプルリクエストという形態をとっていました。
前半は画像等のリソースもリポジトリに含んでいましたが、リソース等に関しては後半、UnityPackageに切り離して扱う等の工夫をしていました。

Gitbucket等の運用については 大学のサークルでGitbucketとGitを使ってゲームコンテンツをチーム開発をしてみた を参照していただけるとうれしいです。

両班大体同じような段階を踏んで開発が行われていて

  1. 設計書をもとに各クラスを実装する(ここは大体設計者はあまりコードには触れない)
  2. 各クラス・モジュールが出来上がってきたら設計者やコード全体を見渡している人が結合する
  3. 大体できたコンテンツを仕様の中心的な策定者が設計者と相談しながら調整する

という風になっていました。

@kimarian 的に成功だったのは3の仕様を決めた人がゲーム調整を行うという部分です。特に草刈りゲームに関してはスコアの伸び方について慎重にちゃんとした基準をもって調整するのはゲーム体験上非常に重要だったと思います。

しかし同時に改善の余地ありと思ったのがゲームバランスを制御するパラメータが散財していたことです。シーンにまたがって同一種類のパラメータを調整するのはかなりめんどくさかった気がします。設計の話に戻ってしまいますが、ゲームシーン全体にかかるパラメータは一か所で編集できる機能を持たせるべきでした。

4.5. 運営

学祭当日はVRコンテンツを2つと、小さな子どもさん向けにEye Trackerを用いた体験コンテンツを展示しました。

VR一つに 4m × 5m 程度の空間を設けて自由に動き回ってもらいました。HMDのケーブル持ち係を設けて、体験者がケーブルに引っかからないように気をつけました。

小学生以下の子供さんにはVRを体験してもらえないので、そういった方々向けにVR以外の展示物を用意したのは正解だったと思います。

<反省>
自由に動き回れるようにしたせいで、ケーブル持ち係の負担がかなり大きくなってしまいました。
また、体験者がくるくるその場で回転してしまうことも多く、ケーブルがねじれて痛みそうになってしまいました。

商業施設であればケーブルを上から吊るすこともできるでしょうが、学祭は教室を借りているだけなのでなかなかそういうわけにもいきません。
ケーブルのことも考慮したゲームデザインにする必要がありそうです。

あるいは、Oculus Questを採用するとか。(スペック的にかなりの最適化が必要そうではあります)

5. 来年に向けて

最後に、来年に向けての反省や改善点をあげていこうと思います。
個々の小さな反省点についてはこれまでに書いたので、全体的な改善方針について書いていきます。

5.1. 勉強会の整備

Unity、C#、Gitなどに全く触れたことがない人が開発に参加するには学ばなければならないことも多く、負荷が大きくなってしまいました。

そこで、学祭プロジェクト前に勉強会やプロジェクト開発の機会を設けるべきだと思いました。

GitやUnity、C#の勉強会を開き、さらに実際にGitを用いて少人数でプロジェクト開発をすると良いと思います。

2日~二週間程度でハッカソン的なのを開いてみるとかも面白そうです。

5.2. 開発フロー見直し

今回、「企画→仕様決定→設計→プロトタイプ→開発→テスト→リリース」

といったウォーターフォール型的開発になりましたが、これは開発期間が開発規模・リソースに対して短かったために、こうせざるを得なかった、というのが正直なところです。

より良いゲーム開発のためには、スパイラルモデルといった他の開発手法も検討すべきだと思います。

そのためにも、勉強会などの整備をして、開発期間をより効率良く使えるようにしたいです。

5.3. 技術的な調査を先行して行う

草刈りVRに関して開発中盤まで「これパフォーマンス的に大丈夫か・・・?」という大きな不安がありました。ある程度動くようになって不安は消え去りましたが、これはあんまりリーダーや設計者的には結構な重圧でした。

今後の開発ではそのコンテンツでどのような技術をつかうか?をきちんと分析し技術的な知見のある人が検証作業を行うべきだと考えます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VCI初心者100本ノック【survey】

途中ですけど時間が時間なのでupします。
記事書きながら迷走していて時間がなくなりました。継続して更新する所存

はじめに

VCIって素晴らしい。好きに持ち込んで出せるし、いろんなことをさせられる。
最近udonとかなんとか話が出始めましたが、これを一年近く前に運用開始しているんだから素晴らしい。でも、、、

めちゃめちゃ敷居が高い!!!
しかも今VCI作ってる人って、だいたいスキルのある人が多いし、
そんな人と同じレベルを求められても、、、、ぶっちゃけ困ります。
そもそも、どこを調べたらいいのかすらよくわからない。それでいてどんどんバージョンアップするので説明が何を指しているのかリアルタイムで追ってない人はさっぱりわからない。
やってみようにも実例を探すのも一苦労で、、、、(注1)

でもね、始めて見れば案外そこまで難しくない。
あれ?色々出来て簡単って最高かな?
VCIの最大の欠点はとっつきにくいことだと思うんです。
せっかく素晴らしいものなのにそのせいで作る人が少ないのはもったいない!ということで私がこういう物があればよかった(いいのに)というものをまとめようという試みです。

作るもの

基本的に周りの記事の寄せ集めです。題目にあるようにsurveyっぽいものです。その上で、こんなサンプル欲しかったんだけど、、、というものを自分で追加していこうと思います。

筆者のスペック

  • 仕事はモデリングとかゲームとは全然関係ない
  • もちろんunityやblenderなんかと関わりも経験も一切ない
  • さらに、仕事も大学も非情報系、経験無し
  • 3Dモデリングもやったことない
  • luaなんてやったことない
  • そもそもwindowsパソコンを家でいじり始めたのが今年の6月から
  • VCIは20個くらいは作ってる。取り込み数も10を超えるものも

こんなクソ雑魚でもできるんだぞ!!

お品書き

VCI

  1. 環境構築する
  2. TSOにアップロードする
    1. 公式
  3. cubeを表示する
  4. materialを設定する
  5. 拡縮できるようにする
  6. 拡縮の等方化をする
  7. 重力を設定する
  8. 止まらなくする
  9. emittionを設定する
  10. 子オブジェクトを設定する
  11. 座標をロックする
  12. コライダーを抜く
  13. コライダーを調整する
  14. istriggerを設定する
  15. 画像を入れる・テクスチャを貼る
  16. 文字を表示する
  17. unityでも日本語文字を表示する
  18. 装着可能にする
  19. コメントを読み取る
  20. VCI作成の環境構築をする
  21. パーティクルエフェクトを入れる
  22. 他のソフトで作ったモデルを入れる
  23. jointを設定する
  24. 背景VCIを作る
  25. VC内部で更新する
  26. ノーマルマップを入れる
  27. matcapを入れる
  28. 透明にする
  29. 透明cutoutする
  30. 持てなくする
  31. マテリアルについて理解する(unlit,standard,Mtoon)
  32. 処理の重さについて理解する
  33. 裏側にもテクスチャを貼る

スクリプト

  1. スクリプトの環境構築をする
    1. 公式
  2. luaスクリプトを入れてハロワする
    1. 公式
  3. サンプルを読んで勉強する
    1. 公式サンプル:割とたくさんあるようですが、書きかけみたいな記事も多いです。ここよりはマシです
  4. マテリアルの色を変える
  5. 処理の流れを理解する
  6. 初期化処理を行う
  7. update()
  8. updateALL()
  9. onGrab()
  10. onUnGrab()
  11. onUse()
  12. onUnuse()
  13. onTriggerEnter()
  14. onTriggerExit()
  15. onCollisionEnter()
  16. onCollisionExit()
  17. イベントの種類を理解する
  18. 所有権を理解する
  19. 所有者のみ処理をする
  20. _ALLを理解する
  21. クォータニオンを理解する
  22. オブジェクトを動かす
  23. 別のオブジェクトから操作する
  24. デバッガを使う
    1. 公式
  25. テクスチャを変更する
  26. UVスクロールを設定する
  27. エフェクトを再生する
  28. ぶつかったときの処理をする
  29. モジュールを使う
  30. 基本的な文法を確認する
  31. コントローラーに触覚を伝える(振動する)
  32. コメントを扱う
    1. 公式サンプル:コメントをすると落ちてくるタライ
  33. カメラを操作する
  34. 位置を取得する
  35. アニメーションを設定する

エフェクト

  1. 散らばるエフェクト
  2. 軌跡が出るエフェクト
  3. エフェクトの組み合わせ
  4. モデルにエフェクトを適用する
  5. エフェクトを入れ子にする

その他

  1. 情報源を確保する(twitter,wiki)
  2. バグ報告をする
  3. デバッグウィンドウを出す
  4. 描画のレベルを調整する
  5. VCIの歴史を把握する
  6. やりながらデバッグする
    1. 公式:動画です。

なんか作ってるうちにTIPSと混ざってきてる気がするなぁ、、もし充実してきたら分離して別に上げます。

注1:結構な頻度で人に聞くと、「昔はもっとひどかった」みたいなこと言われるんですが、すごいです、真似できないです。だから僕はすごくない人にもやってほしいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LeapMotionでLive2Dモデルの指を動かす

はじめに

Unity(C#)でLeapMotionから指の角度を取得してLive2Dモデルをリアルタイムに動かす方法

環境

Unity : 2019.2.2f1
Live2D SDK : 4.0-beta.2
LeapMotion SDK : 4.4.0
Live2Dモデル書き出しファイル : 以下画像
qiita1.png
今回使用した腕モデルはLive2Dイラストレーター・モデラーの鳥総帥様から許可を得て使用しています

準備

Live2D

公式チュートリアルページのSDKをインポートの通りにSDKとモデルをインポート

LeapMotion

Unity向けSDKからUnityPackageをインポート
ここからが問題なんですよね
大抵はLeapHandControllerプレハブをヒエラルキーに入れると書いてあるんですが、少なくとも私の環境ではインポートの問題か何回試してもスクリプトがmissingだったため使えませんでした。
image.png
なのでSDK付属のサンプルシーンからコピペします。
参考にしたのは以下の記述です。

Assets/LeapMotion/Core/Examples/Capsule Hands(Desktop)からLeapMotionControllerとHandModelsをコピペして自分のシーンに貼り付けます。
LeapMotion+Unityでグー・チョキ・パーを認識する より引用

実際のコード

このスクリプトをLive2Dモデルにアタッチします

MovingArm.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Leap;
using Live2D.Cubism.Core;
using Live2D.Cubism.Framework;

public class MoveArm : MonoBehaviour
{
    private CubismModel _model;
    private Controller controller;
    private Dictionary<Leap.Finger.FingerType, CubismParameter> modelfingers;

    // Start is called before the first frame update
    void Start()
    {
        controller = new Controller();
        _model = this.FindCubismModel();

        modelfingers = new Dictionary<Leap.Finger.FingerType, CubismParameter>();
        modelfingers.Add(Leap.Finger.FingerType.TYPE_INDEX, _model.Parameters[1]);
        modelfingers.Add(Leap.Finger.FingerType.TYPE_MIDDLE, _model.Parameters[2]);
        modelfingers.Add(Leap.Finger.FingerType.TYPE_RING, _model.Parameters[3]);
        modelfingers.Add(Leap.Finger.FingerType.TYPE_PINKY, _model.Parameters[4]);
        modelfingers.Add(Leap.Finger.FingerType.TYPE_THUMB, _model.Parameters[5]);
    }
    private void LateUpdate()
    {
        Frame frame = controller.Frame();

        if (frame.Hands.Count != 0)
        {
            List<Hand> hand = frame.Hands;
            var fingers = hand[0].Fingers;
            foreach (Finger finger in fingers)
            {
                if (finger.Type == Leap.Finger.FingerType.TYPE_UNKNOWN) continue;

                var angle = Mathf.PI - finger.Direction.AngleTo(hand[0].Direction);
                var param = EditParam(angle, 0f, Mathf.PI, -30, 30);

                if (finger.Type == Leap.Finger.FingerType.TYPE_THUMB)
                {
                    angle = finger.Direction.AngleTo(hand[0].PalmNormal);
                    param = EditParam(angle, 1.0f, 1.27f, -30, 30);
                    Debug.Log("THUMB " + angle + " " + param);
                }

                modelfingers[finger.Type].Value = param;
            }
        }
    }
    private float EditParam(float param,float leapmin,float leapmax, float modelmin,float modelmax)
    {
        return (param - leapmin) * ((modelmax - modelmin) / (leapmax - leapmin)) + modelmin;
    }
}

解説

using

LeapMotionの機能を使いたい→ using Leap;
Live2Dの機能を使いたい→ using Live2D.Cubism.Core;
using Live2D.Cubism.Framework;

modelfingers って辞書はなに

Fingerクラスに取得したFinger型オブジェクトの指の種類(親指・人差し指・中指・薬指・小指・不明)が取得できるFingerTypeがあるので、モデルの各指パラメータの順番に合わせて(例えば今回モデルの人差し指は2番目の位置にあるので0開始で_model.Parameters[1])各指の種類とモデルのパラメータオブジェクトを当てはめてる
image.png

angleってなに

finger.Directionで指の差す角度(Vector)
hand[0].Directionで手先の角度(Vector)
AngleTo()で各ベクトル間の角度を取得
今回モデルの動き(Angleパラメータ最大(30)で指伸ばし最小(-30)で指曲げ)とAngleToで取得できる数値が逆だったため180度(Mathf.PI)で反転してます

EditParam(angle, 1.0f, 1.27f, -30, 30);

すいません取得した値をログでひろってきて値の最大最小を直接入力するゴリ押しをしました
親指だけ曲がる角度が大きく違うので……
ここはもっと方法があると思います

最後

間違い、説明不足な点などありましたらコメントか編集リクエストをお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【エディタ拡張】リフレクションでprivateな変数も常時表示するDebugInspector【UIElements】

はじめに

本記事はgumi Inc. Advent Calendar 2019の22日目の記事です。

MonoBehaviourのprivateフィールドのクラスのインスタンスやプロパティの値の中身が
どうなっているのか確認したいときはよくあると思います。

たいていはDebug.Logを仕込んだり、ブレークポイントを張ってデバッグするのではないでしょうか。

コードをいじることなく、エディタ拡張ウィンドウで常に値が可視化されている状態にすれば、
デバッグなどで便利なのではないかと考え、今回作成してみました。

作成物

2019-12-22_16h41_55.png

使用方法

  • Window/DebugInspectorから開きます。
  • オブジェクトの選択状態が変わるとゲームオブジェクトにアタッチされたコンポーネントのpublicやprivateフィールドをツリービュー表示します。
  • ネームスペースにUnityEngineが含まれるものはツリーの子に表示されないように除外しています。

環境

  • Unity2019.2.5f1
  • Windows10

ソースコード

Gistにアップしました。
1ファイルのみです。

解説

今回実装して得た知見を解説してゆきます。

リフレクションTips

リフレクションに関してはC#リフレクションTIPS 55連発が大変参考になるかと思います。
いくつか補足的にTipsを書きます。

クラスかどうか

    if (node.GetType().IsClass)

構造体かどうか

    value.GetType().IsValueType

例:Vector3など

プリミティブかどうか

    value.GetType().IsPrimitive

例:int,floatなど

UnityEngineのクラスを除外する

        static readonly List<string> IgnoreNamespaces = new List<string>
        {
            "UnityEngine.",
        };

        var nodeNamespace = node.GetType().Namespace;
        if (nodeNamespace != null)
        {
            foreach (var ignore in IgnoreNamespaces)
            {
                if (nodeNamespace.Contains(ignore)) { return; }
            }
        }

UIElements Tips

Toolbar

ExpandAllボタン、CollapseAllボタンでツリービューの開閉をします。
2019-12-22_17h18_58.png
ToolbarButtonを使用することでこのような見た目のボタンを簡単に作成可能です。

    void BuildToolbarUI()
    {
        var toolbar = new Toolbar();
        var clearButton = new ToolbarButton(() =>
        {
            roots.Clear();
            Refresh();
        });

        var expandAllButton = new ToolbarButton(() => treeView.SetExpanded(roots.Select(x => x.Id).ToArray()));
        expandAllButton.text = "ExpandAll";
        toolbar.Add(expandAllButton);

        var collapseAllButton = new ToolbarButton(() => treeView.CollapseAll());
        collapseAllButton.text = "CollapseAll";
        toolbar.Add(collapseAllButton);

        clearButton.text = "Clear";
        toolbar.Add(clearButton);
        rootVisualElement.Add(toolbar);
    }

IMGUIContainer

TreeViewはIMGUIなので、UIElementsで使用するためにIMGUIContainerを使います。

        var treeContainer = new IMGUIContainer(() =>
        {
            var rect = EditorGUILayout.GetControlRect(false, Screen.height);
            treeView.OnGUI(rect);
            Repaint();
        }); 

おわりに

  • Dictionaryのバリューがクラスの場合の表示には対応できていません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity+ZenjectでCQRS実装メモ

はじめに

DDD(ドメイン駆動開発)で良く用いられるCQRS (Command Query Responsibility Separation)ですが、Unity+Zenjectを使ったわかりやすい実装例が見当たらなかったので、自分で作ってみました。筆者自身CQRSやDDDは初心者なので、もっと良い方法、例があればコメントいただけるとありがたいです。

参考資料

  1. CQRS – example of implementation by Paweł Filipek
  2. .NETのエンタープライズアプリケーションアーキテクチャ第2版 by ディノ エスポシト, アンドレア サルタレロ
  3. CQRS by Convey
  4. A Simple CQRS Pattern Using C# in .NET by JAMES STILL

実装環境

Unity2019.3.0f1とExtenject 9.1.0を使ってます。

CQRSとは?

参考資料2の第10章によると、

DDDの早期導入者が最も苦労したのは、ドメインのすべての側面に対処する単一のモデルを設計することでした。一般的に言えば、ソフトウェアシステムで実行されるアクションはどれも、クエリかコマンドのどちらかに分類されます。この場合のクエリは、システムの状態をいかなる方法でも変更せず、データを返すだけです。これに対してコマンドは、システムの状態を実際に変更します。そして、ステータスコードか確認応答を返すことがあったとしても、それ以外のデータは返しません。クエリとコマンドという2つのアクショングループが同じドメインモデルを使用することを強いられた場合、それらの間に存在する論理的な境界はぼやけてしまいます。このような理由により、ここ数年の間にコマンド/クエリ責務分離(CQRS)と呼ばれる新しいサポートアーキテクチャが登場しました。

平たく言えば、データの読み込みと書き込みを別の処理系にするということです。

CQRSでは、以下のようにドメイン層を2つに分けます(参考資料2 図10-1右)
image.png

つまり、コマンドでは4層構造を維持する一方、クエリではDTO(データ転送オブジェクト)を使い、アプリケーション層とドメイン層は使用しません。

コマンドに用いるインターフェースの準備

ここでは、参考資料1に従ってコマンドを実装してみます。最初に、インターフェースICommandを定義します。ここでは、下のように中身が空のマーカーインターフェースで定義します。

ICommand.cs
public interface ICommand
{
}

マーカーインタフェースは、コマンドの意図、つまりドメインモデルの中での役割を示すために用いられます。

次に、コマンドハンドラを定義します。

ICommandHandler.cs
public interface ICommandHandler
{
}

public interface ICommandHandler<T> : ICommandHandler where T : ICommand
{
    void Handle(T command);
}

この中で、一つのコマンドに対しては一つのハンドラだけを定義することに気を付けてください。

次に定義するのがコマンドバスです。 コマンドバスはプレゼンテーション層からのリクエストに応じて、必要なコマンドハンドラを選択する(通常は)シングルトンのクラスです。

ICommandBus.cs
public interface ICommandBus
{
    void Send<T>(T Command) where T : ICommand;
}

インタフェースの実装

これでインターフェースの準備はできたので、次は実装です。まず、コマンドバスからです。参考資料1をUnity+Zenject用に修整しました。

CommandBus.cs
using Zenject;

public class CommandsBus : ICommandBus
{
    private readonly DiContainer container;

    public CommandsBus(DiContainer container)
    {
        this.container = container;
    }

    public void Send<T>(T command) where T : ICommand
    {
        var handler = container.Resolve<ICommandHandler<T>>();
        handler.Handle(command);
    }
}

DiContainerを直接使うのはよろしくないとZenject公式こちらの記事も述べているのですが、今回はこの書き方しか思いつかなかったので、このままにします。

次はコマンドです。コマンドを実装する場合は、以下のことを守ります(参考資料3)

 - メンバ変数はimmutable
 - コマンドの名前は命令文にする

SetAvatarCommand.cs
public class SetAvatarCommand : ICommand
{
    public string Name { get; }

    public SetAvatarCommand(string name)
    {
        Name = name;
    }
}

そして、このコマンドに対応するコマンドハンドラを実装します。

SetAvatarCommandHandler.cs
using UnityEngine;
public class SetAvatarCommandHandler : ICommandHandler<SetAvatarCommand>
{
    public void Handle(SetAvatarCommand command)
    {
        Debug.Log("Set Avatar done:"+command.Name);
    }
}

最後に、ZenjectでコマンドハンドラとコマンドバスのBindを行って、依存関係を解決します。

CommandInstaller.cs
using Zenject;

public class CommandInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<ICommandHandler<SetAvatarCommand>>().To<SetAvatarCommandHandler>().AsCached();
        Container.Bind<ICommandBus>().To<CommandsBus>().AsCached();
    }
}

CommandInstaller.csは適当なゲームオブジェクトにアタッチし、Scene Contextと関連付けることを忘れずに。

image.png

これでコマンドに関する実装はできたので、以下のように確認します。これも、適当なゲームオブジェクトにアタッチしてください。

Program.cs
using UnityEngine;
using Zenject;

public class Program : MonoBehaviour
{
    [Inject] ICommandBus commandBus;

    void Start()
     {
        commandBus.Send(new SetAvatarCommand("Miku"));
    }
}

実行すると、以下のようにコンソールに表示されます。
image.png

イベントの処理

コマンドを実行すると、何らかの形でシステムの状態が変化するので、それを通知する手段としてイベントを用います(下図: CQRS and Event Sourcing as an antidote for problems with retrieving application statesより)。
image.png

この図にあるように、イベントの処理には、イベント、イベントハンドラ、イベントバスが用いられ、コマンドハンドラからイベントがイベントバスに送られて処理が行われます。

イベントのインターフェースはコマンドと似た形で作ります(参考資料1参照)。

IEvents.cs
public interface IEvents
{
}

IEventsHandler.cs
public interface IEventsHandler
{
}
public interface IEventsHandler<T> : IEventsHandler
    where T : IEvents
{
    void Handle(T events);
}
IEventsBus.cs
public interface IEventsBus
{
    void Publish<T>(T events) where T : IEvents;
}

イベントの実装もコマンドと似ていますが、バスの作り方が少し違います。一つのイベントに対して複数のハンドラが対応する可能性があるので、ResolveAllを使ってます。

EventBus.cs
using Zenject;

public class EventsBus : IEventsBus
{
    private readonly DiContainer container;

    public EventsBus(DiContainer container)
    {
        this.container = container;
    }

    public void Publish<T>(T events) where T : IEvents
    {
        var handlers = container.ResolveAll<IEventsHandler<T>>();
        handlers.ForEach(h => h.Handle(events));
    }
}

その動作確認のために、1つのイベントに対してイベントハンドラを2つ定義してみます

AvatarCreated.cs
public class AvatarCreated : IEvents
{
    public string Name { get; }
    public AvatarCreated(string name)
    {
        Name = name;
    }
}
EventHandlers.cs
using UnityEngine;
public class AvatarCreatedEventHandler : IEventsHandler<AvatarCreated>
{
    public void Handle(AvatarCreated command)
    {
        Debug.Log("avatar created:" + command.Name);
    }
}

public class AvatarIsMikuEventHandler : IEventsHandler<AvatarCreated>
{
    public void Handle(AvatarCreated command)
    {
        Debug.Log("created avatar was definitely miku");
    }
}

このAvatarCreatedイベントは、コマンドハンドラから起こすようSetAvatarCommandHandlerを変更します。

SetAvatarCommandHandler.cs
using UnityEngine;
public class SetAvatarCommandHandler : ICommandHandler<SetAvatarCommand>
{

    private readonly IEventsBus eventBus;
    public SetAvatarCommandHandler(IEventsBus eventBus)
    {
        this.eventBus = eventBus;
    }

    public void Handle(SetAvatarCommand command)
    {
        Debug.Log("Set Avatar done:" + command.Name);
        eventBus.Publish(new AvatarCreated(command.Name));
    }
}

イベントの依存関係もコマンドと同様に解決します。

EventsInstaller.cs
using Zenject;

public class EventsInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<IEventsHandler<AvatarCreated>>().To<AvatarCreatedEventHandler>().AsCached();
        Container.Bind<IEventsHandler<AvatarCreated>>().To<AvatarIsMikuEventHandler>().AsCached();
        Container.Bind<IEventsBus>().To<EventsBus>().AsCached();
    }
}

最後に、Scene ContextにEventsInstallerを追加します。
image.png

これで実行すると、コンソールの表示は以下のようになります。

image.png

一つのイベントで複数のハンドラが動作しているのが確認できます。

クエリの実装

クエリは、バスを経由せず、ハンドラだけで実装します。以下は参考資料4を基にした例です。とりあえず、データベースとして以下を用意します。

AvatarDatabase.cs
using System.Collections;
using System.Collections.Generic;

public static class AvatarDatabase
{
    public static List<Avatar> Avatars { get; }

    static AvatarDatabase()
    {
        Avatars = new List<Avatar>()
        {
            new Avatar {ID = 1, Name = "Miku"},
            new Avatar {ID = 2, Name = "Megu"},
        };
    }
}

このデータベースのエントリーであるAvatarは以下のように定義しておきます。

Avatar.cs
public class Avatar
{
    public int ID { get; set; }
    public string Name { get; set; }
}

更に、必要に応じてクエリに対するレスポンスの型も指定しておきます。

CommandResponse.cs
public class CommandResponse
{
    public bool Success { get; set; }
}

次に、クエリとクエリハンドラを定義します。

IQuery.cs
public interface IQuery<out T> { }
IQueryHandler.cs
public interface IQueryHandler<in TIn, out TOut> where TIn : IQuery<TOut>
{
    TOut Get();
}

これでインターフェースは準備できたので、ここからは実装です。クエリで名前を与えて、データベースから名前の一致するアバターを一つとりだすクエリを行うとします。クエリは以下の通りです。

AvatarSingleMatch.cs
public class AvatarSingleMatch : IQuery<Avatar> {

    public string name;

    public AvatarSingleMatch(string name)
    {
        this.name = name;
    }
}

ハンドラは以下の通りになります。コンストラクタを作るときにクエリを保存して、Getでデータベースでの検索を実行するイメージです。

AvatarSingleMatchHandler.cs
public class AvatarSingleMatchHandler : IQueryHandler<AvatarSingleMatch, Avatar>
{
     private readonly AvatarSingleMatch query;

    public AvatarSingleMatchHandler(AvatarSingleMatch query)
    {
        this.query = query;
    }

     public Avatar Get()
    {
        return AvatarDatabase.Avatars.Find(n => n.Name == query.name);
    }
}

クエリからハンドラを作るのは、Factoryクラスに任せます

QueryHandlerFactory.cs
public static class QueryHandlerFactory
{
    public static IQueryHandler<AvatarSingleMatch, Avatar> Build(AvatarSingleMatch query)
    {
        return new AvatarSingleMatchHandler(query);
    }
}

これで準備は完了したので、あとは実際にクエリを行うだけです。こちらのスクリプトをゲームオブジェクトにアタッチして実行すると、「Miku」を検索して「1」が返ってきます。

GetAvatar.cs
using UnityEngine;

public class GetAvatar : MonoBehaviour
{
    void Start()
    {
        var query = new AvatarSingleMatch("Miku");
        var handler = QueryHandlerFactory.Build(query);
        Debug.Log(handler.Get().ID);
    }
}

これで検索はできますが、今のままだとデータベースに名前の無い場合の処理ができないので、そのチェックのためのクエリとハンドラも追加します。

TryAvatarSingleMatch.cs
public class TryAvatarSingleMatch : IQuery<CommandResponse> {

    public string name;

    public TryAvatarSingleMatch(string name)
    {
        this.name = name;
    }
}
TryAvatarSingleMatchHandler.cs
public class TryAvatarSingleMatchHandler : IQueryHandler<TryAvatarSingleMatch, CommandResponse>
{
     private readonly TryAvatarSingleMatch query;

    public TryAvatarSingleMatchHandler(TryAvatarSingleMatch query)
    {
        this.query = query;
    }

     public CommandResponse Get()
    {
        var commandResponse = new CommandResponse();
        commandResponse.Success = AvatarDatabase.Avatars.Exists(n => n.Name == query.name);
        return commandResponse;
    }
}

Factoryにも追記します。

QueryHandlerFactory.cs
public static class QueryHandlerFactory
{
    public static IQueryHandler<AvatarSingleMatch, Avatar> Build(AvatarSingleMatch query)
    {
        return new AvatarSingleMatchHandler(query);
    }
    public static IQueryHandler<TryAvatarSingleMatch, CommandResponse> Build(TryAvatarSingleMatch query)
    {
        return new TryAvatarSingleMatchHandler(query);
    }
}

これを用いて、「Nao」というデータベースに存在しない名前を検索してみましょう。

GetAvatar.cs
using UnityEngine;

public class GetAvatar : MonoBehaviour
{
    void Start()
    {
        if (QueryHandlerFactory.Build(new TryAvatarSingleMatch("Nao")).Get().Success)
        {
            var handler = QueryHandlerFactory.Build(new AvatarSingleMatch("Nao"));
            Debug.Log(handler.Get().ID);
        }
        else
        {
            Debug.Log("not found");
        }
    }
}

「Nao」はデータベースに無いので"not found"とコンソールに出るはずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity Oculus Quest ハンドトラッキングで5本の指でインタラクションさせる

ハンドトラッキングがサポートされたUnity の Oculus Integration アセットがきて早速試しました。
ハンドトラッキングのサンプルシーン HansInteractionTrainScene を実行するとハンドトラッキングでのインタラクションを試すことができます。
試すとわかる通り、人差し指以外の指ではボタンが反応しません。
https://developer.oculus.com/documentation/quest/latest/concepts/unity-sf-handtracking/
こちらのドキュメントを読むと、InteractableToolsSDKDriverにアタッチされているInteractableToolsCreatorが設定を行うオブジェクトだそうで、サンプルシーンのInteractableToolsCreatorを見ると左右RayToolとFingerTipPokeToolIndexのみが設定されています。
sizeを6に設定し、ほかすべてのFingerTipPokeToolプレハブをアタッチすればOKです。
なお、親指用のFingerTipPokeToolはないため、他の指のFingerTipPokeToolプレハブを Ctrl + D で複製してFingerToFollowをThumbに設定すれば親指用のプレハブとなります。
これですべての指でインタラクションできるようになります。
あとHansプレハブがシーンに配置されていますがこれも必要で、配置されていないとFingerTipPokeToolが生成されません。
ですので、ご自身のシーンにフィンガーインタラクションのあるハンドトラッキングを配置するには、InteractableToolsSDKDriverとHansの2つのプレハブをペアで配置するようにしてください。
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】派生クラスにキャストで失敗

ベースクラスから派生クラスにキャストしようとしたときに、はまってしまったことをまとめておきます。

処理内容

メンバー変数を1つ保持しているベースクラス(CastBase.cs)と
1つ変数を追加した派生クラス(CastA.cs)を用意しました。

using UnityEngine;

/// <summary>
/// ベースクラス
/// </summary>
public class CastBase
{
    /// <summary>
    /// ベースクラスのメンバー変数
    /// </summary>
    public int BaseMemberNum;
}
/// <summary>
/// 派生クラス
/// </summary>
public class CastA : CastBase
{
    public int AMemberNum;
}

2つのクラスを使って、以下のような処理しました。(Unity上で動かしています。)
実行はTestMethod内のキャストで失敗し、null参照でエラーを吐いてしまいます。

using UnityEngine;

/// <summary>
/// メインクラス
/// </summary>
public class Main : MonoBehaviour
{
    void Start()
    {
        var castBase = new CastBase(); //ベースクラスでインスタンス化
        var castA = new CastBase();//ベースクラスでインスタンス化

        TestMethod(castBase, castA);
    }

    //ベースクラスを引数として受け渡すメソッド
    void TestMethod(CastBase castBase, CastBase castA)
    {
        Debug.Log(castBase.BaseMemberNum);

        var castedA = castA as CastA; //キャスト失敗。castedAにはnullが返ってきます。
        Debug.Log(castedA.BaseMemberNum); //null参照でエラー
        Debug.Log(castedA.AMemberNum);
    }
}

修正方法はcastAをインスタンス化するときにベースクラスではなく、
CastA型でインスタンス化する必要があります。
CastA型で宣言をし、あらかじめCastAの領域を確保しないとキャストに失敗してしまうようです。

using UnityEngine;

/// <summary>
/// メインクラス
/// </summary>
public class Main : MonoBehaviour
{
    void Start()
    {
        var castBase = new CastBase(); //ベースクラスでインスタンス化
        var castA = new CastA(); // CastA型でインスタンス化する

        TestMethod(castBase, castA);
    }

    void TestMethod(CastBase castBase, CastBase castA)
    {
        Debug.Log(castBase.BaseMemberNum);

        var castedA = castA as CastA;
        Debug.Log(castedA.BaseMemberNum);
        Debug.Log(castedA.AMemberNum);
    }
}

同じクラスに書かれると、なんてこともない問題ですが、実際に直面したのは、宣言とキャストする場所が離れてしまっていて、なかなか原因を発見することができませんでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでExcelを読み込む【ExcelDataReader】

環境

  • Unity 2019.2.13
  • NuGetForUnity 2.0.0

導入方法

1. NuGetをインストールする

https://github.com/GlitchEnzo/NuGetForUnity/releases

Screen Shot 2019-12-20 at 17.03.34.png

Screen Shot 2019-12-20 at 17.04.50.png

2. ExcelDataReaderをインストールする

Screen Shot 2019-12-20 at 17.06.09.png

Screen Shot 2019-12-20 at 17.08.22.png

ExcelのデータをSystem.Data.DataSetとして受け取るのなら、
ExcelDataReader.DataSetも一緒にインストールする

Screen Shot 2019-12-20 at 17.14.59.png

サンプルコード

https://github.com/tani-shi/unity-excel

using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using ExcelDataReader;
using UnityEngine;

public class Excel {
    public struct Sheet {
        public string name;
        public Cell[] cells;

        public Cell GetCell (int row, int column) {
            return cells.FirstOrDefault (c => c.row == row && c.column == column);
        }

        public Cell[] GetRowCells (int row) {
            return cells.Where (c => c.row == row).ToArray ();
        }

        public Cell[] GetColumnCells (int column) {
            return cells.Where (c => c.column == column).ToArray ();
        }
    }

    public struct Cell {
        public int row;
        public int column;
        public string value;
    }

    public string error {
        get {
            return _error;
        }
    }

    public Sheet[] Sheets {
        get {
            return _sheets;
        }
    }

    private Sheet[] _sheets = null;
    private string _error = string.Empty;
    private string _name = string.Empty;

    public static bool TryRead (string path, out Excel excel) {
        excel = Read (path);
        return string.IsNullOrEmpty (excel._error);
    }

    public static Excel Read (string path) {
        var excel = new Excel ();
        try {
            using (var stream = File.Open (path, FileMode.Open, FileAccess.Read)) {
                var reader = ExcelReaderFactory.CreateOpenXmlReader (stream);
                if (reader != null) {
                    excel.ParseDataSet (reader.AsDataSet ());
                }
            }
        } catch (Exception e) {
            Debug.LogError (e);
            excel._sheets = new Sheet[] { };
            excel._error = e.ToString ();
        }
        return excel;
    }

    public Sheet GetSheet (string name) {
        return _sheets.FirstOrDefault (s => s.name == name);
    }

    private void ParseDataSet (DataSet dataSet) {
        var sheetList = new List<Sheet> ();
        foreach (DataTable table in dataSet.Tables) {
            var sheet = new Sheet ();
            sheet.name = table.TableName;
            var cellList = new List<Cell> ();
            for (int row = 0; row < table.Rows.Count; row++) {
                for (int column = 0; column < table.Columns.Count; column++) {
                    var cell = new Cell ();
                    cell.row = row;
                    cell.column = column;
                    cell.value = table.Rows[row][column].ToString ();
                    cellList.Add (cell);
                }
            }
            sheet.cells = cellList.ToArray ();
            sheetList.Add (sheet);
        }
        _sheets = sheetList.ToArray ();
    }
}
using UnityEngine;
using UnityEditor;

public static class ExcelDemo {
    private const string kSampleExcelPath = "Xlsx/Sample.xlsx";

    [MenuItem("ExcelDemo/Read Sample Excel")]
    private static void ReadSampleExcel () {
        Excel excel;
        Debug.Log("EXCEL READING START");
        Debug.Log("==================");
        if (Excel.TryRead(kSampleExcelPath, out excel)) {
            foreach (var sheet in excel.Sheets) {
                foreach (var cell in sheet.cells) {
                    Debug.Log(string.Format("{0}:{1}:{2} value={3}", sheet.name, cell.row, cell.column, cell.value));
                }
            }
        }
        Debug.Log("==================");
        Debug.Log("EXCEL READING END");
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマホカメラで手のモーションを記録してUnityでピアノ演奏したかった

:santa:この記事は、 North Detail Advent Calendar 2019 の22日目の記事です:christmas_tree:

概要

やりたいこと

  1. ピアノ演奏中の手のモーションをスマホカメラで記録する
  2. Unityでアニメーションしてピアノ演奏する

技術的なこと

  1. google/mediapipeでハンドトラッキング用のiOSアプリをビルド
  2. iPhoneカメラで手のモーションを記録
  3. Blenderでアニメーション化 & ピアノオブジェクト作成
  4. Unityに取り込んで再生、指と鍵盤の当たり判定で音を鳴らす

成果物

※雑音が流れるのでご注意を

......はい(出落ち)

作業環境

・MacBook Pro (Retina, 15-inch, Mid 2014) : Mojave 10.14.6
・iPhoneXs : iOS 13.2.2

・Xcode : 11.3
・Blender : 2.79
・Unity : 2019.2.12

今回はiPhoneのカメラを利用しますが、AndroidやPCのカメラでも同じことができるはず
後述しますが、PCカメラ利用の場合はLinuxでGPUが利用できる環境推奨

Google / MediaPipeでモーション記録用のアプリをビルド

MediaPipeってなぁに?

MediaPipe is a framework for building multimodal (eg. video, audio, any time series data) applied ML pipelines. With MediaPipe, a perception pipeline can be built as a graph of modular components, including, for instance, inference models (e.g., TensorFlow, TFLite) and media processing functions.

MediaPipeは、マルチモーダル(ビデオ、オーディオ、時系列データなど)を適用したMLパイプラインを構築するためのフレームワークです。 MediaPipeを使用すると、たとえばパイプラインモデル(TensorFlow、TFLiteなど)やメディア処理機能など​​、モジュラーコンポーネントのグラフとして認識パイプラインを構築できます。

......なるほど:thinking:(?)

どうやら機械学習パイプライン(画像処理→モデル推論→描画 など)の構築や、
それをグラフとして視覚化できるフレームワークらしい

サンプルではTensorFlowやTensorFlow Liteなどを利用して、
顔認識や物体の識別、手のモーション取得(ハンドトラッキング)などができる

Linux/Win/Macで実行したり、iOS/Android用にアプリをビルドしたりできる

MediaPipeをインストール

0. PCで実行したい人向け

モバイルアプリのビルドではなく、PCで直接実行したい人はLinux環境(OS Xは含まない)推奨です

2019/12/11現在、MediaPipeではデスクトップ用サンプルはLinuxのみGPUサポートが対応しています
(Win/MacでもCPU実行モードはあるのですが私の環境では即座にフリーズしました......)

また、VMやdocker上のLinux環境ではホストGPUが使えないため、
MacならデュアルブートでOSインストールなどが必要です
(これを知らずにVirtualBoxのUbuntu上で作業していて土日まるまる潰しました......超頑張ればできないこともないとかなんとか?)

1. 事前準備

インストールガイドページInstalling on macOSを参考に進めていきます

  • Homebrewをインストール
  • XcodeCommand Line Toolsをインストール

  • Pythonバージョンの確認
    私の環境ではPython 2.7ではビルドが通らなかったため、下記サイトを参考にPython 3.7.5をインストールしました
    参考:pyenvを使ってMacにPythonの環境を構築する

  • "six"ライブラリをインストール

$ pip install --user future six
2. MediaPipeリポジトリをクローン
$ git clone https://github.com/google/mediapipe.git
$ cd mediapipe
3. Bazelをインストール

二通りの方法がありますが、今回はOption 1の方法でやります
※2019/12/11現在、MacではBazel 1.1.0より上のバージョンは対応していないため注意

# Bazel 1.1.0より上のバージョンがインストールされている場合はアンインストール
$ brew uninstall bazel

# Install Bazel 1.1.0
$ brew install https://raw.githubusercontent.com/bazelbuild/homebrew-tap/f8a0fa981bcb1784a0d0823e14867b844e94fb3d/Formula/bazel.rb
$ brew link bazel
4. OpenCVとFFmpegをインストール

こちらも二通りあるのでOption 1の方法でインストール

$ brew install opencv@3
5. Hello World desktop exampleを実行
$ export GLOG_logtostderr=1
$ bazel run --define MEDIAPIPE_DISABLE_GPU=1 \
    mediapipe/examples/desktop/hello_world:hello_world

# しばし待ったのち以下が表示されたらOK
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!

ここで私の環境ではPython2.7で以下のエラーが発生しました
同じ現象が起こった人は1. 事前準備を参考にPythonのバージョンを上げてみてください

ERROR: An error occurred during the fetch of repository 'local_config_git':
   Traceback (most recent call last):

モバイルアプリのビルド

今回は両手のモーションを取得するモバイルアプリをビルドしたいので、以下のサンプルを利用します
Multi-Hand Tracking (GPU)

※PCで直接実行したい場合は以下を利用してください
Multi-Hand Tracking on Desktop

Androidの場合

Androidの方が簡単にビルドできます
トラッキングには2Dと3Dモードの二通りあるので、3Dモードでビルドします

$ bazel build -c opt --config=android_arm64 --define 3D=true mediapipe/examples/android/src/java/com/google/mediapipe/apps/multihandtrackinggpu

かなり時間がかかるので待ちましょう
下記ディレクトリにapkファイルができるので実機にインストールすれば完了です!

mediapipe/bazel-bin/mediapipe/examples/android/src/java/com/google/mediapipe/apps/multihandtrackinggpu/multihandtrackinggpu.apk

iOSの場合

iOSの場合はこちらの設定がもうひと手間必要です
始めの方にあるXcodeやBazelのインストールは既に完了しているので不要です

(しかもこちらの方法でBazelをインストールすると最新バージョンを取得して動かなくなるという罠。間違えてインストールしてしまった場合は前述の方法でv1.1.0に置き換えてください)

1. Provisioning Profileの用意

AppleDeveloperProgram加入者はデベロッパサイトでProvisioning Profileを作成してダウンロードしてください

そうでない方は以下のサイトなどを参考に作成してください
参考:[Xcode][iOS] 有料ライセンスなしでの実機インストール 全工程解説!

Bundle Identifierを固有のものにしないとエラーが起きるので注意
どこかの誰かが使っているIDだとFailed to create provisioning profile.と怒られます
作成されたファイルは以下のpathにあります

~/Library/MobileDevice/Provisioning Profiles/

用意したファイルはprovisioning_profile.mobileprovisionにリネームして、
mediapipe/mediapipe/に配置します

2. BUILDファイルの修正

mediapipe/mediapipe/examples/ios/multihandtrackinggpu/BUILD45行目のbundle_idを、
Provisioning Profileで設定したBundle Identifierに変更

mediapipe/examples/ios/multihandtrackinggpu/BUILD:45
bundle_id = "com.google.mediapipe.MultiHandTrackingGpu",
         ↓
bundle_id = "hoge.fuga.piyo",
3. アプリのビルド

3Dモードでビルドします

$ bazel build -c opt --config=ios_arm64 --define 3D=true mediapipe/examples/ios/multihandtrackinggpu:MultiHandTrackingGpuApp

長いのでしばらく待ちます
ビルドが完了すると下記ディレクトリにipaファイルができます

bazel-bin/mediapipe/examples/ios/multihandtrackinggpu/
4. 実機にインストール

XcodeのWindow > Devices and Simulatorsから、
USB接続した実機デバイスを選択して、
画面下部の+ボタンからipaファイルをインストールすれば完了です!

手の動きをトラッキング

ビルドしたアプリを動かしてみる

hand_tracking.gif

おお!両手の動きがリアルタイムで反映されていますね

このときのiPhoneのプロセス状態を、USB接続したMacのコンソールアプリで確認してみましょう
スクリーンショット 2019-12-12 23.31.10.png
MultiHandTrackingGpuAppというプロセス名で何やらログが表示されています
hand[0]hand[1]の二つあり、それぞれ21個のLandmarkを持っています

先程のgifの片手のポイントの数が21なので、その座標のようですね
それぞれの手のログは約0.05秒毎に表示されているようです

ちなみに、ログ出力のコードは下記ファイルに記述されていました
mediapipe/mediapipe/examples/ios/multihandtrackinggpu/ViewController.mm : L177あたり

フロントカメラではなく背面カメラを使用したい場合は、同ファイルの以下を変更してビルドし直すと可能です

mediapipe/mediapipe/examples/ios/multihandtrackinggpu/ViewController.mm
// 100行目をYESからNOに
_renderer.mirrored = NO;

// 109行目を~Frontから~Backに
_cameraSource.cameraPosition = AVCaptureDevicePositionBack;

Unityで表示してみる

取得した座標をUnityの3D空間に浮かべてみます
スクリーンショット 2019-12-12 21.45.01.png
なぁにこれぇ?

ログをよくみるとz座標だけ数値が異様に大きいですね
倍率を変更してみます
スクリーンショット 2019-12-12 22.03.27.png
おっ!なんかそれっぽくなった!
向きと大きさを調整して線で繋いでみると・・・
スクリーンショット 2019-12-12 23.28.34.png
完全に手だコレ! \\ ٩( 'ω' )و//

あとはピアノを演奏しているところを撮影して座標を取得するだけです

Blenderでアニメーション化

Blenderとは?
  • 3Dモデル作成、アニメーション作成、レンダリングなどができる
  • 高機能・高品質、だけど高難度
  • オープンソースで無料で使える

最近 v2.8がリリースされ、UIや操作方法が変わり直感的に操作できるようになりました
参考サイトはv2.7xの方が多く、古いプロジェクトをインポートすると壊れてしまう場合もあるため今回はv2.79を使用します

「Blender アニメーション 作成」で検索すると、
手動でボーンを動かしてキーフレーム毎にポーズを設定する方法が主流のようです
参考:かんたんBlender講座 アニメーションの基本

この方法は単純なアニメーションだとよいのですが、
ミリ秒単位で複雑な動きをさせる場合は骨が折れます(ボーンだけに)

そのため今回はスクリプトで作成します

Pythonでスクリプト作成

BlenderではPythonスクリプトが実行できます
MediaPipeで取得した座標をCSVファイルにまとめて、スクリプトからアニメーション化します

1. 座標CSVファイルの作成

こんな感じでCSVファイルを作成しました
左から以下の値となってます

  • 手(0=左手、1=右手)
  • Landmark
  • 開始からの秒数
  • x座標
  • y座標
  • z座標

スクリーンショット 2019-12-22 3.21.21.png

2. Landmarkオブジェクトの作成

以下のSphereオブジェクトを追加
これをLandmarkポイントとして動かします

  • Left.000 ~ Left.020
  • Right.000 ~ Right.020
3. スクリプトの作成

Pythonスクリプトはこんな感じ

投稿時間を過ぎてしまったので説明は省略...:bow_tone1:
(倍率とかfpsとか適当。。。)

コメント追記しました

import bpy
import os

leftLandmarks = []
rightLandmarks = []
fps = 24

scale = (0.015,0.015,0.015)

# Landmarkオブジェクトを割当
for i in range(21):
    leftLandmarks.append(bpy.data.objects["Left.0" + str(i).zfill(2)])
    leftLandmarks[i].scale = scale

for i in range(21):
    rightLandmarks.append(bpy.data.objects["Right.0" + str(i).zfill(2)])
    rightLandmarks[i].scale = scale

# csvのあるディレクトリ
dirpath = "/Path/to/csv"
filename = "animation.csv"

filepath = os.path.join(dirpath, filename)

# csvを一行ずつ処理
with open(filepath, mode='rt', encoding='utf-8') as f:
    for line in f:
        params = line.split(",")

        # 左手 or 右手
        landmarks = leftLandmarks if params[0] == "0" else rightLandmarks

        # 座標を指定
        landmarks[int(params[1])].location = (float(params[3]),float(params[4]),float(params[5])/1000)
        # 指定した座標、フレームにキーフレームを設定
        landmarks[int(params[1])].keyframe_insert(data_path="location", frame=int(float(params[2])*fps))
4. スクリプトの実行

Editor Type「Text Editor」にスクリプトを貼り付けてRun Script!

5. Animationの再生

Editor Type「Timeline」から再生!

pianoplay.gif

勝ったな:smirk:(フラグ)

6. オブジェクトのエクスポート

File > Export > FBXなどでエクスポートすれば完了!
.blendファイルもUnityでそのまま読み込めます

Blenderでピアノ作成

白鍵と黒鍵をCubeで並べていきます
アニメーションと合わせるので、実物の鍵盤と同じサイズ比を意識します

スクリーンショット 2019-12-18 22.36.19.png

Unityに取り込んで再生

まずは用意した素材を取り込みます

  • アニメーションを適用した3Dモデル
  • ピアノオブジェクト

ピアノの設定

1. 音源Assetのインポート

ここにきて大誤算
ピアノの単音のフリー音源ってあまり無いんですね。。。

自前の電子ピアノから音源を作成しました:weary:

コーラス音源であれば、以下のUnity Assetを無料で利用できます
(同じ販売者から「グランドピアノ音源」も出品されているのですが$50もする・・・)

2. 鍵盤の動きを設定

鍵盤は以下の条件で動かします

  • 動きはy座標回転のみ(xyz位置とxz回転を制限)
  • 指との衝突で回転
  • 天井と床(鍵盤のみと衝突する)を作り、元の位置より上下しないようにする

鍵盤にはRigidbodyとBoxColliderで当たり判定を付与します

スクリーンショット 2019-12-22 5.00.18.png

3. 打鍵すると音が鳴るよう設定

以下の条件で音が鳴るようにしました

  • ハンドオブジェクトと当たり判定がある(OnCollisionStay関数)
  • かつ鍵盤の角度が一定以下

アニメーションの設定

  • アニメーションコントローラの作成

  • 3Dモデルに適用

サイズ比率の調整

手と鍵盤とのサイズ比を調整します
サイズ比や位置の調整用に別途アニメーションを作成しておくとよいかもしれません

今回調整用のアニメーションも作成していたのですが、
手がドリルしていて使えませんでした。。。

再生

雑音が流れました

まとめ

MediaPipe面白い技術ですね!

想像していたよりはきれいにアニメーションしてくれました
ちゃんと調整すればメロディーを奏でそうなムーブではある

簡単な曲 → 複雑な曲というシナリオだったのですが思い通りにいかず

本当は3Dモデルにアニメーションさせたかったのですが、
時間も技術も足りませんでした。。。

アプリのログからアニメーションを作成するという方法もスマートじゃないですね
Linuxで実行できたらもう少しやりようがあったかもしれません
MediaPipe開発者がUnityのサポートについて反応しているので今後に期待ですね

また先日、VRヘッドセットのOculus Questにハンドトラッキングが実装されました
Unity対応のSDKも今月中にリリースとのことなので、Oculus Questをお持ちの方は是非試してみてください
参考:「Oculus Quest」にハンドトラッキング機能実装。コントローラなしでメニューなどの操作が可能に

ちなみに演奏していた曲は「We Wish You a Merry Christmas」でした
よいクリスマスを✨:santa:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

禍つヴァールハイトで実装された雪シェーダーを紹介します

この記事は KLab Advent Calendar 2019 22日目の記事です。

はじめに

こんにちは、クライアントエンジニアのnorm81です。

禍つヴァールハイトではクリスマスイベントが絶賛開催中です。
今回はクリスマスイベントで実装された雪シェーダーを紹介します。
紹介する動作確認済み環境は Unity 2017.4.29f1 になります。

実装されたシェーダー

・地面に雪が積もるシェーダー
・天井に雪が積もるシェーダー
・スクリーンベースの雪が降るシェーダー

norm81が主に担当したのは地面に雪が積もるシェーダーですが
それ以外も各項で紹介します。

地面に雪が積もるシェーダー

snow1.gif
専用のマスクテスクチャを使ってマスクの白い部分から、
マテリアルに設定されたレンジ値を基に、順々に表示されています。
頂点ごとに接線と角度計算して屋根にも使っています。

マスクテクスチャ
マスクテクスチャ

プロパティ
snow1_prop.gif

関連箇所を抜き取ったシェーダー

snow1.shader
sampler2D _HeightmapTex; // マスクテクスチャ
half _Snow; // Snow Level: レンジ域
half _SnowOffset; // Snow Offset: レンジ基準値オフセット
half _SnowSmooth; // Snow Smooth: 境界値付近のアルファ値
half4 _SnowColor; // Snow Color: 対象を上書きする色
half4 _SnowDirection; // Snow Ditection: 角度差の基準とする法線

v2f vert(appdata_t v)
{
    v2f f;

    // 省略
    float3 worldSpaceNormal = mul(unity_ObjectToWorld, v.normal).xyz;
    worldSpaceNormal = normalize(worldSpaceNormal);

    // NOTE: fragで計算した方がレンジの反映精度が高いが、計算回数の省略目的とstep境界値付近のぼかしにv2fの補間を使っている。
    float theta = dot(worldSpaceNormal, _SnowDirection.xyz);
    float threshold = (0.5 - _Snow) * 2.0;
    f.snowalpha = _SnowColor.a * (1 - step(theta, threshold));
    // 省略
    return f;
}

half4 frag(v2f f) : COLOR
{
    half3 color = tex2D(_MainTex, f.uv);

    // 省略
    half3 Heightmap = tex2D(_HeightmapTex, f.uv.xy);
    float snowHeight = (1.0 - Heightmap.r) + _Snow + _SnowOffset;
    float snowAlpha = f.snowalpha * saturate((snowHeight - 1.0 + 0.0001) / (_SnowSmooth + 0.0001)); // 0.0001: Check division by zero.
    color = lerp(color, _SnowColor.rgb, snowAlpha * step(1.0, snowHeight));
    // 省略
    return half4(color, 1.0);
}

天井に雪が積もるシェーダー

snow2.gif
天井と記載しているのは、上面図のマスクテクスチャを使っているためです。
メリットとして可視範囲を上手くフォローできればテクスチャ最小1枚ですむので
メモリが節約できます。

マスクテクスチャ
マスクテクスチャ

スクリーンベースの雪が降るシェーダー

snow3.gif
下位互換のため、GPU インスタンシングではなく
必要なパーティクル数に分割した格子メッシュを用意します。

用意したメッシュの頂点入力について下記セマンテイクスを流用する形でベイクします。
・法線:座標変換前の基点座標
・カラー:アニメーション座標

マスクテクスチャ

あとは、自前シェーダーでパーティクル要件として座標変換・描画以外に
ビルボード処理などを行なっています。

おわりに

雪の足跡を作るとか、キャラクター形状に雪が積もるとか
某ステルスアクションゲームのような実装みたいなことをやってみたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SpriteAtlasの遅延バインディング完了を待ちインスタンスを作成する

初めに

そこそこ以上の開発規模でSpriteAtlasを扱う場合の記事です。
小規模な開発では遅延バインディングを使わない方が無難な機能です。
また、SpriteAtlasの挙動に関して少し情報が少ない気がしているので知っている限り情報を書きました。
手早く実装方法を知りたい場合は下から読んだ方良いかと思います。

UnityのSpriteAtlasについて

アトラスとは

複数のテクスチャを一つのテクスチャまとめる事をアトラス化と呼びます。
UnityではSpriteAtlasという形でアトラス化を行う機能を提供しています。
SpriteAtlasはUnityのアセットとして実体を持ち、アトラス化したいSpriteへの参照や圧縮設定などを持ちます。

SpriteAtlasのアトラス化対象のSpriteについて

SpriteAtlasのアトラス化対象のSpriteは実行時に自身のテクスチャではなくアトラス化されたSpriteAtlasのテクスチャを参照するようになります。
これによりuGUIのImageコンポーネントにSpriteをセットするだけで、同じSpriteAtlasの参照するSprite同士で描画のバッチング出来るようになります。

SpriteAtlasのアトラス化対象にするだけで、描画バッチング出来るのは非常に使い勝手がよいですが、アセットバンドル化する際は注意が必要です。
こちらに関して、テラシュールブログさんの記事で詳しく理由が書かれています。
【Unity】AssetBundleでSpriteAtlasを使用する際に知らないと起こすかもしれないトラブルと、その回避方法

Include in Buildについて

遅延バインディングに関わる最も重要な設定です。
ただし、自分が把握している限りこちらのパラメーターの有無はSpriteAtlasに対して一切影響を与えません。
SpriteAtlasのアトラス化対象のSpriteに対して影響を与えます。
Include in Buildの有無はSpriteが自身の参照するテクスチャをビルドに含むかどうかになります。

Include in Buildが無効だとSpriteはテクスチャを持ちません。
代わりに遅延バインディングによってSpriteの参照するテクスチャを後から設定することが出来ます。

遅延バインディングのメリット

個人的にこの機能を使う一番のメリットはアプリ内にSpriteを含みつつ、容量を食うテクスチャ部分をアセットバンドルにして外に出せる点です。
各ストアのダウンロードWifi制限の回避策としてUIのテクスチャをアセットバンドル化するのはよくあることですが、この方法だと一般的なUIの作り方をしたままポストプロセス等を利用せずにテクスチャのみを取り除けます。
また、Variant機能を利用する事で各端末が利用できる圧縮形式や画質設定に合わせて、使用するSpriteAtlasを変更できるのも利点です。

遅延バインディングを行う

遅延バインディングを行う方法を示した記事はいくつかありますが、ここで簡単に説明をします。
とりあえず公式のスクリプトリファレンスにざっくりとしたコードが乗っています。

using UnityEngine;
using UnityEngine.U2D;

public class AtlasLoader : MonoBehaviour
{
    void OnEnable()
    {
        SpriteAtlasManager.atlasRequested += RequestAtlas;
    }

    void OnDisable()
    {
        SpriteAtlasManager.atlasRequested -= RequestAtlas;
    }

    void RequestAtlas(string tag, System.Action<SpriteAtlas> callback)
    {
        var sa = Resources.Load<SpriteAtlas>(tag);
        callback(sa);
    }
}

遅延バインディング対象のSpriteが読み込まれるとSpriteAtlasManager.atlasRequestedに登録したイベントが実行されます。
tagにはそのSpriteが含まれるSpriteAtlasのアセット名が入っています。
後はtagに応じたSpriteAtlasを読み込みcallbackに渡すと完了です。

基本的に遅延バインディングのリクエストが発行されたらそのSystem.Action<SpriteAtlas>SpriteAtlasに渡すだけの非常に簡単な操作です。

非同期でSpriteAtlasを読む際の遅延バインディングの問題点

遅延バインディングは便利ですが、バインディング前に画面上に生成されて描画されると正しい見た目になりません。
そのためバインディングの完了を待つ必要があるのですが、バインディングのリクエストはSpriteがロードされなければ発生しません。
ここでSpriteAtlasを同期ロードで読み込めるのであれば描画の前に解決されるため表示は崩れません。
ですが、非同期で読む場合はバンディングが完了するまで対象のオブジェクトの描画を止める必要が出てきます。

であれば、待てばいいと思いますが待つ際に自分が少し手間取ったため記事にすることにしました。

SpriteAtlasの遅延バインディング完了を待ちインスタンスを作成する

先ほど挙げた問題点を解決するためのアプローチとしてSpriteを利用したPrefabをロード後、バインディングが完了した後にInstantiateでインスタンス化します。

アトラスのリクエストはSpriteのロードに紐づくためプレハブでも発火されます。
プレハブ状態では描画されないので、バンディングが完了してからGameObjectを作成するため表示が崩れないという寸法です。
誤差ですがuGUIのImageOnEnableで遅延バインディング用の処理を走らせるため、この方法だとバインド済みなので処理をスキップできます。

また、このアプローチはuGUIが遅延バインディングに対応していないUnityのバージョン(2017あたり)でも利用できます。

アトラスのリクエストを監視する

基本的にSpriteAtlasのリクエストを取り逃すと再度バインドする方法はありません。
そのため、基本的に静的クラスで待ち受けてリクエスト自体は必ず受け取れるようにするのが安全です。

このサンプルコードではRuntimeInitializeOnLoadMethodで起動時にSpriteAtlasManager.atlasRequestedにイベントを登録してリクエストをすべてキャッシュします。
実際のロード処理をAtlasRequestHandler.LoadMethodに設定した際にキャッシュしているリクエストが実行されます。
ロード処理が設定済みの場合は、リクエストが発火されるたびすぐにロード処理が呼び出されます。

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

public static class AtlasRequestHandler
{
    class Request
    {
        public string Tag;
        public Action<SpriteAtlas> Callback;
    }

    static List<Request> s_Requests = new List<Request>();

    public static bool HasRequest { get { return s_Requests.Count > 0; } }

    static Action<string, Action<SpriteAtlas>> s_LoadMethod;

    public static event Action<string, Action<SpriteAtlas>> LoadMethod
    {
        remove { s_LoadMethod -= value; }
        add
        {
            s_LoadMethod += value;
            //ここでキャッシュしてあるリクエストを受け取れる
            foreach (var req in s_Requests)
            {
                value(req.Tag, req.Callback);
            }
        }
    }

    static Queue<Action> s_ObserveComplete = new Queue<Action>();

    /// <summary>
    /// 可能な限り早くイベントを登録する
    /// </summary>
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void Init()
    {
        SpriteAtlasManager.atlasRequested += RequestAtlas;
    }

    static void RequestAtlas(string tag, Action<SpriteAtlas> callback)
    {
        var req = new Request
        {
            Tag = tag,
            Callback = Wrap(tag, callback),
        };
        s_Requests.Add(req);
        if (s_LoadMethod != null)
        {
            s_LoadMethod(req.Tag, req.Callback);
        }
    }

    /// <summary>
    /// ロード時にカウントを減らすためラップする
    /// </summary>
    static Action<SpriteAtlas> Wrap(string tag, Action<SpriteAtlas> callback)
    {
        return (SpriteAtlas atlas) =>
        {
            callback(atlas);
            s_Requests.RemoveAll(x => x.Tag == tag);
            if (s_Requests.Count == 0)
            {
                //大体、呼び出し先でやらかす奴がいるので本当はtry-catchした方がいい
                while (s_ObserveComplete.Count > 0) s_ObserveComplete.Dequeue()();
            }
        };
    }

    public static void ObserveComplete(Action action)
    {
        if (s_Requests.Count == 0)
        {
            action();
            return;
        }
        s_ObserveComplete.Enqueue(action);
    }

}

Prefabをロードしてアトラスのバインドを待つ

失敗したやり方

勘違いなのですが、以下のサンプルコードで動くと思っており、所属する開発チームには多大な迷惑をかけてしまっていました。

    void コールバックを繋げて呼ぶ場合の失敗例()
    {
        var op = Resources.LoadAsync<GameObject>("SampleUI");
        op.completed += (_) =>
        {
            //リクエストがあるか?
            if (AtlasRequestHandler.HasRequest)
            {
                //完了を待つ
                AtlasRequestHandler.ObserveComplete(() =>
                {
                    GameObject.Instantiate(op.asset);
                });
            }
            else
            {
                //リクエストがない場合ここに来る予定が必ず来る
                GameObject.Instantiate(op.asset);
            }
        };
    }

    IEnumerator コルーチンで待つ場合の失敗例()
    {
        var op = Resources.LoadAsync<GameObject>("SampleUI");
        yield return op;
        if (AtlasRequestHandler.HasRequest) //ここを消すと上手く
        {
            //リクエストを待つ
            yield return new WaitUntil(() => AtlasRequestHandler.HasRequest);
        }
        GameObject.Instantiate(op.asset);
    }

失敗した原因

失敗の原因はAtlasRequestHandler.HasRequestop.completedyield return opの直後にtrueになっていません。
つまり、SpriteAtlasManager.atlasRequestedの実行はロード直後よりも後に実行されていそうです。

詳細なイベント順を調査する

この手のイベントの実行順は大抵がOrder of Execution for Event Functionsのページに乗っていますが、今回に必要な情報は見つかりません。
であれば、ログを出して実行順を確認してみます。
Stack Trace Logging/Log/Fullに設定するとより詳細なスタックトレースが取得できます。
不要な部分を削ったのが以下のログになります。

yield return op
0x0000000141302E76 (Unity) StackWalker::GetCurrentCallstack
~~
~~
0x000000003D685BE6 (Mono JIT Code) [DebugBindings.gen.cs:103] UnityEngine.Debug:Log (object) 
0x000000003D685558 (Mono JIT Code) [TestScript.cs:139] TestScript/<Start>c__Iterator0:MoveNext () 
0x000000003D685031 (Mono JIT Code) [Coroutines.cs:17] UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr) 
0x000000003D6851AB (Mono JIT Code) (wrapper runtime-invoke) <Module>:runtime_invoke_void_object_intptr (object,intptr,intptr,intptr)
0x00007FF807A564D7 ((<unknown>)) 
0x00007FF8079A8A31 ((<unknown>)) 
0x0000000140A3A33C (Unity) scripting_method_invoke
0x0000000140A2D7CA (Unity) ScriptingInvocation::Invoke
0x00000001409EF166 (Unity) Coroutine::InvokeMoveNext
0x00000001409F759C (Unity) Coroutine::Run
0x000000014070214D (Unity) AsyncOperation::InvokeCoroutine
0x00000001408077E9 (Unity) PreloadManager::UpdatePreloadingSingleStep
0x0000000140807B3D (Unity) PreloadManager::UpdatePreloading
0x00000001407255CD (Unity) `InitPlayerLoopCallbacks'::`10'::EarlyUpdateUpdatePreloadingRegistrator::Forward
0x00000001407234D0 (Unity) PlayerLoop
0x00000001411
RequestAtlas
~~
~~
0x000000003D685BE6 (Mono JIT Code) [DebugBindings.gen.cs:103] UnityEngine.Debug:Log (object) 
0x000000003D68F7BC (Mono JIT Code) [TestScript.cs:49] AtlasRequestHandler:RequestAtlas (string,System.Action`1<UnityEngine.U2D.SpriteAtlas>) 
0x000000003D68F685 (Mono JIT Code) [SpriteAtlasBindings.gen.cs:22] UnityEngine.U2D.SpriteAtlasManager:RequestAtlas (string) 
0x00000000008DB0C5 (Mono JIT Code) (wrapper runtime-invoke) <Module>:runtime_invoke_bool_object (object,intptr,intptr,intptr)
0x00007FF807A564D7 ((<unknown>)) 
0x00007FF8079A8A31 ((<unknown>)) 
0x0000000140A3A33C (Unity) scripting_method_invoke
0x0000000140A2D7CA (Unity) ScriptingInvocation::Invoke
0x0000000140A2E98A (Unity) ScriptingInvocation::Invoke<bool>
0x0000000140371AEC (Unity) SpriteAtlasManager::RequestAtlasViaScript
0x000000014037326E (Unity) `SpriteAtlasManager::SpriteAtlasManager'::`2'::EarlyUpdateSpriteAtlasManagerUpdateRegistrator::Forward
0x00000001407235B0 (Unity) PlayerLoop
0x00000001411EE685 (Unity) PlayerLoopController::UpdateScene
0x00000001411F8BF7 (Unity) PlayerLoopController::EnterPlayMode
0x00000001411F94B6 (Unity) PlayerLoopController::SetIsPlaying
0x00000001

注目すべきは以下の二つで、これはPlayerLoopと呼ばれるUnityのライフサイクルで実行されている処理になります。

`InitPlayerLoopCallbacks'::`10'::EarlyUpdateUpdatePreloadingRegistrator::Forward
`SpriteAtlasManager::SpriteAtlasManager'::`2'::EarlyUpdateSpriteAtlasManagerUpdateRegistrator::Forward

PlayerLoopの実行順はテラシュールブログさんの記事で確認できます。
【Unity】Unity 2018のPlayerLoopで、Unityが毎フレーム呼ぶ処理を無効にしたり、Update"前"に独自の処理を追加したり

まとめるとyield return Resources.LoadAsync("");Resources.LoadAsync("").completedUpdatePreloadingのタイミングで呼び出されますが、SpriteAtlasManagerUpdateが実行されていないため、遅延バインディングすべき対象かこの時点で判断できません。
そのため、常にバインディング済みとして処理されていました。

成功するやり方

SpriteAtlasManagerUpdateの実行順はEarlyUpdate=Updateサイクルの前のため、Updateサイクルの中もしくはその後にバインディング処理中か判断すれば問題ありません。

実装

現状、一番シンプルに書けるのはおそらくコルーチンを利用した方法です。
WaitUtilUpdateLateUpdateの間ぐらいで評価されるので、その時にはSpriteAtlasManagerUpdateが実行済みになっています。
その他の方法としてop.isDoneMonoBehaviourのUpdateで監視するのも手です。

    IEnumerator コルーチンで待つ場合()
    {
        var op = Resources.LoadAsync<GameObject>("SampleUI");
        yield return op;
        //リクエストを待つ(このWaitUntilはキャッシュ出来るのでstatic変数に置く方がいい)
        yield return new WaitUntil(() => AtlasRequestHandler.HasRequest);
        GameObject.Instantiate(op.asset);
    }

長々と書いた割に結論は短くなりました。
コルーチン以外だと、基本的にUpdate監視になり泥臭く個々のアプリケーションよりに書いた方がいいため省いています。
その他のアプローチとしてPlayerLoopSpriteAtlasManagerUpdateの後に独自の監視用の機能を追加する案を考えましたが、やりたいことに対して規模が大きいため基本的にWaitUntilで問題ないと考えています。

どうでもいいこと

SpriteAtlasのinspectorの項目のコードを見てみるとbindAsDefaultとなっているので、イメージ的にはSpriteAtlasのテクスチャ実体をSpriteに対してバインドするか、どうかのフラグなのかと自分は勝手に思っています。

後、この件を調査して思ったがコルーチンで待った時の実行順は少しややこしい

var op = Resources.LoadAsync("asset");
//isDoneになるタイミングもUpdatePreloadingなので、実はこっちの方が下よりも速く呼ばれるはず
yield return op;
//nullの場合、UpdateとLateUpdateの間にMoveNextが呼び出される
while(!op.isDone) yield return null;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ついにでたOculusQuestハンドトラッキング×VRMでアバター空間満喫してみた

はじめに

楽しみに待っていたOculusQuest ハンドトラッキング対応版 OculusIntegrationが公開されたので、
早速どんなのか試してみました。
とにかく早くQuest触りたいという欲求に忠実に品質度外視・検証速度重視/QCDのD偏重した記録をまとめたものになります。
それゆえに内容は今後修正・加筆されることがあります。

検証環境

  • Unity 2018.4.14f1
  • macOS Catalina Version 10.15
  • Oculus Quest Version12
  • Oculus Integration Version12.0
  • FinalIK Version19.0

組込手順(概要)

  • OculusDeveloper公式のハンドトラッキングのページにそって組み込みする https://developer.oculus.com/documentation/quest/latest/concepts/unity-handtracking/ (Google翻訳使うとだいたい読める日本語になるので手順に沿って進める)
  • OVRCameraRigのCenterEyeAnchor,LeftHandAnchor,RightHandAnchorをFinalIKのSpine/HeadTarget,Spine/Pelvis,LeftArm/Target,RightArm/Targetにそれぞれ設定
  • OVRLipSyncをVRMのVRM Blend Shape ProxyのAIUEOに接続
  • 不定期に瞬きするスクリプトをVRM Blend Shape ProxyのBLINKに接続
  • OVRHandの指状態を手に反映
    • ここだけズルした。VRMの手boneのスケールを0にして非表示にしつつ、OVRHandの手をそのまま使用。

作例

VRMモデル動かしてみた。


  • OVRHand Behaviourから取得できる各種値をそのままデバッグ表示してみた
    • 各指の閉じ具合、各指のピンチon/off、精度High/Lowが取得できる。
    • 指boneのtransformも使えるので個別の関節状態はそちら参照することなるかと。

  • おまけ: ハンドトラッキングしつつ自前計算Particleやってみたもの, 5000triangles
    • 全指個別で粒子触れるようにしたかったのだけれど、全く速度意識しないと重くて使い物にならなかったので省略。

所感

  • ハンドトラッキングの認識精度わりとよい
  • とはいえ手・指を入力装置として使う場合のUIUXの考えかた自体が未成熟なのでそこを今後詰めていく。
    • ユーザーの意図的操作か・無意識動作かをうまく切り分ける仕組みは必要。
  • 検知できる指の特性
    • 手の認識される範囲が結構広くて、下ろしたつもりの手指でピンチ操作した扱いになることがある。
      • 手の高さと組み合わせて、意図的に手を上げての操作のみ対象にしたほうがよいのかも。
    • 右手指で2進数作ると2,6あたりの認識が苦手っぽい。(普通にやる姿勢ではないだろうから実用充分)
    • 人差し指中指・薬指小指でVの字作るのはできた。

謝辞

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OculusQuestのハンドトラッキングについて色々調べてみた

初めに

先日OculusIntegrationのアップデートがされ、QuestのハンドトラッキングがUnityでも試せるようになったので、色々調べてみました。

Oculus公式も言ってますが、ハンドトラッキング機能は現在developer preview版となっているので、本リリースの時にはこの記事が役に立たなくなってる可能性もありますが悪しからず

検証環境

  • Unity2019.1.14f1
  • OculusIntegration ver 12.0
  • OculusQuest ver 12.0
  • Windouws10

余談ですが、Integraitonはver1.43からver12.0に一気に上がりました。
おそらく、Questのバージョン番号と合わせるためかと

公式リファレンス

https://developer.oculus.com/documentation/quest/latest/concepts/unity-handtracking/
これみたら大体書いてあります。

以降、リファレンスの主だった部分をまとめつつ足りない部分を補ったり、試してみての所感を書いていきます。

公式サンプル

  • Oculus/SampleFramework/Usage/HandInteractionTrainScene
  • Oculus/VR/Scenes/HandTest

などのシーンは参考になると思うので、一度見ておくといいと思います。

また、デバック用にOVRHandTest.csが用意されており、今回紹介した諸々の値を見ることが出来ます。

とりあえず動かしてみる

OVRCameraRigのHandAnchor以下にOVRHandPrefabを入れる
image.png
なお、OVRManager/HandTrackingSuppportがControllersOnlyの時は動作しないので、切り替えてあげる必要があります(自分の環境ではサンプルのHandTestシーンではオフになってました)
OVRManager.PNG

OVRHandPrefabの各種設定

  • OVRHand
    • HandType:対応する手を設定
  • OVRMesh
    • MeshType:同上
  • OVRSkelton
    • SkeltonType:同上
    • UpdateRootPose:オンにするとOVRCameraRigとの位置の依存性が無くなる。普通に使う分にはオフのままでいいでしょう
    • UpdateRootScale:現実の手の大きさから、表示される手のモデルの大きさを補正する
    • EnablePhysicsCapsules:コライダーを手につける(Hand_Thumb1_CapsuleColliderとか、Hand_Pinky3_CapsuleColliderなど結構細かくコライダーが分けられている模様)
  • OVRSkeltonRenderer:デフォルトではオフになっているが、オンにすることで手の骨格が表示される

OVRHand

よく使いそうな関数、パラメータをピックアップしていきます

GetFingerIsPinching() と GetFingerPinchStrength()

読んで字のごとくですが、引数として渡した指がピンチしているか否かのbool値とfloat値を返してくれます。
引数には、Thumb, Index, Middle, Ring, Pinky が設定できる

var hand = GetComponent<OVRHand>();

bool isIndexPinching = hand.GetFingerIsPinching(OVRHand.HandFinger.Index);
float  middlePinchStrength = hand.GetFingerPinchStrength(OVRHand.HandFinger.Middle);

Thumb(親指)を指定した場合には、親指と任意の指においてのピンチを見るようです(おそらく親指の先端と一番近い指が選ばれている)

IsTracked

手のトラッキングがとれているかどうか

HandConfidence

トラッキングの信用性
HandConfidence.HighとHandConfidence.Lowがあり、手を高速で動かしたり重ねたりするとLowになりました。

手が重なったときに消える件について

これはOculusホーム画面でも同様の処理が行われてましたが、両手が重なったときには手の表示が消えます。
実際には手の表示が消えていても手の座標等の情報自体は生きている(かなり汚いですが)ので、拍手とかの判定くらいには使えそうです。

OVRMeshRenderer.csにて、トラッキングの精度が下がってるとき(IsDataValid && IsDateHighConfidence == false)には手のメッシュを消すという処理が行われてようなので、これをごにょごにょすれば両手が重なったときにも表示を残せそうです。

・・・が、ハンドトラッキングがまだテスト版で、本来はアクセスすることを想定していない部分を弄ることになるのでよほどのことがない限りおすすめしません。

自分も試しにやってみたんですが、カメラ側の手はほとんどぶれませんが、隠れている方の手は結構飛ぶのでメッシュ消したくなるのもわかる感じでした。

各指のTransform情報は取れてるので、試しに各座標にSphereを生成したらこんな感じになりました。

用途によりますが、拍手くらいならとれそうな感じなので割と実用圏内くらいにはトラッキング出来てる気がします。

ハンドジェスチャーについて

例のOculusマークのハンドジェスチャーはOVRHand.csのIsSystemGestureInProgressでとれるようです。
内部的には手のひらがカメラ側を向いたときに人差し指のピンチを見ているらしい。

基本的なハンドジェスチャーはOculus側で用意してくれてないかな~とか思ったのですが、自分の調べた範囲ではなさそうなので自作する必要がありそうです。
といってもあまり凝ったものでなければ、前述のGetFingerIsPinching() と GetFingerPinchStrength()を使えば簡単に実装できそうです。

とりあえずグー、チョキ、パーを作ったので載せときます。閾値はかなり適当に決めたので調整しつつ使ってもらえればと思います。

public bool Guu
    {
        get
        {
            //薬指と小指は値が0になることが多いのであきらめた
            return (hand.GetFingerPinchStrength(OVRHand.HandFinger.Middle) >= 0.5f &&
                    hand.GetFingerPinchStrength(OVRHand.HandFinger.Index) >= 0.1f);
        }
    }

    public bool Choki
    {
        get
        {
            return ((hand.GetFingerPinchStrength(OVRHand.HandFinger.Pinky) >=0.05f) && (hand.GetFingerPinchStrength(OVRHand.HandFinger.Ring) >= 0.2f));
        }
    }

    public bool Paa
    {
        get
        {
            return (hand.GetFingerPinchStrength(OVRHand.HandFinger.Thumb) <= 0.01f);
        }
    }

void Update()
    {

        if (Guu)
        {
            Debug.Log("GUU");
        }
        else if (Choki)
        {
            Debug.Log("CHOKI");
        }
        else if (Paa)
        {
            Debug.Log("PAA");
        }
        else
        {
            Debug.Log("NO SIGN");
        }

    }

中指と人差し指は割ときれいな値をとれるんですが、薬指小指は若干精度悪い印象でした。

また、グーの判定ですが、実際にグーポーズすればわかると思いますが、指の先端ではなく各指の第1,2関節と親指の先端が交わるため、GetFingerPinchStrength()では満足な値が得られません。

なので、より精度を求める場合には、各指関節の値をとってそれと判定を行った方がいい気がします。(OVRSkelton.Bonesを使って指のボーン情報をとることが出来る)

今回は記事が長くなりそうなので、またの機会にハンドジェスチャーについては書こうと思います。

その他細かな点

  • ハンドトラッキングするためには、Questにビルドする前にQuestのホーム画面の設定で「手を使用」に切り変える必要がある。(アプリ内で切り替える方法があるかは現在未検証)
  • コントローラーと手を同時にトラックすることはできない(今のところは)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む