UnityのSplinesを使って動的に生成される曲線の道を作る【Unity】【Splines】

はじめに

前回の以下の記事で Splinesのサンプルプロジェクトをいろいろ確認したり、Editor拡張を作ったりしました。
今回は、これらから実際に Splineを使用したゲームを作っていきたいと思います。

ayousanz.hatenadiary.jp

デモ

開発環境

  • Unity 2022.2.0f1
  • Splines v2.1.0

Splinesとは

以下のようなサンプルとしてあるように曲線とパスを使用することでいままで標準ではできなかった軌跡を作ることが可能です。詳しくは、公式ドキュメントからの引用をご覧ください

Unity公式 Splines 2.1.0より (deeplにより日本語訳)

曲線とパスで作業する スプラインパッケージを使うと、パスに沿ってオブジェクトや動作を生成したり、軌跡を作成したり、図形を描いたりすることができます。

Splines パッケージには以下が含まれます。

  • Unity エディタでスプラインを作成および操作するためのツール。
  • このパッケージの標準的なスプライン編集ツールをカスタマイズするためのフレームワーク
  • 一般的に使用されるスプラインのための標準データ形式と保存モデル。
  • 道路の作成、スプラインに沿った GameObject の位置と回転のアニメーション、スプラインに沿ったプレハブのインスタンス化による環境の作成など、一般的なスプラインの使用例に対処する実装のサンプル。

Splines 2.0.0以上に含まれている機能

今回の実装でも用いている v1には含まれていない v2に含まれている Splinesの機能は以下になります

  • SplinePath、SplineSlice、SplineRange タイプを追加しました。これらのタイプは、離散スプラインの部分的または完全なセクションの補間と評価を可能にします。

その他の変更点 → com.unity.splines changelog

実装方法

今回は Splinesの Sampleシーンを参考にして実装を行っています。

準備

1 . Unityに Splinesを導入する

2 . Sampleの追加

SplinesのMeshの更新

今回 Splinesに追加の点を追加した際のMesh周りの処理は、サンプルシーンにすでにあるもの以下のスクリプトを改変します。

Assets/Samples/Splines/2.1.0/Spline Examples (requires Shader Graph package)/Runtime/MultipleRoadBehaviour.cs

新たに MultipleRoad.cs を作成して、以下を追加します。

using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.Splines;


namespace _SplineTrain
{
    [DisallowMultipleComponent]
    [RequireComponent(typeof(SplineContainer), typeof(MeshRenderer), typeof(MeshFilter))]
    public class MultipleRoad : MonoBehaviour
    {
        [FormerlySerializedAs("m_Spline")] [SerializeField]
        private SplineContainer mSpline;

        [FormerlySerializedAs("m_SegmentsPerMeter")] [SerializeField]
        private int mSegmentsPerMeter = 1;

        [FormerlySerializedAs("m_Mesh")] [SerializeField]
        private Mesh mMesh;

        [FormerlySerializedAs("m_TextureScale")] [SerializeField]
        private float mTextureScale = 1f;

        private IEnumerable<Spline> RoadSplines
        {
            get
            {
                if (mSpline == null) mSpline = GetComponent<SplineContainer>();
                if (mSpline == null) return null;
                return mSpline.Splines;
            }
        }

        private Mesh RoadsMesh
        {
            get
            {
                if (mMesh != null)
                    return mMesh;

                mMesh = new Mesh();
                GetComponent<MeshRenderer>().sharedMaterial = Resources.Load<Material>("Road");
                return mMesh;
            }
        }

        private int SegmentsPerMeter => Mathf.Min(10, Mathf.Max(1, mSegmentsPerMeter));

        private readonly List<Vector3> _mPositions = new List<Vector3>();
        private readonly List<Vector3> _mNormals = new List<Vector3>();
        private readonly List<Vector2> _mTextures = new List<Vector2>();
        private readonly List<int> _mIndices = new List<int>();

        public void OnEnable()
        {
            //Avoid to point to an existing instance when duplicating the GameObject
            if (mMesh != null)
                mMesh = null;

            CreateRoads();
        }

        public void OnDisable()
        {

            if (mMesh != null)

#if UNITY_EDITOR
                DestroyImmediate(mMesh);
#else
                Destroy(mMesh);
#endif
        }

