テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】構造体のデータを異なる型に「再解釈」する

 DOTS…特にECSで色々とやっていると、内部のデータは同じなのに型が違うせいで変換しないといけないというケースがあります。

 例えば float のバッファを流し込んでバッチ処理を行うAPIがあった時、ローカルのデータで NativeArray<FloatData> のような構造体の配列は直接流し込めません。しかしメモリ的にはこの二つはほぼ同じものです。そこで Reinterpret<T, U>() でデータを再解釈して NativeArray<FloatData>NativeArray<float>として扱ってみます。

動かない例

 例えば下のようなコードがあったとします。 MyData という構造体が定義されていて、これを ShowFloatLog(NativeArray<float> inputs) で一気に表示したい。当然MyDataは中身がfloatであっても扱いはfloatではないので、下のコードはエラーになります。

using Unity.Collections;
using UnityEngine;

struct MyData { public float Value; }

public class Sample: MonoBehaviour
{
    void Start()
    {
        var input1 = new NativeArray<MyData>(new[] {
            new MyData { Value = 11 },
            new MyData { Value = 22 },
            new MyData { Value = 33 },
        }, Allocator.Temp);

        ShowLog(input1 );

        input1.Dispose();
    }

    static void ShowFloatLog(NativeArray<float> inputs)
    {
        foreach (var data in inputs)
            Debug.Log(data);
    }
}

f:id:tsubaki_t1:20191017232117j:plain
型が違うのでエラー

Reinterpret<T, U>()で異なる型へ「再解釈」する

 Reinterpret<T, U>()を使用して、NativeArray<MyData>()をNativeArray()に解釈します。このAPICollectionパッケージに含まれているので、パッケージのインポートが必要です。

f:id:tsubaki_t1:20191017232535j:plain
Entitiesを使うと勝手に入るCollectionパッケージ

 下のように使用します。MyDataの中身がfloatしかないので、floatと解釈することが出来ています。なお、あくまで解釈してるだけなのでDisposeはしてはいけません。参照先のポインタは同じものです。

    void Start()
    {
        var input1 = new NativeArray<MyData>(new[] {
            new MyData { Value = 11 },
            new MyData { Value = 22 },
            new MyData { Value = 33 },
        }, Allocator.Temp);

        // MyDataをFloatに再解釈
        var floatInput = input1.Reinterpret<MyData, float>();

        // NativeArray<float>なので動作
        ShowLog(floatInput);

        input1.Dispose();
    }

 この再解釈はポインタ的に、内部データが同じなら色々なデータに解釈できるみたいです。例えばfloat3の配列をfloatに解釈するといった事も可能です。当然、データの長さが異なるので注意が必要ですが、少し面白いと思わなくもないです。

 なお float3 -> float の場合は特に問題は無いですが、 float -> float3 の場合は3で割り切れる数の要素数でないとエラーになります。多い方がベクタライズに有利になりやすいみたいですが、そこのところは注意が必要です。

    void Start()
    {
        var input1 = new NativeArray<float3>(new[] {
            new float3(111, 222, 33),
            new float3(444, 555, 666),
            new float3(777, 888, 999),
        }, Allocator.Temp);

        // float3をfloatに再解釈
        var floatInput = input1.Reinterpret<float3, float>();

        // output : input length 3, reinterpret length 9
        // float3をfloatにするにあたり、配列の長さが変わっている
        Debug.Log($"input length {input1.Length}, reinterpret length {floatInput.Length}");

        // 111 ~ 999 までの要素を個別に出力
        ShowLog(floatInput);

        input1.Dispose();
    }

感想

 NativeArray<Vector3>を返してくる古いAPIと、NativeArray<float3>を要求する新しいAPIの間で苦しんだ時に思い出すと幸せになるかも。

【Unity】ブラシを使ってステージにGameObjectをサクサクっと配置する

f:id:tsubaki_t1:20191016203522j:plain

