【Unity VFXGraph】レーザーエフェクトを作ってみた

2021/08/11に公開

はじめに

レーザー表現をVisual Effect Graph (VFXGraph)で作ってみました。
https://www.youtube.com/watch?v=LbS1_ju0OXg

今回は Visual Effect Graph やシェーダーについての技術的な解説をしたいと思います。
テクスチャの作り方については解説しません。(テクスチャはSubstance Designerで作成しています)

オブジェクトを狙い撃ちするレーザー

記事の終盤では、オブジェクトを狙い撃ちするレーザーの作り方も紹介します。
https://www.youtube.com/watch?v=DLepFIaTDYI

本記事で使用する数学

以下の事柄についての理解があると、本記事の理解が易しくなります。

  • 三角関数
    • sinΘ, cosΘ
  • ベクトルの合成
    • A + B, A - B の図形的な意味が理解できると良いです。
    • 点P1から点P2へ向かうベクトルは P2 - P1
  • ベクトルの内積
    • A・B = |A| |B| cos Θ
  • ベクトルの外積
    • |A×B| = |A| |B| sin Θ
  • 正射影ベクトル
  • 線形補間
  • 内分

環境

Unity2021.2.0a17
Visual Effect Graph 12.0.0
Universal RP 12.0.0

レーザーVFXGraphのパラメータ

レーザーの発生位置と着弾位置はパラメータとして定義します。

レーザー発生位置と着弾位置を元に、レーザーや火花の向きを計算します。

レーザーの構成要素

今回作成したレーザーエフェクトは以下の要素で構成されています

  1. レーザー発生位置のキラキラマーク
  2. レーザー着弾位置のフレア
  3. 発生位置と着弾位置を結ぶレーザー光線
  4. レーザー消滅時の余韻の粒子
  5. レーザー着弾位置から出る火花
  6. レーザー着弾位置の疑似ライト

要素01 : レーザー発生源のキラキラマーク

使用テクスチャ

実装

レーザー発生位置のキラキラマークの実装は以下のようになっています。

  1. パーティクルはSingle Burstで1つだけ発生させる
  2. パーティクル位置をLaser Src Position (レーザーの発生位置)に合わせる
  3. パーティクル出現時に拡大、消滅時に縮小させる(Age over Lifetime、Sample Curveを利用)
  4. ランダムな点滅を入れて、発光感を出す(GameTimeやValue Noise 1Dを利用)
  5. 自作のVFXShaderGraphを利用してテクスチャコントラストを調整

コントラスト調整ShaderGraph

ノード構成

シェーダーの実装は以下のようになっています。

  • Smoothstepを利用したコントラスト調整
  • カラー情報の乗算
  • ソフトパーティクル処理

ソフトパーティクルの参考ページ :
https://zenn.dev/r_ngtm/books/shadergraph-cookbook/viewer/tips-particle-camera-distance

パラメータ

シェーダーパラメータは以下のようになっています。

プロパティ名 詳細
MainTex メインのテクスチャ
Edge 1 テクスチャのコントラスト調整パラメータ (Smoothstep の Edge 1)
Edge 2 テクスチャのコントラスト調整パラメータ (Smoothstep の Edge 2)
Color カラー情報
SoftParticle FadeStart ソフトパーティクル調整用パラメータ
SoftParticle FadeEnd ソフトパーティクル調整用パラメータ

ランダムな点滅 (Subgraph)

以下のような点滅を作る方法を紹介します。

ランダムな点滅表現を入れるため、自作のSubgraph Operatorを利用しています。
内部ではValue Noise 1DノードやGame Timeノードを使用しています。

Subgraph パラメータ

Subgraph Operatorの入力・出力パラメータは以下のようになっています。

プロパティ名 詳細
Seed 点滅を決めるためのシード値(異なる数値を入れると異なる点滅になる)
Frequency 点滅の速さ
Range 点滅がとる数値の範囲
Power 点滅を補正するPower値
Scale 点滅の出力の係数となるスケール値 (例えば2を入れると、点滅の値は2倍になる)
blink 点滅の出力

