Unity - スクリプトでTerrainに木を植える

Terrainに自動で木を植えたいなと思って調べたが、あまり情報がない感じだったので書く。

環境

  • Unity: 2020.3.26f1 Personal
  • Universal RP: 10.8.1

できあがりイメージ

f:id:bifutek:20220221000339p:plain
Terrain上のランダムな座標に生成された木々。
向きや大きさなどを固定値で生成したので工業製品っぽくなってしまった。。

考え方

全体的には、UnityEditorでTerrainを指した時のInspectorに表示される情報のイメージに近い感じ。

f:id:bifutek:20220221135731p:plain
だいたいこんな感じ

  1. 生成する木のPrefabをTreePrototypeにセットする
  2. TreePrototypeを基にTreeInstanceを量産する
  3. 生成したTreeInstanceをTerrainDataにセットすると木が生成される
  4. Colliderの位置を木の表示位置に同期させるためにTerrainColliderをDisable/Enableする

サンプルスクリプト

  • 以下のスクリプトをTerrainオブジェクトにくっつけ、Inspectorで木のPrefabを設定すればPlayした時に自動的に木を生成する。
  • 分かりやすいようにTerrainの縦横を走査しているが、これだとかなり重い。
    実際に使う場合は、乱数の適合要素を先に抽出してその座標だけ走査するなど、別のアプローチにした方がよい。

TerrainTreeGenerator.cs

using System.Linq;
using UnityEngine;

public class TerrainTreeGenerator : MonoBehaviour
{
    public GameObject _treePrefab;

    [Range(0, 5)]
    public int _density = 1;

    void Start()
    {
        Terrain terrain = GetComponent<Terrain>();
        GenerateTerrainTrees(terrain);
    }

    private void GenerateTerrainTrees(Terrain terrain)
    {
        var terrainData = terrain.terrainData;

        // すべての木の元ネタ
        terrainData.treePrototypes = new TreePrototype[] {
            new TreePrototype() {
                prefab = _treePrefab
            }
        };

        // デフォルトは513のはず
        var terrainSideLength = terrainData.heightmapResolution;

        var noise = new CreationNoiseGener(terrainSideLength, _density);

        // TerrainのHeightMap解像度を単位として各座標に木を生成するか判定し、
        // 適合した座標にTreeInstanceで木を生成する
        var points = Enumerable.Range(0, terrainSideLength);
        var instances = points.AsParallel().SelectMany(x =>
        {
            var row = points.Where(z => noise.ShouldCreate(x, z)).Select(z =>
            {
                var normalizedX = (float)x / terrainSideLength;
                var normalizedZ = (float)z / terrainSideLength;
                var position = new Vector3(normalizedX, 0, normalizedZ);
                return CreateTreeInstance(position);
            });
            return row;
        }).ToArray();
        terrainData.SetTreeInstances(instances, true);

        // Colliderを更新しないと木とColliderの位置がずれたままになる
        var collider = terrain.GetComponent<TerrainCollider>();
        collider.enabled = false;
        collider.enabled = true;
    }

    private TreeInstance CreateTreeInstance(Vector3 position)
    {
        var instance = new TreeInstance()
        {
            prototypeIndex = 0,
            position = position,
            // 簡単のために向きや高さなどを固定値にしているが、
            // 揺らぎを付けるともっと自然な感じになる
            rotation = 0,
            widthScale = 1,
            heightScale = 1,
            color = Color.white,
            lightmapColor = Color.white,
        };
        return instance;
    }

    /// <summary>
    /// 各座標にオブジェクトを生成するかランダム判定する用ユーティリティ
    /// </summary>
    private class CreationNoiseGener
    {
        private int[] _randAtPoint;
        private int _threshold;
        private int _resolution;

        public CreationNoiseGener(int resolution, int density)
        {
            _threshold = 100 - density;
            _resolution = resolution;

            var ints = GenerateRandomInts(resolution * resolution);
            _randAtPoint = ints.Select(e => e * 100 / byte.MaxValue).ToArray();
        }

        public bool ShouldCreate(int x, int z)
        {
            return _threshold < _randAtPoint[x * _resolution + z];
        }

        private int[] GenerateRandomInts(int size)
        {
            var bytes = new byte[size];
            new System.Random().NextBytes(bytes);

            var ints = new int[size];
            bytes.CopyTo(ints, 0);

            return ints;
        }
    }

}

補足

  1. TreeInstanceで生成した木のColliderは、TerrainのTerrainColliderが適用されるようになる(木のPrefabに付いていたものでなく)。
  2. 木の生成座標は(0,0,0)~(1,1,1)の範囲で指定する。
    f:id:bifutek:20220221130549p:plain
    分かりやすい座標に木を生成したもの

