20201118のUnityに関する記事は11件です。

NavMeshAgentを使いたいが、AI用のオブジェクトが空中からスタートするからエラーになってしまうとき

この記事でできること

  • とりいそぎ、空中からスタートしなくてもよいのであれば、AI用のオブジェクトがエラーを出さずにNavMeshAgentとして機能できるようになる。

前提

  • あえて空中からスタートしなくても良い場合。

やり方

  • 地面に近い場所にtransform.positionを変更する。

「地面に近い」場所が分からない場合

  • 実行後、AI用のオブジェクトは地面に接しているはずなので、そこでコピーコンポーネントを行う。
  • 実行を終了して、AI用のオブジェクトのtrnasformコンポーネントで、ペーストコンポーネントバリューを行う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

型の違うReactivePropertyをCombineLatestしたい

型の違うReactiveProperty

private StringReactiveProperty sentence = new StringReactiveProperty(string.Empty);
private BoolReactiveProperty flag = new BoolReactiveProperty(false);

どちらかに何かあったときに、両方の値を参照できるようにしたい。

普通にはCombineLatestできない

sentence.CombineLatest(flag).Subscribe((s, f) =>
  {
    // ERR
  }).AddTo(this);

Tupleで合成するといける

sentence.CombineLatest(flag, Tuple.Create).Subscribe(tuple =>
  {
    var (sentence, flag) = tuple;
    // OK
  }).AddTo(this);

Zip, ZipLatestはいける
Merge, Ambは無理

両方の変数が参照できる位置にあるなら、sentenceflagに同じ処理をSubscribeするのが楽でいいのかも。

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

unityで物理エンジンを使わず当たり判定をした話 その①-2

前回からの焼きまわしです

前回はこちら unityで物理エンジンを使わず当たり判定をした話 その①
本稿ではまだ範囲検索は書いてません。
あくまで、①の最適化です。
ビット演算を主にしたコアな内容に成ってます。

次に書く予定の②で範囲検索を模索します。

前回の記事との違い

前回、8ボクセルに分割したモートンオーダーを使った、空間分割について着目しました。
次回以降検索で負荷が掛る事が予想されます。
おさらいをしながら、今のうちに前回の記事の内容を最適化します

言葉

  • 次元
    • 通常次元はユークリッド空間における概念で語られます
    • ゲームにおいても0次元(点)~3次元(xyz軸)等でデザインされます。
    • 本稿においては、単に次元と指す場合はモートンオーダーにおける、階数を指す事とします。
    • 2×2×2の立方が一つの単位(3次元)になります。
    • 本稿ではプログラミングの効率化の為に更に3倍したものを単次元とします。

 例)3次元(2*2*2)
image.png
 例)6次元(4*4*4)
image.png
 例)9次元(16*16*16)
image.png

  • グリッド
     最小のグリッドの単位を本稿では一辺0.01としています。
    グリッドの大きさは次元によって大小します。
    image.png

  • キー
     単にキーと指す場合はモートンオーダーにおける3次元を一つの数値に変換した物(モーザー数列)とします。

  • モートンオーダー
     長いのでzオーダーと言います。

物体が収まる最小次元の選定

一部前回の記事とかぶりますが、前回の部分も最適化も兼ねます。

  • ある物体のサイズがおさまる次元数
    • 以下の黄色丸の半径は0.03です。紫の収まる次元を求めます。

image.png

  • 半径を2倍した直径(0.06) を100倍して整数にします。端数は切り捨てます。この切り捨て行為が グリッド化になります。
  • 例)0.065468は6になり、0.69999も同様の6に丸められ同グリッドに収まると言う風に考えて下さい。
    更に6は何の2の累乗範囲内に収まるか?と考えます。
    2の累乗と言えば2進数ですね!
    で、6は二進数で表すと0110です。
    見た感じ、6の一桁上だと、必ず6が収まりそうですね
    10進数でいうと、99は100に収まるのと同じ考えです。

  • 一番左のビット(MSBと言います)が何番目かを求めます。

        private int  Msb32Bit(int v) {
            if (v == 0) return -1;
            v |= (v >> 1);
            v |= (v >> 2);
            v |= (v >> 4);
            v |= (v >> 8);
            v |= (v >> 16);
            return Count32Bit(v) - 1;
        }
        private int Count32Bit(int v) {
            var count = (v & 0x55555555) + ((v >> 1) & 0x55555555);
            count = (count & 0x33333333) + ((count >> 2) & 0x33333333);
            count = (count & 0x0f0f0f0f) + ((count >> 4) & 0x0f0f0f0f);
            count = (count & 0x00ff00ff) + ((count >> 8) & 0x00ff00ff);
            return (count & 0x0000ffff) + ((count >> 16) & 0x0000ffff);
        }
  • 6だと2(0番目から数えます)と言う数値が得られますね。更に+1します。
    • これによって、9次元(本稿の便宜上3倍して9次元にしてます。)には収まりそうだ!と分かります。
    • 9次元と言うとグリッドが8*8*8なので、0.08 * 0.08 * 0.08の範囲内に0.06直径の物体が収まって居る事が分かりますね!
算出式
        private int GetLvFromSize(float hsize){
            var norm = hsize * 2 * 100;
            var msb = Msb32Bit((int)norm) + 1;
            return msb * 3;
        }

コーナーの算出

前回の記事だと、位置から8点それぞれ計算して割り出していました。
ですが、これは無駄で、位置とサイズで、どのボクセルが必要か求まります。

  • 位置とサイズから、左上の角と右下の角の2点コーナーポイントを算出します。
        private void Get3DCorner(float3 xyz,float hsize,out ulong start ,out ulong end){

            xyz += 10000;//座標にマイナスが発生しないようオフセット+10000

            var negative = (xyz - hsize) * 100;//offset 最小ボクセル0.01の正規化
            var positive = (xyz + hsize) * 100;//offset 最小ボクセル0.01の正規化
            start = BitSeparateFor3D((ulong)negative.x) | BitSeparateFor3D((ulong)negative.y)<<1 | BitSeparateFor3D((ulong)negative.z)<<2;
            end = BitSeparateFor3D((ulong)positive.x) | BitSeparateFor3D((ulong)positive.y)<<1 | BitSeparateFor3D((ulong)positive.z)<<2;
        }

        private ulong BitSeparateFor3D(ulong n )
        {
            var s = n;
            s = ( s | s<<32 ) & 0xffff00000000ffff;
            s = ( s | s<<16 ) & 0x00ff0000ff0000ff;
            s = ( s | s<<8 )  & 0xf00f00f00f00f00f;
            s = ( s | s<<4 )  & 0x30c30c30c30c30c3;
            s = ( s | s<<2 )  & 0x9249249249249249;
            return s;
        }

次元の端数

ここから更に少し難しくなります。

  • 単次元は2×2×2です。
    前述の通り2D、3Dの世界とは違って、zオーダーで言うところの単次元は0~1×0~1×0~1の8つの立方体で構成されています。
    さらにキー変換するとzyx一組、3ビットで表現されます。
    zyxで一組と言うと、xが欠けたzyだけ(2ビット)の世界はどうなるのか?と言うことを考えます。

  • 図示するとこんな形になります。
    image.png

  • yxが欠けたZだけの世界はどうなのかと言うと、こうなります。

