【Unity】スプラインに沿ってメッシュを変形させる

こじゃらこじゃら

Unity提供のスプラインに沿ってメッシュを変形させる方法を知りたいの。

このはこのは

スクリプトで自作する必要があるけど可能だわ。今回は基本的な部分を解説していくね。

Unity公式のスプライン(Splinesパッケージ)に沿ってメッシュを変形させる方法の解説記事です。

次のように、既存のメッシュをスプライン曲線に沿って変形させるところを目標とします。

処理の実装方法は一通りではありませんが、本記事では次の方法でメッシュ変形を実現するものとします。

メッシュ変形の実現方法
  • スクリプトからMeshオブジェクトにアクセスし、頂点と法線を書き換える
  • スプライン上の位置や傾きなどはSplineオブジェクトから取得する
  • 全てメインスレッドのCPU上で処理する

スプラインに沿ってメッシュを作成するコンポーネントとしてSpline Extrudeがありますが、これはチューブ状のメッシュを生成する機能のため、上記のメッシュを変形させるためにはスクリプトで独自実装するなどの手段を取る必要があります。

参考:Extrude a mesh along a spline | Splines | 2.5.2

本記事では、スプラインに沿って既存のメッシュを変形する方法を解説していきます。

注意

本記事ではメッシュ変形処理を全てCPU上で計算する方法に絞って解説します。

ランタイムで使用する場合は、パフォーマンスのネックになる可能性があるためご注意ください。

動作環境
  • Unity 2023.2.19f1
  • Splines 2.5.2

スポンサーリンク

前提条件

本記事の内容を実践するためには、Unity 2022.1以降でプロジェクトを作成し、Splinesパッケージがインストールされている必要があります。

ここまでの導入方法が分からない方は以下記事をご覧ください。

また、本記事を読み進めるにあたっては、スプラインをスクリプトから扱う方法を押さえておくと理解がスムーズです。

本記事では、次のような線路のメッシュを変形させることを例に解説していきます。

こちらのモデルは以下アセットのものを使用させていただきました。

メッシュ変形の基本的な考え方

手順に入る前に、スプラインに沿ってメッシュを変形させる考え方について解説します。不要な方は次の章までスキップしてください。

頂点の変形

メッシュの頂点座標は、オブジェクトのローカル空間で表現されます。

メッシュ頂点の定義例

この頂点座標をスプライン曲線のデータに基づいて変換していきます。

まず、メッシュ頂点のxyz成分のうち、どの軸をスプラインに沿わせるかを定義します。本記事では、頂点座標のz軸をスプライン曲線に沿わせるように変形するものとして解説を進めます。

次に、スプライン曲線の始点からメッシュ頂点のz成分だけ進んだ位置を求めます。

同時に、得られたスプライン位置における接線と上向きベクトルも計算しておきます。

スプライン位置における接線と上向きベクトル

スプライン上の位置・接線・上向きベクトルは、後述するSplinesパッケージのAPIから簡単に計算できます。また、接線(前方ベクトル)と上向きベクトルから、右向きベクトルを求めることが出来ます。

最後に、右向きベクトル方向にx軸成分、上向きベクトル方向にy軸成分だけ頂点位置をずらして完了です。

最終的な変形結果

位置ずらしの計算は、得られたスプライン位置における回転(クォータニオン)を用いて計算できます。回転は接線と上向きベクトルから求められます。

メモ

ずらし位置の計算は、右向きベクトル、上向きベクトルそれぞれに対してxy成分の内積を取ることで求めることも可能です。

本記事は後述する法線ベクトルでクォータニオンが必要になるため、クォータニオンでずらし位置を計算するものとします。

法線の変形

元メッシュの法線ベクトルの変形は、前述で計算されたスプライン位置における回転を掛けることで実現できます。

メッシュの法線の回転

この回転は、前述のスプラインの接線と上向きベクトルから求めたクォータニオンを流用すれば良いです。

メッシュの準備

ここからメッシュを変形させるための手順の解説に入ります。

まず、変形対象のメッシュを用意します。本記事では以下の線路モデルとします。

メッシュ変形は頂点操作によって実現するため、ある程度ポリゴンが分割されている必要があります。

注意

ポリゴンの分割が粗いと、カーブしたときに角ばった変形になってしまう可能性が高くなります。

ポリゴン数の少ないメッシュを変形させた例