Subgraph Operatorのノード構成

  1. Game TimeノードのTotal TimeにSeed値を足す
  2. Value Noise 1Dノード の Coordinate に時間を入力
  3. Value Noise 1Dノード が出力するノイズ値(0~1)をPowerノードで補正(0~1)
  4. Powerノードの結果にRemapノードをかけ、ノイズ出力の値域を0~1から Range.x ~ Range.yに補正する
  5. 最後にScaleを乗算し、blinkとして出力する

要素02 : レーザー着弾位置のフレア

使用テクスチャ

フレアの実装

レーザー着弾位置のフレアの実装は以下のようになっています。

  1. パーティクル位置はLaser Dst Position(レーザー着弾位置)に合わせる
  2. パーティクル発生時、パーティクルをランダムに回転させる
  3. 時間経過でパーティクルの色、サイズ、SpawnRate(パーティクル発生率)変化させる

パーティクルのランダムな回転

パーティクルを Orient: Face Camera Planeでカメラ方向に向けている場合、
パーティクルのZ軸はカメラ方向を向きます。

Set AngleにてZ回転量にランダムな数値を入れることにより、パーティクルをランダムに回転させることができます。

時間経過による色変化

時間経過による色変化を作る方法を紹介します。
Shurikenでいうところの、Start Color(Gradient) です。

以下のようなノード構成で、時間経過の色変化を作ることができます。

時間経過によるパーティクル発生量のコントロール

時間経過でパーティクル発生量を変化させる方法を紹介します。
Shurikenでいうところの、Rate over Time (Curve)です。

以下のようなノードを組むことで、パーティクル発生率を時間変化させることができます。

Animation Curveは以下のようになっています。
時間の範囲0~1、数値値の範囲も0~1にしています。

パーティクル寿命による色変化

パーティクル寿命を利用したサイズ変化をつける方法を紹介します。
Shurikenでいうところの、Size over Lifetimeです。

Age over LifetimeとSample Cruveを利用します。

要素03 : レーザー光線

レーザー光線の実装

レーザー光線の実装は以下のようになっています。

  1. レーザー始点と終点のちょうど中間に球モデルを配置
  2. LookAtノードでモデルを始点から終点へ向くようにする
  3. 球モデルを引き伸ばし、始点と終点をつなぐ
  4. レーザー終了時、球モデルをディゾルブで削る
  5. レーザーは点滅させる

3Dモデルを始点-終点の中間に配置

Lerpを利用することで、始点と終点の中間の位置を求めることができます。

3DモデルをFrom-Toの方向へ向ける

LookAtノードを使うと、モデルのZ軸を座標Fromから座標Toへ向けることができます。

モデルのY軸を向ける方法

Y-upモデルだと工夫が必要

モデルY軸が上である場合、座標ToへY軸を向けるために工夫する必要があります。


Unity標準CylinderはY-up

Z-upのモデルならLookAtでFrom-Toへモデルを向けることができますが、Y-upのモデルの場合は横を向いてしまいます。

LookAtノードは入力from, to, upを利用して、XYZ軸ベクトルを計算しています。(Transform型のデータとして出力します)

  1. fromからtoを向くような単位ベクトルviewVectorを計算し、その正規化をz軸ベクトルとして設定
  2. upとz軸の外積をとり、x軸ベクトルを計算
  3. z軸とx軸の外積をとり、y軸ベクトルを計算
  4. 得られたx軸,y軸,z軸からMatrix4x4を計算(w成分にはfromを設定)

LookAtノードが計算するベクトルXYZ軸を図にすると、以下のようになります。

得られたXYZ軸は、X軸周りに90度回転させれば、fromからtoへY軸を向かせることができます。

ノード構成

LookAtノード出力のanglesのX成分に90を加算すれば、x軸周りに90°回転させることができます。

LookAtノードの実装コード

LookAtノードの実装コードは以下のファイルから読むことができます。

com.unity.visualeffectgraph@12.0.0\Editor\Models\Operators\Implementations\LookAt.cs