image.png

  • これを次元の端数とみなすと、例えば101があったとして、こう言えます。
    1は0.1次元、10は0.2次元、101は0.3次元=1次元
    こう見ると、端数を三つづつなので、端数を整数で扱いやすいよう予め3の倍数しておきます。

  • 具体的には
    1が1次元で10は2次元で101は3次元となります。
    こうして、1ビット分が1次元に成りました!
    3ビット一組3次元で2×2×2の単次元が出来るみたいなイメージですね。

端数次元を踏まえどのボクセルに収まるかを算出

前述したコーナーポイントから、物体が収まる次元数(図の赤枠の大きさ)を求め、そこから更に2点間が入る最適な次元を求めます。
イメージとしてはこんな感じです

  • 緑色の得たポイントを丸めによって、赤枠まで拡張します。
    (簡単の為2Dで説明)
    image.png

  • その後、端数を算出し加えます。

//lvが次元
//start,endがコーナーポイント

            //丸め
            var lvstart = start >> lv;
            var lvend = end >> lv;

            //次元の端数算出
            var fraction = GetFractionCount(lvstart,lvend);

            lv += fraction;
  • 端数次元算出処理です。 赤枠一つで収まるのか、2個必要なのかをXYZ軸で調べて、端数の次元数とします。 不格好ですが、場合分けの方がスッキリします。
        private int GetFractionCount(ulong start ,ulong end){
            var shift = 0;
            if (!(start & 0x1).Equals(0) || (end & 0x1).Equals(0)) return shift;
            shift++;
            if (!(start & 0x2).Equals(0) || (end & 0x2).Equals(0)) return shift;
            shift++;
            if (!(start & 0x4).Equals(0) || (end & 0x4).Equals(0)) return shift;
            shift++;
            return shift;
        }

image.png

  • キーが図のように1~31まであったとします。
    (簡単の為2Dで説明)
    数値が順にZ字を書くようにうねっています。
    軸だけ見るとxbit、ybitは全て01の交互である事が分かります。
    この時にレベル2で見た時に(図は2D図なので2次元づつ上がってます。)2×2マスになります。

  • 例えば赤字の5と16をみて下さい。レベル2で見ると緑と黄いろのマスが浮かびます。
    05は00 01 01です
    16は01 00 00です
    太字を見てください。太字箇所がレベル2です。

この時に太字箇所の1ビット目が05と16だと1→0と逆転の関係にある事が分かります。
この事は一つのグリッドで表現出来ないグリッドの分割が発生する軸と言うことを意味します。
以降グリッド跨ぎと言います。

  • 例えば
    17は01 00 01
    20は01 01 00
    で太字1ビット目が0→1の関係なので跨いでいないと言うことに成ります。

  • ここまでの説明をすると
    跨いでいない状態をXYZ軸で順次チェックして、端数の次元数がカウントされます。
    そして最後に端数を次元に足しこめば、元々立方体だったコーナーにおける最小グリッドが、少数次元まで落ちた形(横長だったり板状だったりします。)で算出されます。
    この外形を使ってコーナーに配置していきます。

コーナーへの配置

  • 端数次元によって、最小グリッドの次元が分かりました。
  • スタート地点と終了地点が分かってます。
  • 他の点がまだ分かって居ません。
  • 分割数と方向が分かれば配置場所が分かります

分割数と分割する軸を求めます。

            //lvstartは元の立方体で丸められた左上コーナー。lvendは右下
            var dim = (byte)(Truncate(lvstart ^ lvend,fraction) & 0x7);//差分体積の共有空間の端数次元3ビットのみを抽出
            start = Truncate(start, lv);
            end = Truncate(end, lv);
切り捨て
        private ulong Truncate(ulong v,int lv,uint offset = 0){
            return ((v >> lv ) + offset) << lv;
        }

下図の場合、赤丸の左上コーナーは4で右下コーナーは7になります。
(簡単の為2Dで説明)
image.png

  • 分割数算出
    4は0100
    7は0111
    XORを取ると差分が出て
    0011になります。
    又、前述の関数を使うと端数が2次元になります。
    truncate関数は次元以降を切り捨てる関数になります。
    0011を2次元分切り捨てると0になります。
    これの意味するところは、X方向に0(一個分)Y方向に0(一個分)必要となります。

  • もう一つやってみます。
    4は0100
    6は0110
    xorは0010
    今回の端数次元は0になります。
    切り捨てが発生しないので、0010となります。
    これの意味するところは、X方向に0(一個分)Y方向に1(2個分)必要となります。

このように計算によって縦横の必要数が分かります。
後は、配置していきます。

            start = Truncate(start, lv);
            end = Truncate(end, lv);
            var oct = new OctreeVoxel();
            switch (dim){
                case 0:{//1box内
                    oct.len = 1;
                    oct.v[0] =start;//0
                    break;}
                case 1:{//左右2box
                    oct.len = 2;
                    oct.v[0] = start;//0
                    oct.v[1] = end;//1
                    break;
                }
                case 2:{//上下2box
                    oct.len = 2;
                    oct.v[0] = start;//0
                    oct.v[1] = end;//2
                    break;
                }
                case 3:{//前面x,y 4box
                    oct.len = 4;
                    oct.v[0] = start;//0
                    Octree4Box(&oct,1, 2, start, end);
                    oct.v[3] = end;//7
                    break;
                }
                case 4:{//前後2box
                    oct.len = 2;
                    oct.v[0] = start;//0
                    oct.v[1] = end;//4
                    break;
                }
                case 5:{//上面 x,z 4box
                    oct.len = 4;
                    oct.v[0] = start;//0
                    Octree4Box(&oct,1, 4, start, end);
                    oct.v[3] = end;//5
                    break;
                }
                case 6:{//側面 y,z 4box
                    oct.len = 4;
                    oct.v[0] = start;//0
                    Octree4Box(&oct,2, 4, start, end);
                    oct.v[3] = end;//6
                    break;
                }
                case 7:{//8boxel
                    oct.len = 8;
                    oct.v[0] = start;//0
                    Octree8Box(&oct,start, end);
                    oct.v[7] = end;//7
                    break;
                }
            }

コーナーへの登録をします
最初のtruncate処理の所で始端と終端を端数付きの次元でそれぞれ丸めます。
dimには得られた縦横配置場所が入っています。ビットで表現され、最大7(111)です。zyxが何個要るか?が分かって居ます。0なら1個、1なら2個です。
Z字を書くように象限分けをすると、何番に配置するか分かります。
例えばdimが6だったらビットで表現すると110となり、前後2×縦2×横1の配置になり
下図で言うと0246の象限への配置が必要と分かります。
image.png

  • ケースで場合分けしているのは、後の処理を勘案するとソーティングされていた方が効率的なのでこの時点で既にソーティングしてしまいます。 この時に最小のソーティングで済ませる為に、場合分けで済ませます。
  • ex)下図の例だと、1→4→3→6となってしまう。 こういうパターンもある為 (簡単の為2Dで説明)

image.png

値は最大で8点を格納できる構造体を用意して格納します。

端点群を格納する構造体
        [Serializable]
        public unsafe struct OctreeVoxel{
            public byte len;
            public fixed ulong v[8];
        }
  • 2boxの場合はstartとendだけの登録で終わりです。

それ以上の分割数

  • 4box
    OctreeCompositに与える1番目の引数は象限を与えます。
    それに応じたビット数をstartとendのビット数を抽出し交互に合成することで、象限のキーが得られます。
    startとendは決まっているので、間の2象限の値を比較して少ない順に入れ替えます。

  • 8boxの方もやって居る事は一緒ですが、ソートの入れ替えが少ない回数で行っています。