ステージに木や岩を配置したい

 ステージを作る際、 木や岩をランダムに配置したい という物があります。出来れば地形に沿った形で配置出来ればよいです。規模が小さかったり規則的に配置している場合はGameObjectで配置(Ctrl + Shiftで地形に沿って配置)を使用すればよいのですが、規模が少し大きめだったりランダムに配置したい場合、これを行うのは少し手間が大きいです。
 良い感じに「適当に配置」した後、「適当に向きを回転」し、場合によっては「適当に拡縮」するというのは予想以上に時間が取られます。まぁ楽しいんですが草木や岩、ゴミ等の自然に配置されないオブジェクトに関しては、出来ればブラシ等で簡単に配置したい所でしょう。

f:id:tsubaki_t1:20191016204915j:plain
規則的に並んでいたり、全てがユニークなオブジェクトならプログラムで何とか…

 ブラシで配置となるとTerrainの機能で草木を配置するのはあるんですが、これは後々の微調整が面倒くさいといった問題があります。なので今回はPolyBrushとProBuilderを使用してオブジェクトの配置を行います。

今回やること

 今回は、PolyBrushでステージ上に木や岩といったGameObjectを配置してみます。この量になるとD&Dではしんどいのですが、ブラシ単位だと割と何とかなります。

f:id:tsubaki_t1:20191016205739j:plain
ステージを岩や木で飾る

 なお注意点として、オブジェクトを配置する土台となるメッシュのスケールはscale( 1.1.1)が望ましいです。異なっている場合でも配置は可能ですが、ブラシで選択した場所と異なる場所に配置されることがあります。
 Planeを拡縮で地面を作っている場合、代りにProBuilderで地面を作ってください。

手順1:準備

 オブジェクトを配置していきます。まずはPolyBrushを現在のプロジェクトにインポートして、オブジェクトを配置する前までの操作を行います。

  1. Window>PackageManagerでパッケージマネージャーを開きPolyBrushをインポート
  2. Tools>PolyBrush>PolyBrushWindowでPolyBrushのウィンドウを開く
  3. Scatter Prefabs on Mesh(右から2番目)を選択

f:id:tsubaki_t1:20191016210722j:plain
PolyBrushのセットアップ

手順2:配置するPrefabの登録

 PolyBrushで配置するPrefabを登録します。ここではPrefabの向きやサイズ等の調整も行います。

  1. PrefabPalette-Defaultを選択して、Add Palette...を選択。新しいパレットを作成。
    • ここで指定した名前がパレット名になります。
  2. CurrentPalleteにPrefabをドラッグ&ドロップで登録。
  3. CurrentPalleteにある登録したPrefabを選択。設定を埋める
    ※この作業は登録した全てのPrefabに対して行う
  4. ブラシで配置したいPrefabにチェックを入れる

f:id:tsubaki_t1:20191016212705g:plain
パレットの作成とPrefabの登録

f:id:tsubaki_t1:20191016213415j:plain
Prefabを配置するときの設定

f:id:tsubaki_t1:20191016213618j:plain
配置するPrefabの選択

手順3:ブラシで塗る

 最後にブラシでPrefabを配置していきます。最初に塗るブラシの最大サイズや強度を設定しておくと後々楽なので、先にセットアップしておきます。

  1. Brush Radius Min/MaxでMaxを適当な大きさ(だいたい編集したい範囲の1/3くらいが楽な印象)を設定
  2. Strangthを0.8~0.9辺りに調整

あとは塗っていきます。

  • メッシュ上でクリックしながらドラッグで、ブラシの範囲内にPrefabを配置
  • Ctrlを押しながらドラッグで、範囲内のPrefabを削除(PolyBrushで配置したもののみ)
  • Ctrlを押しながらスクロールで、ブラシの大きさを変更