考え方の解説ではz軸成分をスプラインの移動方向としていましたが、後述する例ではどの向きでも対応可能にするため、xyz軸いずれかに沿っていれば問題ありません。

また、スクリプトからメッシュ操作するためには、モデルアセット(FBXファイル等)のプロパティのMeshes > Read/Write項目にチェックを入れる必要があります。

これは、GPUだけでなくシステムメモリ(CPUからアクセス可能)にもメッシュデータを保持する設定です。

参考:Model タブ – Unity マニュアル

注意

Meshes > Read/Write項目を有効にする設定は、実行時におけるシステムメモリ使用量を増大させる要因となるため、不要な場合はOFFにしておくことをお勧めします。デフォルト設定ではOFFになっています。

シーンの準備

メッシュをどのように変形させるかを指定するためのスプラインを作成します。既に作成済みの場合はスキップして構いません。

ヒエラルキーウィンドウの左上の「+」アイコン(またはトップメニューのComponent)からSplines > Draw Splines Tool …を選択し、スプラインの制御点をクリックで配置し、ドラッグで接線を調整します。

「Esc」キーで配置を終了できます。

参考:Create a spline | Splines | 2.6.1

また、変形対象のゲームオブジェクトシーンの適当な場所に配置しておきます。

最終的に、スプラインとメッシュオブジェクトがシーンに配置されていれば良いです。

変形処理の実装

ここまで準備したスプラインとメッシュを元に、メッシュをスプラインに沿って変形させるスクリプトを実装していきます。

最初に実装例を示し、その後に処理の流れを解説します。

サンプルスクリプト

以下、実行時にメッシュをスプラインに沿って動的に変更するスクリプトの実装例です。

SplineMeshDeform.cs
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;

public class SplineMeshDeform : MonoBehaviour
{
    // スプライン情報を格納するコンポーネント
    [SerializeField] private SplineContainer _container;

    // スプライン上の位置を示すパラメータ(0~1の範囲で指定)
    [SerializeField, Range(0, 1)] private float _t;

    // メッシュの前方向の軸
    [SerializeField] private Vector3 _forwardAxis = Vector3.forward;

    // メッシュの上方向の軸
    [SerializeField] private Vector3 _upAxis = Vector3.up;

    private bool _isStarted;

    // 変形対象のメッシュ
    private MeshFilter _meshFilter;

    // メッシュコライダー(オプション)
    private MeshCollider _meshCollider;

    // 変形されたメッシュ情報
    private Mesh _deformedMesh;
    private Vector3[] _originalVertices;
    private Vector3[] _originalNormals;

    // メッシュを変形する
    public void Deform()
    {
        if (_container == null || _meshFilter == null)
            return;

        if (_deformedMesh == null)
        {
            // メッシュ情報のコピー
            _deformedMesh = _meshFilter.mesh;
            _originalVertices = _deformedMesh.vertices;
            _originalNormals = _deformedMesh.normals;
        }

        // 元軸からの回転クォータニオンを計算
        var axisRotation = Quaternion.Inverse(Quaternion.LookRotation(_forwardAxis, _upAxis));

        // スプラインのローカル座標からメッシュのローカル座標に変換する行列
        // スプラインのローカル座標→ワールド座標→メッシュのローカル座標の順で変換する必要がある
        var splineToLocalMatrix = transform.worldToLocalMatrix * _container.transform.localToWorldMatrix;

        // メッシュの頂点情報を一時的に格納する配列
        NativeArray<float3> deformedVertices = default;
        NativeArray<float3> deformedNormals = default;

        try
        {
            // 配列の確保
            deformedVertices = new NativeArray<float3>(_originalVertices.Length, Allocator.Temp);
            deformedNormals = new NativeArray<float3>(_originalNormals.Length, Allocator.Temp);

            // スプラインの情報を格納するNativeSplineを作成
            using var spline = new NativeSpline(_container.Spline, splineToLocalMatrix);

            // スプラインの長さを計算
            var splineLength = spline.CalculateLength(splineToLocalMatrix);

            // メッシュの頂点情報をスプラインに沿って変形
            for (var i = 0; i < deformedVertices.Length; i++)
            {
                // 元の頂点座標
                var originalVertex = _originalVertices[i];

                // 軸の回転補正
                originalVertex = math.mul(axisRotation, originalVertex);

                // 頂点座標の前方向成分をスプライン上の位置に変換
                var t = originalVertex.z / splineLength + _t;

                // 囲まれたスプラインなら、tの値をループさせる
                if (spline.Closed) t = Mathf.Repeat(t, 1);

                // 計算されたtに置ける位置・接線・上向きベクトルを取得
                spline.Evaluate(
                    t,
                    out var splinePos,
                    out var splineTangent,
                    out var splineUp
                );

                // スプラインに対するオフセットの回転クォータニオン
                var rotation = quaternion.LookRotationSafe(splineTangent, splineUp);

                // スプライン上の位置に対して水平垂直なずらし位置を計算
                var offset = math.mul(rotation, new float3(originalVertex.x, originalVertex.y, 0));

                // スプライン位置に対する頂点座標を計算
                deformedVertices[i] = splinePos + offset;

                // スプライン位置に対する法線ベクトルを計算
                // 法線にも軸の回転補正を適用
                var normal = math.mul(axisRotation, _originalNormals[i]);
                deformedNormals[i] = math.mul(rotation, normal);
            }

            // メッシュ情報を更新
            _deformedMesh.SetVertices(deformedVertices);
            _deformedMesh.SetNormals(deformedNormals);

            // バウンディングボックスの再計算
            _deformedMesh.RecalculateBounds();

            // メッシュコライダーの更新
            if (_meshCollider != null)
                _meshCollider.sharedMesh = _deformedMesh;
        }
        finally
        {
            // メモリ解放
            if (deformedVertices.IsCreated) deformedVertices.Dispose();
            if (deformedNormals.IsCreated) deformedNormals.Dispose();
        }
    }