LookAt.cs
override protected VFXExpression[] BuildExpression(VFXExpression[] inputExpression)
{
    VFXExpression from = inputExpression[0];
    VFXExpression to = inputExpression[1];
    VFXExpression up = inputExpression[2];

    VFXExpression viewVector = to - from;

    VFXExpression z = VFXOperatorUtility.Normalize(viewVector);
    VFXExpression x = VFXOperatorUtility.Normalize(VFXOperatorUtility.Cross(up, z));
    VFXExpression y = VFXOperatorUtility.Cross(z, x);

    VFXExpression matrix = new VFXExpressionVector3sToMatrix(x, y, z, from);
    return new[] { matrix };
}

LookAtノード使用上の注意点 : from-toベクトルとupベクトルは違う向きにする

LookAtノードにて指定しているupベクトルと、from-toベクトルの向きが一致した場合、レーザーが表示されなくなります。(向きが同じベクトルの外積結果は0になるため)

例えば、from = (0, 10, 0), to = (0, 0, 0)とするとレーザーが表示されなくなってしまいます。
(up = (0, 1, 0)の場合)

この場合、up = (1, 0, 0)とすればレーザーは正しく表示されます。

今回のレーザー表現では、高いところから地面に向けてレーザーを飛ばすので、レーザーが真横を向くというケースはありません。
なので up = (1, 0, 0)や up = (0, 0, 1)といった地面に平行なベクトルをupとするのが良さそうです。

レーザー3Dモデルのスケール変化

時間経過でレーザーがだんだん細くなるようにしています。
以下のような方法で実装できます。

  • 3DモデルのXZ軸スケールをパーティクルの寿命で変化させる. (Y-upのモデルを使う場合)

Z-upのモデルを使う場合は、3DモデルのXY軸スケールを変化させます

レーザー消滅時のディゾルブ表現

レーザーが消滅する直前で、レーザーが削れるような表現(ディゾルブ表現)を入れています。

使用テクスチャ

ディゾルブ表現には以下のノイズテクスチャを使用しています

ディゾルブの作り方

  1. ディゾルブ用のShaderGraphを作成し、ディゾルブのしきい値をプロパティとして定義
  2. ディゾルブのしきい値をVFXGraph側で変化させる

ディゾルブ用ShaderGraph

ノイズテクスチャをStepで二値化し、Alphaとして出力することでディゾルブ表現を作ります。

ShaderGraphパラメータ

シェーダーパラメータは以下のようになっています。

プロパティ名 詳細
Color カラー情報
DissolveTex ディゾルブのテクスチャ
Dissolve UV Scale UVスケール(ディゾルブのスケール感を調整するためのパラメータ)
Dissolve Threshold ディゾルブの二値化しきい値

ディゾルブのコントロール

Sample Curveを利用し、Dissolve Thresholdの値を変化させます。

要素04 : レーザー消滅時の粒子

レーザーが消滅するタイミングで、以下のような粒子を出して余韻を表現しています。

使用テクスチャ

粒子の実装

実装は以下のようになっています。

  1. レーザーが消滅するタイミングでパーティクルを出す
  2. レーザーの始点と終点を結ぶ線分上のランダムな位置からパーティクルを発生させる
  3. パーティクル発生時、位置を球状にばらつかせる
  4. Turbulenceノードでゆらぎを入れる

粒子の発生タイミングを遅らせる

Spawnコンテキストをクリックし、Inspectorタブにて Delay Mode を Before Loop に設定します。

Spawnコンテキストに、ループの発生のDelayを設定できるようになります。

Delay Before Loop に2を設定した場合、VFXの再生開始から2秒後にパーティクルが発生します。

パーティクルを球状にばらつかせる

Set Position (Shape: Sphere) を利用すると、球内のランダムな位置にパーティクルを設定できます。

線分上のランダムな位置からパーティクルを発生させる

まず、点P1と点P2を結ぶ線分をt:1-tに内分することを考えます。
内分位置はlerpを利用することで求まります。

0~1のランダム値をtとすることで、線分上のランダムな位置をとることができます。
VFXGraphのLerpノードのSが上記の数式のtに相当します。

粒子位置を設定するノード構成

Set Position (Shape: Sphere) を実行した後、線分のランダム位置を足します。
これにより、レーザー始点と終点を結ぶ線分周囲のランダムな位置に発生するパーティクルを作ることができます。

パーティクルにゆらぎの動きを入れる