f:id:tsubaki_t1:20191016215104g:plain
Prefabを塗る

 なお、PrefabScatterSettingsでPrefabを配置するときの調整が出来ます。とりあえずUse PivotとHit Surface is Parentにチェックは入れて、Avoid Overlapは木を生やす時には入れて、それ以外はStrengthで調整するのが良さそうです。

  • Use Pivot:Pivotがちゃんとしてれば配置したPrefabが浮かなくなります。
  • Hit Surface is Parent:配置したオブジェクトの子オブジェクトとしてPrefabを配置します。
  • Avoid Overlap:同じ個所に何度もPrefabを配置しなくなります。例えば木が密集しすぎるのを防ぎます。

f:id:tsubaki_t1:20191016215610j:plain
Prefabを配置するときのオプション

補足

  • これ、配置するのはステージだけに限定する機能ではありません。敵とかを大量に配置する場合、コレで並べてしまうのは割と面白いと思います。その時はAvoid Overlapにチェックを。
  • 配置するのがPrefabなので、LODやスクリプト、ダメージ判定等も設定出来ます。
  • 配置する際、同じ場所に何度もメッシュが配置されるかもしれませんが(通称:ぶっ刺し)、最近の端末ならある程度は気にしなくても大丈夫です。下手にユニークなメッシュを作るより同じオブジェクトを向きを変えつつ大量に配置した方が安い時もあります。
  • 仕組み上、大量にGameObjectを配置します。ある程度は何とかなるかもしれませんが、量が桁違いになるとSubSceneや、Transformをキャッシュしておいて実行時に配置するスクリプトを用意するのが良いかもしれません。オブジェクトが4万個とか超えるなら。
  • 配置するPrefabはVariantで元のPrefabから派生しておくと、コンポーネントをつけたり調整する場合に色々と楽出来ます。
  • 配置するPrefabのマテリアルはInstancingを有効にしましょう。
  • 配置する際にブラシで配置する機能は土台となるオブジェクトにColliderは無くても動作します。移動判定をNavMeshで作る場合にはうれしい?
  • 配置するPrefabを切り替える場合、プレハブにチェックを入れたり外したりするよりPaletteを切り替える方が楽です。Paletteの複製はPaletteのアセットを複製すれば良いです
  • アセットの操作でPaletteを複製した場合、一度Paletteを変更した後に一覧表示されるようになります。
  • PaletteからPrefabを消すにはCtrl + Backspaceです。

関連

一定以上の規模になると、プロシージャルで作った方が良い

learning.unity3d.jp

規模が少ない場合に使えるTips

tsubakit1.hateblo.jp

ProBuilderで地面を作る場合

  1. Window>PackageManagerでパッケージマネージャーを開きProBulderをインポート
  2. Tools>ProBuilder>Editors>Open Shape Editor Menu Itemを選択
  3. Shape SelectorでPlaneを選択し、設定を埋めてBuildをクリック
    • Width:幅
    • Length:奥行
    • Width/length Segments:幅・奥行のメッシュ分割数

f:id:tsubaki_t1:20191016211356j:plain
Open Shape Editor Menu Itemを開く

f:id:tsubaki_t1:20191016211326j:plain
Shape Toolで作るメッシュの大きさを指定

【Unity】LODGroupの設定を一気に変更する

f:id:tsubaki_t1:20191015221051g:plain

LODGroup

 視覚上のポリゴンを削減するという用途でLODGroupという機能は非常にありがたい機能です。これは単純にローポリのモデルに差し替えるというだけではなく、遠距離に要るキャラクターのメッシュを非表示にする(≒アニメーションとスキニングのコストを削減出来る)といった点でもありがたい技術ではあります。

 このLOD Group、モデル切り替えの条件が「画面内におけるモデルの高さ」という条件で、調整には少し苦労するかもしれません。距離ベースのLODと異なり、近づいた時にいきなり巨大なオブジェクトが唐突に表れるという事は無いのですが、どの距離で表示するのかといった事は開発者のノリに依存します。

