【Unity】Line Rendererで破線を描画する

こじゃらこじゃら

破線を表示する方法を教えてほしいの。できればLine Rendererから手軽に使えれば最高だけど…

このはこのは

カスタムシェーダーを自作すれば可能だわ。Line Rendererでの描画もマテリアルとスクリプトの設定で手軽に使えるようになるわ。

Unity提供のLine Rendererで破線を描画する方法の解説記事です。

本記事の内容を実践すると、次のような破線を描画できるようになります。

Line Renderer自体には破線を表示する機能はなく、表示に使われるメッシュは実線です。

そのため、破線を表示するためにはマテリアル側で対処する必要があります。

Line Rendererで破線を表示するには次の手順を実施すれば良いです。

破線を表示する手順の流れ
  • Line Renderer側で線データを設定する
  • 破線描画用のシェーダーを作成する
  • 破線描画用のマテリアル作成・設定
  • Line Rendererに上記マテリアルを適用する
  • 破線の長さや間隔を設定するスクリプトを実装して適用(必要ならば)

本記事では、上記の流れでLine Rendererとカスタムシェーダーで破線描画する方法を解説していきます。

動作環境
  • Unity 2023.1.1f1
  • Universal RP 15.0.6
  • Shader Graph 15.0.6

スポンサーリンク

前提条件

本記事では、URP(Universal Render Pipeline)環境を前提とします。また、破線シェーダーはShader Graphを用いて作成するものとします。

注意

ビルトインレンダーパイプラインでは動作しませんのでご注意ください。

どのレンダーパイプラインが適切かは開発するアプリケーションによって異なります。事前に可能かどうかご確認の上、本記事の手順を実施してください。

本記事と同様の環境で確認する際は、以下パッケージをパッケージマネージャーからインストールしてください。

インストールするパッケージ
  • Universal RP
  • Shader Graph

パッケージのインストールは以下手順により行います。

パッケージのインストール手順
  • UnityのトップメニューよりWindow > Package Managerの順に選択
  • Package Managerウィンドウが開いたら、左上にあるPackage: ~から「Package: Unity Registry」を選択
  • 左上にあるAllタブを選択(古いUnityバージョンでは不要です)
  • 左側のPackagesの一覧から該当パッケージ名を選択
  • 右上(または右下)のInstallボタンをクリックしてパッケージをインストール

最終的に次のようにパッケージにチェックマークがついていればインストールが完了しています。

URPの有効化

URPの有効化がまだで有効化したい場合は、次の手順を実施してください。

まず、プロジェクトウィンドウの何もないところで右クリックし、Create > Rendering > URP Asset (with Universal Renderer)を選択します。

すると、次のようにアセットが2つ追加されます。

トップメニューのEdit > Project Settingsの順に選択し、Project Settingsウィンドウを開きます。

そして、左からGraphicsを選択し、前の手順で作成したUniversal Render Pipeline AssetScriptable Render Pipeline Asset項目に設定します。

Line Renderer側の事前準備

破線を描画するために、まずLine Rendererおよび直線データを作成しておく必要があります。

本記事では、次のようにシーン上に「DashedLine」という名前のゲームオブジェクトを作成し、Line Rendererコンポーネントをアタッチするものとします。

直線の作成方法は幾つか存在しますが、ここではマウスで直線を作成してみます。

Line RendererコンポーネントのScene Tools項目の+ボタンをクリックし、InputからMouse Positionを選択した状態でシーンビューをクリックすると、直線を配置できます。

また、本記事ではカメラから確認できるようにLine Rendererの位置を調整して画面に収まるようにします。

また、Texture ModeStretchであることを前提として解説を進めます。 [1]

破線シェーダーの作成

ここからは、破線描画用シェーダーを作成していきます。

Line RendererのUVマッピング

シェーダーを作成する前に、Line Renderer側のメッシュのUVマッピングについて把握しておく必要があります。

Line RendererのUVマップはTexture ModeがStretchの場合、次のように直線と平行な方向をU軸垂直な方向をV軸としています。UV共に0~1の間でマッピングされます。

Line RendererメッシュのUVマッピング

そのため、U軸方向に対して不透明→透明→不透明→・・・を繰り返すようにすれば実現できます。

破線の描画イメージ

メモ

Line Renderer側が生成するメッシュのUVマッピングは、Line RendererコンポーネントのTexture Modeの設定でも変更できますが、 本記事ではStretchであることを前提としています。

このような表現はUnlitシェーダーなどのテクスチャのタイリングでも実現できますが、後述する破線の長さと間隔の調整をスムーズに行えるようにするためカスタムシェーダーを作成するものとします。

Shader Graphでシェーダーを作成する

Shader Graph用のシェーダーを作成します。

プロジェクトウィンドウで右クリックし、Create > Shader Graph > URP > Unlit Shader Graphの順に選択し、シェーダー名を入力します。

本記事では「DashedLine」としました。

