20190228のUnityに関する記事は8件です。

力尽く!!「Leap Motion + Unity」で自作モデルを動かす

はじめに

この記事は、公式ドキュメントを読むことをあきらめた一匹のゴリラが、腕力にものを言わせてLeap Motionで自作モデルを動かすまでのストーリーです。

公式ドキュメントにならった綺麗な実装が書いてあるわけではないのであしからず

ターゲット

以下の同士(ゴリラ)諸氏におかれては、ためになるかもしれない

  • 公式ドキュメント読むの疲れたよー
  • そもそもドキュメントが英語でよくわからないよー
  • 「Final IK」ってなに?
  • Leap Motion買って、金欠だよー
  • とりあえず、それっぽく動けばいいや

あらすじ

ゴリラ「Leap Motion買ったし、SDKも準備したし、手のモデルも作ったし、そろそろ動かすか!」

今回使用するモデル

~サンプルシーンを眺めるゴリラ~
inspector.png

ゴリラ「設定項目多すぎ、26個もQuaternion手打ちとか、正気の沙汰じゃない」

公式ドキュメント(英語)を眺めるゴリラ~
ゴリラ「AutoRigで自動設定とかあるんだ、やってみよ」
Hand.png
ゴリラ「手の形変わってるし、握ったとき指重なってるし、なんかやだ」

どうするの

公式ドキュメントを探し続ければ、確信的な設定項目が見つかるかもしれないが、すでにサンプルシーンではそれらしく動く手が存在している。
LeapHand.gif
もうさ、こいつの動きをトレースしちゃおうよ、やっちゃおうよ

こうするの

ソースコード

Offset.cs
/// <summary>
/// Quaternionの差分の保持
/// </summary>
private class Offset
{
    /// <summary>
    /// 対象モデルのQuaternion
    /// </summary>
    private Quaternion Model;
    /// <summary>
    /// 参照モデルのQuaternion
    /// </summary>
    private Quaternion Leap;

    /// <summary>
    /// Quaternionの差分を保持し、参照モデルのQuaternionの変化を対象モデルへと伝搬する
    /// </summary>
    /// <param name="model">対象モデル</param>
    /// <param name="leap">参照モデル</param>
    public Offset(Quaternion model, Quaternion leap)
    {
        Model = model;
        Leap = Quaternion.Inverse(leap);
    }

    /// <summary>
    /// オフセットの適用
    /// </summary>
    /// <param name="currentLeap">現在の参照モデルのQuaternion</param>
    /// <returns>オフセットを適用した対象モデルのQuaternion</returns>
    public Quaternion ApplyOffset(Quaternion currentLeap)
    {
        return currentLeap * Leap * Model;
    }
}

/// <summary>
/// 指のオフセット
/// </summary>
private class FingerOffset
{
    private Offset Bone1;
    private Offset Bone2;
    private Offset Bone3;
    private Offset Bone4;

    public FingerOffset(Finger model, Finger leap)
    {
        if (model.Bone1 != null && leap.Bone1 != null)
        {
            Bone1 = new Offset(model.Bone1.transform.rotation, leap.Bone1.transform.rotation);
        }
        Bone2 = new Offset(model.Bone2.transform.rotation, leap.Bone2.transform.rotation);
        Bone3 = new Offset(model.Bone3.transform.rotation, leap.Bone3.transform.rotation);
        Bone4 = new Offset(model.Bone4.transform.rotation, leap.Bone4.transform.rotation);
    }

    public void ApplyOffset(Finger model, Finger leap)
    {
        if (Bone1 != null)
        {
            model.Bone1.rotation = Bone1.ApplyOffset(leap.Bone1.rotation);
        }
        model.Bone2.rotation = Bone2.ApplyOffset(leap.Bone2.rotation);
        model.Bone3.rotation = Bone3.ApplyOffset(leap.Bone3.rotation);
        model.Bone4.rotation = Bone4.ApplyOffset(leap.Bone4.rotation);
    }
}

private class HandOffset
{
    private FingerOffset Thumb;
    private FingerOffset Index;
    private FingerOffset Middle;
    private FingerOffset Ring;
    private FingerOffset Pinky;
    private Offset Wirst;
    private Vector3 OffsetWirst;

    public HandOffset(Hand model, Hand leap)
    {
        Thumb = new FingerOffset(model.Thumb, leap.Thumb);
        Index = new FingerOffset(model.Index, leap.Index);
        Middle = new FingerOffset(model.Middle, leap.Middle);
        Ring = new FingerOffset(model.Ring, leap.Ring);
        Pinky = new FingerOffset(model.Pinky, leap.Pinky);

        Wirst = new Offset(model.Wirst.rotation, leap.Wirst.rotation);
        OffsetWirst = model.Wirst.position - leap.Wirst.position;
    }

    public void ApplyOffset(Hand model, Hand leap)
    {
        Thumb.ApplyOffset(model.Thumb, leap.Thumb);
        Index.ApplyOffset(model.Index, leap.Index);
        Middle.ApplyOffset(model.Middle, leap.Middle);
        Ring.ApplyOffset(model.Ring, leap.Ring);
        Pinky.ApplyOffset(model.Pinky, leap.Pinky);

        model.Wirst.rotation = Wirst.ApplyOffset(leap.Wirst.rotation);

        model.Wirst.position = leap.Wirst.position + OffsetWirst;
    }
}
LeapMotion.cs
[SerializeField]
private Hand HandLeft;
[SerializeField]
private Hand HandLeapLeft;
[SerializeField]
private Hand HandRight;
[SerializeField]
private Hand HandLeapRight;


private HandOffset HandOffsetLeft;
private HandOffset HandOffsetRight;


private void Awake()
{
    HandOffsetLeft = new HandOffset(HandLeft, HandLeapLeft);
    HandOffsetRight = new HandOffset(HandRight, HandLeapRight);

    GameObject obj;
    obj = new GameObject("LeftHandOffset");
    obj.transform.parent = gameObject.transform;
    obj = new GameObject("RightHandOffset");
    obj.transform.parent = gameObject.transform;
}

private void Update()
{
    HandOffsetLeft.ApplyOffset(HandLeft, HandLeapLeft);
    HandOffsetRight.ApplyOffset(HandRight, HandLeapRight);
}

ソース全体はこちら

解説

特に解説する余地もない、力技のソースですが、簡単に説明すると

  1. サンプルのモデル(参照モデル)と自作モデル(対象モデル)の各関節ごとのQuaternionの差分を保持する。
  2. 参照モデルの動き(Quaternonの変化)にオフセットを適用して、対象モデルのQuaternionを更新する。
  3. 1.2.をすべての手、すべての指に適用する。

おわりに

実行結果

Hand2.png

今後の課題

結果としてAutoRigより見た目はマシだが、依然として設定項目が多いので(指(5本) × 骨(4本)のTransformを設定する必要がある)、このスクリプトにもAutoRigのような自動で指のボーンのTransformを設定するスクリプトを組み込みたい。

感想

個人的にはそれなりに動くし、及第点ではあるんだけど、設定項目が多すぎるのがやっぱりつらいところ

いっそのこと、公式ドキュメントを漁った方がより早く幸せになれたかもしれない。
より早く幸せになった人は、是非ともその方法を教えてください。