boxポイント
        private unsafe void Octree4Box(OctreeVoxel* oct, byte l,byte r,ulong start,ulong end){
            oct->v[1] = OctreeComposit(l, start, end);
            oct->v[2] = OctreeComposit(r, start, end);
            CompareSwap(ref oct->v[1],ref oct->v[2]);
        }
        private unsafe void Octree8Box(OctreeVoxel* oct,ulong start, ulong end){
            oct->v[1] = OctreeComposit(1, start, end);
            oct->v[2] = OctreeComposit(2, start, end);
            oct->v[3] = OctreeComposit(3, start, end);
            oct->v[4] = OctreeComposit(4, start, end);
            oct->v[5] = OctreeComposit(5, start, end);
            oct->v[6] = OctreeComposit(6, start, end);
            CompareSwap(ref oct->v[2], ref oct->v[3]);
            CompareSwap(ref oct->v[4], ref oct->v[5]);
            CompareSwap(ref oct->v[1], ref oct->v[3]);
            CompareSwap(ref oct->v[4], ref oct->v[6]);
            CompareSwap(ref oct->v[1], ref oct->v[2]);
            CompareSwap(ref oct->v[5], ref oct->v[6]);
            CompareSwap(ref oct->v[1], ref oct->v[5]);
            CompareSwap(ref oct->v[2], ref oct->v[6]);
            CompareSwap(ref oct->v[2], ref oct->v[4]);
            CompareSwap(ref oct->v[3], ref oct->v[5]);
            CompareSwap(ref oct->v[1], ref oct->v[2]);
            CompareSwap(ref oct->v[3], ref oct->v[4]);
            CompareSwap(ref oct->v[5], ref oct->v[6]);
        }
        private ulong OctreeComposit(byte pos,ulong start,ulong end){
            switch (pos){
                case 0:{return start;}
                case 1:{return ((start & 0x6db6db6db6db6db6) | (end & 0x9249249249249249));}
                case 2:{return ((start & 0xdb6db6db6db6db6d) | (end & 0x2492492492492492));}
                case 3:{return ((start & 0x4924924924924924) | (end & 0x66db6db6db6db6db));}
                case 4:{return ((start & 0x66db6db6db6db6db) | (end & 0x4924924924924924));}
                case 5:{return ((start & 0x2492492492492492) | (end & 0xdb6db6db6db6db6d));}
                case 6:{return ((start & 0x9249249249249249) | (end & 0x6db6db6db6db6db6));}
                default:{return end;}
            }
        }
        private void CompareSwap(ref ulong x,ref ulong y){
            if (x.Equals(y)){return;}
            if (x <= y) return;
            x ^= y;
            y ^= x;
            x ^= y;
        }

ここから先は範囲検索が出来ないと無理なので

  • ここまでで、おさらいが入った最適化をしています。 bit演算とzオーダーの親和性の高さから前回に比べビット演算を多用してます。 今までと概念ががらっと変わったように見えますが、欲しい結果は前回と同じです。

OctreeVoxelの中身が取得できれば検索範囲だろうが、オブジェクトが持つAABBだろうが、位置とサイズさえあれば、いきなり範囲ボクセルを取得できるようになります。

試してみたかったら、下のスクリプトを作成して適当なオブジェクトにMortonTestをアタッチしてもらえれば確認できます。
サイズに値を入れて、オブジェクトをぐりぐり動かしてみてください。
最適化されたグリッドが表示されます。

ここから先AABBと範囲の衝突可能性になりますが
ざっくり言うと、分割したボクセルを全て並べて範囲検索すれば良いだけの話なのですが
次が範囲検索と言うことで、複雑な問い合わせが予想されます。
そもそもがパフォーマンス向上が目的なので、ここできっちり最適化を行いました。

次はいつになるか分かりませんが、今度コソ②へ!

MortonTest.cs
using System;
using Unity.Mathematics;
using UnityEngine;

public unsafe class MortonTest :  MonoBehaviour
{
    [SerializeField]
    private float size;

    [SerializeField]
    private int maxlv;


    [SerializeField]

    private ulong[] mlist;
    [Serializable]
    public unsafe struct OctreeVoxel{
        public byte len;
        public fixed ulong v[8];
    }
    void OnDrawGizmos()
    {
        // Draw a yellow sphere at the transform's position
        var lscale = (float3) (this.transform.localScale);
        var scale = lscale.x > lscale.y ? 
            lscale.x > lscale.z ? lscale.x : lscale.z :
            lscale.y > lscale.z ? lscale.y : lscale.z;


        var pos = (float3) this.transform.position ;
        var csize = scale * size;
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(pos,csize);
        Gizmos.color = Color.magenta;
        var ml = GetOctree(pos, csize,out maxlv);
        mlist = new ulong[8];
        for (var i = 0; i < mlist.Length; i++){
            var mli = ml.v[i];
            if (!mli.Equals(0)){
                var start = Truncate(mli, (int)maxlv);
                var outpos1 = GetPosFromMorton(start);
                var end = Truncate(mli , (int)maxlv,1);
                var outpos2 = GetPosFromMorton(end - 1);
                var dsize = (outpos2 - outpos1);
                Gizmos.DrawWireCube(outpos1 + (dsize * 0.5f), dsize);

            }
            mlist[i] = ml.v[i];
        }


    }

