VFXGraphとShaderGraphを連携させて綺麗なパーティクルを作ろう
こんばんは。ハツェです。
今回はョョョねこ Advent Calendar 2021の記事として参加しています。
当記事では、VFXGraphの簡単な作例例をご紹介します。よろしくお願いします。
動作環境はUnity(URP) : 2021.2.6f1、VisualEffectGraph : 12.1.2です。
目次
概要
今回は以下のようなパーティクルを作成します。
チュートリアル感覚で記事を書いていくので、よければ見ていってください。

その⓪: VisualEffectGraphをインポートする
Window > Package Manager
からPackages > VisualEffectGraph
を選択し、右下のInstallを押します。

その①: Shaderを作る
最初に、Cubeに使うShaderを作成します。
ShaderGraphの詳細な説明は、以下のマニュアルを見てください。
docs.unity3d.com
ShaderGraphを作成する
Projectタブの任意のフォルダ内にてCreate > ShaderGraph > URP > Unlit Shader Graph
を選択し、.shadergraphを作成します。名前は任意で大丈夫です。

作成したらダブルクリックしてノードエディタを開きます。
ShaderGraphの下準備をする
Shaderをいじる前に、やっておくことがあるため、先にそれを行います。
ノードエディタ右上にあるGraph Inspector
をクリックして、以下の画像の赤線の箇所を変更してください。特にSupportVFXGraphにチェックを入れておかないと、VFXとShaderGraphの連携が出来なくなります。

もう一つ、Graph Inspector
の左にあるBlackboard
ボタンを押し、以下の三つのプロパティを作成しておきましょう。それぞれ、Color、Float、Floatです。

Shaderを作る
さて、Shaderを作っていきます。最初はUVを縁取りするようなマスクを作ります。以下の感じです。

UVを分解する
Splitノードを用いて、UVの次元を分解します。ノードエディタの何もないところでスペースキーを押すことで検索画面が出てくるので、そこでノード名のSplit
と検索することでSplitノードを出すことができます。同じ要領でUVノードも出した後、UVからSplitへとノードを繋げてみてください。

U軸方向のマスクを作る
詳細なノードの説明は省きます。以下の通りにノードを繋げてください。

V軸方向のマスクを作る
似たような感じでV軸方向のマスクも作ります。以下の通りにノードを繋げてください。

それぞれのマスクを足してマスクを完成させる
U軸側、V軸側それぞれのマスクを足して、輪郭が出るようにします。以下の通りにノードを繋げてください。
ちなみに、saturationはマスクの角が過加算されているのを修正するために使っています。

ここまで綺麗に出来ていれば、Widthを0~1の範囲で動かすと以下の通りになるはずです。

マスクを基に色をつける
最後に、作ったマスクと色を掛け合わせて完成です。以下の通りです。

これで、必要となるShaderを作成することが出来ました。最後に、左上のSave Assetを押して保存しておいてください。
その②: VFXを作って、ShaderGraphと連携する
ここからは、実際にVFXGraphでパーティクルを作りながらShaderGraphと連携していく方法を紹介します。
詳細な使い方は、以下のマニュアルを見てください。
docs.unity3d.com
VisualEffectオブジェクトを作成する
まず、HierarchyでCreate > Visual Effect > VisualEffect
からVisualEffectを作成します。

次に、Newボタンから.vfxファイルを作成します。名前は任意で良いです。

すると、GameObjectの位置にこのようなパーティクルが出ると思います。

変数を作成する
ShaderGraphと同様、最初に変数を作成します。以下の五つを用意してください。

Spawnノードをバースト化させる
Spawnノードは文字通りパーティクルのスポーン設定をするシステムです。
初期ノードにおけるSpawnノードは、1秒間に16回パーティクルがスポーンする設定のアトリビュートがアタッチされています。

ですが、今回はこれではなく一定間隔ごとに一気にパーティクルが出るものを用いるため、このアトリビュートは使用しません。なので、最初にConstant Spawn Rate
を選択し、キーボードのDeleteボタンを押して削除します。
その後、Press space to add blocks
と書いてある部分にカーソルを持ってきた後、Spaceキーを押してPeriodic Burst
を生成し、以下の通りに変数を繋げます。

Initialize Particleノードの設定
次にInitialize Particleノードのアトリビュートを変更します。
Initialize Particleノードはパーティクルが生成される瞬間に速度など、様々な初期設定をするシステムです。
先程の要領で、以下の画像の通りに設定してみてください。今回はアトリビュート内の数値まで揃える必要があります。
また、一部検索しても出てこないものがあります。その場合は、似たようなものを一度作った後、そのアトリビュートを選択した状態でInspectorを見ると詳細設定が出るので、そこで調整してあげることで画像と同じ状態にすることが出来ると思います。