自分の場合は、AutoRig微妙ではあったんだけど、モデル作成の段階からきれいに動かすノウハウとかあるのかもしれない。
そんなノウハウを知ってる方は(略

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

UnityでAndroidのアプリ作成実践

はじめに

UnityでAndroidのアプリ作成を試してみたくて、まずはチュートリアルを試してみました。
https://unity3d.com/jp/learn/tutorials/projects/hajiuni-jp

開発環境

Unity 2018.3.0f2
Visual Studio 2017
Android Studio 3.3.1
Unity Remote5
GitHub for Unity 1.2.1

チュートリアルから修正

チュートリアルの説明に従い1~7まで作成したところで、一応PC上ではゲームとして遊べる状態になったので、Android用のビルド、実機上のデバッグまで実行してみました。
手順は以下ページが参考になりました。
https://qiita.com/taroyan/items/2ff7f6aadd640221bde5

チュートリアルはPC向けのため操作はキーボードなのですが、このままではAndroid上で操作できません。
操作に対応するコードPlayerControllerをタッチ操作できるように書き換えます。
以下ページが参考になりました。
https://docs.unity3d.com/jp/460/ScriptReference/Input.GetTouch.html

Unity Remote

実機上でデバッグするためにはAndroid用にビルドする必要があるのですが、非常に時間がかかります。
PC上だけでは、タッチ操作などAndroidに依存する機能はデバッグできないため、簡易的にデバッグするツールとしてUnity Remoteが使えます。

ソースコード管理

ここまでできたところで、GitHub上でソースコード管理してみようと考えてGitHub for Unityを導入しました。
手順は以下ページが参考になりました。
http://kan-kikuchi.hatenablog.com/entry/GitHub_for_Unity
Unity上から簡単にGitHubへのpushまで行えました。
gitignoreファイルが自動で作成されるので非常に助かります。
今回作成したリポジトリは以下になります。
https://github.com/SatoshiTakahama/Roll-a-Ball.git

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

Androidアプリの GPGSはストア経由じゃないと動作しない?

その1

GPGS(GooglePlayGameServices)の機能でリーダーボード(ランキング)を実装しようと
いろいろと調べて作成しました。

主にこちらのブログを参考にして
https://indie-du.com/entry/2017/02/13/070000

認証つまずくも、試行錯誤の末できたはずが・・
apkを作成して、端末にインストール

Social.ShowLeaderboardUI();

でリーダーボードが表示された。
おお!
では、スコアを登録

Social.ReportScore(mScore, nLeaderboard[mPlayMode], (bool success) => {
       // handle success or failure
       isFinished = true;
       if (!success){
           isError = true;
       }
});

できたようだ。

再び、スコアボードの表示を行うと、対象のボードを表示するとエラーが

Playゲームで問題が発生しました
のエラーが表示されてランキング表示されない。
スコアを登録するとこの現象が発生し、未登録の場合は

このゲームではまだハイスコアが公開されていません。

のメッセージ・・

調査した結果
Unityや、adb.exe 経由でinstallしたアプリだと、GPGSの動作に影響がでるようで意図した動作にならなかった。
logcat で確認したところ、それっぽい警告が出ていたのですが、保存してなかったのと
再現ができなくてちょっと確信がもてないけど・・

ストア経由で同じapkを入れたところ問題なく動作ができた。

エビデンスがちゃんと取れていないので、個人的な備忘録です。

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

[SerializeField] 属性をつけていると、CS0649警告が出るようになった。消したい。

  • 環境 2018.3.5f1

Unity 2018.3 にアップグレードしたら、
.NET 3.5 (deprecated) ということで、.NET 4.x にしました。

そしたら、とてもたくさんの・・・
100個以上たくさんの、、Warning が表示される
xxx.cs(23,37): warning CS0649: Field 'xxx.obj' is never assigned to, and will always have its default value null

邪魔すぎるので、消す方法を調べる。

Warning CS0649 not suppressed properly when field is marked as [SerializeField] - Unity Forum

before [SerializeField] GameObject obj;

方法1 [SerializeField]を忘れて、すべてを 'public'にする

public GameObject obj;

方法2 #pragma warning disable 649

#pragma warning disable 649
[SerializeField] GameObject obj;
#pragma warning restore 649

方法3 '-nowarn:0649'を含むcsc.rspファイル

Assets直下に、「csc.rsp」というファイルを作成する

csc.rsp
-nowarn:0649

方法4 デフォルト値で初期化する

[SerializeField] GameObject obj = default;

検索:\[SerializeField\]([^=]+?);
置換:[SerializeField]$1 = default;

Visual Studio for Mac だと、
Command + Shift + F キー
[フォルダーを指定して検索]/[フォルダーを指定して置換]

スクリーンショット

方法1だけは無いなと思うし、個人的には方法4が
この中ではお気に入りです。

おわり

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

元プロゲーマーが知識0からプログラマーになって2年でアプリ8本リリースするまでの話

0.はじめに

プロゲーマー を辞めて、知識0から ゲームプログラマー になり2年が経ったので、
アプリを8本リリースしたりしながら得た知見を備忘録代わりに記事にします。

また、理系でもなくプログラムに一切触れずに生きてきて、音大志望だった私がこの業界に飛び込み、
プログラマーとしての「あるべき思考回路」「一般的な視点」を習得するまでに大変苦労したので、2年前の自分が知っておきたかった心構えも後半にまとめます。

あと個人的な事ですが…能動的に露出する事に抵抗を覚える人間なので、その苦手意識を克服したいという目的もあります。

1.どんなプロゲーマーだった?

プロゲーマーの定義が多種多様なので、まず私自身がどのようなプロゲーマーだったのか軽く触れておきます。
私がプレーしていたのはLeague of Legends(通称LoL)という世界で最も競技人口が多く月間約1億人がプレイしているゲームで当時LoLでは日本1位のチーム「DetonatioN」に1年半所属していました。

LoLのチームはゲーミングハウスといわれるシェアハウスで、「選手」「アナリスト(ゲームを分析、チームを改善する人)」「栄養管理士(シェフ)」「コーチ」「スタッフ」…等おおくの人が共同生活を送り、日々世界一を目指して活動しています。

私は「選手」として契約していて、衣食住の確保、スポンサーからの支援(Logicool、NVIDIA、KDDI etc)、大会の賞金などがあり、プロゲーミングチームとして日本初の月額給与制を取り入れたチームでもあり、とても恵まれた環境でゲームに競技として真剣に向き合う事ができました。

2.プログラマーになるまで

現役を引退した時に、ゲームをプレイする側から作る側にシフトしたいと考え、就職活動を開始しました。
人生は環境ゲーな所があるので、技術習得がしやすいか、将来像がマッチしてるか、人間関係が良好そうかを
判断基準にとりあえず自分の希望に合った会社を探して行きました。

転職支援サービスをふんだんに使い、
大雑把に目星を付ける(300社)→優先順位を付け書類を送る(100社)→優先順位が高い所から書類を通し面接していくという流れでした。

知識0未経験の人間がただ書類を出すだけでは通ると思えなかったので、
各会社用に企画書を作ったり元プロゲーマーとしてのキャリアをどう活用できるかをアピールしたりした結果、
幸いにも興味を持って下さった会社が複数あり、いくつかの内定を頂けました。
その中に、自分にマッチしていると感じる会社が見つかったので、およそ3カ月で就活終了

面接の中で印象的だったのは、
未経験なことを受け入れてポテンシャルを見ようとしてもらえる会社から、圧迫面接でテンプシーロールを食らわせてくる会社まで様々だったことでした。プロゲーマーという肩書きは興味を持ってもらうところまでは効果ありますが、その後は「ゲームとどうやって向き合ってきたか」が問われる気がします。

3.プログラマーとして

ソシャゲの開発&運用、VR、AI…等をやっている会社で働きました。

初期は本当に何もわからなかったのでUnityとC# を中心に勉強をはじめ、ゲームのレベルデザイン、バグ対応、ソシャゲ運用(中級コンテンツの作成)などに携わり徐々にプログラマーとしての知識を蓄えていきます。
技術力が身につくと共にソシャゲ開発チームに配属されたり、個人でアプリを開発するような業務を任されるようになりました。

ちなみにプログラマーとして歩み始めた頃は、独習C#をざーっと読んだり、Unityの玉転がしで阿鼻叫喚したり、Unity猫本で猫と戯れたり、会社から無理難題を投げつけられ火を吹いたり色々しました。
また、運用中のソシャゲのバランス調整やレベルデザインを適切なものにするにはゲームを誰よりも理解している必要がある→まずランキング1位になる必要があるのでは、ということで実際に対人要素で1位までやりこんだこともあります。
このあたりはプロゲーマーのスキルが直接役に立った部分です。

幸運なことに、会社の先輩は人柄が良く尊敬できる方ばかりでした。こんな未経験の雑魚が業務に携われるようになったのはひとえに親身に教えて下さった方々のおかげなので、とても恵まれた環境の中でプログラマーとしてのキャリアをスタートできたと思います。

プログラム チョット デキル

4.アプリリリース8本する

入社して1年ちょっとでアプリ開発をはじめ、これまでにカジュアルゲームといわれるアプリを8本リリースしました。

その中で身にしみた、プログラマーとしての忘れたくない(独断と偏見による)心構えを3つ書き記します。

5.プログラマーとしての3つの心構え

①わからないことを明確にする


「ゲーム作りたい。でもなんか難しそう。」「ゲーム上手くなりたい。でも何すれば上手くなるのかわからない」「水飲みたい。でも水って何?どれ?」「なんかよくわかんない」
.

わからないことがわからない状態

そんな状態は自分も他人も誰も解決できません。

これはプログラマーにとってとても危険な状態ですが、私も入社時はこの状態になることが多くありました。

はじめはわからない事をわかる(理解する、解決する)ようにならなくてもよくて、
わからない事を明確にする事(何がわからないのか?)が重要だと考えを切り替えるのが最初の一歩。

... 何がわからないかが明確になれば、あとはgoogle先生が答えを見つけてくれるんだ。

.
.

勿論、はじめから理解している量が多ければ多いほど良いけれど、プログラムの世界において全部記憶しておくのはほとんどの人間にとって無理だし、この「わからないことを明確にして」「googleに答えを見つけてもらう手法」さえ構築すれば生きていけるとわかりました。

私自身は、その認識に辿り着くまでに無駄な努力を多く挟んでしまいました。これは覚えておかないととか、これは絶対に理解しておかないとという強迫観念に追われて時間を使うよりも、その都度わからないことを調べて解決するフローに徹した方が、技術力が上がるスピードも上がりました。

googleさんに効率的に助けてもらううえで役に立つのは、英語で検索する力をつけること、Debug Log信者になること、今の状態を言語化して考えること…等でしょうか。

.
.

②物事を細分化して考える


何か目的に向かっていく時、細分化して考えたほうが解決が早くなります。

例えば

「RPGゲームが作りたい」 
→ 「Unityで」
終わり

のように、抽象的に物事を完結させて行動するよりも、

「RPGゲームが作りたい」

→「戦闘の実装をターン制にする」→「攻撃/魔法/防御コマンドをまず作る」→「攻撃時の処理」
→「会話シーンを作りたい」→「会話シーン用の汎用的な実装を作るべき」→「会話シーン発生時に呼び出す処理をかく」→ 
                               「会話シーン用に適切なメッセージを呼び出せる処理と会話データベースを作る」

といった風に、細かく具体的であればあるほど解決が早くなります。

ゲームでいえば
思い返せばプロゲーマーの時も同じことをしていたような気がします。当時の思考を辿るとこんな感じでしょうか。

「対人ゲームが上手くなりたい」
「何が問題点か?」
・「操作精度が悪い」→「試合前に30分ミスしやすい操作を練習する」、「入力の仕方を見直す」
・「使用キャラクターの理解度が低い」→「仕様を理解する」、「上級者の動きを動画で学ぶ」
・「何で負けてるのかわからない」→「試合ごとにターニングポイント(勝敗に最も貢献した時)を明確にする」→
「その原因を試合ごとに言語化して、脳内でよく反芻する」

ということをよくしていました。あくまで私の場合はですが、プログラミングを学習するうえで、ゲームの上達方法を応用できる部分は想像以上に多かったと感じています。

また、この能力を培う為には日々、考える視点を持つことが効果的です。

例え:
上級者のゲームプレイ動画を見た時... 自分なら同じことをするか? できるか? 彼らが何をしているか理解できてるか?
新しいゲームアプリを見た時... このゲームのこの部分はどういう実装になってるのか?
音楽を聞いた時... 曲を聞いた時に楽器構成、曲の構成を認識して脳内で遊ぶ。
外で歩いている時(美術)... あの屋根は自分ならどういう風に描くか? どういった画材を使いどういう風に色を混ぜるか?

こういった視点の癖をプログラマーとして持つ事によって、 頭の中でコードを創造する技術が上がる事に気がつきました。

③形にする


とにかく「完成」を目指す。この時目指すものは「完璧」ではなくていい。

Twitterで見た開発者ツイートで印象的だったのが(ソース紛失)

リリースできる人とできない人の違い

できる人
「β版0.9リリースしました」
「バグ修正のβ版0.95リリースしました」
「機能追加のβ版0.98リリースしました」
「諸々修正のβ版0.99リリースしました」

できない人
「UIもっとよくしたい」
「戦闘部分改良中」
「デザインもっとかっこよいのにしたい」
「バグ沢山あるので修正してる」

というのがとても的を射ているなぁと。

プロゲーマーなんかは特に完璧主義 な人が多いと思うのですが、そういうタイプの人って「リリースしない人」になりがちな気がします。
なんか許せないんですよね。自分で納得できない状態で人前に出すのが。

しかし、ここは「開発」です。
「犬」を作りたいならば、犬の「目」を完璧に作ってから「足」を完璧に作るなんて事してたら途方に暮れてしまいます。

「犬」を作るなら「最低限犬と認識できるもの」をまず作るのが優先で、「骨格」「色」くらい作っとけば
とりあえず β版完成 です。リリースしないにしても、開発の全体像に目を向けずに欲望で細かいところにこだわっていても完成しません。
.
.

今よりも良い実装なんて探せば何かしら存在するだろうし、納得する形を求めだしたら欲望の底なし沼に嵌ります、拘り出したらキリがないのが開発だと思いました。
今ある力で実現可能な中で最高の完成を目指せば良いのであって、その為には不必要な完璧は断捨離しないと、いつまで経っても完成品ができない。

.
.
これは極論ですが、UnityでUpdateにGetComponentするという死刑級の事をしても、処理上問題なく動く形になってるならまずはそれでいいんです。初学者なら特に。次の修正版でより良い実装を調べて改善すればいいし、まずは完成させるための事だけに傾注すべきです。

.
.
最終型を頭に描いてまず作ってみる、やってみるという姿勢は、リリースできるかどうかだけでなく自分の成長にも大きく影響してくると感じています。
これもプロゲーマー時代の知見ですが、

プロゲーマーの先駆者ウメハラさんの「結果としての勝敗そのものは、それほど重要視するものではない。...継続的なところにこそ、本当の勝利はある」という考えに触れて、私がプロゲーマーだった時も「その時の勝敗ではなく、将来の強さにつながる行動」の重要性を意識するようになりました。
棋士の羽生善治さんも、「常に一番強い手でいってはいけない。その局面において一番強い手が最善の手ではない。...先を見越した一手には、見通す目と理性が必要」と言っています。
ゲームにおいて、目の前の試合で勝てる行動将来的に長く勝てるようになる行動が違うように、開発においても完成に目を向けずに局所的な最適化をする「今の完璧」と、自分の開発力が結果的にあがる「将来の完璧」は違うように思います。※1※2

将来の力を上げる上で、とにかく「完成」を目指す。

今、こんなものが作りたいと思う気持ちがあるなら半年後に作れる姿になる為の最適な行動とは? と考えると
人に見られて恥ずかしいとか、プライドがどうとか、自分が許せないとか、そういう邪魔な要素を排除して、とにかく半年後から逆算した思考でやるのが大切な気がしました。

この記事を書いてるのも、アウトプットの選択肢として文章も持っていた方が強いので、その練習の一環という意味もあったりします。

6.プロゲーマーとしてのキャリアがどのようにプログラマーとして活かされたか?

野球の選手にしろ、棋士にしろ、プロゲーマーにしろ、何かの世界でトップを目指す人の中には、どんな物事に取り組んでも上に行くための筋道を立てる思考を鍛えている人が多いと思います。私の場合も、ゲームを極めようとした方法論が、プログラムの習得に想像以上に応用できた感覚があります。
特に私は現役時代から、1つの特殊な山を登る能力よりも、どんな山でも登れる普遍的な能力を意識して生きてきたので、プログラマーとしても実際に0知識から山を登り始めている感覚を持てて喜んでいます。

とりあえず、ゲームをプレイするだけだった人でもゲームを作る人(まだそれがゴミコードでも)になれるらしい。

7.あとがき

ここ最近、また新しい分野に手を出していて自分の考え方とか知見を振り返る事が多く、将来の自分のために一度整理しておきたかったのに加えて、何も知識がない人間が少しずつ色んなことができるようになる過程を見て勇気を持つ人がいればと思いまとめました。少しでもプログラムの楽しさに触れる人が多くなれば嬉しいです。いまのところ、プログラミングはゲームに負けないぐらい楽しいです。

実践というより精神論的な話ですし、文章を書くことに不慣れでわれながら稚拙ですが、内向的な性格との決別も込めて投下します。
未熟者ゆえどうかお手柔らかに。

参照

プロゲーマー時代の自分のTwitter https://twitter.com/yumeoti_lol

プロゲーミングチーム「Detonation」http://team-detonation.net/aboutus

※1 直感力(羽生善治著)
※2 勝負論~ウメハラの流儀~(梅原大吾著)

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

【Unity】UnsafeUtilityについて纏めてみる

Unity2018.x系統からNativeContainerが入り、それに伴う機能としてかUnityのエンジン側で確保したネイティブメモリ領域であるアンマネージドメモリを操作すること等が出来る「UnsafeUtility と言うAPIが追加されました。

UnsafeUtility自体はNativeConteinerの内部実装などで使用されている機能であり、使い方を把握すれば自身でNaticeContainerを自作すると言った事も可能となります。1

NativeConteinerの性質上、ひょっとしたらECS/JobSystemと言ったアンマネージドメモリに対する操作を必要とする処理以外ではあまり使う機会が無いかもしれませんが...必要とする所で使い方を把握しておくとコンテナの自作以外にも色々と応用を利かせられるかもしれません。
そこで今回は自分が趣味でECSやJobSystemを使う上で応用してみた実装例などを踏まえつつ、知見の方を備忘録序にメモしていこうと思います。

実装/動作環境

  • Unity 2018.3.6f1

※その他注意点

  • UnsafeUtility自体まだあまり情報が出ていないために、今回の解説や実装例に関しても割と手探りな部分が多い内容となります。(ひょっとしたら推奨される使い方をしていないと言った可能性も無きにしもあらず...)
  • 前提としてポインタの知識は必要となります。こちらの記事中ではポインタについての解説まではそこまで触れていないのでご了承下さい。。

▽ 補足: Unityのメモリ管理について

本題に入る前にUnityのメモリ管理について軽くおさらいしておきます。
Unityには大きく分けて以下の2種類のメモリがあります。

  • C#メモリ(記事中では主に「マネージドヒープ」と記載)
    • C#側で使用されているメモリであり、GC(Garbage Collection)の対象。
    • メモリが確保出来なくなると自動的にメモリ領域を拡張する。
      • 拡張されたメモリ領域は基本返ってこない。
      • CGのアルゴリズムの性質上、ヒープが拡張するのに応じてパフォーマンスが劣化。
        • ※上記を踏まえて圧迫に注意する必要がある。
  • Unityメモリ(記事中では主に「アンマネージドメモリ」と記載)
    • Unityのエンジン側が確保するネイティブメモリ領域。(Textureと言ったAsset等が含まれる)
    • GCの対象外
      • 確保したら自分で解放する必要がある。
    • 後述のNativeArray(UnsafeUtility.Malloc)で確保したメモリもこちらに含まれる。

▽ メモリの確保について

先ず最初に「UnsafeUtility.Malloc」と「UnsafeUtility.Free」について軽く解説していきます。
一言で言うとメモリの確保と解放です。
※確保したメモリの使いみちと言った具体例については後述。

以下に実装例を載せます。

// 確保するサイズ
var size = UnsafeUtility.SizeOf<int>();
// メモリアライメント
var alignment = UnsafeUtility.AlignOf<int>();
// アンマネージドメモリの確保
void* ptr = UnsafeUtility.Malloc(size, alignment, Allocator.Persistent);

// 確保したメモリの解放
UnsafeUtility.Free(ptr, Allocator.Persistent);

やっている事は単純にSizeOfAlignOfでサイズとアライメント2の値を取得し、それらをMallocに渡して割り当てを行っている感じです。後は必要に応じたAllocatorの方も指定します。※ Allocatorについては後述
確保したメモリについてはポインタが戻り値として返ってくるので、基本的にはこちらを経由してデータのアクセスを行う形となります。後は使い終わったタイミングでFreeを呼び出して解放を行えばokです。

※ちなみにポインタについては操作をミスるとEditorがクラッシュしたりする事もあるので取扱には注意する必要があります。
(現時点で分かっているEditorを確実にクラッシュさせる方法としては、解放済みのポインタに対しもう一度Freeすると死が訪れます。設計的に複数回呼ばれる想定がある場合には注意する必要があるかもです。)

▼ Allocatorについて

指定できるAllocatorの種類についても軽く補足しておきます。
Allocatorは以下の5種類から指定することが可能ですが、実際に使うのは太字の3種類になるかと思われます。
※ちなみにNativeArray生成時に前者2つを指定するとArgumentExceptionが投げられる。

  • Invalid
    • 無効な割り当て?ECSの内部実装で見た覚えがあるが詳細は調べきれていない...
  • None
    • 割り当て無し。こちらもどのタイミングで使うかについては調べきれておらず...
  • Temp
    • メモリの割り当てと解放が最も高速
    • そのフレームのみで有効
  • TempJob
    • 割り当てと解放はTempよりは遅い
    • 4フレーム以内に解放しないとエラー
    • JobのフィールドにDeallocateOnJobCompletion属性をセットしておく事でJob終了時に自動で解放する事が可能
  • Persistent
    • 割り当てと解放は最も遅い
    • 永続的に使用可能

※ちなみに補足情報として@pCYSl5EDgoさんから教えて貰ったこちらのForumに以下の書き込みがあり、中の人曰く↓との事です。

UnsafeUtility.Malloc has different allocators with massively different performance characteristics.

Temp is a stack allocator per thread. TempJob is reusing on a per frame basis across jobs. (Both of those are very fast and meant for allocations every frame)

Persistent is a TLSF allocator when lifetime is unknown.
All of them significantly faster than system allocation.

Essentially using Marshal.AllocHGlobal is always a bad idea.

Allocator.PersistentについてはTLSFメモリアロケータになるみたいですね。

▼ メモリリークに注意

Mallocで確保したメモリについては当然ながら解放を忘れるとメモリリークします。
※その上で挙動を見た感じだとEditor上でメモリリークしたらEditorを再起動するまで返ってこない気がする...。

自前メモリ管理だとどうしても付き纏ってきてしまう問題ではありますが...
NativeArray等についてはEditor環境のみではあるもののリークを監視する機能が予め備わっており、仮に解放が抜けていてメモリリークを起こしても以下のようなエラーが出力されて検知できるようになっております。

A Native Collection has not been disposed, resulting in a memory leak. It was allocated at (発生したソースのパス):(行数).

「NativeArray等については」上記のように保守性の高い実装となっているので、漏れた際のエラーハンドリングも比較的行いやすくなっているかと思われますが...UnsafeUtility.Mallocを直で叩いて確保するのはまた別の話であり、Mallocの内部実装自体にリーク検知が実装されていないので仮に解放を忘れてしまうと無言のままリークしてしまうことになります。その為に直で確保する際には特に注意する必要があります。

とは言え...注意すれと言えども限界があるかと思われるので、使う際にはMallocを直に叩かずにNativeArray同様にリーク検知機能を付ける形でラップしてから使うように検討してみても良いかもしれません。

以下のコードは私の方で試しに実装してみたものとなります。
ぶっちゃけやっている事としては長さが1のNativeArrayと変わりない感ありますが...ご参考までに。
※ちなみにリーク検知用にDisposeSentinel(参照型)をフィールドに持つためにNativeArrayと同様に非Blittableな構造体となっている点だけ注意。その為にBlittable制限のある構造体のフィールドに渡す際にはGetUnsafePtr経由でポインタを渡す必要があったりする。

// https://gist.github.com/mao-test-h/f1ed901083426d539afb823449e5a1b8
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;

public unsafe struct NativeObject<T> : IDisposable
    where T : unmanaged
{
    [NativeDisableUnsafePtrRestriction] readonly T* _buffer;
    readonly Allocator _allocatorLabel;

#if ENABLE_UNITY_COLLECTIONS_CHECKS
    [NativeSetClassTypeToNullOnSchedule] DisposeSentinel _disposeSentinel;
    AtomicSafetyHandle _safety;
#endif

    public T Value
    {
        get
        {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            AtomicSafetyHandle.CheckReadAndThrow(_safety);
#endif
            return *_buffer;
        }
        set
        {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            AtomicSafetyHandle.CheckWriteAndThrow(_safety);
#endif
            *_buffer = value;
        }
    }

    public bool IsCreated => _buffer != null;

    public T* GetUnsafePtr
    {
        get
        {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            AtomicSafetyHandle.CheckWriteAndThrow(_safety);
#endif
            return _buffer;
        }
    }

    public T* GetUnsafeReadOnlyPtr
    {
        get
        {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            AtomicSafetyHandle.CheckReadAndThrow(_safety);
#endif
            return _buffer;
        }
    }

    public T* GetUnsafeBufferPointerWithoutChecks => _buffer;



    public NativeObject(Allocator allocator, T value = default)
    {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
        if (allocator <= Allocator.None)
        {
            throw new ArgumentException("Allocator must be Temp, TempJob or Persistent", nameof(allocator));
        }

        if (!UnsafeUtility.IsBlittable<T>())
        {
            throw new ArgumentException(string.Format("{0} used in NativeObject<{0}> must be blittable", typeof(T)));
        }
#endif

        var size = UnsafeUtility.SizeOf<T>();
        this._buffer = (T*) UnsafeUtility.Malloc(size, UnsafeUtility.AlignOf<T>(), allocator);
        this._allocatorLabel = allocator;
        *this._buffer = value;

#if ENABLE_UNITY_COLLECTIONS_CHECKS
        DisposeSentinel.Create(out _safety, out _disposeSentinel, 0, allocator);
#endif
    }

    public void Dispose()
    {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
        if (!UnsafeUtility.IsValidAllocator(_allocatorLabel))
        {
            throw new InvalidOperationException("Can not be Disposed because it was not allocated with a valid allocator.");
        }
        DisposeSentinel.Dispose(ref _safety, ref _disposeSentinel);
#endif

        UnsafeUtility.Free(_buffer, _allocatorLabel);
    }
}

▽ 使用例

メモリの確保周りの解説が終わった所で使用例についても軽く纏めておきます。
※例と言えども解説するのはあくまで私個人の考察となるのでご了承下さい。

▼ Blittable制限がある構造体に持たせられる共通データとして使用

フィールドにBlittable制限がある構造体として、例えばNativeArrayとして持つ想定のデータやECSのIComponentDataを実装したデータなどが挙げられますが、これらは参照型をフィールドの持たせることが出来ないために基本的にはBlittable型をベースとした値のやり取りが発生します。
※Blittable型の詳細については後述の「▼ Blittable型について」にて解説しているのでそちらを参照。

この際に出てくる課題として「変更する可能性のある共通のデータ」を持たせたいとした場合には共通データとして参照型を渡すことが出来ないので、やるとしたら主に以下の実装などが考えられるかと思われます。

  • 愚直に値をインスタンス毎にコピーして渡す
    • → 値が不変であるならまだしも、変更が掛かるなら反映のコストが掛かる可能性
    • → メモリ使用量が多くなる
  • static領域に置いて参照
    • → データ管理の観点やJobに於けるBurstCompilerが使えなくなると言った懸念点がある
  • 共通データのポインタを渡して参照させる
    • → メモリリークに気を付ける必要がある。

幾つかの例はあれど今回解説する内容としては一番下の項目にあるポインタ渡しによる共通データ管理について軽く掘り下げていきます。

共通データ自体は上述のUnsafeUtility.Mallocでメモリを確保することでGCの影響が掛からないメモリのポインタを取得する事が可能です。ポインタ型自体はBlittable型となるので、後は取得したポインタを必要とする構造体のフィールドに渡す事で運用が可能となります。
※補足としてメモリの確保についてはMallocによるアンマネージドメモリの確保でなくとも、マネージド側にてGCHandleでアドレスと固定しポインタを取得すると言った手も考えられる。(但し長く持ってるとGCの効率が悪くなりそうな予感..使い所によりそう)

簡単な実装例としては以下な感じとなります。
NativeArray<Bullet>として持つstruct BulletのフィールドにSharedBulletParam*を持たせております。
後はロジック側でポインタから値を参照するだけです。

// 弾の共通データ
public struct SharedBulletParam
{
    public float Speed;
    public float Damage;
}

// 個別に持つ弾データ(NativeArrayとして持つのでBlittable型である必要)
public unsafe struct Bullet
{
    public SharedBulletParam* SharedBulletParamPtr;
    public float Angle;        // 角度
    public float Lifespan;     // 生存時間
}

void Initialize()
{
    // 確保するメモリサイズ
    var size = UnsafeUtility.SizeOf<SharedBulletParam>();
    // メモリアライメント
    var alignment = UnsafeUtility.AlignOf<SharedBulletParam>();
    // アンマネージドメモリの確保
    var sharedBulletPtr = (SharedBulletParam*)UnsafeUtility.Malloc(
        size, alignment, Allocator.Persistent);

    const int BulletCount = 1000;
    var bullets = new NativeArray<Bullet>(BulletCount, Allocator.Persistent);
    for (int i = 0; i < BulletCount; i++)
    {
        bullets[i] = new Bullet
        {
            // 各インスタンスに共通データのポインタを持たせる
            SharedBulletParamPtr = sharedBulletPtr,
        };
    }

    // ※使い終わったらNativeArrayとMallocしたポインタを解放すること
}

実際に上記の管理方法自体は「ECSで弾幕STGを実装した際の共通データの管理周り」や「ECS/JobSystemベースのSpringBoneを実装した際の共通データの管理」などで使用しております。
以下に参考記事や公開プロジェクトのリンクを載せておくので宜しければ御覧ください。

※補足 : ISharedComponentDataとの違い

ECSの話題にはなりますが、こちらには共通データとして持たせる想定のISharedComponentDataという物があります。
その為に「ECSならポインタでなくても共通データはISharedComponentDataで管理すれば良いのでは?」と思わなくも無かったので、両者の違いについて軽く纏めてみました。

  • ISharedComponentData
    • フィールドに参照型を持てる。(故にMaterial等を必要とする既存の描画システム(Hybrid Renderer)辺りでは使用されていたりする)
    • 値が変化する想定の作りでは無いように思われる。
    • BurstCompilerを有効にしたJobのフィールドにSharedComponentDataArrayを渡すことが出来ない?
  • ポインタ型
    • Blittable型の制限は付くものの要件を満たせるなら使い勝手は良い。
      • ポインタが指すデータを書き換えることで動的な変更も可能。
      • BurstCompilerを有効にしたJobでも使用可能。
    • メモリ管理に注意する必要がある。(解放忘れなど)

SharedComponentDataについては完全に調べきれていないところもありますが...見た感じだとお互いに向き不向きがある印象なので、どちらか一方だけで済ませずに状況に応じて使い分けていけば良いという感じはしました。

▼ データのキャッシュに使用

以前「【Unity】ファイルを非同期で読み込んでアンマネージドメモリに展開できるAsyncReadManagerを試してみた」という記事を書きました。
簡単に概要を説明すると、Unity2018.3辺りからAsyncReadManagerと言うAPIが追加されたので、こちらを用いて非同期且つ読み込んだファイルデータをアンマネージドメモリに乗っける形でキャッシュ出来ないか検証してみたという内容となります。3

記事中での検証内容としては巨大なCSVをパースして読み込んだデータをアンマネージドメモリにキャッシュする所までとなりますが、もう少し発展させて「Jobベース行う様々なデータ形式のシリアライズ/デシリアライズ」「暗号化/復号化処理」と言った所を検証して使えるところまで調べきれると、非同期且つマネージドヒープを圧迫しないデータ管理が出来るかもしれないと言った可能性があります。

あくまで仮説を前提とした内容且つ簡単な検証までとなりますが、詳細に関しては記事の方を御覧下さい。

▽ 補足情報

▼ Blittable型について

Blittable型とは「マネージド(C#)とネイティブ(C++)でメモリレイアウトが同じになる型」の事を指します。
該当する型について簡単に纏めると以下の型などが該当します。
※注意点としては値型とイコールの関係ではなく、例えばchar型やbool型などは非Blittable型となります。

  • byte
  • sbyte
  • short
  • ushort
  • int
  • uint
  • long
  • ulong
  • IntPtr
  • UIntPtr
  • float
  • double
  • Blittable型の固定長一次元配列
  • Blittable型のみを含む構造体

こちらを指定する意味合いとしては、マネージド側とネイティブ側でやり取りをする際のマーシャリングコスト(メモリコピー)を回避するためになるかと思われます。
詳細については以下の記事がわかりやすいです。

▼ データのコピー周りに関するTips

UnsafeUtilityにはデータのコピー周りに関するAPIが幾つか存在します。
例えば個人的によく使うものとして、構造体の値をポインタにコピー出来るCopyStructureToPtrとその逆を行えるCopyPtrToStructureがあります。
以下に簡易実装例を載せます。

struct SampleStr
{
    public int Param1;
    public float Param2;
    public SampleStr(int index)
    {
        Param1 = index;
        Param2 = index + index / 10f;
    }
    public override string ToString()
    {
        return $"{Param1}, {Param2}";
    }
}

void Start()
{
    // 確保するメモリサイズ
    var size = UnsafeUtility.SizeOf<SampleStr>();
    // メモリアライメント
    var alignment = UnsafeUtility.AlignOf<SampleStr>();
    // アンマネージドメモリの確保
    var ptr = (SampleStr*)UnsafeUtility.Malloc(size, alignment, Allocator.Persistent);

    // ----------------------------------------
    // 構造体の値をポインタにコピー
    SampleStr sample = new SampleStr(8);
    UnsafeUtility.CopyStructureToPtr(ref sample, ptr);

    // > 8, 8.88
    Debug.Log(*ptr);


    // ----------------------------------------
    // ポインタが指す値を構造体にコピー
    var dest = new SampleStr();
    UnsafeUtility.CopyPtrToStructure(ptr, out dest);

    // > 8, 8.88
    Debug.Log(dest);


    UnsafeUtility.Free(ptr, Allocator.Persistent);
}

▼ その他UnsafeUtility

UnsafeUtilityは他にも幾つかの種類があり、例えばNativeArrayに関する操作に特化したNativeArrayUnsafeUtilityと言うAPI等があったりします。

出来る事の一例を上げるとGetUnsafePtrと言う拡張メソッドを呼び出すことでNativeArrayのポインタを取得することが出来ます。
→ こちらについては上述の「▼ Blittable制限がある構造体に持たせられる共通データとして使用」でもご紹介したBlittable制限のある構造体のフィールドにNativeArrayを持たせる際などに使えたりします。

以下にポインタの取得からフィールドのアクセスを踏まえたサンプルコードを載せておきます。

struct SampleStr
{
    public int Param1;
    public float Param2;
    public SampleStr(int index)
    {
        Param1 = index;
        Param2 = index + index / 10f;
    }
    public override string ToString()
    {
        return $"{Param1}, {Param2}";
    }
}

void Start()
{
    // SampleStrを8個生成
    var nativeArray = new NativeArray<SampleStr>(8, Allocator.Persistent);
    for (var i = 0; i < nativeArray.Length; i++)
    {
        nativeArray[i] = new SampleStr(i);
    }

    // 拡張メソッドとして実装されているのでこれで取得可能
    var ptr = (SampleStr*) nativeArray.GetUnsafePtr();

    // > "0, 0"
    // ※"Debug.Log(nativeArray[0])"と同等
    Debug.Log(*(ptr));

    // > "2, 2.2"
    // ※"Debug.Log(nativeArray[2])"と同等
    Debug.Log(*(ptr + 2));

    // > "3.3"
    // ※アロー演算子(->)でフィールドにアクセスできる
    // ※"Debug.Log(nativeArray[3].Param2)"と同等
    Debug.Log((ptr + 3)->Param2);

    nativeArray.Dispose();
}

ちなみにNativeArrayUnsafeUtilityには上記で説明したGetUnsafePtr以外にもGetUnsafeReadOnlyPtrGetUnsafeBufferPointerWithoutChecksと言う拡張メソッドも存在します。
こちらについて調べてみた所、実装としては以下のようになっていたので、主な違いとしてはメモリチェックの有無のようでした。
最終的に取得できる値自体に違いは無いものの、可能であれば明示的に指定した方が安全性/コードの見通しを含めて良いかもしれません。

public static unsafe void* GetUnsafePtr<T>(this NativeArray<T> nativeArray) where T : struct
{
    AtomicSafetyHandle.CheckWriteAndThrow(nativeArray.m_Safety);
    return nativeArray.m_Buffer;
}

public static unsafe void* GetUnsafeReadOnlyPtr<T>(this NativeArray<T> nativeArray) where T : struct
{
    AtomicSafetyHandle.CheckReadAndThrow(nativeArray.m_Safety);
    return nativeArray.m_Buffer;
}

public static unsafe void* GetUnsafeBufferPointerWithoutChecks<T>(NativeArray<T> nativeArray) where T : struct
{
    return nativeArray.m_Buffer;
}

▽ 参考/関連サイト

▼ 自作NativeContainerについて


  1. 例えばNativeArrayも内部実装で使用している。詳細はNativeArrayの公開ソースを参照。 

  2. メモリアライメントについてはこちらの記事がわかりやすいです。 

  3. アンマネージドメモリに乗っける意味合いとしては、サイズの大きいデータを取り扱う際のI/Oのメモリ負荷は地味に高く、場合によってはマネージドヒープを圧迫してしまう事が考えられたのでこの問題を回避するため。 

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

Unityでモバイル端末のバイブレーションをコントロールする

Unityに用意されているバイブレーションAPI

Handheld.Vibrate ();

これで、1秒くらいのバイブレーションがブーンとなりますが、
使うシーンによってはちょっとマッチしない。
タップだったりに合わせて鳴らすにはもう少し短いものがほしいですよね。

調べるといろいろ出てきて、その寄せ集めの情報にはなりますが、
iOSとAndroidの情報がまとまってるものが意外となかったので書いておきます。

iOS

秒数での制御ができないので、プリセットのリストから選んで鳴らすことになります。

IOSUtil.cs
#if UNITY_IOS && !UNITY_EDITOR
        [DllImport ("__Internal")]
        static extern void _playSystemSound(int n);
#endif

        public static void PlaySystemSound(int n) //引数にIDを渡す
        {
#if UNITY_IOS && !UNITY_EDITOR
            _playSystemSound(n);
#endif
        }
IOSUtil.mm
#import <Foundation/Foundation.h>
#import <AudioToolBox/AudioToolBox.h>

extern "C" void _playSystemSound (int soundId)
{
    AudioServicesPlaySystemSound(soundId);
}

タップに反応する短いものだと 1519 なんかが良さそう。
ただ、ドキュメントを探しても、1519 1520 1521 あたりは載って無くて、
使ってて大丈夫なのかは若干不安なところ..

Android

秒数(ミリ秒)で制御することができます。参考リンクまんまですが。

AndroidUtil.cs
#if UNITY_ANDROID && !UNITY_EDITOR
    public static AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
    public static AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
    public static AndroidJavaObject vibrator = currentActivity.Call<AndroidJavaObject>("getSystemService", "vibrator");
#else
        public static AndroidJavaClass unityPlayer;
        public static AndroidJavaObject currentActivity;
        public static AndroidJavaObject vibrator;
#endif

        public static void Vibrate(long milliseconds)
        {
            if (isAndroid())
                vibrator.Call("vibrate", milliseconds);
            else
                Handheld.Vibrate();
        }
        private static bool isAndroid()
        {
#if UNITY_ANDROID && !UNITY_EDITOR
            return true;
#else
            return false;
#endif
        }

端末によって差異はありそうですが、
試した感じ結構細かい数値の違いでも、体感の変化を感じられました。
上記で記載したiOSの 1519 の感触に近づけるには 3ms くらいの短い指定でもよさそう。

Handheld.Vibrate(); の記述について、
コードに残しておくことでAndroidのパーミッションをUnity側でビルド時に自動で追加してくれます。
手動で管理する場合はこの分岐は省略して良いかと思います。

プレビュー

実機で動かして試せるアプリを用意しました。
https://github.com/mrhdms/VibrationTester
iOS, Androidそれぞれで動きます。

iOSはプリセットリストから選択して再生。(音しかならないもの、音とバイブのもの、バイブレーションだけのものが混在)
Androidは0-1秒の間でスライダで調整しながら確認できます。

参考

http://smartgames.hatenablog.com/entry/2019/02/17/125413
http://greenkour.hateblo.jp/entry/2017/11/20/100000

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

UNITY DrawCall調査 GPU Instancing ~ UNITY2018.3.6f1 ~

UNITYのパフォーマンス向上には、DrawCall削減という話はよく聞くと思うし、いろいろな手法が書いてあるが
実際に手で動かさなければ納得できないので、実際に調査です
手抜きで ビルトインのStandardShaderで行います

DrawCall、SetpassCall、TotalBatchesって何?

SetPass

マテリアルの変更のためにGPUへグラフィックコンテキストを送る回数です
グラフィックコンテキストの変更はとてもコストが大きいため
この数を減らすことが最重要
基本的にはマテリアル数を減らす事で下げます

DrawCall

これはGPUに対して頂点の描画命令を出した回数です
同じマテリアルでも異なるオブジェクトの場合はDrawCallが呼ばれる事もあるので
DrawCall≧SetPass になるでしょう

TotalBatches

なんだろうね

素のScene

UNITYを新規で作り実行すると下記の画面になります
0.PNG

これをプロファイリングしましょう
Windows->Analytics->Profiler でプロファイル表示

Renderingをみます

SetPass Calls: 2    Draw Calls: 2       Total Batches: 2    Tris: 1.0k  Verts: 4.1k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
Used Textures: 2 - 1.0 KB
RenderTextures: 9 - 23.4 MB

何もしてないのに SetPass、DrawCallが2 ??
SkyBoxですね!

ってことで

SkyBox消して本当に何もない世界

SnapCrab_NoName_2019-2-27_18-44-11_No-00.png

カメラのSkyBox削除したりRendering設定のSkybox消したり
で、本当の無ができました

SetPass Calls: 0    Draw Calls: 0       Total Batches: 0    Tris: 0     Verts: 0
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
Used Textures: 0 - 0 B
RenderTextures: 7 - 16.3 MB

本当の無ができました

Cube1個表示

画面にCube1個置きます

SnapCrab_NoName_2019-2-27_19-15-0_No-00.png

SetPass Calls: 5    Draw Calls: 5       Total Batches: 5 

おや?5個?
影の設定を消します

SetPass Calls: 2    Draw Calls: 2       Total Batches: 2 

2?
よくわかりませんが、2個(Standardはマルチパスなのかな?)

同じマテリアルのCube4個

当然全部影をOFFにしておきます

SnapCrab_NoName_2019-2-27_19-27-17_No-00.png

SetPass Calls: 2    Draw Calls: 2       Total Batches: 2 

同じマテリアルはUNITYでは可能な限りまとめて送ってくれるので(CPUバッチング)
2のままですね

スフィア1個

SetPass Calls: 2    Draw Calls: 2       Total Batches: 2 

Cubeと変わらず2回

スフィア4個

SetPass Calls: 2    Draw Calls: 8       Total Batches: 8 

スフィアは頂点数が多いので、まとめてくれません
ので DrawCallは8です(2x4)
が、マテリアルは1個なので SetPassは2ですんでます

色違いのマテリアルをエディタ上で作成しアタッチ

4個のうち2個を色違いのマテリアルを作り設定

4SnapCrab_NoName_2019-2-27_19-39-17_No-00.png

SetPass Calls: 4    Draw Calls: 4       Total Batches:  

4になった

同じマテリアルを使いスクリプトでシェーダーカラーを変更

4つのCubeに下記のスクリプトを設定し、そのうち2個をRedにする

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

public class materialize : MonoBehaviour
{
    public Color color = Color.white;
    private Material mat = null;

    void Start()
    {
        Renderer renderer = GetComponent<Renderer>();
        mat = renderer.material;
    }

    void Update()
    {
        mat.SetColor("_Color", this.color);
    }
}
SetPass Calls: 8    Draw Calls: 8       Total Batches: 8 

超増えた!!
どうやら全部別々のマテリアルだと認識するようです
GPU Instancingしてもかわらない

MaterialPropertyBlockを使う

上記のスクリプトを変更

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

public class materialize : MonoBehaviour
{
    public Color color = Color.white;
    private Material mat = null;
    private MaterialPropertyBlock materialPropertyBlock = null;
    private Renderer renderer = null;

    void Start()
    {
        materialPropertyBlock = new MaterialPropertyBlock();
        renderer = GetComponent<Renderer>();
    }

    void Update()
    {
        materialPropertyBlock.SetColor(id, color);
        renderer.SetPropertyBlock(materialPropertyBlock);
    }
}
SetPass Calls: 4    Draw Calls: 4       Total Batches: 4 

4になった(別マテリアルと同じ)

GPU Instancingを4つのCubeに設定

SetPass Calls: 3    Draw Calls: 3       Total Batches: 3    Tris: 0     Verts: 0
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 8   Batches: 3  Tris: 0     Verts: 0
Used Textures: 3 - 0 B
RenderTextures: 8 - 17.3 MB

GPU Instancingも効き 3になった
MaterialPropertyBlockでは、マテリアルを複製せず、同じマテリアルとして設定だけ変更してくれるようです

いっぱい表示してみよう

Updateで毎フレーム CubeをInstancingし
ランダムで色をつけてランダムな方向に飛ばすパーティクルもどきを作り
負荷を比較する

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

public class Shoot : MonoBehaviour
{
    public GameObject go;

    // Update is called once per frame
    void Update()
    {
        var obj = Instantiate(go);
        var rigidbody = obj.GetComponent<Rigidbody>();
        var script = obj.GetComponent<materialize>();
        var col = new Color( Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f), Random.Range(0.0f, 1.0f));
        script.color = col;

        rigidbody.AddForce( Random.Range(-20.0f, 20.0f), Random.Range(3.0f, 50.0f), Random.Range(-20.0f, 20.0f), ForceMode.Impulse);
    }
}

SnapCrab_NoName_2019-2-28_3-0-39_No-00.png

MaterialPropertyBlock不使用&GPU Instancing OFF

SetPass Calls: 370      Draw Calls: 370         Total Batches: 370  Tris: 4.1k  Verts: 8.2k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0 

400付近。Batchなし

MaterialPropertyBlock不使用&GPU Instancing ON

SetPass Calls: 358      Draw Calls: 358         Total Batches: 358  Tris: 4.1k  Verts: 8.2k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 358     Batches: 358    Tris: 4.1k  Verts: 8.2k

Batchにはなっているが、Drawcall減らず

MaterialPropertyBlock使用&GPU Instancing OFF

SetPass Calls: 348      Draw Calls: 348         Total Batches: 348  Tris: 4.1k  Verts: 8.2k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0

あまり変わらず

MaterialPropertyBlock使用&GPU Instancing ON

SetPass Calls: 197      Draw Calls: 197         Total Batches: 197  Tris: 4.1k  Verts: 9.2k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 392     Batches: 197    Tris: 4.1k  Verts: 9.2k

GPU Instancingが効いて、全てのDrawが約400に対して、SetPass、DrawCallは200と半分に!

・・・予想ではSetPassが20ぐらいまで下がるはずだったんだけど思ったほどさがらなかった
どこかにミスあるのか?

おまけ、GPU Instancing ONで同じ色だけ描画

SetPass Calls: 4    Draw Calls: 4       Total Batches: 4    Tris: 4.1k  Verts: 9.2k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 392     Batches: 4  Tris: 4.1k  Verts: 9.2k

400のDrawに対して SetPass4。完全に GPU Instancing効いてる!

まとめ

UNITYは同じマテリアル&設定のモデルは可能な限りまとめて表示してくれる(CPU バッチング)
スフィアのような頂点数が多いものはバッチング出来ない(がマテリアルが同じであればSetPassは増えない)
異なるマテリアル、パラメータの異なるマテリアルはバッチング出来ない
MaterialPropertyBlockを使えばパラメータの異なるマテリアルでも新たにマテリアルが作成されないようだ
GPU Instancingに対応したシェーダを使う事でSetPass CallやDrawCallを削減できる

で、納得いった??
いや、絶対納得しないよね!

という事でシェーダー作る事にした

シェーダー作成

Instancingというシェーダーを作った

Shader "Instancing"
{
    Properties
    {
        _Color("Color", Color) = (1, 1, 1, 1)
    }
        SubShader
    {
        Tags { "RenderType" = "Opaque" "DisableBatching" = "True"}

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #pragma multi_compile_instancing

            #include "UnityCG.cginc"
            #include "UnityStandardParticleInstancing.cginc"

        struct appdata
        {
            float4 vertex : POSITION;
            float4 color : COLOR;
            UNITY_VERTEX_INPUT_INSTANCE_ID
        };

        struct v2f
        {
            float4 color : TEXCOORD1;
            float4 vertex : SV_POSITION;
        };


        UNITY_INSTANCING_BUFFER_START(Props)
            UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
        UNITY_INSTANCING_BUFFER_END(Props)

        v2f vert(appdata v)
        {
            UNITY_SETUP_INSTANCE_ID(v);

            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.color = UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

            return o;
        }

        fixed4 frag(v2f i) : SV_Target
        {
            return i.color;
        }

            ENDCG
        }
    }
}