    private float3 GetPosFromMorton(ulong aa)
    {
        var tmp = float3.zero;
        for (var i = 0; i < 64; i++){
            var d = (i + 1)  % 3;
            if (!(aa & 0x01).Equals(0)){
                var div = (int) (i / 3);
                switch (d){
                    case 1:{tmp.x += (1 << div);break;}
                    case 2:{tmp.y += (1 << div);break;}
                    default:{tmp.z += (1 << div);break;}
                }                    
            }
            aa >>= 1;

        }

        tmp /= 100;
        tmp -= 10000;
        return tmp;
    }
    private int GetLvFromSize(float hsize){
        var norm = hsize * 200;
        var msb = Msb32Bit((int)norm) + 1;
        return msb * 3;
    }
    private int  Msb32Bit(int v) {
        if (v == 0) return -1;
        v |= (v >> 1);
        v |= (v >> 2);
        v |= (v >> 4);
        v |= (v >> 8);
        v |= (v >> 16);
        return Count32Bit(v) - 1;
    }
    private int Count32Bit(int v) {
        var count = (v & 0x55555555) + ((v >> 1) & 0x55555555);
        count = (count & 0x33333333) + ((count >> 2) & 0x33333333);
        count = (count & 0x0f0f0f0f) + ((count >> 4) & 0x0f0f0f0f);
        count = (count & 0x00ff00ff) + ((count >> 8) & 0x00ff00ff);
        return (count & 0x0000ffff) + ((count >> 16) & 0x0000ffff);
    }
    private void Get3DCorner(float3 xyz,float hsize,out ulong start ,out ulong end){

        xyz += 10000;//座標にマイナスが発生しないようオフセット+10000

        var negative = (xyz - hsize) * 100;//offset 最小ボクセル0.01の正規化
        var positive = (xyz + hsize) * 100;//offset 最小ボクセル0.01の正規化
        start = BitSeparateFor3D((ulong)negative.x) | BitSeparateFor3D((ulong)negative.y)<<1 | BitSeparateFor3D((ulong)negative.z)<<2;
        end = BitSeparateFor3D((ulong)positive.x) | BitSeparateFor3D((ulong)positive.y)<<1 | BitSeparateFor3D((ulong)positive.z)<<2;
    }
    private ulong BitSeparateFor3D(ulong n )
    {
        var s = n;
        s = ( s | s<<32 ) & 0xffff00000000ffff;
        s = ( s | s<<16 ) & 0x00ff0000ff0000ff;
        s = ( s | s<<8 )  & 0xf00f00f00f00f00f;
        s = ( s | s<<4 )  & 0x30c30c30c30c30c3;
        s = ( s | s<<2 )  & 0x9249249249249249;
        return s;
    }
    private int GetFractionCount(ulong start ,ulong end){
        var shift = 0;
        if (!(start & 0x1).Equals(0) || (end & 0x1).Equals(0)) return shift;
        shift++;
        if (!(start & 0x2).Equals(0) || (end & 0x2).Equals(0)) return shift;
        shift++;
        if (!(start & 0x4).Equals(0) || (end & 0x4).Equals(0)) return shift;
        shift++;
        return shift;

    }
    private ulong Truncate(ulong v,int lv,uint offset = 0){
        return ((v >> lv ) + offset) << lv;
    }
    private OctreeVoxel GetOctree(float3 pos,float hSize,out int lv){
        lv = GetLvFromSize(hSize);
        Get3DCorner(pos,hSize,out var start, out var end);
        var lvstart = start >> lv;
        var lvend = end >> lv;
        var fraction = GetFractionCount(lvstart,lvend);//次元の端数
        lv += fraction;

        var dim = (byte)(Truncate(lvstart ^ lvend,fraction) & 0x7);//差分体積の共有空間の端数次元3ビットのみを抽出
        start = Truncate(start, lv);
        end = Truncate(end, lv);
        var oct = new OctreeVoxel();
        switch (dim){
            case 0:{//1box内
                oct.len = 1;
                oct.v[0] =start;//0
                break;}
            case 1:{//左右2box
                oct.len = 2;
                oct.v[0] = start;//0
                oct.v[1] = end;//1
                break;
            }
            case 2:{//上下2box
                oct.len = 2;
                oct.v[0] = start;//0
                oct.v[1] = end;//2
                break;
            }
            case 3:{//前面x,y 4box
                oct.len = 4;
                oct.v[0] = start;//0
                Octree4Box(&oct,1, 2, start, end);
                oct.v[3] = end;//7
                break;
            }
            case 4:{//前後2box
                oct.len = 2;
                oct.v[0] = start;//0
                oct.v[1] = end;//4
                break;
            }
            case 5:{//上面 x,z 4box
                oct.len = 4;
                oct.v[0] = start;//0
                Octree4Box(&oct,1, 4, start, end);
                oct.v[3] = end;//5
                break;
            }
            case 6:{//側面 y,z 4box
                oct.len = 4;
                oct.v[0] = start;//0
                Octree4Box(&oct,2, 4, start, end);
                oct.v[3] = end;//6
                break;
            }
            case 7:{//8boxel
                oct.len = 8;
                oct.v[0] = start;//0
                Octree8Box(&oct,start, end);
                oct.v[7] = end;//7
                break;
            }
        }

        return oct;
    }
    private  void Octree4Box(OctreeVoxel* oct, byte l,byte r,ulong start,ulong end){
        oct->v[1] = OctreeComposit(l, start, end);
        oct->v[2] = OctreeComposit(r, start, end);
        CompareSwap(ref oct->v[1],ref oct->v[2]);
    }