f:id:tsubaki_t1:20191015222835g:plain
元々の大きさに関係なく、画面の占有率(高さベース)でLODが切り替わる

複数のLODを編集したい

 このLODの仕組みで困るのが、複数のLODを一括変更できないという点です。複数のLODを選択すると、Multi Object Editing not supportと表示され、編集ができません。

 これは少し面倒くさいです。例えばキャラクター等をLODでカリングしようと思ったとき、キャラクター毎にLOD Groupを調整する必要が出てきます。LOD Groupは複数の設定項目を持てる上、数字入力が出来ないので本当に面倒くさいです。

 

f:id:tsubaki_t1:20191015223457j:plain
LODGroupを複数調整する場合、本当に面倒くさい

LODの設定を一気に上書きするコード

 LODの設定はスクリプトから一気に更新してしまいます。例えば下のようなコードで

using UnityEngine;

public class LODGroupSettings : MonoBehaviour
{
    [SerializeField] LODGroup src;

    void Start()
    {
        UpdateLOD();
    }

    [ContextMenu("UpdateLOD")]
    public void UpdateLOD()
    {
        var dst = GetComponent<LODGroup>();
        var srcLOD = src.GetLODs();
        var dstLOD = dst.GetLODs();

        for (int i = 0; i < srcLOD.Length; i++)
        {
            dstLOD[i].screenRelativeTransitionHeight = srcLOD[i].screenRelativeTransitionHeight;
            dstLOD[i].fadeTransitionWidth = srcLOD[i].fadeTransitionWidth;
        }
        dst.SetLODs(dstLOD);
    }
}

src にコピー元となるLODGroup(コピー先と同じLODの数)のPrefabを登録します。これで実行時にLODの設定が差し変わる他、コンテキストメニューからUpdateLODを選択すればLODが切り替わります。

 

f:id:tsubaki_t1:20191015224349j:plain
コピー元を登録して、コンテキストメニューでUpdateLODを選択する場合

注意:複数編集にPresetが使えそうに見えるが、使ってはいけない

 複数編集という点で、使えそうな設定にPresetという機能があります。この機能を使用すると、確かにLODを一気に更新することができますが、実はコレは罠です。フフフ…

 Presetの設定はコンポーネントの設定をコピーしてくれますが、実はLODGroupで表示・非表示を切り替えるメッシュもコピーしてしまいます。つまり、階層下ではない別のLODGroupに所属するRendererを対象にしてしまいます。これでLODが効かなくなったり、レンダラーの登録が全て剥がれたりして大惨事を引き起こすかもしれません。

f:id:tsubaki_t1:20191015225338g:plain
違う階層のLODを参照してしまっている図

感想

 LODの切り替え距離それ自体はProjectSettings>QualityのLOD Biasで大雑把に調整出来ますが、全体で調整されてしまうので、特定の物をテンプレートに従って一気に差し替えられるというアプローチは、便利と感じています。

 まぁ現状モバイルだとポリゴンを削減するという点でのLODは(一画面1200万ポリゴンとか行ってないなら)そこまで気にしなくても良いと思いますが。ポリゴン数以外でも、スキンメッシュやAnimatorの計算を距離で削減出来るのは、ゲームによっては結構アリかなと思います。

【Unity】出来るだけ簡単にNew Input Systemを使いたい

新しいInput Systemは少し面倒くさい

 新しいInput Systemですが、正直な意見として面倒くさいというのがあります。アクションにキーを割り振るという思想は非常に理にかなっていますし、応用で色々出来るのは悪くないとは思いますが、以前のInput Managerのようにすぐ使えないのはすごくマイナスです。

 そこで、サクッと使える使い方について確認してみます。