作成したShader Graphのシェーダーファイルをダブルクリックすると、次のようにShader Graphの編集画面が開きます。

プロパティの定義

破線の長さ(表示部分)間隔(非表示部分)を指定するためのプロパティを追加します。

シェーダー名の+アイコンから追加できます。Length(長さ)Space(間隔)Float型として、破線の色ColorColor型のプロパティとして追加します。

参考:Property Types | Shader Graph | 16.0.2

このままでも良いですが、プレビューしやすくするためデフォルト値を0以外の値に変更しておきます。プロパティをクリックで選択し、Graph InspectorDefault項目から変更できます。

例ではLengthの初期値を0.1、Spaceの初期値を0.05としました。

UVのタイリング

破線を実現するために、UV座標のU成分でタイリングさせます。

タイリングの周期破線の長さ(Length) + 破線の間隔(Width)となります。この周期でタイリングさせるようにノードを構築していきます。

まず、何も無いところで右クリックし、ポップアップメニューよりCreate Nodeを選択し、その後検索欄に「UV」と入力し、Input > Geometry > UVを選択し、UVノードを追加します。

参考:UV Node | Shader Graph | 16.0.2

次に、UV座標のU成分だけを取り出すために、Splitノードを追加し、UVノードのOutからSplitノードのInドラッグ&ドロップで接続します。

UVをLength + Spaceの周期で繰り返すために、LengthとSpaceプロパティをドラッグ&ドロップで何も無いところに配置し、Addノードを追加し、AとBにそれぞれLengthとSpaceを接続します。

参考:Split Node | Shader Graph | 16.0.2

次に、剰余演算を行うModuloノードを追加し、AにSplitノードのR、BにAddノードのOutを接続します。

参考:Modulo Node | Shader Graph | 16.0.2

表示・非表示のパターン作成

前述のタイリングされた結果を元に、破線の表示・非表示用のパターンを作成していきます。表示部分は1非表示部分は0の2値化されたパターンとします。

まず、Stepノードを追加し、EdgeにLengthプロパティを、Inに前述のModuloノードのOutを接続します。

すると、StepノードのEdge以上の部分が1それ以外の部分は0に2値化されます。

参考:Step Node | Shader Graph | 16.0.2

次に、結果を反転させるため、One Minusノードを追加し、Inノードに前述のStepノードのOutを接続します。

参考:One Minus Node | Shader Graph | 16.0.2

Tips

一度One Minusノードで反転させるのは、表示→非表示→表示→・・・の順に繰り返すようにするためです。

逆に、非表示→表示→非表示→・・・の順に繰り返したい場合は、以下のようなノード構成にすれば良いです。

Fragmentノードへの反映

前述の破線のパターンを最終的にフラグメントシェーダーに接続する部分を作ります。

まず、Graph InspectorGraph Settingsタブを選択し、Surface TypeTransparentに変更します。

すると、FragmentノードにAlphaが追加されます。

次に、ColorプロパティをFragmentノードのBase Colorに接続し、前述のOne MinusノードのOutをFragmentノードのAlphaに接続します。

ここまでの手順を実施したら、Colorプロパティの初期値を変更してみましょう。

手順に問題なければ、次のようにプレビューに指定された色の縞模様が反映されるはずです。

編集したShader Graphの情報は、Save Assetボタンで忘れずに保存しておきます。

以下、ここまでの手順で作成したシェーダーの全体像です。

マテリアルの作成・適用

プロジェクトウィンドウから、前述の作成した破線シェーダー(DashedLine)を右クリックし、Create > Materialの順に選択します。

メモ

シェーダー以外の場所から右クリックでマテリアルを新規作成した場合は、マテリアルを選択し、インスペクターのShader項目からShader Graphs > [作成したシェーダー名]の順に選択してください。

マテリアルを作成したらLine Rendererに適用します。Line RendererコンポーネントMaterials項目に作成したマテリアルを指定します。

例では、線の太さやコーナーの頂点数を調整することとします。

破線の長さや間隔、色はマテリアルのインスペクターより編集できます。

線の長さが変わっても破線の長さと間隔を維持する

ここまで解説したシェーダーでは、Line Rendererの直線全体の長さを1として破線の長さと間隔を指定していました。

実際の直線の長さに基づくためには、スクリプト側で直線の長さを計算し、ここからマテリアルのプロパティに指定すべき割合を計算する必要があります。

直線全体の長さがシェーダー上では1として計算されるため、破線の長さと間隔を、直線全体の長さで除算すれば良いです。

float totalLength = 直線全体の長さ;
float _length, _space;

・・・(中略)・・・

// 全体の長さに基づき、長さとスペースの割合を計算
var ratio = 1 / totalLength;
var lengthRatio = _length * ratio;
var spaceRatio = _space * ratio;

サンプルスクリプト

以下、直線の長さに基づいて破線の長さと間隔を指定するスクリプトです。

事前にLine Rendererに破線のマテリアルが設定されている必要があることにご注意ください。

DashedLineRenderer.cs
using UnityEngine;