    // 初期化処理
    private void Start()
    {
        _isStarted = true;

        TryGetComponent(out _meshFilter);
        TryGetComponent(out _meshCollider);

        // 初回に変形を行う
        Deform();
    }

    // 後処理
    private void OnDestroy()
    {
        // MeshFilterから取得したメッシュは明示的に破棄する必要がある
        if (_deformedMesh != null)
            Destroy(_deformedMesh);

        _deformedMesh = null;
    }

#if UNITY_EDITOR
    // インスペクターから操作された際に変形を反映
    private void OnValidate()
    {
        // 開始前にOnValidateが呼ばれることがあるため、_isStartedを確認
        if (!_isStarted) return;

        Deform();
    }
#endif
}

上記をSplineMeshDeform.csという名前でUnityプロジェクトに保存し、メッシュを変形させる対象のオブジェクトに追加し、インスペクターのContainerスプラインオブジェクトを指定してください。

また、必要に応じてメッシュをどの向きに変形させるかの情報を指定します。

Forward Axis項目スプラインに沿わせる方向の軸のベクトルを、Up Axis項目上向きのベクトルを設定してください。

例では線路がy軸に沿っていて、上向きがz軸正方向になっていたため、Forward Axisに(0, 1, 0)を、Up Axisに(0, 0, 1)を設定することとしました。

実行結果

Spline Mesh DeformコンポーネントのTの値をインスペクターから変更すると、リアルタイムにメッシュが変形されていることが確認できます。

Unityの再生を停止すると、メッシュが元に戻ります。

スクリプトの説明

最初に示したサンプルスクリプトの各処理について解説していきます。

変形処理の前準備

まず、初期化時に変形対象のメッシュ情報にアクセスしたいため、MeshFilterコンポーネントを取得して保持しておきます。MeshColliderがアタッチされている可能性もあるため、あれば一緒に取得します。

TryGetComponent(out _meshFilter);
TryGetComponent(out _meshCollider);

そして、その直後にメッシュの変形処理を実行します。

// 初回に変形を行う
Deform();

Deformメソッドは、独自実装したpublicメソッドです。このメソッドの中では、最初に変形元のメッシュの頂点と法線の情報をコピーして保持しておきます。

if (_deformedMesh == null)
{
    // メッシュ情報のコピー
    _deformedMesh = _meshFilter.mesh;
    _originalVertices = _deformedMesh.vertices;
    _originalNormals = _deformedMesh.normals;
}

_deformedMesh = _meshFilter.meshの行でMeshインスタンスを複製して_deformedMeshフィールドに保持する挙動になります。

これは、複製されていない状態でmeshプロパティをgetするとインスタンスが複製されるためです。

参考:MeshFilter-mesh – Unity スクリプトリファレンス

注意

meshプロパティにアクセスしてMeshインスタンスを複製した場合、明示的にDestroyで破棄する必要があります。