PlayerInputを使用する

 サクッとプレイヤーの入力を取得するアプローチとしてPlayerInputコンポーネントがあります。このコンポーネントを使えば面倒くさい作業が多少楽になります。

  • WASDやGamepadに対応した移動等を含むInputActionsを生成してくれる。
  • SendMessageで該当のメソッドを呼び出してくれる
  • InputSystemの初期化

 特にInputActionsの生成は面倒くさい項目の一つなので、とりあえず動くものがサクッと手に入るのは非常に楽でよいです。ここで生成するInputActionsのアクションマップには「WASDキーやGamePadでの移動」「マウス操作や右スティックの視点移動」「マウスクリックやゲームパッドに反応する射撃」の操作が含まれます。

f:id:tsubaki_t1:20191014213847j:plain
作られたInputActions。とりあえず移動、視点移動、射撃がカバーされる

 なおこの記事はUnity 2019.2 × InputSystem 1.0 の組み合わせで作成されています。

PlayerInputの利用手順

  1. 入力を受け付けたいオブジェクトにPlayerInputコンポーネントを登録。
  2. PlayerInputのCreateActionsを押して、アクションマップを生成。
  3. PlayerInputのDefaultMapPlayerに変更。
  4. PlayerInputのBehaviourをSendMessage`に変更

 

f:id:tsubaki_t1:20191014210641j:plain
PlayerInputからActionMapを作るボタン

 あとは下のコードを記述してPlayerInputをアタッチしたGameObjectに追加します。入力はそのまま使用せず、いったんフィールドにキャッシュして使用します。これは現状InputSystemが変化した値を検出するためです。

 なおSendMessageと聞いて顔をしかめるかもしれませんが、今の端末性能的に一フレーム数回のSendMessageなど誤差のようなものです。

using UnityEngine;
using UnityEngine.InputSystem;

public class Example: MonoBehaviour
{
    public Vector2 move { get; private set; }
    public Vector2 look { get; private set; }
    public bool fire { get; private set; }

    public void OnMove(InputValue value) => move = value.Get<Vector2>();
    public void OnLook(InputValue value) => look = value.Get<Vector2>();
    public void OnFire(InputValue value) => fire = value.Get<float>() > 0;

    void Update()
    {
        Debug.Log($"Move[{move}], Look[{look}], Fire[{fire}]");
    }
}

 もしポーリングするスタイルで使用したい場合、下のような記述になります。playerInput.actions["アクション名"]でアクションの情報が取得できるので、あとはReadValueで現在の状態を取得していく感じです。

using UnityEngine;
using UnityEngine.InputSystem;

public class Example: MonoBehaviour
{
    InputAction move, look, fire;
    void Start()
    {
        var playerInput = GetComponent<PlayerInput>();
        move = playerInput.actions["Move"];
        look = playerInput.actions["Look"];
        fire = playerInput.actions["Fire"];
    }

    void Update()
    {
        var m = move.ReadValue<Vector2>();
        var l = look.ReadValue<Vector2>();
        var f = fire.ReadValue<float>() > 0;

        Debug.Log($"Move {m}, Look {l}, Fire {f}");
    }
}

 なおPlayerInputにはUnityEventを受け取るという機能もありますが、これはUnity 2019.3では動作しませんでした。同梱のサンプルも動作しないのでバグと思われます(2019.2では動作します)。Unity 2019.3 b7だと正常に動作しました

キー入力やゲームパッドの取得

 デバッグ目的等でサクッと特定のキー入力を使用したい場合、下のような形で取得出来ます。InputActionのように入力をアクション単位で抽象化出来ないので、サクッと使える反面、複数の環境に対応させようと思ったら少し面倒くさいことになるかもしれません。

// マウスの左クリックを検出
Mouse.current.leftButton.wasPressedThisFrame;
// キーボードのスペースキーを検出
Keyboard.current.spaceKey.wasPressedThisFrame;
// ゲームパッドの左スティックを検出
Gamepad.current.leftStick.ReadValue();

関連

tsubakit1.hateblo.jp