参考リンク

サンプルで使用したアセット

Unity - URPのShader Graphで雲が流れるSkyboxを作る

UnityのURPでProcedural skyboxに雲を足したものが欲しくて作り方を調べたが、情報が少ない感じだったので書く。

環境

  • Unity: 2020.3.26f1 Personal
  • Universal RP: 10.8.1
  • Shader Graph: 10.8.1

できあがりイメージ

f:id:bifutek:20220122221744g:plain
空と雲と太陽

f:id:bifutek:20220123122857p:plain
雲の質感の例。GIFだとファイルサイズが肥大化してしまうので静止画。

  • 太陽 … 割と情報ありそうだったのでこの記事ではおまけ。
  • … 天頂付近・中空・地平線付近・地面の4層で個別に色を指定し、各層の間でグラデーションさせる。
  • … Noiseノードを使って生成する(テクスチャファイルを使わない)。

ShaderGraph

(画像のリンク先はGoogleDrive*1。700KBくらい。)

ShaderGraph全体

プロパティ設定例

f:id:bifutek:20220123210620p:plain
プロパティ設定例

完成品.unitypackage

手元の環境で動くのか手っ取り早く確認するための完成品パッケージ。
チュートリアルをやり切ってから動かないことが分かるとつらい。

CloudySkyboxShaderSample.unitypackage (47KBくらい) - Google ドライブ

各要素の補足

太陽

  • 実装は最低限。
  • Bloomで光り、Directional LightRotationで傾きを制御できる。
  • 日の傾きによる光の強弱や色の変化などには対応していない。
    参考リンクでそれらを含めて実装できるチュートリアルを紹介している。
  • 太陽を描画する方角と色を求めるためにCustom Functionノードを使う。
    中身はこんな感じ。

f:id:bifutek:20220117230130p:plain
MainLight(Custom Function)のGraph Inspector

HLSL部分:

#ifdef SHADERGRAPH_PREVIEW
  Direction = float3(0.5, 0.5, 0);
  Color = 1;
#else
  Light light = GetMainLight();
  Direction = light.direction;
  Color = light.color;
#endif

  • 天頂付近で空が深く、濃くなるようにしたかったのでグラデーションを4層にした。
    4層だとLerpが使えないので、MinimumSubtractで各層の差分を取って色を付け、重ね直すというやり方をしている。
    もっといい方法があるかもしれない。

  • 雲はSimple NoiseGradient Noiseで生成する。
    Gradient Noise(1つめ)で雲の発生パターン、Simple NoiseGradient Noise(2つめ)で雲の表面パターンを作る。
    Gradient Noise(2つめ)は雲の位置とずらして生成することで、雲が移動するにつれて形を変えていく感じにした。
    ただ、雲の流れを早くしたり、ずっと同じ場所を見つめたりしているとNoiseのパターンが見えてしまう。

  • 雲の移動はC#スクリプトで制御する。
    ShaderGraph側のClouds Offsetプロパティに_CloudsOffsetというReferenceを割り当て、それをC#スクリプト側から制御するようにした。

f:id:bifutek:20220117230127p:plain
`Clouds Offset`プロパティのGraph Inspector

MoveClouds.cs:

using UnityEngine;

public class MoveClouds : MonoBehaviour
{
    public Vector4 _cloudsSpeed = new Vector4(0.5f, 0.5f);

    private const string CLOUDS_OFFSET = @"_CloudsOffset";

    private void Update()
    {
        var skybox = RenderSettings.skybox;

        Vector4 current = skybox.GetVector(CLOUDS_OFFSET);
        Vector4 next = current + (_cloudsSpeed * Time.deltaTime);
        skybox.SetVector(CLOUDS_OFFSET, next);
    }
}
  • C#スクリプトの代わりにTimeノードを使えば、すべてをShaderGraph内で完結できるので散からなくてよかったのだが、PC(UHD630)がうなりを上げてしまうのでやめた。

参考リンク

  • Unity ShaderGraph Procedural Skybox Tutorial Pt.1 – Coster-Graphics
    ShaderGraphでSkyboxを作るチュートリアル
    ShaderGraphの作り方から、空、太陽、テクスチャを使った雲や星空など、SkyboxShaderの作成に必要な知識を体系的に学べる。

    また、Pt.2ではC#スクリプトを使ってDirectional Light(太陽)を回転させ、その傾きに応じて光の色や強度を変化させたり、朝や深夜など特定のタイミングでUnityEventを発生させるといった、より踏み込んだ内容も含まれている。

    Unity2019時点で執筆されたものだが、Unity2020.3でも動作した。
    (ShaderGraph上のノードのプレビューイメージがやや異なるが)

