- 投稿日:2020-11-18T18:43:05+09:00
NavMeshAgentを使いたいが、AI用のオブジェクトが空中からスタートするからエラーになってしまうとき
- 投稿日:2020-11-18T17:39:44+09:00
型の違う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
は無理両方の変数が参照できる位置にあるなら、
sentence
とflag
に同じ処理をSubscribe
するのが楽でいいのかも。
- 投稿日:2020-11-18T17:24:25+09:00
unityで物理エンジンを使わず当たり判定をした話 その①-2
前回からの焼きまわしです
前回はこちら unityで物理エンジンを使わず当たり判定をした話 その①
本稿ではまだ範囲検索は書いてません。
あくまで、①の最適化です。
ビット演算を主にしたコアな内容に成ってます。次に書く予定の②で範囲検索を模索します。
前回の記事との違い
前回、8ボクセルに分割したモートンオーダーを使った、空間分割について着目しました。
次回以降検索で負荷が掛る事が予想されます。
おさらいをしながら、今のうちに前回の記事の内容を最適化します言葉
- 次元
- 通常次元はユークリッド空間における概念で語られます
- ゲームにおいても0次元(点)~3次元(xyz軸)等でデザインされます。
- 本稿においては、単に次元と指す場合はモートンオーダーにおける、階数を指す事とします。
- 2×2×2の立方が一つの単位(3次元)になります。
- 本稿ではプログラミングの効率化の為に更に3倍したものを単次元とします。
例)3次元(2*2*2)
例)6次元(4*4*4)
例)9次元(16*16*16)
キー
単にキーと指す場合はモートンオーダーにおける3次元を一つの数値に変換した物(モーザー数列)とします。モートンオーダー
長いのでzオーダーと言います。物体が収まる最小次元の選定
一部前回の記事とかぶりますが、前回の部分も最適化も兼ねます。
- ある物体のサイズがおさまる次元数
- 以下の黄色丸の半径は0.03です。紫の収まる次元を求めます。
- 半径を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ビット)の世界はどうなるのか?と言うことを考えます。yxが欠けたZだけの世界はどうなのかと言うと、こうなります。
これを次元の端数とみなすと、例えば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点間が入る最適な次元を求めます。
イメージとしてはこんな感じです//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; }
キーが図のように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で説明)
分割数算出
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の象限への配置が必要と分かります。
- ケースで場合分けしているのは、後の処理を勘案するとソーティングされていた方が効率的なのでこの時点で既にソーティングしてしまいます。 この時に最小のソーティングで済ませる為に、場合分けで済ませます。
- ex)下図の例だと、1→4→3→6となってしまう。 こういうパターンもある為 (簡単の為2Dで説明)
値は最大で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>
- 投稿日:2020-11-18T15:47:18+09:00
Unityテクスチャアトラス
- 投稿日:2020-11-18T15:06:49+09:00
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;
の設定を追加しました。
EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle; EditorUserBuildSettings.exportAsGoogleAndroidProject = true; // 実行 var buildReport = BuildPipeline.BuildPlayer( sceneList, //!< ビルド対象シーンリスト Path.Combine(Directory.GetCurrentDirectory(), AndroidBuildDirName), //!< 出力先 BuildTarget.Android, //!< ビルド対象プラットフォーム BuildOptions.Development //!< ビルドオプション );無事に実行でき、指定の出力先に Gradle プロジェクトができていました。
Unity 2018 のときとディレクトリ構成は変わっています。参考
- フォーラム
- BuildOptions.AcceptExternalModificationsToPlayer
- https://docs.unity3d.com/ja/current/ScriptReference/BuildOptions.AcceptExternalModificationsToPlayer.html
- Unity 2018 から変更があるように見えないため、実行時エラーになったのはなんでだろうと思っている。
- EditorUserBuildSettings.exportAsGoogleAndroidProject
- 投稿日:2020-11-18T15:06:49+09:00
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;
の設定を追加しました。
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 のメンテナンスが楽になりそうな印象。参考
- フォーラム
- BuildOptions.AcceptExternalModificationsToPlayer
- https://docs.unity3d.com/ja/current/ScriptReference/BuildOptions.AcceptExternalModificationsToPlayer.html
- Unity 2018 から変更があるように見えないため、実行時エラーになったのはなんでだろうと思っている。
- EditorUserBuildSettings.exportAsGoogleAndroidProject
- 投稿日:2020-11-18T07:36:14+09:00
[Unity初級]デバッグをひと工夫(Debug.Logの応用)
- 投稿日:2020-11-18T03:25:07+09:00
[Unity]モーションのフレームレートを使ったアニメチックな表現
アニメっぽい表現
アウトラインを付けたり影を閾値で分けたりすることで、3Dモデルをアニメっぽいビジュアルにする、いわゆる セルルック とか トゥーンシェーダー と呼ばれる表現は随分前から使われています。
昨今はその技術もかなり一般化されていて、MMDのモデルに適用したり、Vtuberさんのモデルで使われるなど、非商業作品の中でも目にすることが多い印象です。
▲ユニティちゃんトゥーンシェーダーのお世話になった人は多いはず映像としてのアニメっぽい表現
そんな中、よりアニメっぽい絵作りをするためにこれから普及してきそうなテクニックが「フレーム落とし」かなーと個人的に思ってます。
まずはこちらをご覧ください。What’s up danger? #MilesMoralesPS5 pic.twitter.com/a657EjeHnY
— Miguel Lozada (@MLozada) November 13, 2020絶賛プレイ中の人も多いはず。PS5のスパイダーマンのスパイダーバースリスペクトな衣装でのみ見れる表現(たぶん。誰かPS5ください)
ドラゴンボールファイターズの感動は忘れられない!原作再現度もさることながら、アークのセルルックグラフィックのこだわりは尋常じゃない。ギルティ新作も楽しみ…!
— いつものバーガー (@flankids) August 18, 2020
#グラフィック最強ゲーム選手権 pic.twitter.com/h4VR22iYhNドラゴンボールファイターズのアニメーション。カット演出では特に顕著に見れる表現。
お分かりいただけたでしょうか?
つまり、カメラやオブジェクトの位置・角度変化のfpsはそのままに、モーションのfpsのみを落として、テレビアニメっぽい雰囲気を出す
というネタです。最近はアニメの品質がどんどん上がってきて、フレームレートが高くぬるぬる動くものもよく見かけますが、とはいえゲームで主流の60fpsで動くアニメはほぼ無いはずです。(雑に調べたかぎり、普通は8fps、フルアニメーションで12fpsだとか)
ここで紹介したものは、そういう現状を汲んで「キャラクターの動きのフレームレートを落とすと普段目にするアニメに近い雰囲気になる」という文脈の表現だと勝手に解釈してます。というわけでやってみた
▼これが・・・
10FPS pic.twitter.com/OYP3wSjdo8
— いつものバーガー (@flankids) November 17, 2020
▲こうなりました。なんとなくスパイダーバースっぽくなったような気がしません?どうでしょう。
実装方法
指定した AnimationClip を任意のfpsで再生する
AnimationFPSController.cs
を作ります。AnimationFPSController.csusing 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オブジェクトにコンポーネントを追加します。
- Clip:再生したいアニメーションファイル
- Fps:再生時のフレームレート
※AnimatorにはAnimatorControllerを設定しないでください
簡単な解説
Animatorの低レイヤーな部分を制御できる、
PlayableAPI
を使っています。
(詳細はテラシュールブログさんにお任せします・・・)Updateメソッド内でモーションの更新処理をスキップさせ、設定した
Fps
に応じてタイミングが来たら更新を実行しています。
ポイントは、「スキップした時間を保持して、更新タイミングでその分アニメーション時間を進める」 というところです。その他
- トゥーンシェーダーを使ったモデルとの親和性は非常に高いと思います。是非組み合わせてみてください
- SpringBoneなどの揺れものをつかったモデルの場合、揺れものの更新fpsも一致させないと違和感が出そうです。注意!
- 表現としてだけでなく、LOD的な用途にも向いてると思います。近年のゲームで結構見かけます
- マリオオデッセイのニュードンクシティの街の人々
- モンハンワールドの一部の敵
- ドラクエモンスターズ2 3DS版のフィールドの敵シンボル
PlayableAPI
だけでのモーションの制御は難易度が高そう…うまくやればもっと汎用的にできる?
- 投稿日:2020-11-18T03:25:07+09:00
[Unity]スパイダーバース感!?モーションFPS制御によるアニメチックな映像表現
アニメっぽい表現
アウトラインを付けたり影を閾値で分けたりすることで、3Dモデルをアニメっぽいビジュアルにする、いわゆる セルルック とか トゥーンシェーダー と呼ばれる表現は随分前から使われています。
昨今はその技術もかなり一般化されていて、MMDのモデルに適用したり、Vtuberさんのモデルで使われるなど、非商業作品の中でも目にすることが多い印象です。
▲ユニティちゃんトゥーンシェーダーのお世話になった人は多いはず映像としてのアニメっぽい表現
そんな中、よりアニメっぽい絵作りをするためにこれから普及してきそうなテクニックが「フレーム落とし」かなーと個人的に思ってます。
まずはこちらをご覧ください。What’s up danger? #MilesMoralesPS5 pic.twitter.com/a657EjeHnY
— Miguel Lozada (@MLozada) November 13, 2020絶賛プレイ中の人も多いはず。PS5のスパイダーマンのスパイダーバースリスペクトな衣装でのみ見れる表現(たぶん。誰かPS5ください)
ドラゴンボールファイターズの感動は忘れられない!原作再現度もさることながら、アークのセルルックグラフィックのこだわりは尋常じゃない。ギルティ新作も楽しみ…!
— いつものバーガー (@flankids) August 18, 2020
#グラフィック最強ゲーム選手権 pic.twitter.com/h4VR22iYhNドラゴンボールファイターズのアニメーション。カット演出では特に顕著に見れる表現。
お分かりいただけたでしょうか?
つまり、カメラやオブジェクトの位置・角度変化のfpsはそのままに、モーションのfpsのみを落として、テレビアニメっぽい雰囲気を出す
というネタです。最近はアニメの品質がどんどん上がってきて、フレームレートが高くぬるぬる動くものもよく見かけますが、とはいえゲームで主流の60fpsで動くアニメはほぼ無いはずです。(雑に調べたかぎり、普通は8fps、フルアニメーションで12fpsだとか)
ここで紹介したものは、そういう現状を汲んで「キャラクターの動きのフレームレートを落とすと普段目にするアニメに近い雰囲気になる」という文脈の表現だと勝手に解釈してます。というわけでやってみた
▼これが・・・
10FPS pic.twitter.com/OYP3wSjdo8
— いつものバーガー (@flankids) November 17, 2020
▲こうなりました。なんとなくスパイダーバースっぽくなったような気がしません?どうでしょう。
実装方法
指定した AnimationClip を任意のfpsで再生する
AnimationFPSController.cs
を作ります。AnimationFPSController.csusing 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オブジェクトにコンポーネントを追加します。
- Clip:再生したいアニメーションファイル
- Fps:再生時のフレームレート
※AnimatorにはAnimatorControllerを設定しないでください
簡単な解説
Animatorの低レイヤーな部分を制御できる、
PlayableAPI
を使っています。
(詳細はテラシュールブログさんにお任せします・・・)Updateメソッド内でモーションの更新処理をスキップさせ、設定した
Fps
に応じてタイミングが来たら更新を実行しています。
ポイントは、「スキップした時間を保持して、更新タイミングでその分アニメーション時間を進める」 というところです。その他
- トゥーンシェーダーを使ったモデルとの親和性は非常に高いと思います。是非組み合わせてみてください
- ストップモーションアニメ風とも取れるので、クレイアニメっぽい雰囲気も作れそう(ニャッキとかピングーみたいな)
- SpringBoneなどの揺れものをつかったモデルの場合、揺れものの更新fpsも一致させないと違和感が出そうです。注意!
- 表現としてだけでなく、LOD的な用途にも向いてると思います。近年のゲームで結構見かけます
- マリオオデッセイのニュードンクシティの街の人々
- モンハンワールドの一部の敵
- ドラクエモンスターズ2 3DS版のフィールドの敵シンボル
PlayableAPI
だけでのモーションの制御は難易度が高そう…うまくやればもっと汎用的にできる?
- 投稿日:2020-11-18T03:09:56+09:00
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を起動
どうも,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を開きます.
ここでどのボタンがどれに対応しているのかを,
ボタンを押したときBITと書かれた部分の数値が動いたをボタンをメモしておきます.
すべてのボタンのメモが取り終わったら,上記画像のオレンジ色の矢印の部分をクリックしてください.
すると上記画像のようなウィンドウが開きます.
この画像のOffsetの隣のBITの部分がボタンに割り当てられているBITになります.
先ほどとったメモを元にボタンとBITの対応表を作ります
今回のHORIPAD_Sの場合はこのようになりました.C#にBITとボタンの対応を落とし込む.
調査が終わったのでマニュアルを読みながらC#に落とし込んでみます.
HoriPadSController.csusing 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で設定を取る方法もあります.
[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を調べておきましょう
上記画像の[HID Descriptor]をクリックすると
このような画面が出てきますのでVenderIDとProductIDをメモしておきます.
Windowsのデバイスマネージャーからでも調べることができますのでUnity側で機器が認識されない場合は
デバイスマネージャー>調べたい機器のプロパティ>詳細タブ>ハードウェアID>
「~VID_XXXX&PID_XXXX~」部分がVenderIDとProductID
メモし終わったら以下のようにプログラムします
HoriPadS.csusing 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の下の方に
このような感じにclass名で命名したGamePadが登録されているはずです.
Unity側で認識されていない機器であればこの時点で接続してやれば,おそらく動くはずです.しかし,HORIPAD_Sのように既に別の機器として認識されている物はこのままでは動きません.
HoriPadS.csusing 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<>で既に登録されている構成に上書きしないと動かないです.
英語のマニュアルでは新しく構成を登録して終わりとなっているので,注意してください.上手くいくと
上記のDevices直下のアイコンが目的のものに変化しているはずです.
この後は正常に動作しているかの確認作業になりますので,InputSystemをDLしたPackageManagerから
Visualizersなどを追加DL・使用して入力が想定通りに動いているかなどをチェックしてください.
スティック部分のデータ構造は変更前のものはoffset以外あまり当てにならないので既定のSwitchのProコンなどの設定を参考に
データ形式を変えたりoffsetを変更して調整してください.最後に
今回は新しいInputSystemで誤認されているHIDデバイスの修正方法を,英語のマニュアルを元に実践しました.
実際に私が行ってみてわかりづらかった部分などは補足したつもりですが,InputSystemの理解の一助になれば幸いです.※修正
HoriPadSController.csのbit表記がenumでグループ分けしたものだったのを
通常の数字に修正.以前の表記方式はGitHubに上がってます……2020/11/19
- 投稿日:2020-11-18T00:09:16+09:00
[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実践ゲームプログラミング」という本があるので、がっつり触ってみたい!という人はこちらを参照すると良いかと思います。