_Colorというプロパティに対してInstancingを行う何の変哲もないシェーダーだ

pragma multi_compile_instancing
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, Color)
UNITY_INSTANCING_BUFFER_END(Props)
UNITY_SETUP
INSTANCE_ID(v);
UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

のあたりがキーとなる

そもそもGPU Instancingとは、GLSLやHLSLのインスタンシングである

http://maverickproj.web.fc2.com/d3d11_18.html

それをUNITYは上記のマクロを使い非常に巧妙にマルチプラットフォームに対応させている
コード見れば見るほど素晴らしさがわかる!

コードの説明を行おうとおもったが、凹みさんが丁寧に解説してるので紹介にとどめる

http://tips.hecomi.com/entry/2018/09/24/232125

とりあえず上記のコードで、GPU Instancingが可能になったので測定する

まず、GPU Instancingチェックなし

SetPass Calls: 191      Draw Calls: 191         Total Batches: 191  Tris: 2.0k  Verts: 4.1k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0

DrawCall 約200。今までの半分なのはやはり、Standardのシェーダーは2パス走っていたのだろう
(ライト1個、影なしだけど、何のパスだろうか?? ForwardAddかShadowCasterが怪しい)

そして、GPU Instancingあり

SetPass Calls: 1    Draw Calls: 1       Total Batches: 1    Tris: 2.0k  Verts: 4.1k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 207     Batches: 1  Tris: 2.0k  Verts: 4.1k