    private  void Octree8Box(OctreeVoxel* oct,ulong start, ulong end){
        oct->v[1] = OctreeComposit(1, start, end);
        oct->v[2] = OctreeComposit(2, start, end);
        oct->v[3] = OctreeComposit(3, start, end);
        oct->v[4] = OctreeComposit(4, start, end);
        oct->v[5] = OctreeComposit(5, start, end);
        oct->v[6] = OctreeComposit(6, start, end);
        CompareSwap(ref oct->v[2], ref oct->v[3]);
        CompareSwap(ref oct->v[4], ref oct->v[5]);
        CompareSwap(ref oct->v[1], ref oct->v[3]);
        CompareSwap(ref oct->v[4], ref oct->v[6]);
        CompareSwap(ref oct->v[1], ref oct->v[2]);
        CompareSwap(ref oct->v[5], ref oct->v[6]);
        CompareSwap(ref oct->v[1], ref oct->v[5]);
        CompareSwap(ref oct->v[2], ref oct->v[6]);
        CompareSwap(ref oct->v[2], ref oct->v[4]);
        CompareSwap(ref oct->v[3], ref oct->v[5]);
        CompareSwap(ref oct->v[1], ref oct->v[2]);
        CompareSwap(ref oct->v[3], ref oct->v[4]);
        CompareSwap(ref oct->v[5], ref oct->v[6]);
    }
    private ulong OctreeComposit(byte pos,ulong start,ulong end){
        switch (pos){
            case 0:{return start;}
            case 1:{return ((start & 0x6db6db6db6db6db6) | (end & 0x9249249249249249));}
            case 2:{return ((start & 0xdb6db6db6db6db6d) | (end & 0x2492492492492492));}
            case 3:{return ((start & 0x4924924924924924) | (end & 0x66db6db6db6db6db));}
            case 4:{return ((start & 0x66db6db6db6db6db) | (end & 0x4924924924924924));}
            case 5:{return ((start & 0x2492492492492492) | (end & 0xdb6db6db6db6db6d));}
            case 6:{return ((start & 0x9249249249249249) | (end & 0x6db6db6db6db6db6));}
            default:{return end;}
        }
    }
    private void CompareSwap(ref ulong x,ref ulong y){
        if (x.Equals(y)){return;}
        if (x <= y) return;
        x ^= y;
        y ^= x;
        x ^= y;
    }
}
``` 
</div></details>

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

Unityテクスチャアトラス

画面右が元のテクスチャ

image.png

RawImage

UV Rect
左上 xy 0.0 0.5 wh 0.5 0.5
右上 xy 0.5 0.5 wh 0.5 0.5

左下 xy 0.0 0.0 wh 0.5 0.5
右下 xy 0.5 0.0 wh 0.5 0.5

image.png

左下が原点かな。

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

Unity 2019 アップデート時 Android ビルドスクリプト修正した

概要

Unity で Android ビルドをしていたスクリプトが Unity 2018 から Unity 2019 にアップデートしたときに動かなくなってしまったため修正しました。

Android ビルドは一度 Gradle プロジェクトを出力するビルドパイプラインにしています。

Unity 2018 のビルドスクリプト

Gradle プロジェクトを出力するコード部分の抜粋です。

        EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
        // 実行
        var buildReport = BuildPipeline.BuildPlayer(
                              sceneList,                          //!< ビルド対象シーンリスト
                              Path.Combine(Directory.GetCurrentDirectory(), AndroidBuildDirName), //!< 出力先
                              BuildTarget.Android,                    //!< ビルド対象プラットフォーム
                              BuildOptions.AcceptExternalModificationsToPlayer |
                              BuildOptions.Development            //!< ビルドオプション
                          );

このコードを実行すると指定の出力先に Gradle プロジェクトができていました。

Unity 2019 のビルドスクリプト

同じスクリプトを Unity 2019 で実行したところ、BuildPipeline.BuildPlayer 実行時に InvalidOperationException: The build target does not support build appending. というエラーがでて実行できなかった。

以下のフォーラムを参考に、

  • BuildOptions.AcceptExternalModificationsToPlayer のオプション指定を削除
  • かわりに EditorUserBuildSettings.exportAsGoogleAndroidProject = true; の設定を追加

しました。

フォーラム: https://forum.unity.com/threads/oculus-build-invalidoperationexception-the-build-target-does-not-support-build-appending.994930/

        EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
        EditorUserBuildSettings.exportAsGoogleAndroidProject = true;
        // 実行
        var buildReport = BuildPipeline.BuildPlayer(
                              sceneList,                          //!< ビルド対象シーンリスト
                              Path.Combine(Directory.GetCurrentDirectory(), AndroidBuildDirName), //!< 出力先
                              BuildTarget.Android,                    //!< ビルド対象プラットフォーム

                              BuildOptions.Development            //!< ビルドオプション
                          );

無事に実行でき、指定の出力先に Gradle プロジェクトができていました。
Unity 2018 のときとディレクトリ構成は変わっています。

参考

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

Unity 2019 アップデート時 Android ビルドスクリプトを修正した

概要

Unity で Android ビルドをしていたスクリプトが Unity 2018 から Unity 2019 にアップデートしたときに動かなくなってしまったため修正しました。

Android ビルドは一度 Gradle プロジェクトを出力するビルドパイプラインにしています。

Unity 2018 のビルドスクリプト

Gradle プロジェクトを出力するコード部分の抜粋です。

        EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
        // 実行
        var buildReport = BuildPipeline.BuildPlayer(
                              sceneList,                          //!< ビルド対象シーンリスト
                              Path.Combine(Directory.GetCurrentDirectory(), AndroidBuildDirName), //!< 出力先
                              BuildTarget.Android,                    //!< ビルド対象プラットフォーム
                              BuildOptions.AcceptExternalModificationsToPlayer |
                              BuildOptions.Development            //!< ビルドオプション
                          );

このコードを実行すると指定の出力先に Gradle プロジェクトができていました。

Unity 2019 のビルドスクリプト

同じスクリプトを Unity 2019 で実行したところ、BuildPipeline.BuildPlayer 実行時に InvalidOperationException: The build target does not support build appending. というエラーがでて実行できなかった。

以下のフォーラムを参考に、

  • BuildOptions.AcceptExternalModificationsToPlayer のオプション指定を削除
  • かわりに EditorUserBuildSettings.exportAsGoogleAndroidProject = true; の設定を追加

しました。

フォーラム: https://forum.unity.com/threads/oculus-build-invalidoperationexception-the-build-target-does-not-support-build-appending.994930/

        EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
        EditorUserBuildSettings.exportAsGoogleAndroidProject = true;
        // 実行
        var buildReport = BuildPipeline.BuildPlayer(
                              sceneList,                          //!< ビルド対象シーンリスト
                              Path.Combine(Directory.GetCurrentDirectory(), AndroidBuildDirName), //!< 出力先
                              BuildTarget.Android,                    //!< ビルド対象プラットフォーム

                              BuildOptions.Development            //!< ビルドオプション
                          );

無事に実行でき、指定の出力先に Gradle プロジェクトができていました。

また、Unity 2018 のときと出力されるGradle プロジェクトのディレクトリ構成が変わっています。これはディレクトリ構成確認して gradle 実行位置を修正をすることで解決しました。生成されている local.properties に UnityEditor が同梱する SDK, NDK のパスが指定されるようになって CI のメンテナンスが楽になりそうな印象。

参考

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

[Unity初級]デバッグをひと工夫(Debug.Logの応用)

概要

手軽にログの見た目を変えて、気を付けるべき箇所に注意を向けやすくなります。

本文

        if (a < 4) {
            Debug.Log("普通のデバッグログ");
        } else if(a<100) {
            Debug.LogWarning("異常を知らせるログ(LogWarning)");
        } else {
            Debug.LogError("異常を知らせるログ(LogError)");
        }

スクリーンショット (1222).png

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

[Unity]モーションのフレームレートを使ったアニメチックな表現

アニメっぽい表現

アウトラインを付けたり影を閾値で分けたりすることで、3Dモデルをアニメっぽいビジュアルにする、いわゆる セルルック とか トゥーンシェーダー と呼ばれる表現は随分前から使われています。

昨今はその技術もかなり一般化されていて、MMDのモデルに適用したり、Vtuberさんのモデルで使われるなど、非商業作品の中でも目にすることが多い印象です。
image.png
ユニティちゃんトゥーンシェーダーのお世話になった人は多いはず

映像としてのアニメっぽい表現

そんな中、よりアニメっぽい絵作りをするためにこれから普及してきそうなテクニックが「フレーム落とし」かなーと個人的に思ってます。
まずはこちらをご覧ください。

絶賛プレイ中の人も多いはず。PS5のスパイダーマンのスパイダーバースリスペクトな衣装でのみ見れる表現(たぶん。誰かPS5ください)

ドラゴンボールファイターズのアニメーション。カット演出では特に顕著に見れる表現。

お分かりいただけたでしょうか?
つまり、カメラやオブジェクトの位置・角度変化のfpsはそのままに、モーションのfpsのみを落として、テレビアニメっぽい雰囲気を出すというネタです。

最近はアニメの品質がどんどん上がってきて、フレームレートが高くぬるぬる動くものもよく見かけますが、とはいえゲームで主流の60fpsで動くアニメはほぼ無いはずです。(雑に調べたかぎり、普通は8fps、フルアニメーションで12fpsだとか)
ここで紹介したものは、そういう現状を汲んで「キャラクターの動きのフレームレートを落とすと普段目にするアニメに近い雰囲気になる」という文脈の表現だと勝手に解釈してます。

というわけでやってみた

▼これが・・・


▲こうなりました。

なんとなくスパイダーバースっぽくなったような気がしません?どうでしょう。

実装方法

指定した AnimationClip を任意のfpsで再生する AnimationFPSController.cs を作ります。

AnimationFPSController.cs
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;

[RequireComponent(typeof(Animator))]
public class AnimationFPSController : MonoBehaviour {
    [SerializeField]
    AnimationClip _clip;

    [SerializeField, Range(1, 30)]
    int _fps = 30;

    Animator _animator;

    PlayableGraph _graph;

    AnimationClipPlayable _clipPlayable;

    /// <summary>しきい値時間</summary>
    float _thresholdTime;

    /// <summary>スキップされた更新時間</summary>
    float _skippedTime;

    void Awake() {
        _animator = GetComponent<Animator>();
        _graph = PlayableGraph.Create();
        _clipPlayable = AnimationClipPlayable.Create(_graph, _clip);

        AnimationPlayableOutput output = AnimationPlayableOutput.Create(_graph, "output", _animator);
        output.SetSourcePlayable(_clipPlayable);

        InitializeThresholdTime();

        _graph.Play();
    }

    /// <summary>
    /// しきい値時間の初期化
    /// </summary>
    void InitializeThresholdTime() {
        _thresholdTime = 1f / _fps;
    }

    /// <summary>
    /// 更新処理
    /// </summary>
    void Update() {
        if (_fps >= 30) {
            return;
        }

        // Playable側で自動更新しないようにポーズにする
        if (_skippedTime < Mathf.Epsilon) {
            _clipPlayable.Pause();
        }

        if (_thresholdTime > _skippedTime) {
            _skippedTime += Time.deltaTime * (float)_clipPlayable.GetSpeed();
            return;
        }
        // _skippedTimeが0の場合、更新されなくなるので意図的に行う
        else if (_skippedTime < Mathf.Epsilon) {
            _skippedTime += Time.deltaTime * (float)_clipPlayable.GetSpeed();
        }

        // アニメーションの時間を計算する
        double currentTime = _clipPlayable.GetTime() + _skippedTime;
        _skippedTime = 0f;

        if (currentTime > _clip.length) {
            currentTime -= _clip.length;
        }
        _clipPlayable.SetTime(currentTime);
    }

    /// <summary>
    /// オブジェクト削除時の処理
    /// </summary>
    void OnDestroy() {
        _graph.Destroy();
    }

    /// <summary>
    /// Inspectorの値変更時の処理
    /// </summary>
    void OnValidate() {
        InitializeThresholdTime();
    }
}

モデルを動かすAnimatorオブジェクトにコンポーネントを追加します。

AnimationFPSController.png

  • Clip:再生したいアニメーションファイル
  • Fps:再生時のフレームレート

※AnimatorにはAnimatorControllerを設定しないでください

簡単な解説

Animatorの低レイヤーな部分を制御できる、PlayableAPIを使っています。
(詳細はテラシュールブログさんにお任せします・・・)

Updateメソッド内でモーションの更新処理をスキップさせ、設定したFpsに応じてタイミングが来たら更新を実行しています。
ポイントは、「スキップした時間を保持して、更新タイミングでその分アニメーション時間を進める」 というところです。

その他

  • トゥーンシェーダーを使ったモデルとの親和性は非常に高いと思います。是非組み合わせてみてください
  • SpringBoneなどの揺れものをつかったモデルの場合、揺れものの更新fpsも一致させないと違和感が出そうです。注意!
  • 表現としてだけでなく、LOD的な用途にも向いてると思います。近年のゲームで結構見かけます
    • マリオオデッセイのニュードンクシティの街の人々
    • モンハンワールドの一部の敵
    • ドラクエモンスターズ2 3DS版のフィールドの敵シンボル
  • PlayableAPI だけでのモーションの制御は難易度が高そう…うまくやればもっと汎用的にできる?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity]スパイダーバース感!?モーションFPS制御によるアニメチックな映像表現

アニメっぽい表現

アウトラインを付けたり影を閾値で分けたりすることで、3Dモデルをアニメっぽいビジュアルにする、いわゆる セルルック とか トゥーンシェーダー と呼ばれる表現は随分前から使われています。

昨今はその技術もかなり一般化されていて、MMDのモデルに適用したり、Vtuberさんのモデルで使われるなど、非商業作品の中でも目にすることが多い印象です。
image.png
ユニティちゃんトゥーンシェーダーのお世話になった人は多いはず

映像としてのアニメっぽい表現

そんな中、よりアニメっぽい絵作りをするためにこれから普及してきそうなテクニックが「フレーム落とし」かなーと個人的に思ってます。
まずはこちらをご覧ください。

絶賛プレイ中の人も多いはず。PS5のスパイダーマンのスパイダーバースリスペクトな衣装でのみ見れる表現(たぶん。誰かPS5ください)

ドラゴンボールファイターズのアニメーション。カット演出では特に顕著に見れる表現。

お分かりいただけたでしょうか?
つまり、カメラやオブジェクトの位置・角度変化のfpsはそのままに、モーションのfpsのみを落として、テレビアニメっぽい雰囲気を出すというネタです。

最近はアニメの品質がどんどん上がってきて、フレームレートが高くぬるぬる動くものもよく見かけますが、とはいえゲームで主流の60fpsで動くアニメはほぼ無いはずです。(雑に調べたかぎり、普通は8fps、フルアニメーションで12fpsだとか)
ここで紹介したものは、そういう現状を汲んで「キャラクターの動きのフレームレートを落とすと普段目にするアニメに近い雰囲気になる」という文脈の表現だと勝手に解釈してます。

というわけでやってみた

▼これが・・・


▲こうなりました。

なんとなくスパイダーバースっぽくなったような気がしません?どうでしょう。

実装方法

指定した AnimationClip を任意のfpsで再生する AnimationFPSController.cs を作ります。

AnimationFPSController.cs
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;

[RequireComponent(typeof(Animator))]
public class AnimationFPSController : MonoBehaviour {
    [SerializeField]
    AnimationClip _clip;

    [SerializeField, Range(1, 30)]
    int _fps = 30;

    Animator _animator;

    PlayableGraph _graph;

    AnimationClipPlayable _clipPlayable;

    /// <summary>しきい値時間</summary>
    float _thresholdTime;

    /// <summary>スキップされた更新時間</summary>
    float _skippedTime;

    void Awake() {
        _animator = GetComponent<Animator>();
        _graph = PlayableGraph.Create();
        _clipPlayable = AnimationClipPlayable.Create(_graph, _clip);

        AnimationPlayableOutput output = AnimationPlayableOutput.Create(_graph, "output", _animator);
        output.SetSourcePlayable(_clipPlayable);

        InitializeThresholdTime();

        _graph.Play();
    }

    /// <summary>
    /// しきい値時間の初期化
    /// </summary>
    void InitializeThresholdTime() {
        _thresholdTime = 1f / _fps;
    }

    /// <summary>
    /// 更新処理
    /// </summary>
    void Update() {
        if (_fps >= 30) {
            return;
        }

        // Playable側で自動更新しないようにポーズにする
        if (_skippedTime < Mathf.Epsilon) {
            _clipPlayable.Pause();
        }

        if (_thresholdTime > _skippedTime) {
            _skippedTime += Time.deltaTime * (float)_clipPlayable.GetSpeed();
            return;
        }

        // アニメーションの時間を計算する
        double currentTime = _clipPlayable.GetTime() + _skippedTime + Time.deltaTime * (float)_clipPlayable.GetSpeed();
        _skippedTime = 0f;

        if (currentTime > _clip.length) {
            currentTime -= _clip.length;
        }
        _clipPlayable.SetTime(currentTime);
    }

    /// <summary>
    /// オブジェクト削除時の処理
    /// </summary>
    void OnDestroy() {
        if (_graph.IsValid()) {
            _graph.Destroy();
        }
    }

    /// <summary>
    /// Inspectorの値変更時の処理
    /// </summary>
    void OnValidate() {
        InitializeThresholdTime();
    }
}

モデルを動かすAnimatorオブジェクトにコンポーネントを追加します。

AnimationFPSController.png

  • Clip:再生したいアニメーションファイル
  • Fps:再生時のフレームレート

※AnimatorにはAnimatorControllerを設定しないでください

簡単な解説

Animatorの低レイヤーな部分を制御できる、PlayableAPIを使っています。
(詳細はテラシュールブログさんにお任せします・・・)

Updateメソッド内でモーションの更新処理をスキップさせ、設定したFpsに応じてタイミングが来たら更新を実行しています。
ポイントは、「スキップした時間を保持して、更新タイミングでその分アニメーション時間を進める」 というところです。

その他

  • トゥーンシェーダーを使ったモデルとの親和性は非常に高いと思います。是非組み合わせてみてください
  • ストップモーションアニメ風とも取れるので、クレイアニメっぽい雰囲気も作れそう(ニャッキとかピングーみたいな)
  • SpringBoneなどの揺れものをつかったモデルの場合、揺れものの更新fpsも一致させないと違和感が出そうです。注意!
  • 表現としてだけでなく、LOD的な用途にも向いてると思います。近年のゲームで結構見かけます
    • マリオオデッセイのニュードンクシティの街の人々
    • モンハンワールドの一部の敵
    • ドラクエモンスターズ2 3DS版のフィールドの敵シンボル
  • PlayableAPI だけでのモーションの制御は難易度が高そう…うまくやればもっと汎用的にできる?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

InputSystemで誤認されているHIDデバイスの日本語での修正マニュアル.

目的

UnityのInputSystemで誤認されているHIDデバイスを取得したいカテゴリに修正する.

最初に

ソースだけクレって方はGitHubでお取りください.
きっかけは,InputSystemで現在接続されているゲームパッドの情報を受け取ろうと

var pad = Gamepad.current;
Debug.Log("Gamepad ="+ pad.current);

と入力したところnullが出て名前が表示されなかったことです.
PCにはホリ製のホリパッド for Nintendo Switch[以下:HORIPAD_S]が刺さっていたので
名前が表示されるはずだと考えWindow>Analysis>Input Debuggerを起動
joy0.png
joy.png
どうも,Joystickとして認識されているようです.
これを解決するために頑張りました.

マニュアルを読む

Inputsystemにはとても英語のマニュアルがこちらに用意されています.
HID Support | Input System | 1.0.0
マニュアルを読んでいくとJoyStickとなぜ認識されてしまうのか?
認識がずれている物をどうすればいいのか?が解説されています.

これを元に手順を洗い出すと
1.コントローラーのボタンを押したときにBITがどのように反応するか調べる

2.1を元にC#でプログラムを記述

3. InputSystem.RegisterLayoutで機器を検出できるようにする

以上の手順で進めてみます.

ボタンの割り当てBITの調査

今回は誤認はしていますがInput Debuggerで表示されているのでこの情報画面から調査をします.
Unityで認識もされないよ!という場合はGamepad Testerで調査してください.

UnityのWindow>Analysis>Input Debuggerを開きます.
info0.png

ここでどのボタンがどれに対応しているのかを,
ボタンを押したときBITと書かれた部分の数値が動いたをボタンをメモしておきます.
すべてのボタンのメモが取り終わったら,上記画像のオレンジ色の矢印の部分をクリックしてください.
info1.png
すると上記画像のようなウィンドウが開きます.
この画像のOffsetの隣のBITの部分がボタンに割り当てられているBITになります.
先ほどとったメモを元にボタンとBITの対応表を作ります
info2.png
今回のHORIPAD_Sの場合はこのようになりました.

C#にBITとボタンの対応を落とし込む.

調査が終わったのでマニュアルを読みながらC#に落とし込んでみます.

HoriPadSController.cs
using System;
using System.Runtime.InteropServices;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using UnityEngine;
using UnityEditor;


[StructLayout(LayoutKind.Explicit, Size = 20)]
public struct HoriPadSState : IInputStateTypeInfo
{

    public FourCC format => new FourCC('H','I','D');

    //LeftStick
    [InputControl(name = "leftStick", format = "VC2S", layout = "Stick")]
    [InputControl(name = "leftStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")]
    [InputControl(name = "leftStick/left", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.15,clampMax=0.5,invert")]
    [InputControl(name = "leftStick/right", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=0.85")]
    [InputControl(name = "leftStick/y", offset = 1, format = "USHT", parameters = "invert,normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")]
    [InputControl(name = "leftStick/up", offset = 1, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.15,clampMax=0.5,invert")]
    [InputControl(name = "leftStick/down", offset = 1, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=0.85,invert=false")]
    [FieldOffset(3)] public ushort leftStickX;
    [FieldOffset(4)] public ushort leftStickY;

    //RightStick
    [InputControl(name = "rightStick", format = "VC2S", layout = "Stick")]
    [InputControl(name = "rightStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")]
    [InputControl(name = "rightStick/left", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0,clampMax=0.5,invert")]
    [InputControl(name = "rightStick/right", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=1")]
    [InputControl(name = "rightStick/y", offset = 1, format = "USHT", parameters = "invert,normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5")]
    [InputControl(name = "rightStick/up", offset = 1, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.15,clampMax=0.5,invert")]
    [InputControl(name = "rightStick/down", offset = 1, format = "USHT", parameters = "normalize,normalizeMin=0.15,normalizeMax=0.85,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=0.85,invert=false")]
    [FieldOffset(5)] public ushort rightStickX;
    [FieldOffset(6)] public ushort rightStickY;

    //Dpad
    [InputControl(name ="dpad", format ="BIT", layout = "Dpad", sizeInBits = 4,defaultState=8)]
    [InputControl(name ="dpad/up", format ="BIT", layout = "DiscreteButton", parameters = "minValue=7,maxValue=1,nullValue=8,wrapAtValue=7", bit = 0, sizeInBits = 4)]
    [InputControl(name ="dpad/right", format ="BIT", layout = "DiscreteButton", parameters = "minValue=1,maxValue=3", bit = 0, sizeInBits = 4)]
    [InputControl(name ="dpad/down", format ="BIT", layout = "DiscreteButton", parameters = "minValue=3,maxValue=5", bit = 0, sizeInBits = 4)]
    [InputControl(name ="dpad/left", format ="BIT", layout = "DiscreteButton", parameters = "minValue=5,maxValue=7", bit = 0, sizeInBits = 4)]
    [FieldOffset(3)] public byte dpadButton;

    //buttons1
    [InputControl(name = "buttonWest", displayName = "Y",bit = 0,usage = "SecondaryAction")]
    [InputControl(name = "buttonSouth", displayName = "B",bit = 1, usage = "Back")]
    [InputControl(name = "buttonEast", displayName = "A",bit = 2, usage = "PrimaryAction")]
    [InputControl(name = "buttonNorth", displayName = "X",bit = 3)]
    [InputControl(name = "leftShoulder", displayName = "L",bit = 4)]
    [InputControl(name = "rightShoulder", displayName = "R",bit = 5)]
    [InputControl(name = "leftTrigger", offset= 1,displayName = "ZL", format ="BIT", bit = 6)]
    [InputControl(name = "rightTrigger",offset= 1, displayName = "ZR", format ="BIT", bit = 7)]
    [FieldOffset(1)] public uint button1;

    //buttons2
    [InputControl(name = "select", displayName = "Minus",bit = 0)]
    [InputControl(name = "start", displayName = "Plus",bit = 1)]
    [InputControl(name = "leftStickPress", displayName = "Left Stick",bit = 2)]
    [InputControl(name = "rightStickPress", displayName = "Right Stick",bit = 3)]
    [FieldOffset(2)] public uint button2;

このようになりました.
offsetの設定はとりあえずInput Debuggerの手順1で表示されていたものを設定しました.
paramatersの設定やStickの設定は
Packages>Input System>Plugins>Switch>SwitchProControllerHID.csの設定を参考に設定しています.
他の方法として,既定のデバイスからJsonで設定を取る方法もあります.
info3.png
[Copy Layout as JSON]をクリックすると情報がコピーされます.
新しくJSONファイルを作成しペーストすると,今のボタンに設定されているパラメーターなどが見ることができます.

HORIPAD_S.json
        {
            "name": "stick/x",
            "layout": "",
            "variants": "",
            "usage": "",
            "alias": "",
            "useStateFrom": "",
            "offset": 0,
            "bit": 0,
            "sizeInBits": 8,
            "format": "BIT ",
            "arraySize": 0,
            "usages": [],
            "aliases": [],
            "parameters": "normalize=true,normalizeMin=0,normalizeMax=1,normalizeZero=0.5",
            "processors": "axisDeadzone",
            "displayName": "",
            "shortDisplayName": "",
            "noisy": false,
            "synthetic": false,
            "defaultState": "127",
            "minValue": "",
            "maxValue": ""
        }

上記のようなパラメーターの設定や判定の基準になるButtonやDpadの設定が書かれたJSONを作成できます.
ただ,ここで設定されているParameterはかなり極端になっていますので,今回のようにサードパーティ製のコントローラーの場合は本家のコントローラーの設定をとりあえず拝借してくるのがよいと思います.

InputSystem.RegisterLayoutで機器を検出できるようにする.

ここまでで,HORIPAD_SのLayoutを記述できました.
次はInputSystemに該当のLayoutを持つ機器を検知してもらえるようにInputSystem.RegisterLayouで登録します.

事前にデバイスのVenderIDとProductIDを調べておきましょう
info4.png

上記画像の[HID Descriptor]をクリックすると

info5.png
このような画面が出てきますのでVenderIDとProductIDをメモしておきます.
Windowsのデバイスマネージャーからでも調べることができますのでUnity側で機器が認識されない場合は
デバイスマネージャー>調べたい機器のプロパティ>詳細タブ>ハードウェアID>
「~VID_XXXX&PID_XXXX~」部分がVenderIDとProductID
Vendor.png

メモし終わったら以下のようにプログラムします

HoriPadS.cs
using System.Runtime.InteropServices;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using UnityEngine;
using UnityEditor;

[InputControlLayout(stateType = typeof(HoriPadSState))]
#if UNITY_EDITOR
[InitializeOnLoad]
#endif
public class HoriPadS : Gamepad
{
    static HoriPadS()
    {
        //Gamepadとして登録
        InputSystem.RegisterLayout<HoriPadS>(
            matches: new InputDeviceMatcher()
                .WithInterface("HID")
                .WithDeviceClass("Gamepad")
                 //企業名と製品名でも判定できます.
                 //名前は3個上の矢印が書いてある画像のProductとManufactuerです.
                .WithManufacturer("HORI CO.,LTD.")
                .WithProduct("HORIPAD S")
                 //こちらが先ほどメモしたvendorIdとproductIdで登録する方法
                .WithCapability("vendorId", 0xF0D)
                .WithCapability("productId", 0xC1) 
        );
    }

    [RuntimeInitializeOnLoadMethod]
    static void Init() { 
         UnityEngine.Debug.Log("initController");
    }
}

このように記述することで,Input Debuggerの下の方に
Layout.png
このような感じにclass名で命名したGamePadが登録されているはずです.
Unity側で認識されていない機器であればこの時点で接続してやれば,おそらく動くはずです.

しかし,HORIPAD_Sのように既に別の機器として認識されている物はこのままでは動きません.

HoriPadS.cs
using System.Runtime.InteropServices;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using UnityEngine;
using UnityEditor;

[InputControlLayout(stateType = typeof(HoriPadSState))]
#if UNITY_EDITOR
[InitializeOnLoad]
#endif
public class HoriPadS : Gamepad
{
    static HoriPadS()
    {
            //こちらに変更
            InputSystem.RegisterLayoutMatcher<HoriPadS>(
            new InputDeviceMatcher()
                .WithInterface("HID")
                .WithCapability("vendorId", 0xF0D)
                .WithCapability("productId", 0xC1) 
        );
    }

    [RuntimeInitializeOnLoadMethod]
    static void Init() { 
         UnityEngine.Debug.Log("initController");
    }
}

上記のプログラムのようにInputSystem.RegisterLayoutMatcher<>で既に登録されている構成に上書きしないと動かないです.
英語のマニュアルでは新しく構成を登録して終わりとなっているので,注意してください.

上手くいくと
tori00.png
上記のDevices直下のアイコンが目的のものに変化しているはずです.
この後は正常に動作しているかの確認作業になりますので,InputSystemをDLしたPackageManagerから
Visualizersなどを追加DL・使用して入力が想定通りに動いているかなどをチェックしてください.
スティック部分のデータ構造は変更前のものはoffset以外あまり当てにならないので既定のSwitchのProコンなどの設定を参考に
データ形式を変えたりoffsetを変更して調整してください.

最後に

今回は新しいInputSystemで誤認されているHIDデバイスの修正方法を,英語のマニュアルを元に実践しました.
実際に私が行ってみてわかりづらかった部分などは補足したつもりですが,InputSystemの理解の一助になれば幸いです.

※修正
HoriPadSController.csのbit表記がenumでグループ分けしたものだったのを
通常の数字に修正.以前の表記方式はGitHubに上がってます……2020/11/19

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

[TIPS]UnityのML-Agentsに関する情報ざっくりまとめ

最初に

どうも、ろっさむです。

今回はUnityで機械学習を行う際に使用できる有名なオープンソースプラグインである「ML-Agents」に関して調べたことをざっくりまとめた記事となります。

「機械学習かー、興味はあるんだけどねー…でもUnityでできるなら…」

という方の、導入になれば幸いです。
(自分がそうです)

ML-Agentsとは何か

Unity初の機械学習ができるオープンソースプラグインであり、正式名称は「Machine Learning Agents」となります。2017年9月17日にリリースされて以降、現在もアップデートを重ねています。
気になるGitHubのリンクは以下となります。

https://github.com/Unity-Technologies/ml-agents

Unityでの公式ページは以下の通りです。

https://unity3d.com/jp/machine-learning

このプラグインの目的としては、機械学習の最新の技術をゲーム開発者がUnityをプラットフォームとして扱えるようにすることです。
上記のライブラリの中にはエージェント(学習者)を定義するためのC#のSDKと、2D、3D及びVR/AR環境用のエージェントをトレーニングするための最先端の機械学習ライブラリが含まれています。
ここでのC#のSDKはゲームやUnity環境に簡単に組み込めるようになっており、APIも機能が豊富で、安定しているようです。
ライブラリは4つのパッケージで構成されており、1つのUnityパッケージと3つのPythonパッケージとなっています。
フレームワークの内部ではUnityとPython間の通信を行っており、Pythonパッケージ側で機械学習アルゴリズムを使用してゲーム内のキャラクターを学習させることができます。

今後の展望として、ML-Agentsを使用してローカルマシンで機械学習を行うことには限界があるため、クラウドサービスを提供し、ML-Agentsのユーザーがスケーラブルなクラウド環境で機械学習を行うことができるようにしていくみたいです。これによって、トレーニングを多数のマシンで同時に行ったりして、より速く結果を求められるようになります。
ML-AgentsCloudへの早期アクセスも始まっているみたいです。

ML-Agentsを使用することで、2つの深層強化学習アルゴリズム、Proximal Policy Optimization(PPO)とSoft Actor-Critic(SAC)を使用したトレーニングが可能となります。

「なんだか難しそうな単語が並んでるな…」と思ったらまずはお試しで触れそうな記事から見ていきましょう。
上から順番に難易度順です(個人の独断と偏見によります)。

そして最後に「Unityではじめる機械学習・強化学習 Unity ML-Agents実践ゲームプログラミング」という本があるので、がっつり触ってみたい!という人はこちらを参照すると良いかと思います。

image.png

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