public class DashedLineRenderer : MonoBehaviour
{
    // LineRendererコンポーネント
    [SerializeField] private LineRenderer _lineRenderer;

    // 破線の長さ
    [SerializeField] private float _length = 1f;

    // 破線の間隔
    [SerializeField] private float _space = 1f;

    // マテリアルのプロパティID
    private static readonly int PropLength = Shader.PropertyToID("_Length");
    private static readonly int PropSpace = Shader.PropertyToID("_Space");

    // マテリアルインスタンス
    private Material _material;

    // 破線の長さと間隔を更新する
    public void Refresh()
    {
        if (_lineRenderer == null) return;

        // マテリアルを取得
        if (_material == null)
            _material = _lineRenderer.material;

        // 全体の長さを計算
        var totalLength = CalculateLength();

        // 全体の長さに基づき、長さとスペースの割合を計算
        var ratio = 1 / totalLength;
        var lengthRatio = _length * ratio;
        var spaceRatio = _space * ratio;

        // マテリアルに割合を設定
        _material.SetFloat(PropLength, lengthRatio);
        _material.SetFloat(PropSpace, spaceRatio);
    }

    // 初期化
    private void Start() => Refresh();

    // 後処理
    private void OnDestroy()
    {
        // 参照したマテリアルは明示的に破棄する必要がある
        if (_material != null)
            Destroy(_material);

        _material = null;
    }

    // 全体の直線の長さを計算する
    private float CalculateLength()
    {
        var totalLength = 0f;

        for (var i = 0; i < _lineRenderer.positionCount - 1; i++)
        {
            totalLength += Vector3.Distance(
                _lineRenderer.GetPosition(i),
                _lineRenderer.GetPosition(i + 1)
            );
        }

        if (_lineRenderer.loop)
        {
            // ループする場合は、最初と最後の頂点の距離も加算する
            totalLength += Vector3.Distance(
                _lineRenderer.GetPosition(0),
                _lineRenderer.GetPosition(_lineRenderer.positionCount - 1));
        }

        return totalLength;
    }

#if UNITY_EDITOR

    // インスペクターから更新されたら、マテリアルを更新する
    private void OnValidate()
    {
        if (!UnityEditor.EditorApplication.isPlaying) return;

        Refresh();
    }

#endif
}

上記をDashedLineRenderer.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターよりLine Renderer項目に該当のLine Rendererを、LengthとSpace項目にそれぞれ破線の長さと間隔を指定してください。

実行結果

LengthとSpaceのパラメータをインスペクターから編集すると、直線全体の長さに基づき再計算されていることが確認できました。

実行時では初回のみ計算されますが、それ以降はRefreshメソッドを呼び出して明示的に更新する必要があります。

スクリプトの説明

まず、Line Rendererで定義されている直線全体の長さを計算します。

一つ一つの点との距離を合計した結果が全体の長さです。

var totalLength = 0f;

for (var i = 0; i < _lineRenderer.positionCount - 1; i++)
{
    totalLength += Vector3.Distance(
        _lineRenderer.GetPosition(i),
        _lineRenderer.GetPosition(i + 1)
    );
}

if (_lineRenderer.loop)
{
    // ループする場合は、最初と最後の頂点の距離も加算する
    totalLength += Vector3.Distance(
        _lineRenderer.GetPosition(0),
        _lineRenderer.GetPosition(_lineRenderer.positionCount - 1));
}

Line Rendererの直線が閉じている(ループしている)時は、終点から始点に向かう部分の距離も計算する必要があることに注意します。

破線シェーダー側のプロパティに反映するためには、Line Rendererのマテリアルを取得します。

// マテリアルを取得
if (_material == null)
    _material = _lineRenderer.material;

このマテリアルは破線シェーダーを前提としています。

また、LineRenderer.materialプロパティから取得したマテリアルは、取得側で責任を持って破棄する必要があることに注意します。

// 後処理
private void OnDestroy()
{
    // 参照したマテリアルは明示的に破棄する必要がある
    if (_material != null)
        Destroy(_material);

    _material = null;
}

マテリアルに反映する破線の長さと間隔は、直線全体の長さで割ればよいです。

// 全体の長さを計算
var totalLength = CalculateLength();

// 全体の長さに基づき、長さとスペースの割合を計算
var ratio = 1 / totalLength;
var lengthRatio = _length * ratio;
var spaceRatio = _space * ratio;

// マテリアルに割合を設定
_material.SetFloat(PropLength, lengthRatio);
_material.SetFloat(PropSpace, spaceRatio);

さいごに

破線を描画する方法は様々ですが、カスタムシェーダーとLine Rendererを組み合わせれば簡単に破線を描画できるようになります。

また、Line RendererのUVマップの都合上、シェーダー側に渡すパラメータはスクリプト側で直線の長さを計算するなどすれば便利になる場合があります。

一つの方法として参考にしていただければ幸いです。

関連記事

参考サイト

スポンサーリンク