UpdateコンテキストにTurbulenceブロックを入れると、パーティクルが揺らぐようになります。

要素05 : レーザー着弾位置から出る火花

使用テクスチャ

火花の実装

実装は以下のようになっています。

  • レーザーの始点・終点から火花の速度を計算する
  • Turbulenceノードで揺らぎを入れる
  • Linear Dragでパーティクルを減速させる

火花の向きの計算

  1. レーザーの向きベクトルをXZ平面に投影したベクトルと、Y軸上方向ベクトルを合成した速度を計算
  2. 半球面に分布する速度ベクトルを計算
  3. 2 と 3 で求めた二つの速度ベクトルをLerpで合成


向きベクトルと半球ベクトルの合成イメージ

ベクトルのXZ平面への投影

ベクトルのY成分を0にすると、ベクトルのXZ平面への投影を作れます。

3次元ベクトルのXZ平面への投影

火花の方向ベクトルの計算

レーザー向きベクトルのXZ平面投影と、Y軸上方向ベクトルを加算して、火花の向きベクトルを計算します。

半球内に分布するベクトルの計算

Set Position(Shape: Sphere)で設定したパーティクルの位置を、そのまま速度として利用します。
Y軸にAbsoluteを適用し、球が地面に埋もれないようにします。

二つのベクトルを合成する

方向ベクトルのみを火花の速度に設定した場合、まっすぐ伸びるような火花になります。

方向ベクトルのみをパーティクルに反映

半球ベクトルのみを設定した場合は、拡散するような火花になります。

半球ベクトルのみをパーティクルに反映

方向ベクトルと半球ベクトルを 7: 3 の比率で混ぜると、以下のような火花になります。

方向ベクトルと半球ベクトルを7:3の比率で合成

二つのベクトルをランダムに合成する

火花の方向ベクトルと、半球内に分布するベクトルの二つをLerpで合成します。
ブレンド率はランダム値を使い、方向ベクトルと半球ベクトルがランダムに混ざるようにしています。

結果は以下のようになります。
https://youtu.be/qRloWM5_MYo

Animation Curveを利用して乱数に偏りを持たせる

0~1のランダム値を利用してAnimationCurveをサンプリングします。

横軸を入力とすると、タテ軸は出力になります。

いろいろなAnimationCurve

1だけを出力するAnimationCurve

以下の直線にすると、0~1のいずれの入力に対しても1が出力されます。

どの値も等しく出るAnimationCurve

以下のような直線にすると、0~1の入力をそのまま出力します。

大きい数値が出にくいAnimationCurve

以下のようなカーブにすると、1付近の数値があまり出なくなります。

大きいパーティクルを時々混ぜたい、といったケースで使用するAnimationCurveです。

小さい数値が出にくいAnimationCurve

以下のようなカーブにすると、0付近の数値があまり出なくなります。

0.5付近が出やすいAnimationCurve

以下のようなカーブにすると、0.5付近が出やすくなります。

パーティクルを進行方向に伸ばす

火花パーティクルは、パーティクルを進行方向に伸ばしています。
以下のような方法で実装しています。

  1. パーティクルY軸スケールを伸ばす
  2. Orient: Along Velocity で パーティクルを速度方向に向ける

パーティクルの横方向のサイズと縦方向のサイズはそれぞれプロパティ化しておき、サイズ感を細かく調整できるようにしています。

火花VFXGraph全体

火花はSubgraph化する

今回のレーザー表現では火花を3種類重ねて表現しています。
火花をレーザーとは別のVFXGraphとして作成し、レーザーVFXGraphから火花VFXGraphを呼び出す形で利用しています。

要素06 : レーザー着弾位置の疑似ライト表現

レーザー着弾位置にフレア系テクスチャをを配置し、ライトに照らされているように見せています。
疑似ライトあり/なしを横に並べてみました。

使用テクスチャ

疑似ライトの実装

  1. レーザー着弾位置にフレア系パーティクルを出す
  2. Orient: Look At PositionでY軸上方向を向かせる

その他 : Boundsの設定

VFXGraphにはBoundsというものが存在し、Initializeコンテキストにて設定できます。

Boundsは箱のようなもので、
Boundsがエフェクト全体を覆っていない場合、エフェクトの一部が画面外に出た場合に
エフェクトが表示されなくなるという不具合を引き起こします。