初速をランダムにする
VisualEffectGraphでは、プロパティに計算式を入力することも可能となっているので、それについても紹介出来ればと思います。
Initialize Particleノードの外にカーソルを持ってきた後、Spaceキーを押してMultiply
と検索するとMultiplyノードが出てきます。これを用いて変数の値をBに、変数に-1を掛けた値をAにそれぞれSet Velocity Random
に繋げます。

Updateノード
Updateノードはパーティクルが生きている間、ずっと影響し続けるシステムです。例えば、Add VelocityをUpdateノードに入れると、生きている間はずっとパーティクルに力を与え続けます。
このノードでは、抗力係数(空気抵抗みたいなやつ)とかを設定することも出来ますが、今回は特に何も入れていません。

出力するパーティクルの形状決める
最後はOutputノードです。Outputノードはパーティクルの色や形状を決めるシステムです。デフォルトではOutput Particle Quad
が繋がっています。

ですが、今回はQuadじゃなくてCubeを出したいのでOutput Particle Quad
は消して、Output Particle Mesh
を作成してUpdateノードから繋げてください。

その後、Meshの箇所をCubeにします。
パーティクルの色合いを決める
Output Particle MeshをUpdateから繋げたら、まずShaderGraphのところに最初作ったShaderを入れてください。

その後、Set Size over LifetimeとSet Color over Lifetimeを追加してSizeのカーブをいじります。このカーブはパーティクルの大きさを決めるものなので好きにいじってもらって構わないですが、本記事では以下のようにしています。

ShaderGraphのプロパティに計算式を当てはめる
最後に、ShaderGraphのプロパティにVFXの変数や計算式を当てはめます。
AlphaとWidthはVFXの変数をそのまま繋げます。
EmissionColorには、HSVで決めた色を渡してあげます。今回は色相をランダム値で決め、それ以外は固定値で決めたHSVカラーをEmissionColorとします。

これでVFXGraph側も完成なので、Saveボタンで保存しておいてください。
Inspectorでプロパティの設定をする
VFXGraphによるシステム作りはこれで終わったので、最後にプロパティでパーティクルの数や枠の太さなどを調整します。ここではShaderGraphのプロパティ値も調整できるので、好きな見た目になるように値を決めてみてください。

完成
完成です。お好みでポストプロセスを入れてみてください。

終わりに
今回はシンプルなVFXとShaderGraphの連携方法について紹介しました。
ShaderGraphを記事で説明するのは難しいですね。どうしても写真だらけになっちゃいますよね。けど、ShaderGraphは途中結果も見れるので、デバッグや試行錯誤がしやすいのが特徴ですね。良いシェーダーを作って良いパーティクルを作ってみてください。
本記事のサンプルはこちらのGithubからclone出来ます。ライセンスを一読した上でご使用ください。
github.com
以上です。ありがとうございました。
明日はqueさんの記事になります!お楽しみにー!
UdonのLate-Joiner対応3選
こんばんは。ハツェです。お久しぶりです。
Udonに関する様々な記事や作成例が公開されてきたおかげか、Udonで面白いものを作る人が増えましたね。嬉しい話です。
さて今回は、VRChat Advent Calendar 2021の記事として参加しており、主に同期周りの話をしようと思います。
動作環境は、Unity : 2019.4.31f1、VRCSDK : 2021.11.24.16.19、U# : v0.20.3です。
目次
今回の概要
今回はワールドのインスタンスに後からJoinしてきた人(以降Late-Joinerと呼ぶ)に対して、それまでの処理や変数を同期させる方法について語ります。
私自身も作っていてどう作るんだっけ?って悩むことが何回かあったので、情報をまとめる感じで伝えられたらと思います。
それではやっていきましょう!
前置き
今回は一応、bool, int, animationの三つを同期させる方法を各種紹介します。

また、変更させるAnimatorは以下の通りになっているものとします。