        public void CreateRoads()
        {
            RoadsMesh.Clear();
            _mPositions.Clear();
            _mNormals.Clear();
            _mTextures.Clear();
            _mIndices.Clear();

            foreach (var spl in RoadSplines)
            {
                CreateRoad(spl);
            }

            RoadsMesh.SetVertices(_mPositions);
            RoadsMesh.SetNormals(_mNormals);
            RoadsMesh.SetUVs(0, _mTextures);
            RoadsMesh.subMeshCount = 1;
            RoadsMesh.SetIndices(_mIndices, MeshTopology.Triangles, 0);
            RoadsMesh.UploadMeshData(false);

            GetComponent<MeshFilter>().sharedMesh = mMesh;
        }

        private void CreateRoad(Spline roadSpline)
        {
            if (roadSpline == null || roadSpline.Count < 2)
                return;

            var length = roadSpline.GetLength();

            if (length < 1)
                return;

            var segments = (int)(SegmentsPerMeter * length);
            int vertexCount = segments * 2, triangleCount = (roadSpline.Closed ? segments : segments - 1) * 6;

            var prevVertexCount = _mPositions.Count;
            _mPositions.Capacity += vertexCount;
            _mNormals.Capacity += vertexCount;
            _mTextures.Capacity += vertexCount;
            _mIndices.Capacity += triangleCount;

            for (var i = 0; i < segments; i++)
            {
                var index = i / (segments - 1f);
                var control = SplineUtility.EvaluatePosition(roadSpline, index);
                var dir = SplineUtility.EvaluateTangent(roadSpline, index);
                var up = SplineUtility.EvaluateUpVector(roadSpline, index);

                var scale = transform.lossyScale;
                //var tangent = math.normalize((float3)math.mul(math.cross(up, dir), new float3(1f / scale.x, 1f / scale.y, 1f / scale.z)));
                var tangent = math.normalize(math.cross(up, dir)) * new float3(1f / scale.x, 1f / scale.y, 1f / scale.z);

                var w = 1f;

                _mPositions.Add(control - (tangent * w));
                _mPositions.Add(control + (tangent * w));
                _mNormals.Add(Vector3.up);
                _mNormals.Add(Vector3.up);
                _mTextures.Add(new Vector2(0f, index * mTextureScale));
                _mTextures.Add(new Vector2(1f, index * mTextureScale));
            }

            for (int i = 0, n = prevVertexCount; i < triangleCount; i += 6, n += 2)
            {
                _mIndices.Add((n + 2) % (prevVertexCount + vertexCount));
                _mIndices.Add((n + 1) % (prevVertexCount + vertexCount));
                _mIndices.Add((n + 0) % (prevVertexCount + vertexCount));
                _mIndices.Add((n + 2) % (prevVertexCount + vertexCount));
                _mIndices.Add((n + 3) % (prevVertexCount + vertexCount));
                _mIndices.Add((n + 1) % (prevVertexCount + vertexCount));
            }
        }
    }
}

サンプルスクリプトをそのまま使う際は、[ExecuteInEditMode] が付いていることに注意してください。

ExecuteInEditMode は以下のように 呼ばれる際に制限がかかっている状態になっています。

  • Updateは、シーンに何か変化があったときだけ呼ばれます。
  • OnGUIは、Game ViewがEventを受け取ったときに呼び出されます。
  • OnRenderObject と他のレンダリングコールバック関数は、シーンビューやゲームビューが再描画されるたびに呼び出されます。

詳細は以下のドキュメントをご覧ください。 docs.unity3d.com

選択したSplinesの取得

ここで Splnies v2から新しく追加された SplineSliceSplineRange を使用します。

使用している SplineContainerから任意の長さのSplineのポイントを取得して返します。

public SplinePath GetNextPath()
        {
            return new SplinePath(new[]
            {
                new SplineSlice<Spline>(CurrentSpline, new SplineRange(0, CurrentSplineCount), _containerTransform),
            });
        }

Splineが更新されたときに SplineのMeshの更新

今回のプロジェクトでは、動的にSplineを増やしているためSplineにポイントを追加するたびにMeshを更新する必要があります。

        private void Start()
        {
            Spline.Changed += OnSplineOnChanged;
        }

        private void OnSplineOnChanged(Spline spline, int i, SplineModification splineModification)
        {
            if (spline == _editRoadModel.CurrentSpline)
            {
                _multipleRoad.CreateRoads();
            }
        }

サンプルコードから引用してきたコードの中にある CreateRoads を Splineが更新されるたびに呼び出します。
Spline には以下の Actionsが存在しているので、今回はこちらを使用します。今回は上記のコードにもあるように Start関数内でeventを発火して追加されるごとにMeshを更新しています。

public static event Action<Spline, int, SplineModification> Changed;

Simple Sample Demo

サンプルコードとして、Splinesを使用したMeshが一定時間ごとに増えていくSimple Sample ProjectのRepositoryも公開しています。

github.com