以下のように、エフェクト全体をBoundsが覆うように設定すればこの問題は回避できます。

レーザーの始点・終点からBoundを自動計算する

始点・終点を含む直方体を計算することで、レーザー全体を覆うBoundsになります。

C#との連携 : オブジェクトをレーザーで狙い撃ちする

C#プログラムと連携して、オブジェクトを狙い撃ちするレーザーを作ってみましょう。
https://www.youtube.com/watch?v=EuERLnhDTgc

一定間隔でレーザーを撃つC#スクリプト

VFXLaserPlayer.cs
using System.Collections;
using UnityEngine;
using UnityEngine.VFX;

/// <summary>
/// 一定時間ごとにLaserを撃つMonoBehaviourクラス
/// </summary>
public class VFXLaserPlayer : MonoBehaviour
{
    [SerializeField] private VisualEffect visualEffect; // レーザーVisualEffect
    [SerializeField] private Transform laserTarget; // レーザーが狙い撃ちする対象
    [SerializeField] private float interval = 4f; // レーザーを撃つ間隔

    private IEnumerator Start()
    {
        while (true)
        {
            yield return new WaitForSeconds(interval); 
            visualEffect.Reinit(); // VFXの初期化
            visualEffect.Play(); // VFX再生
        }
    }

    void Update()
    {
        // レーザー終点位置(Laser Dst Position)にオブジェクト座標を代入
        // Position型のプロパティに値を設定する場合、プロパティ名_positionを指定する必要がある
        visualEffect.SetVector3("Laser Dst Position_position", laserTarget.position);
    }
}
C#で指定するプロパティ名を知る方法

SetVector3などで指定するプロパティ名を見る方法を紹介します。

Inspector右上からDebugモードに切り替えます。

Visual EffectコンポーネントのProperty Sheetという項目で、プロパティの内部的な名前を見ることができます。
プロパティLaser Src Positionは内部的には Laser Src Position_position という名前になっていることが分かります。

VFXLaserPlayer

VFXLaserPlayerコンポーネント上では、レーザーのVisualEffectとレーザーが狙い撃ちする対象のオブジェクトを登録します。

結果

レーザーがオブジェクトを狙い撃ちするようになりました。
https://www.youtube.com/watch?v=EuERLnhDTgc

レーザーの光線や火花は、レーザーの始点位置・終点位置を元に全自動で計算されるので、
C#プログラムからは位置の座標を書き換えるだけで、いろいろな場所を狙い撃ちするレーザーが作れるというわけです。

C#との連携 : レーザーをオブジェクトにぶつける

レーザーが途中でオブジェクトにぶつかるようにしてみます。
https://www.youtube.com/watch?v=DLepFIaTDYI

実装の流れ

  1. 疑似ライトを法線方向に向ける
  2. C#プログラムにて、レーザーがぶつかる場所の位置をレーザー終点に設定
  3. 法線ベクトルnから火花の飛ぶ方向を計算

法線情報から火花の飛ぶ方向を計算する(VFXGraph)

VFXGraphにレーザー終点の法線ベクトルを定義し、法線を元に火花の飛ぶ方向を計算します。

手順1 : 法線ベクトルの定義

法線ベクトルのパラメータを定義します。
今回は Vector3型プロパティのCollision Normalを追加します。

手順2 : 疑似ライトを法線方向に向ける

疑似ライトは(0, 1, 0)の方向を向けていました。
今回は疑似ライトの向きが面に合うように法線方向に向けます。

手順3 : 火花の反射ベクトルの計算

これまでのやり方は、レーザーがXZ平面で反射する方向を火花の飛ぶ方向としていました。(左)
今回の方法では、オブジェクトの表面で火花を反射させます。(右)

ベクトルの平面射影

ベクトルvの平面への射影はv - (v・n)nで求まります。

ベクトルの平面射影(VFXGraph)

平面射影は自作のSubgraphとして定義しています。

Subgraphの中身は以下のようになっています。

反射ベクトルの計算

レーザー向きの平面射影を定数倍したベクトルと、法線(Collision Normal)を定数倍したベクトルを加算して、
火花の向きベクトルを計算します。

