Unity - スクリプトでTerrainに木を植える
Terrainに自動で木を植えたいなと思って調べたが、あまり情報がない感じだったので書く。
環境
- Unity: 2020.3.26f1 Personal
- Universal RP: 10.8.1
できあがりイメージ
考え方
全体的には、UnityEditorでTerrainを指した時のInspectorに表示される情報のイメージに近い感じ。
- 生成する木のPrefabをTreePrototypeにセットする
- TreePrototypeを基にTreeInstanceを量産する
- 生成したTreeInstanceをTerrainDataにセットすると木が生成される
- 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; } } }
補足
- TreeInstanceで生成した木のColliderは、TerrainのTerrainColliderが適用されるようになる(木のPrefabに付いていたものでなく)。
- 木の生成座標は(0,0,0)~(1,1,1)の範囲で指定する。
参考リンク
- unity3d - Add Trees to Terrain C# with TreeInstance - Stack Overflow
- TreeInstance, UnityEngine C# (CSharp) Code Examples - HotExamples
- Finally - Removing trees AND the colliders - Unity Forum
- Adding Trees in to the Terrain? - Unity Forum
サンプルで使用したアセット
Unity - URPのShader Graphで雲が流れるSkyboxを作る
UnityのURPでProcedural skyboxに雲を足したものが欲しくて作り方を調べたが、情報が少ない感じだったので書く。
環境
- Unity: 2020.3.26f1 Personal
- Universal RP: 10.8.1
- Shader Graph: 10.8.1
できあがりイメージ
- 太陽 … 割と情報ありそうだったのでこの記事ではおまけ。
- 空 … 天頂付近・中空・地平線付近・地面の4層で個別に色を指定し、各層の間でグラデーションさせる。
- 雲 … Noiseノードを使って生成する(テクスチャファイルを使わない)。
ShaderGraph
(画像のリンク先はGoogleDrive*1。700KBくらい。)
プロパティ設定例
完成品.unitypackage
手元の環境で動くのか手っ取り早く確認するための完成品パッケージ。
チュートリアルをやり切ってから動かないことが分かるとつらい。
CloudySkyboxShaderSample.unitypackage (47KBくらい) - Google ドライブ
各要素の補足
太陽
- 実装は最低限。
Bloom
で光り、Directional Light
のRotation
で傾きを制御できる。- 日の傾きによる光の強弱や色の変化などには対応していない。
参考リンクでそれらを含めて実装できるチュートリアルを紹介している。 - 太陽を描画する方角と色を求めるために
Custom Function
ノードを使う。
中身はこんな感じ。
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
が使えないので、Minimum
やSubtract
で各層の差分を取って色を付け、重ね直すというやり方をしている。
もっといい方法があるかもしれない。
雲
雲は
Simple Noise
とGradient Noise
で生成する。
Gradient Noise
(1つめ)で雲の発生パターン、Simple Noise
とGradient Noise
(2つめ)で雲の表面パターンを作る。
Gradient Noise
(2つめ)は雲の位置とずらして生成することで、雲が移動するにつれて形を変えていく感じにした。
ただ、雲の流れを早くしたり、ずっと同じ場所を見つめたりしているとNoiseのパターンが見えてしまう。雲の移動はC#スクリプトで制御する。
ShaderGraph側のClouds Offset
プロパティに_CloudsOffset
というReferenceを割り当て、それをC#スクリプト側から制御するようにした。
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); } }
参考リンク
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を使う。
環境
できあがりイメージ
やり方
メニュー画面のオブジェクトを作る
特別なことはなし。
今回はPanel
とButton
を置いて、Button
選択時の色を緑にしただけ。
ActionMapsにボタン割り当てを追加する
プレイヤーキャラクターの移動入力などで使用している.inputactions
に、メニューの開閉と選択項目決定のためのボタン割り当てを追加する。
Player側
メニュー画面の呼び出しはプレイヤーキャラクター操作時に反応してほしいのでPlayer
側に追加する。
今回は以下のように割り当てた。
アクション | ActionMapsでの名称 | ボタン割り当て |
---|---|---|
メニュー画面を開く | Menu | Button North (Yボタン/△ボタン) |
|
|
Button South (Aボタン/×ボタン) |
「メニュー画面を閉じる」はUI側に移した。 ActionMapの切り替えを考えると、UI側にまとまっていた方がよさそう。
UI側
Button
コンポーネントを押す操作はSubmit
になるようだ。
アクション | ActionMapsでの名称 | ボタン割り当て |
---|---|---|
メニュー項目の決定 | Submit | Button East (Bボタン/〇ボタン) |
メニュー画面を閉じる | Cancel | Button South (Aボタン/×ボタン) |
プレイヤー操作用スクリプトにメニュー画面の開閉イベント処理を書く
書く。
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側に書いた。 } }
オブジェクト類とスクリプトを関連付ける
くっつけたらできあがり。
できあがりイメージで使用したアセット
- Space Robot Kyle | 3D Robots | Unity Asset Store
- Basic Motions FREE | 3D Animations | Unity Asset Store