*1:このブログだと大きい画像は自動で縮小されるのでフォントがつぶれてしまう

Unity - Input Systemを使ってゲームパッドからuGUIを操作する

UnityでInpu System + ゲームパッド + uGUIという構成にしたくて調べたが、情報が少ない感じだったので書く。
UnityのGUIは今後UI Toolkitというものに移っていくらしいが、2021/12の時点ではまだPreviewなのでuGUIを使う。

環境

できあがりイメージ

メニュー画面の呼び出し、カーソルの移動、決定、閉じるをゲームパッドで操作する

やり方

メニュー画面のオブジェクトを作る

特別なことはなし。
今回はPanelButtonを置いて、Button選択時の色を緑にしただけ。

メニュー画面

ActionMapsにボタン割り当てを追加する

プレイヤーキャラクターの移動入力などで使用している.inputactionsに、メニューの開閉と選択項目決定のためのボタン割り当てを追加する。

Player側

メニュー画面の呼び出しはプレイヤーキャラクター操作時に反応してほしいのでPlayer側に追加する。
今回は以下のように割り当てた。

アクション ActionMapsでの名称 ボタン割り当て
メニュー画面を開く Menu Button North(Yボタン/△ボタン)
メニュー画面を閉じる Cancel Button South(Aボタン/×ボタン)

「メニュー画面を閉じる」はUI側に移した。 ActionMapの切り替えを考えると、UI側にまとまっていた方がよさそう。

Player側のボタン割り当て

UI側

Buttonコンポーネントを押す操作はSubmitになるようだ。

アクション ActionMapsでの名称 ボタン割り当て
メニュー項目の決定 Submit Button East(Bボタン/〇ボタン)
メニュー画面を閉じる Cancel Button South(Aボタン/×ボタン)

UI側のボタン割り当て

プレイヤー操作用スクリプトにメニュー画面の開閉イベント処理を書く

書く。

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField] private GameObject _menuUi;
    private ExampleInputSysWithUGUIinputactions _input;

    private void Start()
    {
        SetupInputSystem();
    }

    private void SetupInputSystem()
    {
        _input = new ExampleInputSysWithUGUIinputactions();
        _input.Enable();

        var player = _input.Player;

        // 移動とか回転とかジャンプとか
        // player.Move.performed += (ctx) => _inputtedMove2d = ctx.ReadValue<Vector2>();
        //  …


        // メニュー画面の開閉
        player.Menu.performed += (_) =>
        {
            if (_menuUi && _menuUi.activeSelf) return;

            Time.timeScale = 0;
            _menuUi.SetActive(true);
            Debug.Log("メニューを開く");
        };

        _input.UI.Cancel.performed += (_) =>
        {
            if (!_menuUi || !_menuUi.activeSelf) return;

            _menuUi.SetActive(false);
            Time.timeScale = 1f;
            Debug.Log("メニューを閉じる");
        };

    }

}

メニュー画面用スクリプトにメニュー項目操作イベント処理を書く

マウスでUI操作する時と同様にonClick.AddListenerが使える。
今回はメニュー項目(Button)を押すとログが出るようにした。

なお、カーソルの移動はButtonコンポーネントNavigationがいい感じにやってくれるので書かなくてよいが、カーソルの初期位置はセットしておく必要がある。

using UnityEngine;
using UnityEngine.UI;

public class MenuController : MonoBehaviour
{
    [SerializeField] private Button _itemButton;
    [SerializeField] private Button _equipmentButton;
    // 他のButtonは省略

    private void Start()
    {
        SetupMenuUiEvents();
    }

    // 今回はメニュー画面の表示/非表示をSetActiveで切り替えるので、
    // カーソルの初期位置指定はOnEnableに書く(Startでなく)。
    private void OnEnable()
    {
        // いずれかのButtonを選択状態にしておかないとGamepadでカーソルを移動できない。
        _itemButton.Select();
    }

    private void SetupMenuUiEvents()
    {
        _itemButton.onClick.AddListener(() =>
        {
            Debug.Log("アイテムボタンが押された");
        });

        _equipmentButton.onClick.AddListener(() =>
        {
            Debug.Log("装備ボタンが押された");
        });

        // 閉じるアクションは開くアクションとまとめておきたいのでPlayer側に書いた。
    }

}

オブジェクト類とスクリプトを関連付ける

くっつけたらできあがり。

くっつける

できあがりイメージで使用したアセット