その① : 素直に同期変数を用いる
最初は王道の方法を紹介します。というか特段の事情が無ければ、同期変数を用いてあげる方が早いですし、安定しますし、公式も推奨しているぐらいなので、この方法がいいと思います。
Late-Joiner対応は、OnValueChangedの仕組みで勝手にやってくれます。
スクリプト
using UdonSharp; using UnityEngine; using UnityEngine.UI; using VRC.SDKBase; [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] public class UseSyncVariable : UdonSharpBehaviour { private const string DEFAULT_STATE_NAME = "Init"; private const string ANIMATION_STATE_NAME = "PingPong"; [SerializeField] private Text _boolLabel; [SerializeField] private Text _intLabel; [SerializeField] private Animator _animator; [UdonSynced, FieldChangeCallback(nameof(BoolData))] private bool _boolData; [UdonSynced, FieldChangeCallback(nameof(IntData))] private int _intData; [UdonSynced, FieldChangeCallback(nameof(AnimData))] private bool _animData; public bool BoolData { get => _boolData; set { _boolData = value; // 実際の処理 _boolLabel.text = $"bool: {_boolData}"; } } public int IntData { get => _intData; set { _intData = value; // 実際の処理 _intLabel.text = $"int: {_intData}"; } } public bool AnimData { get => _animData; set { _animData = value; // 実際の処理 _animator.Play(_animData ? ANIMATION_STATE_NAME : DEFAULT_STATE_NAME); } } public void ToggleBool() { // 同期変数変更するためのオーナー譲渡 SetOwner(); // 値変更 BoolData = !BoolData; RequestSerialization(); } public void AddInt() { // 同期変数変更するためのオーナー譲渡 SetOwner(); // 加算処理 IntData++; RequestSerialization(); } public void ToggleAnimation() { // 同期変数変更するためのオーナー譲渡 SetOwner(); // 値変更 AnimData = !AnimData; RequestSerialization(); } private void SetOwner() { Networking.SetOwner(Networking.LocalPlayer, this.gameObject); } }
メリット
- SDKベースに作れるので楽
- 同期ミスがほとんどない
- setterに処理内容を書いておけば良いので、値を変更するだけで動いてくれる
- Late-Joinerのためのコードが必要ない
デメリット
- 同期変数の数に限りがある
その② : 一度Ownerに問い合わせて同期させる
Ownerの値に応じて、それに適した処理をSendCustomNetworkEventで全員に送る方法です。引数を持てない現状では、boolのような二値データの処理に使うぐらいが限界です。intのような想定出来ない値になるもの対しては数値範囲を制限して使う場合に限り、この方法が使えると思います。
Late-Joiner対応は、OnPlayerJoined関数内で行っています。
スクリプト
using UdonSharp; using UnityEngine; using UnityEngine.UI; using VRC.SDKBase; [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] public class ViaOwnerRequest : UdonSharpBehaviour { private const string DEFAULT_STATE_NAME = "Init"; private const string ANIMATION_STATE_NAME = "PingPong"; [SerializeField] private Text _boolLabel; // [SerializeField] private Text _intLabel; [SerializeField] private Animator _animator; private bool _boolData; // private int _intData; private bool _animData; public override void OnPlayerJoined(VRCPlayerApi player) { // Late-Joiner対応箇所 if (Networking.LocalPlayer.IsOwner(this.gameObject)) { // Ownerがtrueの状態になっていたら、一度全員にtrue処理を渡す if (_boolData) { SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, nameof(BoolTrueProcess)); } if (_animData) { SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, nameof(AnimationTrueProcess)); } } } public void ToggleBool() { // _boolData = !_boolDataと同じ処理内容 SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, _boolData ? nameof(BoolFalseProcess) : nameof(BoolTrueProcess)); } public void BoolTrueProcess() { // _boolDataがfalseの人にだけ処理させる(Late-Joiner対応にも使うため) if (!_boolData) { _boolData = true; // 実際の処理 UpdateBoolLabel(); } } public void BoolFalseProcess() { _boolData = false; // 実際の処理 UpdateBoolLabel(); } public void AddInt() { // SendCustomNetworkEventに引数が使えたら、以下のようなコードで処理が出来そう // _intData++; // SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, ReceivedIntProcess(_intData)); } public void ToggleAnimation() { // _animData = !_animDataaと同じ処理内容 SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, _animData ? nameof(AnimationFalseProcess) : nameof(AnimationTrueProcess)); } public void AnimationTrueProcess() { // _animDataがfalseの人にだけ処理させる(Late-Joiner対応にも使うため) if (!_animData) { _animData = true; // 実際の処理 UpdateAnimation(); } } public void AnimationFalseProcess() { _animData = false; // 実際の処理 UpdateAnimation(); } private void UpdateBoolLabel() { _boolLabel.text = $"bool: {_boolData}"; } private void UpdateAnimation() { _animator.Play(_animData ? ANIMATION_STATE_NAME : DEFAULT_STATE_NAME); } }
メリット
- 同期変数を用いない
- 連打されても比較的崩れない
デメリット
- 関数の数が増えるので、コードが煩雑になりやすい
- 値一個ずつに対して関数を作成するので、intのようなほぼ無限に関数が必要になるような場合には使えない
その③ : ObjectSyncを活用して同期させる
最後は同期はObjectSyncに任せて、そのtransformの値を使って疑似的に同期処理をしようという方法です。
Late-Joiner対応は、同じくOnPlayerJoined関数内で行っています。

スクリプト
using UdonSharp; using UnityEngine; using UnityEngine.UI; using VRC.SDKBase; [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] public class UseTransformData : UdonSharpBehaviour { private const string DEFAULT_STATE_NAME = "Init"; private const string ANIMATION_STATE_NAME = "PingPong"; [SerializeField] private Text _boolLabel; [SerializeField] private Text _intLabel; [SerializeField] private Animator _animator; [SerializeField] private Transform _transformForData; public override void OnPlayerJoined(VRCPlayerApi player) { // Late-Joiner対応箇所 if (player.isLocal) { // Late-Joinerにだけ、ObjectSyncのTransform値を再取得させる UpdateBoolLabel(); UpdateIntLabel(); UpdateAnimation(); } } public void ToggleBool() { SetOwner(); // x座標が四捨五入で1ならtrue、0ならfalseという扱い // 処理内容はbool値を反転させているだけ var position = _transformForData.position; position.x = Mathf.RoundToInt(position.x) == 1 ? 0 : 1; _transformForData.position = position; // 全員にテキストの更新をさせる SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, nameof(UpdateBoolLabelBody)); } public void UpdateBoolLabel() { // すぐ実行すると、Owner以外は同期に時間がかかるため、誤った結果にならないように遅延させている SendCustomEventDelayedSeconds(nameof(UpdateBoolLabelBody), 0.1f, VRC.Udon.Common.Enums.EventTiming.Update); } public void UpdateBoolLabelBody() { // 実際の処理 _boolLabel.text = $"bool: {Mathf.RoundToInt(_transformForData.position.x) == 1}"; } public void AddInt() { SetOwner(); // y座標の値をそのままintの値とする // 処理内容はintをインクリメントしているだけ var position = _transformForData.position; position.y = Mathf.RoundToInt(position.y) + 1; _transformForData.position = position; // 全員にテキストの更新をさせる SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, nameof(UpdateIntLabel)); } public void UpdateIntLabel() { // 実際の処理 _intLabel.text = $"int: {Mathf.RoundToInt(_transformForData.position.y)}"; } public void ToggleAnimation() { SetOwner(); // z座標が四捨五入で1ならtrue、0ならfalseという扱い // 処理内容はbool値を反転させているだけ var position = _transformForData.position; position.z = Mathf.RoundToInt(position.z) == 1 ? 0 : 1; _transformForData.position = position; // 全員にテキストの更新をさせる SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, nameof(UpdateAnimation)); } public void UpdateAnimation() { // すぐ実行すると、Owner以外は同期に時間がかかるため、誤った結果にならないように遅延させている SendCustomEventDelayedSeconds(nameof(UpdateAnimationBody), 0.1f, VRC.Udon.Common.Enums.EventTiming.Update); } public void UpdateAnimationBody() { // 実際の処理 _animator.Play(Mathf.RoundToInt(_transformForData.position.z) == 1 ? ANIMATION_STATE_NAME : DEFAULT_STATE_NAME); } private void SetOwner() { // UdonとObjectSync両方のOwnerになる必要がある var localPlayer = Networking.LocalPlayer; Networking.SetOwner(localPlayer, this.gameObject); Networking.SetOwner(localPlayer, _transformForData.gameObject); } }
メリット
- 同期変数を用いない
デメリット
- データ用のTransformが原点から離れすぎると、浮動小数の精度が悪くなり、座標がブレブレになる
- コードが煩雑
- 処理を連続で行うと、値が人によってズレる
おわりに
今回はLate-Joinerにも使える同期方法を三つ紹介しました。
結論としてはやっぱり同期変数を使う方法が一番良いです。何しろ記述が楽ですし、安定してデータが届きます。
他二つも紹介しましたが、特段の理由がない限りは使わない方が良いでしょう。私としてはオススメ出来ません。
今回のサンプルはGithubに置いてあります。
ライセンスを確認したうえで、お使いください。
github.com
以上です。
VRChat Advent Calendar 2021の明日の記事は、bd_さんの記事になりますー