ノードにすると以下のようになります。

手順4 : 半球状に分布するベクトルを作る

地面(XZ平面)に関して半球ベクトルを計算する際、球状に分布するベクトルのY座標にAbsを適用し、地面に埋もれないような半球を作っていました。

3Dオブジェクト上の面に関して半球を作りたい場合は、この方法では不十分です。

面の方向でベクトル反転

以下のように面の下に潜り込んでいるベクトルを面に関して反転させ、半球を作ります。

面に関するベクトル反転は、ベクトルvの反射ベクトルrを計算することで求まります。

面の下を向いているかどうかは内積を利用することで判定できます。

これを実装することにより、面の下にははみ出ない火花にすることができます。

反射ベクトルの計算(VFXGraph)

ベクトル反転をVFXGraphで実装すると、以下のようになります。

Subgraphの中身は以下のようになっています。

手順5 : 火花の入射角で速度に強弱をつける

レーザーの入射角によって火花の飛び方を変える

レーザーが鈍く入射してきた場合、レーザーが反射する方向へ火花を飛ばします。

レーザーが鋭く入射してきた場合、全体的に広がるような火花にします。

外積を利用した強弱

外積L×nのベクトル長を利用することで、面の入射角でベクトルに強弱をつけることができます。
(レーザーが垂直に近づくとΘ=0に近づくので、外積のベクトル長もゼロに近づきます)

入射角で強弱をつけるノード構成

入射角による強弱を求めるノードは以下のようになります。

火花の速度ベクトルに、今回の強弱を乗算します。

手順6 : 火花を計算する最終的なノード構成

火花が飛ぶ方向を決定するノードは以下のようになります。

  1. レーザー反射方向の速度ベクトルを求める
  2. 半球上に分布するベクトルを求める
  3. 1.の速度ベクトルと2.の半球ベクトルを合成する
  1. 火花の速度ベクトルを求めるノードは以下になります。

  2. 半球ベクトルを求めるノードは以下になります。

  3. これら二つをLerpで合成します。

  4. 3)で合成した速度にランダム値を乗算し、火花の動きにばらつきを持たせています。

コリジョン法線やコリジョン位置をVFXGraphに渡すC#プログラム

先ほどのVFXLaserPlayer.csを改造し、レーザーがぶつかる点の座標や法線をVFXGraphに渡すようにします。

VFXLaserPlayer.cs
using System.Collections;
using UnityEngine;
using UnityEngine.VFX;

/// <summary>
/// 一定時間ごとにLaserを撃つMonoBehaviourクラス
/// </summary>
public class VFXLaserPlayer : MonoBehaviour
{
    [SerializeField] private VisualEffect visualEffect; // レーザーVisualEffect
    [SerializeField] private Transform laserTarget; // レーザーが狙い撃ちする対象
    [SerializeField] private float interval = 4f; // レーザーを撃つ間隔

    private IEnumerator Start()
    {
        while (true)
        {
            yield return new WaitForSeconds(interval); 
            _visualEffect.Reinit();
            _visualEffect.Play();
        }
    }

    void Update()
    {
        // Ray情報の作成
        var origin = _visualEffect.GetVector3("Laser Src Position_position"); // レーザー始点
        var direction = (laserTarget.position - origin).normalized; // レーザー向きベクトル
        var ray = new Ray(origin, direction);

        // Raycast実行
        if (Physics.Raycast(ray, out RaycastHit hit))
        {
            // Rayが当たった位置をレーザー終点に設定
            _visualEffect.SetVector3("Laser Dst Position_position", hit.point);

            // 法線情報の設定
            _visualEffect.SetVector3("Collision Normal", hit.normal);
        }
        else
        {
            // レーザー終点の設定
            _visualEffect.SetVector3("Laser Dst Position_position", laserTarget.position);

            // 法線情報の設定
            _visualEffect.SetVector3("Collision Normal", new Vector3(0f, 1f, 0f));
        }
    }
}

結果

オブジェクトにレーザーをぶつけた際、火花がそこそこイイ感じに飛んでくれるようになります。
https://www.youtube.com/watch?v=DLepFIaTDYI

Discussion