Unity - スクリプトでTerrainに草を生やす

前回Terrainに自動で木を植えたので下草も生やしたいなと思って調べたが、やっぱり情報があんまりない感じだったので書く。

環境

  • Unity: 2020.3.26f1 Personal
  • Universal RP: 10.8.1

できあがりイメージ

f:id:bifutek:20220224194518p:plain
草です

考え方

木の時とだいたい同じ。
こちらはTerrainの座標に対応する配列にフラグ値を詰めていく形。

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

  1. 生成する草のPrefabをDetailPrototypeにセットする
  2. Terrain上の座標に対応する二次元配列(int[,])に、草を生やすかどうかのフラグ値を詰めていく
    • 0 … 生やさない
    • 1以上 … 生やす
  3. 詰め終わった二次元配列をTerrainDataにセットすると草が生成される

サンプルスクリプト

以下のスクリプトをTerrainオブジェクトにくっつけ、Inspectorで草のTexture2Dを設定すればPlayした時に自動的に草を生成する。

TerrainGrassGenerator .cs

using System.Linq;
using UnityEngine;

public class TerrainGrassGenerator : MonoBehaviour
{
    public Texture2D _grassTexture;

    [Range(0, 100)]
    public int _density = 50;

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

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

        // prototypeTextureはnullでもエラーにならなくて分かりづらいので例外投げとく
        if (_grassTexture == null) {
            throw new MissingReferenceException("コンポーネントに草が付いてないよ!");
        }

        // すべての草の元ネタ
        terrainData.detailPrototypes = new DetailPrototype[]
        {
            new DetailPrototype()
            {
                prototypeTexture = _grassTexture,
                renderMode = DetailRenderMode.GrassBillboard
            }
        };

        // デフォルトは1024のはず
        var detailResolution = terrainData.detailResolution;

        var noise = new CreationNoiseGener(detailResolution, _density);

        // 草の生成状態を数値で表す四角形配列 / 1以上の値が入っている座標に草が生える
        var detailMap = new int[detailResolution, detailResolution];
        var detailPoints = Enumerable.Range(0, detailResolution).ToList();
        detailPoints.AsParallel().ForAll(x =>
        {
            detailPoints.Where(z => noise.ShouldCreate(x, z))
            .ToList().ForEach(z =>
            {
                detailMap[x, z] = 1;
            });
        });

        terrainData.SetDetailLayer(0, 0, 0, detailMap);
    }

    /// <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;
        }
    }

}

参考リンク

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