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