次に、変形の計算過程ではどの軸に沿って変形させるかの基準軸が変わる可能性があるため、基準軸を元に回転(クォータニオン)を計算しておきます。

// 元軸からの回転クォータニオンを計算
var axisRotation = Quaternion.Inverse(Quaternion.LookRotation(_forwardAxis, _upAxis));

また、スプラインに一致させてメッシュを配置させるためには、座標変換を行う必要があります。

スプラインの制御点およびメッシュはそれぞれ別々のゲームオブジェクトのローカル座標であるため、その変換をするための行列を計算しておきます。

// スプラインのローカル座標からメッシュのローカル座標に変換する行列
// スプラインのローカル座標→ワールド座標→メッシュのローカル座標の順で変換する必要がある
var splineToLocalMatrix = transform.worldToLocalMatrix * _container.transform.localToWorldMatrix;

変換結果を格納するための配列を確保します。

// メッシュの頂点情報を一時的に格納する配列
NativeArray<float3> deformedVertices = default;
NativeArray<float3> deformedNormals = default;

try
{
    // 配列の確保
    deformedVertices = new NativeArray<float3>(_originalVertices.Length, Allocator.Temp);
    deformedNormals = new NativeArray<float3>(_originalNormals.Length, Allocator.Temp);

そして、座標系変換されたスプラインNativeSpline構造体として作成します。

// スプラインの情報を格納するNativeSplineを作成
using var spline = new NativeSpline(_container.Spline, splineToLocalMatrix);

コンストラクタの第1引数にスプライン(Spline型インスタンス)、第2引数に前述で計算した座標系の変換行列を指定します。

参考:Struct NativeSpline | Splines | 2.6.1

スプライン全体の長さも予め計算して保持しておきます。

// スプラインの長さを計算
var splineLength = spline.CalculateLength(splineToLocalMatrix);

参考:Class SplineUtility | Splines | 2.6.1

ここまでが変形処理までの前準備処理です。

頂点の変形処理

実際の変形処理は、以下ループ内で各頂点毎に実施していきます。

// メッシュの頂点情報をスプラインに沿って変形
for (var i = 0; i < deformedVertices.Length; i++)
{
    // 元の頂点座標
    var originalVertex = _originalVertices[i];

まず、変形の基準軸を変更するため、最初に計算しておいた基準軸の回転クォータニオンを掛けて頂点に補正をかけます。

// 軸の回転補正
originalVertex = math.mul(axisRotation, originalVertex);

これで、必ずz軸方向がスプライン方向、y軸正方向が上向きであることが保証されるようになります。

参考:Method mul | Mathematics | 1.3.2

予め計算しておいたスプラインの長さ用いて、スプラインにおける割合tを計算します。

// 頂点座標の前方向成分をスプライン上の位置に変換
var t = originalVertex.z / splineLength + _t;

割合スプラインの始点が0、終点が1と正規化された値として指定する必要があります。zの位置を割合に変換するために長さで割る必要があります。

また、スプラインはループ(始点と終点が接続)している可能性もあるため、その場合は割合tもループするようにします。

// 囲まれたスプラインなら、tの値をループさせる
if (spline.Closed) t = Mathf.Repeat(t, 1);

参考:Mathf-Repeat – Unity スクリプトリファレンス

そして、実際に割合tにおけるスプライン位置・接線・上向きベクトルを計算する処理は以下部分です。

// 計算されたtに置ける位置・接線・上向きベクトルを取得
spline.Evaluate(
    t,
    out var splinePos,
    out var splineTangent,
    out var splineUp
);

Spline.Evaluateメソッドにより、引数から一度に3つの結果を受け取れます。

参考:Class SplineUtility | Splines | 2.6.1

得られたスプライン位置に対して、メッシュをxy方向にずらす必要があります。これは、次の部分です。

// スプラインに対するオフセットの回転クォータニオン
var rotation = quaternion.LookRotationSafe(splineTangent, splineUp);

// スプライン上の位置に対して水平垂直なずらし位置を計算
var offset = math.mul(rotation, new float3(originalVertex.x, originalVertex.y, 0));

// スプライン位置に対する頂点座標を計算
deformedVertices[i] = splinePos + offset;

接線と上向きベクトルから回転のクォータニオンを求め、これをローカルのxy座標に適用しています。これにより、最終的なずらし位置offsetが計算できます。

最後に、法線もずらし位置同様に回転させる必要があるため、前述のクォータニオンを掛けています。

// スプライン位置に対する法線ベクトルを計算
// 法線にも軸の回転補正を適用
var normal = math.mul(axisRotation, _originalNormals[i]);
deformedNormals[i] = math.mul(rotation, normal);

変換結果の適用

変換した頂点と法線をMeshインスタンスに適用します。

// メッシュ情報を更新
_deformedMesh.SetVertices(deformedVertices);
_deformedMesh.SetNormals(deformedNormals);

また、頂点の移動によりバウンディングボックスも変わる可能性があるため、忘れずに再計算しておきます。

// バウンディングボックスの再計算
_deformedMesh.RecalculateBounds();

参考:Mesh-RecalculateBounds – Unity スクリプトリファレンス

もしMeshColliderがあれば、それも更新しておきます。

// メッシュコライダーの更新
if (_meshCollider != null)
    _meshCollider.sharedMesh = _deformedMesh;

最後に、変換処理で一時的に確保したバッファを開放して終了です。

finally
{
    // メモリ解放
    if (deformedVertices.IsCreated) deformedVertices.Dispose();
    if (deformedNormals.IsCreated) deformedNormals.Dispose();
}

後処理

変形されたメッシュはスクリプトから複製されたインスタンスのため、最後に明示的に破棄するようにしています。

private void OnDestroy()
{
    if (_mesh != null)
        Destroy(_mesh);

    _mesh = null;
}

アセットファイルとして保存出来るようにする

ここまで解説した方法は、実行時にメッシュを変形する方法でした。

実行時ではなく、実行前に変形しておきたい場合、変形結果のMeshインスタンスをアセットとして保持しておく方法があります。

2つ目の例では、元のメッシュを編集することなく、変形したメッシュをアセットとして保持しておく方法を紹介します。

なお、変形結果のメッシュのアセット化の実装は、Splinesパッケージ提供のSplinesExtrudeクラスの内部実装を参考にさせていただきました。

参考:Class SplineExtrude | Splines | 2.6.1

サンプルスクリプト

1つ目の例を元に、変形結果をアセットとして保持するように修正した例です。

SplineMeshDeformStatic.cs
using System.IO;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Splines;

public class SplineMeshDeformStatic : MonoBehaviour
{
    // スプライン情報を格納するコンポーネント
    [SerializeField] private SplineContainer _container;

    // 変形対象のメッシュ
    [SerializeField] private Mesh _sourceMesh;

    // 変形後のメッシュ
    [SerializeField] private Mesh _deformedMesh;

    // スプライン上の位置を示すパラメータ(0~1の範囲で指定)
    [SerializeField, Range(0, 1)] private float _t;

    // メッシュの前方向の軸
    [SerializeField] private Vector3 _forwardAxis = Vector3.forward;

    // メッシュの上方向の軸
    [SerializeField] private Vector3 _upAxis = Vector3.up;

    private bool _isStarted;

    // メッシュコライダー(オプション)
    private MeshCollider _meshCollider;

    // 変形されたメッシュ情報
    private Vector3[] _originalVertices;
    private Vector3[] _originalNormals;

    // メッシュを変形する
    public void Deform()
    {
        if (_container == null || _sourceMesh == null || _deformedMesh == null)
            return;

        // メッシュ情報のコピー
        _originalVertices ??= _sourceMesh.vertices;
        _originalNormals ??= _sourceMesh.normals;

        // 元軸からの回転クォータニオンを計算
        var axisRotation = Quaternion.Inverse(Quaternion.LookRotation(_forwardAxis, _upAxis));

        // スプラインのローカル座標からメッシュのローカル座標に変換する行列
        // スプラインのローカル座標→ワールド座標→メッシュのローカル座標の順で変換する必要がある
        var splineToLocalMatrix = transform.worldToLocalMatrix * _container.transform.localToWorldMatrix;

        // メッシュの頂点情報を一時的に格納する配列
        NativeArray<float3> deformedVertices = default;
        NativeArray<float3> deformedNormals = default;

        try
        {
            // 配列の確保
            deformedVertices = new NativeArray<float3>(_originalVertices.Length, Allocator.Temp);
            deformedNormals = new NativeArray<float3>(_originalNormals.Length, Allocator.Temp);

            // スプラインの情報を格納するNativeSplineを作成
            using var spline = new NativeSpline(_container.Spline, splineToLocalMatrix);

            // スプラインの長さを計算
            var splineLength = spline.CalculateLength(splineToLocalMatrix);

            // メッシュの頂点情報をスプラインに沿って変形
            for (var i = 0; i < deformedVertices.Length; i++)
            {
                // 元の頂点座標
                var originalVertex = _originalVertices[i];

                // 軸の回転補正
                originalVertex = math.mul(axisRotation, originalVertex);

                // 頂点座標の前方向成分をスプライン上の位置に変換
                var t = originalVertex.z / splineLength + _t;

                // 囲まれたスプラインなら、tの値をループさせる
                if (spline.Closed) t = Mathf.Repeat(t, 1);

                // 計算されたtに置ける位置・接線・上向きベクトルを取得
                spline.Evaluate(
                    t,
                    out var splinePos,
                    out var splineTangent,
                    out var splineUp
                );

                // スプラインに対するオフセットの回転クォータニオン
                var rotation = quaternion.LookRotationSafe(splineTangent, splineUp);

                // スプライン上の位置に対して水平垂直なずらし位置を計算
                var offset = math.mul(rotation, new float3(originalVertex.x, originalVertex.y, 0));

                // スプライン位置に対する頂点座標を計算
                deformedVertices[i] = splinePos + offset;

                // スプライン位置に対する法線ベクトルを計算
                // 法線にも軸の回転補正を適用
                var normal = math.mul(axisRotation, _originalNormals[i]);
                deformedNormals[i] = math.mul(rotation, normal);
            }

            // メッシュ情報を更新
            _deformedMesh.SetVertices(deformedVertices);
            _deformedMesh.SetNormals(deformedNormals);

            // バウンディングボックスの再計算
            _deformedMesh.RecalculateBounds();

            // メッシュコライダーの更新
            if (_meshCollider != null)
                _meshCollider.sharedMesh = _deformedMesh;
        }
        finally
        {
            // メモリ解放
            if (deformedVertices.IsCreated) deformedVertices.Dispose();
            if (deformedNormals.IsCreated) deformedNormals.Dispose();
        }
    }

    // 初期化処理
    private void Start()
    {
        _isStarted = true;

        TryGetComponent(out _meshCollider);

        // 初回に変形を行う
        Deform();
    }

#if UNITY_EDITOR
    // インスペクターから操作された際に変形を反映
    private void OnValidate()
    {
        if (_sourceMesh == null)
            return;

        if (UnityEditor.EditorApplication.isPlaying)
        {
            // 開始前にOnValidateが呼ばれることがあるため、_isStartedを確認
            if (_isStarted)
                Deform();
        }
        else
        {
            // 新しいメッシュアセットを作成
            if (_deformedMesh == null)
                _deformedMesh = CreateMeshAsset();

            if (_deformedMesh == null)
                return;

            Deform();
        }
    }

    private void Reset()
    {
        // MeshFilterがあれば、そのメッシュを入力とする
        if (TryGetComponent<MeshFilter>(out var meshFilter))
        {
            _sourceMesh = meshFilter.sharedMesh;
            _deformedMesh = CreateMeshAsset();

            // MeshFilterを設定
            meshFilter.sharedMesh = _deformedMesh;
        }

        // MeshColliderがあれば、同様に設定
        if (TryGetComponent<MeshCollider>(out var meshCollider))
            meshCollider.sharedMesh = _deformedMesh;
    }

    // メッシュアセットの作成処理
    // 実装はSplinesExtrudeのものを参考にしました
    private Mesh CreateMeshAsset()
    {
        if (_sourceMesh == null)
            return null;

        // 新しいメッシュアセットを複製
        var mesh = Instantiate(_sourceMesh);
        mesh.name = name;

        // メッシュアセットの保存パスを決定
        var scene = SceneManager.GetActiveScene();
        var sceneDataDir = "Assets";

        if (!string.IsNullOrEmpty(scene.path))
        {
            var dir = Path.GetDirectoryName(scene.path);
            sceneDataDir = $"{dir}/{Path.GetFileNameWithoutExtension(scene.path)}";
            if (!Directory.Exists(sceneDataDir))
                Directory.CreateDirectory(sceneDataDir);
        }

        var path = UnityEditor.AssetDatabase.GenerateUniqueAssetPath(
            $"{sceneDataDir}/SplineMesh_{mesh.name}.asset");

        // メッシュアセットの保存
        UnityEditor.AssetDatabase.CreateAsset(mesh, path);

        // メッシュアセットを選択状態にする
        UnityEditor.EditorGUIUtility.PingObject(mesh);

        return mesh;
    }
#endif
}

上記をSplineMeshDeformStatic.csという名前で保存し、メッシュを変形したいゲームオブジェクトにアタッチし、インスペクターから必要な設定を行ってください。

1つ目の例との相違点は、変形元と変形先のメッシュをインスペクターから指定する部分です。

また、スクリプトをアタッチする際に自動でメッシュを複製するようにしました。

実行結果

ゲームを実行していない状態でも変形できるようになりました。また、変形するとアセット側にも反映されていることが分かります。

スクリプトの説明

スクリプトが追加されたときなどに、変形後のメッシュアセットの作成や、MeshFilterなどのメッシュ差し替えを自動化するため、次のResetイベントでその処理を行っています。

private void Reset()
{
    // MeshFilterがあれば、そのメッシュを入力とする
    if (TryGetComponent<MeshFilter>(out var meshFilter))
    {
        _sourceMesh = meshFilter.sharedMesh;
        _deformedMesh = CreateMeshAsset();
        
        // MeshFilterを設定
        meshFilter.sharedMesh = _deformedMesh;
    }
    
    // MeshColliderがあれば、同様に設定
    if (TryGetComponent<MeshCollider>(out var meshCollider))
        meshCollider.sharedMesh = _deformedMesh;
}

参考:MonoBehaviour-Reset() – Unity スクリプトリファレンス

メッシュの作成処理では、メッシュを複製し、シーン名とオブジェクト名に基づきパスを決定し、アセットとして保存しています。

// メッシュアセットの作成処理
// 実装はSplinesExtrudeのものを参考にしました
private Mesh CreateMeshAsset()
{
    if (_sourceMesh == null)
        return null;

    // 新しいメッシュアセットを複製
    var mesh = Instantiate(_sourceMesh);
    mesh.name = name;

    // メッシュアセットの保存パスを決定
    var scene = SceneManager.GetActiveScene();
    var sceneDataDir = "Assets";

    if (!string.IsNullOrEmpty(scene.path))
    {
        var dir = Path.GetDirectoryName(scene.path);
        sceneDataDir = $"{dir}/{Path.GetFileNameWithoutExtension(scene.path)}";
        if (!Directory.Exists(sceneDataDir))
            Directory.CreateDirectory(sceneDataDir);
    }

    var path = UnityEditor.AssetDatabase.GenerateUniqueAssetPath(
        $"{sceneDataDir}/SplineMesh_{mesh.name}.asset");
    
    // メッシュアセットの保存
    UnityEditor.AssetDatabase.CreateAsset(mesh, path);
    
    // メッシュアセットを選択状態にする
    UnityEditor.EditorGUIUtility.PingObject(mesh);

    return mesh;
}

メッシュアセットを作成すると、そのアセットを選択状態にするためにEditorGUIUtility.PingObjectメソッドを使用しています。

参考:EditorGUIUtility-PingObject – Unity スクリプトリファレンス

インスペクターから割合tなどを操作したときに反映するため、以下OnValidateイベントでメッシュの変形処理を実施しています。

// インスペクターから操作された際に変形を反映
private void OnValidate()
{
    if (_sourceMesh == null)
        return;

    if (UnityEditor.EditorApplication.isPlaying)
    {
        // 開始前にOnValidateが呼ばれることがあるため、_isStartedを確認
        if (_isStarted)
            Deform();
    }
    else
    {
        // 新しいメッシュアセットを作成
        if (_deformedMesh == null)
            _deformedMesh = CreateMeshAsset();

        if (_deformedMesh == null)
            return;

        Deform();
    }
}

実行時とそうでない場合で処理を分けています。もしメッシュの複製がまだなら、複製してから変形を実行するようにしています。

さいごに

スプラインに沿ってメッシュを変形させるには、SplinesパッケージのAPIを活用して頂点を変形すれば実現できます。

本記事ではCPUのメインスレッドで全て実施しているため、実行時に変形処理を行うケースではパフォーマンスに注意する必要があります。

これを回避するためには、アセット化して事前に変形したり、Compute Shaderや頂点シェーダーなどのGPUリソースを活用するなどの手段があります。

GPUリソースを用いて高速化する方法は、別記事で改めて解説させていただきます。

関連記事

参考サイト

スポンサーリンク