キマシタワーーーー!!

200 Drawですが、1マテリアルなのでSetPass Callは1回で、全部GPU Instancing化された!

念のためスフィアも

GPU Instancing無し

SetPass Calls: 207      Draw Calls: 207         Total Batches: 207  Tris: 158.7k    Verts: 106.5k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0

GPU Instancing無し

SetPass Calls: 1    Draw Calls: 1       Total Batches: 1    Tris: 157.7k    Verts: 105.5k
(Dynamic Batching)  Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Static Batching)       Batched Draw Calls: 0   Batches: 0  Tris: 0     Verts: 0
(Instancing)        Batched Draw Calls: 206     Batches: 1  Tris: 157.7k    Verts: 105.5k

CPUバッチングでは、頂点数(正確には頂点要素数)が多いとバッチング出来ないので
スフィアは個別にDrawCallが走っていたが
GPU Instancingでは やはり DrawCallは1回で終わる!!
これが、CPUバッチングは多ポリゴンメッシュに弱いが、GPU Instancingはハイポリゴンに強いというやつだ

そうそう、これを期待してた!!
Standardシェーダーが、DrawCallが半分にしかならなかった理由は
おそらく、シェーダーが2パスあり、1つのパスはGPU Instancingが効いて激減したが
もう1つのパスがGPU Instancingに対応しておらず、半分という結果だったと思われる

やっと寝れる

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