🤔

[Unity]VFX Graphを自作レンダーパイプラインに導入する

に公開

概要

自作レンダーパイプラインでVFXGraphのパッケージを使えるようにする方法について調査しました。

Unityのバージョンは6000.0.42です。
自作レンダーパイプラインというのはURPやHDRPを改造するという話ではなく、Core RP Libraryで一から作成するレンダーパイプラインという意味です。詳しくは以下もご覧ください。
https://zenn.dev/nithink/articles/3810a563f2e2d6

この記事では、レンダーパイプラインをパッケージとして開発していて、既に最低限の描画フローは実装できている前提とします。
またVFXGraphのツールとしての使い方については概ね習得しているものとします。

はじめに

VFXGraphといえばツールの使い方についてのドキュメントは充実していますが、内部の仕組みについてはほとんど情報がありません。また、VFXGraphの実装はURP/HDRPと密結合的になっているところがあります。そのため、自作レンダーパイプラインでVFXGraphを使えるようにするのはなかなか厳しい道のりとなります。

警告が出る

自作レンダーパイプラインでVFXGraphのパッケージを導入すると、折に触れて以下の警告が出るようになります。

The Visual Effect Graph is supported in the High Definition Render Pipeline (HDRP) and the Universal Render Pipeline (URP). Please assign your chosen Render Pipeline Asset in the Graphics Settings to use it.

(意訳:VFXGraphはHDRPかURPでしかサポートされていません。)

この警告内容を素直に受け取るなら、独自のレンダーパイプラインでVFXGraphを使うことはできないということになります。しかしURPもHDRPもとどのつまりは単なるパッケージなので、(ある程度の困難を伴いそうな予感はあるものの)不可能ということはないはずです。

VFXSRPBinderを継承しようとすると...

まず、VFXSRPBinderを継承したクラスを作成する必要があります。(VFXGraphパッケージ内のEditorのクラスです。)
https://discussions.unity.com/t/vfx-graph-custom-srps/926065/2
しかしこのクラスはアクセス修飾子がpublicでないため、VFXGraphパッケージ(Assembly)の外部から参照することができません。
そのため、にわかには信じられないような方法を使って参照することになります。

不正アクセス

まず、VFXGraphのパッケージを見ると、Editorフォルダ内にPackageInfoというクラスがあります。
Packages/com.unity.visualeffectgraph/Editor/PackageInfo.cs
ここではInternalsVisibleToという属性とともにさまざまな他のパッケージのAssemblyDefinition名が列挙されています。

Packages/com.unity.visualeffectgraph/Editor/PackageInfo.cs
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Unity.VisualEffectGraph.EditorTests")]
[assembly: InternalsVisibleTo("Unity.VisualEffectGraph.EditorTests-testable")]
[assembly: InternalsVisibleTo("Unity.VisualEffectGraph.RuntimeTests")]
[assembly: InternalsVisibleTo("Unity.VisualEffectGraph.RuntimeTests-testable")]
[assembly: InternalsVisibleTo("Unity.Testing.VisualEffectGraph.Editor")]
[assembly: InternalsVisibleTo("Unity.Testing.VisualEffectGraph.Editor-testable")]
[assembly: InternalsVisibleTo("Unity.Testing.VisualEffectGraph.Tests")]
[assembly: InternalsVisibleTo("Unity.Testing.VisualEffectGraph.Tests-testable")]
[assembly: InternalsVisibleTo("Unity.Testing.VisualEffectGraph.EditorTests")]
[assembly: InternalsVisibleTo("Unity.Testing.VisualEffectGraph.EditorTests-testable")]
[assembly: InternalsVisibleTo("Unity.Testing.VisualEffectGraph.PerformanceEditorTests")]
[assembly: InternalsVisibleTo("Unity.Testing.VisualEffectGraph.PerformanceEditorTests-testable")]
[assembly: InternalsVisibleTo("Unity.Testing.VisualEffectGraph.PerformanceRuntimeTests")]
[assembly: InternalsVisibleTo("Unity.Testing.VisualEffectGraph.PerformanceRuntimeTests-testable")]
[assembly: InternalsVisibleTo("Unity.RenderPipelines.HighDefinition.Editor")]
[assembly: InternalsVisibleTo("Unity.RenderPipelines.HighDefinition.Editor-testable")]
[assembly: InternalsVisibleTo("Unity.RenderPipelines.Universal.Editor")]
[assembly: InternalsVisibleTo("Unity.RenderPipelines.Universal.Editor-testable")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

namespace UnityEditor.VFX
{
    static class VisualEffectGraphPackageInfo
    {
        public static string name => "com.unity.visualeffectgraph";
        public static string assetPackagePath => $"Packages/{name}";
    }
}

InternalsVisibleTo属性は、Assemblyの外部からinternalな要素へのアクセスを許すために使われるものです。
https://www.hanachiru-blog.com/entry/2022/07/18/120000
例えばアクセス修飾子としてinternalを付けた要素は通常Asemblyの外部から参照できませんが、[assembly: InternalsVisibleTo("Unity.RenderPipelines.Universal.Editor")]と書いておくことで、Unity.RenderPipelines.Universal.Editorという名前のAssemblyからのアクセスができるようになります。名指しでの許可制ということですね。

つまり、VFXSRPBinderにアクセスするには、以下のいずれかの手段をとることになります。

  • VFXパッケージを書き換え、自作レンダーパイプライン側のAssemblyがアクセスできるようにする。
  • VFXパッケージを書き換えず、自作レンダーパイプライン側のAssembly名を偽装することで不正アクセスする。

まず前者の方法では、VFXGraphパッケージ側のPackageInfoクラスに自作レンダーパイプラインのAssembly名を追加するか、あるいはVFXSRPBinderクラスのアクセス修飾子をpublicにすることになります。
VFXGraphパッケージをCustom化することになってしまうのがネックです。

Packages/com.unity.visualeffectgraph/Editor/PackageInfo.cs
[assembly: InternalsVisibleTo("自作レンダーパイプラインのAssembly名")]

後者の方法としては、PackageInfoクラス内でInternalsVisibleTo属性により列挙されているAssembly名の中のどれかに擬態する(名乗る)ことになります。さながら不正アクセスのようです。

例えばUnity.RenderPipelines.Universal.Editorに擬態する場合は、AssemblyDefinition(.asmdefファイル)を作成し、NameをUnity.RenderPipelines.Universal.Editorにします。

このAssemblyDefinitionによりコンパイルされたcsファイル内であれば、VFXSRPBinderクラスにアクセスでき、継承できるようになります。
あまりにハッキーな方法なので抵抗がありますが、そういうAssemblyDefinitionを作成するのはVFXGraph対応用のフォルダの部分だけで良いですし、VFXGraphパッケージをCustom化するのに比べたらマシだと思います。なお以下のフォーラムでも上記同様の結論に達しており、やはり現状では仕方がないものと思われます。
https://discussions.unity.com/t/state-of-vfx-graph-for-custom-srps/804389/2

VFX GraphパッケージとURP/HDRPパッケージは、このように思いのほか密結合になっています。

asmdefの設定

基本設定

例えば以下のようなフォルダ構成において、asmdefファイルを作成・配置します。

また、asmdefファイルのAssembly Definition ReferencesでVFXGraphパッケージのエディター版のassembly(Unity.VisualEffectGraph.Editor)を参照する設定にしておきます。

先ほどの話にあったようにVFXGraph側が指定するassembly名を騙る必要があるので、Nameを例えばUnity.RenderPipelines.Universal.Editorにします。

VFXGraphを使わない場合

これからレンダーパイプライン側でVFXGraphのAPIを使うことになりますが、レンダーパイプラインをパッケージとして開発している場合は、当然ながらレンダーパイプラインをパッケージとして導入するプロジェクト側でも同じくVFXGraphパッケージを導入していないとコンパイルエラーになります。しかしプロジェクト側では必ずしもVFXGraphを導入するとは限らないため、導入しない場合であってもエラーにはならないようにしたいです。
そこで、asmdefファイルのVersion Definesにcom.unity.visualeffectgraphを追加し、set defineにHAS_VFX_GRAPHを追加します。

以後、レンダーパイプライン側からVFXGraphのAPIを用いるC#ファイルは、全体を以下のようにHAS_VFX_GRAPHで囲むようにします。

#if HAS_VFX_GRAPH
...
#endif

これにより、VFXGraphパッケージがない場合には#if HAS_VFX_GRAPH~#endifで囲まれた部分のコードがコンパイルに含まれなくなります。したがって、VFXGraphを導入していないプロジェクトであってもエラーが出なくなります。

VFXSRPBinderを継承

上記対応によりVFXSRPBinderクラスを問題なく継承できるようになりました。
先述の通りこのクラスはEditorのクラスなので、継承するクラスもEditor以下に配置します。フォルダ構成の例を以下に示します。

VFXSRPBinderクラスは抽象クラスなので、以下のようにabstractなメンバやメソッドをoverrideしなければなりません。
(レンダーパイプライン名は仮でXRPとしています。)

VFXXrpBinder
#if HAS_VFX_GRAPH
using UnityEngine;
using UnityEditor.VFX;
using System;
namespace XrpEditor
{
	class VfxXrpBinder : VFXSRPBinder
	{
		public override string templatePath { get { return "テンプレートファイルのパス(詳細は後述)"; } }
		public override string runtimePath { get { return "hlslファイルのパス(詳細は後述)"; } }
		public override string SRPAssetTypeStr { get { return typeof(レンダーパイプラインアセット).Name"; } }
		public override Type SRPOutputDataType => null;
		public override bool IsShaderVFXCompatible(Shader shader) => true;
	}
}
#endif

まずSRPAssetTypeStrをoverrideしましょう。レンダーパイプラインを制作する上でRenderPipelineAssetクラスを継承したクラスを作成しているはずなので、その型名を返すようにしてください。
これで先ほど触れた以下の警告が出なくなります。

The Visual Effect Graph is supported in the High Definition Render Pipeline (HDRP) and the Universal Render Pipeline (URP). Please assign your chosen Render Pipeline Asset in the Graphics Settings to use it.

例えばVFXGraphアセットをEdit・Saveしたときにこの警告が出なければうまくいっています。
これはVFXSRPBinderを継承したクラスをVFXGraph側が内部的に登録・照合する際にDictionaryのキーとして用いる文字列です。

しかしこれでもまだVFXGraphは描画されません。最低限何か描画するには、以下で紹介するVFXManagerクラスの各種メソッドの呼び出しと、テンプレートファイルの設定が必要になります。

VFXManager

レンダーパイプラインの描画処理において、カメラの描画ごとに以下のVFXManagerクラスのメソッドの呼び出しが必要です。
なおこのクラスはなぜかパッケージ側でなくUnityエンジン側にあるので、#if HAS_VFX_GRAPHで囲む必要はありません。

VFXManager.PrepareCamera(Camera);
RenderGraphのUnsafePassで用いる場合
CommandBufferHelpers.VFXManager_ProcessCameraCommand(Camera, UnsafeCommandBuffer, VfxCameraXRSettings, CullingResults);
UnsafePassでない場合
VFXManager.ProcessCameraCommand(Camera, CommandBuffer, VfxCameraXRSettings, CullingResults);

VfxCameraXRSettingsは、VR対応などを想定していない場合は適当にdefaultでも良いようです。

テンプレートファイル

ディレクトリの設定

テンプレートファイル(.template)とは、VFXGraphがシェーダーコードを生成する際に用いる独自のフォーマット・構造を持つテキストファイルです。
例えばURPパッケージ内の以下のディレクトリでテンプレートファイルの作例を見ることができます。
Packages/com.unity.render-pipelines.universal/Editor/VFXGraph/Shaders/Templates

まず、テンプレートファイルのディレクトリを指定する必要があります。先ほどのVFXSRPBinderを継承したクラスでtemplatePathをoverrideしてディレクトリを指定します。
例)

VFX***Binder
public override string templatePath { get { return "Packages/nithink.xrp/Editor/VfxGraph/Shaders"; } }

Outputコンテクストに応じて

上記で指定したディレクトリにテンプレートファイルを作成します。
とはいっても必要なテンプレートファイルにはOutputコンテクストに応じたいろいろな種類があります。

OutputParticleUnlitQuadコンテクスト

まずはVFXGraphに最初から用意されているOutputParticleUnlit(Quad)コンテクストで最低限描画できるようにしてみましょう。VFXGraphで以下のようにOutputParticleUnlitQuadコンテクストを繋いでいる状態のVFXGrapgアセットを用意します。

例えばVFXGraphアセット作成時に以下のSimpleLoopを選択すれば良いです。

この状態で.vfxファイルをセーブすると以下のエラーが出ます。

Unity cannot compile the VisualEffectAsset at path "Assets/HogeVfx.vfx" because of the following exception:
System.IO.FileNotFoundException: Could not find file "パッケージのパス\editor\vfxgraph\shaders\templates\vfxparticleplanarprimitive.template"

このことから、OutputParticleUnlit(Quad)コンテクストを使っている状態ではVFXParticlePlanarPrimitive.templateというテンプレートファイルが必要になることが分かります。
少し深堀りすると、このコンテクストはVFXGraphパッケージ内のVFXPlanarPrimitiveOutputクラスによって実装されています。このクラス内のcodeGeneratorTemplateプロパティで、このコンテクストが必要とするテンプレートファイル名が以下の通り指定されています。

Packages/com.unity.visualeffectgraph/Editor/Models/Contexts/Implementations/VFXPlanarPrimitiveOutput.cs
public override string codeGeneratorTemplate { get { return RenderPipeTemplate("VFXParticlePlanarPrimitive"); } }

このことからも、やはりOutputParticleUnlit(Quad)コンテクストがVFXParticlePlanarPrimitive.templateを必要としていることが分かります。

なお、このコンテクストではQuad以外にもTriangleやOctagonのモードがあります。

これらはパーティクルのメッシュの形状に関わるモードですが、いずれも全て同様にVFXPlanarPrimitiveOutputクラスとVFXParticlePlanarPrimitive.templateで実装されています。(詳細は後述します。)
PlanarPrimitiveという名前は、「平面の(=Planar)基本的な形状(=Primitive、すなわちQuadや TriangleやOctagon)」という意味になります。要はビルボードです。

VFXParticlePlanarPrimitive.templateとVFXタグ

URPでは、VFXParticlePlanarPrimitive.templateは次のような内容になっています。

Packages/com.unity.render-pipelines.universal/Editor/VFXGraph/Shaders/Templates/VFXParticleLitPlanarPrimitive.template
{
	SubShader
	{	
		Cull Off
		
		${VFXInclude("Shaders/VFXParticleHeader.template")}
		${VFXInclude("Shaders/ParticlePlanarPrimitives/PassSelection.template")}
		${VFXInclude("Shaders/ParticlePlanarPrimitives/PassDepth.template"),IS_OPAQUE_PARTICLE}
		${VFXInclude("Shaders/ParticlePlanarPrimitives/PassDepthNormal.template"),IS_OPAQUE_PARTICLE}
		${VFXInclude("Shaders/ParticlePlanarPrimitives/PassVelocity.template"),USE_MOTION_VECTORS_PASS}
		${VFXInclude("Shaders/ParticlePlanarPrimitives/PassForward.template")}
		${VFXInclude("Shaders/ParticlePlanarPrimitives/PassShadowCaster.template"),USE_CAST_SHADOWS_PASS}
		${VFXIncludeRP("Templates/ParticlePlanarPrimitives/PassForward2D.template")}
	}
}

SubShaderCull Offという部分から、これはShaderLabファイルの元になるものであろうことが分かります。
とすると、$から始まる独特な記述が気になります。
これはVFXGraphのテンプレートファイル独自の仕組みに関係しており、VFXGraphがコードを生成する上でさまざまな意味を持つようです。かなり多くの種類があるようですが、ドキュメントやコメント等がないため詳細は一つ一つ調べてみなければ分かりません。取り急ぎ、このような記述のことをこの記事ではVFXタグと呼ぶことにしたいと思います。
ひとまず上記ファイル内で登場するVFXタグは次の二つです。

  • ${VFXInclude("パス", define)}
  • ${VFXIncludeRP("パス", define)}

これらはもしシェーダーのdefineがtrueなら指定したパスにあるテンプレートファイルにまるごと置き換えるという機能を持ちます。#include "パス"のようなものです。defineの記述は省略することもできます。
RPが付いていない方(VFXInclude)はVFXGraphパッケージ側にあるテンプレートファイルのパスで、RPが付いている方(VFXIncludeRP)はレンダーパイプライン側にあるテンプレートファイルを指します。後者は先ほどVFXSRPBinderを継承したクラスのtemplatePathで指定したテンプレートのパスとなります。
例えば、

${VFXInclude("Shaders/VFXParticleHeader.template")}

という記述は、
Packages/com.unity.visualeffectgraph/Shaders/VFXParticleHeader.template
の中身にまるごと置き換えられます。
また、

${VFXIncludeRP("Templates/ParticlePlanarPrimitives/PassForward.template")}

は、
テンプレートのパス/Templates/ParticlePlanarPrimitives/PassForward.template
の中身にまるごと置き換えられます。
さらに、

${VFXInclude("Shaders/ParticlePlanarPrimitives/PassShadowCaster.template"),USE_CAST_SHADOWS_PASS}

という記述は、シェーダー内でUSE_CAST_SHADOWS_PASSというdefineがあるなら、
Packages/com.unity.visualeffectgraph/Shaders/ParticlePlanarPrimitives/PassShadowCaster.template
の中身にまるごと置き換えられます。defineが無いならただの空行となります。

(実装の詳細はVFXCodeGenerator.GetFlattenedTemplateContentメソッド参照。)

改めて上記VFXParticlePlanarPrimitive.templateを見ると、一つのSubShaderの中に複数のInclude系のVFXタグがあり、そこで指定されているパス名には「ShadowCaster」「Forward」「Depth」といったなじみのあるキーワードが見受けられます。これらは明らかにURPのUnlitシェーダーのPassを表しており、つまりこれらのVFXタグがそれぞれシェーダーのPassに置き換えられることで、Unlitシェーダーが出来上がるであろうことが分かります。
しかし独自のレンダーパイプラインでは必ずしもこのようなマルチパスの構成を想定しているとは限りません。レンダーパイプラインの実装に合わせてこのマルチパスの構成を変える必要があります。
例えばDepthOnlyパスは実装していないというレンダーパイプラインもあると思います。その場合はPassDepthは記載しなくても問題ありません。

また、上記パスの多くはVFXIncludeRPでなくVFXIncludeなので、これらのパスのテンプレートファイルはVFXGraph側に用意されているものであることが分かります。つまり、基本的なパスの実装はVFXGraphパッケージに用意されているということです。

シンプルなシェーダーの生成を試してみる

VFXParticleHeader.template

一旦、SRPDefaultUnlitパスのみを持つシンプルなシェーダーを生成することを目指します。
先ほどのURPのVFXParticlePlanarPrimitive.templateではURPのマルチパスの構成になっていたのを変える必要がありますが、VFXParticleHeader.templateのincludeは必要なのでそのままにしておきます。
また、PassForwardはSRPDefaultUnlitパスに該当するのでこちらもそのまま残しておきます。(つまりVFXGraphパッケージにSRPDefaultUnlitパスとして用意されているものを使うことにします。)

VFXParticlePlanarPrimitive.template
${VFXInclude("Shaders/VFXParticleHeader.template")}
${VFXInclude("Shaders/ParticlePlanarPrimitives/PassForward.template")}

まずVFXParticleHeaderのテンプレートファイルは${VFXInclude()}であることから分かるようにVFXGraphパッケージ側にあり、シェーダーの以下のような記述を担っています。

  • ShaderLabのコマンド(Tag、Blend、ZTest、ZWrite、Cull)
  • HLSLINCLUDE
  • #define、#include、構造体や変数の定義など

具体的に見るため、コード全文を以下に添付します。

Packages/com.unity.visualeffectgraph/Shaders/VFXParticleHeader.templateコード全文
Packages/com.unity.visualeffectgraph/Shaders/VFXParticleHeader.template
${VFXShaderTags}

${VFXInclude("Shaders/VFXParticleCommon.template")}
${VFXOutputRenderState}

HLSLINCLUDE
${VFXPragmaOnlyRenderers}
${VFXPragmaRequire}
${VFXGlobalInclude}
${VFXGlobalDeclaration}

#define VFX_NEEDS_COLOR_INTERPOLATOR (VFX_USE_COLOR_CURRENT || VFX_USE_ALPHA_CURRENT)
#if HAS_STRIPS
#define VFX_OPTIONAL_INTERPOLATION
#else
#define VFX_OPTIONAL_INTERPOLATION nointerpolation
#endif

#if VFX_USE_INSTANCING
#define VFX_VERTEX_OUTPUT_INSTANCE_INDEX nointerpolation uint2 instanceIndices : INDEX0; //instanceCurrentIndex, instanceActiveIndex
#define VFX_VARYINGS_INSTANCE_CURRENT_INDEX instanceIndices.x
#define VFX_VARYINGS_INSTANCE_ACTIVE_INDEX instanceIndices.y
#ifdef UNITY_INSTANCING_ENABLED
    #define VFX_FRAG_SETUP_INSTANCE_ID(i) unity_InstanceID = i.VFX_VARYINGS_INSTANCE_CURRENT_INDEX
#else
    #define VFX_FRAG_SETUP_INSTANCE_ID(i)
#endif
#else
#define VFX_VERTEX_OUTPUT_INSTANCE_INDEX
#endif

ByteAddressBuffer attributeBuffer;

#if VFX_HAS_INDIRECT_DRAW
StructuredBuffer<uint> indirectBuffer;
#endif

#if USE_DEAD_LIST_COUNT
StructuredBuffer<uint> deadList;
#endif

#if HAS_STRIPS_DATA
StructuredBuffer<uint> stripDataBuffer;
#endif

#if VFX_FEATURE_MOTION_VECTORS
ByteAddressBuffer elementToVFXBufferPrevious;

#if defined(VFX_FEATURE_MOTION_VECTORS_VERTS)

#define VFX_DECLARE_MOTION_VECTORS_STORAGE(coordA, coordB)\
noperspective float4 cPosPreviousAndNonJiterred : TEXCOORD##coordA;

#define VFX_DECLARE_MOTION_VECTORS_VARYING_PREVIOUS cPosPreviousAndNonJiterred.xy
#define VFX_DECLARE_MOTION_VECTORS_VARYING_NONJITTER cPosPreviousAndNonJiterred.zw

#else

#define VFX_DECLARE_MOTION_VECTORS_STORAGE(coordA, coordB)\
float4 cPosPrevious : TEXCOORD##coordA;\
float4 cPosNonJiterred : TEXCOORD##coordB;

#define VFX_DECLARE_MOTION_VECTORS_VARYING_PREVIOUS cPosPrevious
#define VFX_DECLARE_MOTION_VECTORS_VARYING_NONJITTER cPosNonJiterred

#endif
#endif

CBUFFER_START(outputParamsConst)
    ${VFXInstancingConstants}
    float3 cameraXRSettings;
CBUFFER_END

UNITY_INSTANCING_BUFFER_START(PerInstance)
    UNITY_DEFINE_INSTANCED_PROP(float, _InstanceIndex)
    UNITY_DEFINE_INSTANCED_PROP(float, _InstanceActiveIndex)
UNITY_INSTANCING_BUFFER_END(PerInstance)

// Helper macros to always use a valid instanceID
#if defined(UNITY_STEREO_INSTANCING_ENABLED)
	#define VFX_DECLARE_INSTANCE_ID     UNITY_VERTEX_INPUT_INSTANCE_ID
	#define VFX_GET_INSTANCE_ID(i)      unity_InstanceID
#else
	#define VFX_DECLARE_INSTANCE_ID     uint instanceID : SV_InstanceID;
	#define VFX_GET_INSTANCE_ID(i)      i.instanceID
#endif

ENDHLSL

VFXタグ

また新しいVFXタグがいくつか登場しています。

${VFXShaderTags}
${VFXOutputRenderState}
ShaderLabのSubShaderのタグやコマンドの記述に置き換えられます。
Passごとのタグは各テンプレートファイルに直接書かれています。

Tags { "Queue"="Transparent+0" "IgnoreProjector"="True" "RenderType"="Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
ZTest LEqual
ZWrite Off
Cull Off

上記のようなSubShaderの置き換え処理はVFXAbstractParticleOutputクラスにありますが、以下のようなHLSLの置き換え処理はVFXCodeGeneratorクラスにあるようです。

${VFXPragmaOnlyRenderers}
VFXGraphがサポートしている環境を#pragma only_renderersとして列挙します。これら以外の環境ではシェーダーがコンパイルされなくなります。

#pragma only_renderers d3d11 glcore gles3 metal vulkan xboxone xboxone xboxseries playstation ps5 switch webgpu

${VFXPragmaRequire}
VFXGraphでCubemapの配列を用いる場合、以下の行に置き換えられます。

#pragma require cubearray

Cubemapの配列はサポートされる環境に限りがあるため、もし使う場合にはこのようにrequireで明示しているものと思われます。
https://docs.google.com/spreadsheets/d/1nQGdLQ_M9Ku7QfAyBy4B84DmjNpDS0ArnGyD-LOQ6pU/edit?gid=2068299752#gid=2068299752

${VFXGlobalInclude}
${VFXGlobalDeclaration}
さまざまなdefineやincludeに置き換えられます。

...
#define VFX_USE_LIFETIME_CURRENT 1
#define VFX_USE_POSITION_CURRENT 1
...
#define VFX_WORLD_SPACE 1
#include_with_pragmas "ランタイムのパス/VFXDefines.hlsl"

#define VFX_WORLD_SPACE 1という記述は、OutputコンテクストのLocal/Worldに対応しています。

Localの場合は#define VFX_WORLD_SPACE 1となります。

VFXDefines.hlslの用意

上記の#include_with_pragmas "ランタイムのパス/VFXDefines.hlsl"という記述に注目します。ディレクトリの部分はVFXSRPBinderを継承したクラスのruntimePathで指定したものが用いられますので、以下のようにディレクトリを指定しておきます。

VFX***Binder
public override string runtimePath { get { return "Packages/パッケージ名/Runtime/VfxGraph/Shaders"; } }

上記で指定したディレクトリにVFXDefines.hlslを作成します。
VFXDefines.hlslの中身についてはURPの実装例を参考にするのが良いかと思います。フォグ関係などのさまざまな定義と、URPのCore.hlslのincludeが行われていますが、今回は一旦最低限の実装を試してみるという趣旨なので、以下のようにします。

ランタイムのパス/VFXDefines.hlsl
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#define CULL_VERTEX(o) { o.VFX_VARYING_POSCS.x = VFX_NAN; return o; }

VFXPasses.templateの用意

VFXCodeGeneratorの仕様により、以下のテンプレートファイルを用意する必要があります。
テンプレートのパス/VFXPasses.template
URPの実装を参考にすると、このファイルはシェーダーの各Passで用いる記述をまとめる場所のようです。#pragma multi_compileといったシェーダーバリアントの定義や、各種hlslファイルのinclude、関数などがあります。
コードの一部を抜粋して以下に添付します。

Packages/com.unity.render-pipelines.universal/Editor/VFXGraph/Shaders/VFXPasses.template(一部)
Packages/com.unity.render-pipelines.universal/Editor/VFXGraph/Shaders/VFXPasses.template(一部)
${VFXBegin:VFXPassForward}"UniversalForwardOnly"${VFXEnd}
${VFXBegin:VFXPassForward2D}"Universal2D"${VFXEnd}
${VFXBegin:VFXPassShadow}"ShadowCaster"${VFXEnd}
${VFXBegin:VFXPassVelocity}"MotionVectors"${VFXEnd}
${VFXBegin:VFXPassDepth}"DepthOnly"${VFXEnd}
${VFXBegin:VFXPassDepthNormal}"DepthNormalsOnly"${VFXEnd}

${VFXBegin:VFXPassDepthDefine}
#if defined(WRITE_NORMAL_BUFFER)
#define SHADERPASS SHADERPASS_DEPTHNORMALSONLY
#else
#define SHADERPASS SHADERPASS_DEPTHONLY
#endif
${VFXEnd}
${VFXBegin:VFXPassShadowDefine}#define SHADERPASS SHADERPASS_SHADOWS${VFXEnd}
${VFXBegin:VFXPassVelocityDefine}#define SHADERPASS SHADERPASS_MOTION_VECTORS${VFXEnd}

${VFXBegin:VFXPassForwardAdditionalPragma}
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
#pragma multi_compile _ DEBUG_DISPLAY
#pragma multi_compile_fog
#include_with_pragmas "Packages/com.unity.render-pipelines.core/ShaderLibrary/FoveatedRenderingKeywords.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Debug/Debugging3D.hlsl"
${VFXEnd}

VFXタグ

また新しいVFXタグが登場しています。
${VFXBegin:任意の名称}
${VFXEnd}
このVFXタグの処理はVFXCodeGenerator.SubstituteMacroメソッドにあるのですが、コードを見る限り、${VFXBegin:任意の名称}${VFXEnd}で囲んだ部分を後から利用できる機能のようです。シェーダーのマクロとかなり似ています。
例えば、

${VFXBegin:Hoge}
aaa
bbb
ccc
${VFXEnd}

のように記述しておき、後で、

xxx
${Hoge}
yyy

と記述すると、

xxx
aaa
bbb
ccc
yyy

のようなコードが生成されるということです。
なお、元々${VFXBegin:任意の名称}${VFXEnd}で囲んで記述した部分自体は生成コードに含まれません。
シェーダーのマクロはHLSL部分でしか使えませんが、このVFXタグはShaderLab部分でも使えることが一応メリットとなります。
${VFXBegin}${VFXEnd}の二つは必ずセットで用いなければなりません。例えば${VFXBegin}に対応する${VFXEnd}が無かったり、${VFXEnd}が単独で出現したりするとエラーになります。

つまりVFXPasses.templateの役割とは?

URPの作例を見る限りでは、VFXPasses.templateの内容は全て${VFXBegin}${VFXEnd}で囲まれた記述の列挙となっています。シェーダーの各パスで用いる記述をこのファイルにまとめて記述しているようです。
今回は一旦SRPDefaultUnlitパスのみのシンプルな実装を目指しているため、以下のように記述します。VFXBeginタグ名はURPやVFXGraphパッケージに倣ってVFXPassForwardとします。

${VFXBegin:VFXPassForward}"SRPDefaultUnlit"${VFXEnd}
${VFXBegin:VFXPassForwardAdditionalPragma}
//#pragma multi_compile_fog
${VFXEnd}

VFXMatricesOverride.hlslのinclude

VFXMatricesOverride.hlslはVFXGraph内に用意されているVFXGraph用の行列の変数・マクロをまとめたファイルです。
そもそもレンダーパイプラインを自作するにあたってはCore RP LibraryのSpaceTransforms.hlslが要求する各種行列の変数・マクロの定義が必要です。
https://catlikecoding.com/unity/tutorials/custom-srp/draw-calls/
この辺りはレンダーパイプラインを制作する上でかなり初期・初歩の話になるので、既に適切に実装対応が完了しているものとして話を進めます。
VFXGraphではこれらの行列の変数・マクロを再定義するなどの対応が必要になるようです。そこでVFXGraphを使っている場合にはVFXMatricesOverride.hlslをincludeするという処理が必要になります。これは、以下のように記述することで実現できます。

#ifdef HAVE_VFX_MODIFICATION
#include "Packages/com.unity.visualeffectgraph/Shaders/VFXMatricesOverride.hlsl"
#endif

また、VFXMatricesOverride.hlslは行列の変数・マクロの再定義なのですから、以下のように「各種行列のマクロの定義」と「SpaceTransforms.hlslのinclude」の間に記述する必要があります。

レンダーパイプラインの行列の変数等を定義しているファイル
...
#define UNITY_PREV_MATRIX_M unity_prev_MatrixM
#define UNITY_PREV_MATRIX_I_M unity_prev_MatrixIM

#ifdef HAVE_VFX_MODIFICATION
#include "Packages/com.unity.visualeffectgraph/Shaders/VFXMatricesOverride.hlsl"
#endif

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"

マクロの再定義の警告を解消するため、VFXDefines.hlslのincludeを書き換えます。
上記の行列の変数等を定義しているファイルをincludeするようにします。

ランタイムのパス/VFXDefines.hlsl
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "レンダーパイプラインの行列の変数等を定義しているファイル.hlsl"
#define CULL_VERTEX(o) { o.VFX_VARYING_POSCS.x = VFX_NAN; return o; }

VFXCommon.hlslの用意

最後にVFXCommon.hlslを用意します。これも仕様上必ず必要なものになります。
ランタイムのパス/VFXCommon.hlsl

URPの作例を参考に、内容を詰めていきます。

  • URP系の#includeは削除します。
  • GetCurrentViewPosition関数はレンダーパイプライン側で用意するものですが、単にビルドイン変数_WorldSpaceCameraPosを返す関数であれば良いです。
  • unity_WorldToCameraunity_CameraToWorld等が必要なので、まだレンダーパイプライン側で宣言していないビルドイン変数があるようなら宣言しておきます。
  • 以下もレンダーパイプライン側での準備が必要になるものですが、一からレンダーパイプラインを自作している方の多くはまだ実装対応していない場合もあるかと思います。しかしVFXCommonの関数自体は必要なものが多い(VFXGraphパッケージ側から使われているため)ので削除せずにうまくやり過ごす必要があります。
    • GetNormalizedScreenSpaceUV関数。
    • PreviousやJitterと名の付く、モーションブラー関係のものと思われる関数・関数。
      • 一旦Previousと付かないバージョンの変数に置き換えるなどします。
    • フォグ系。
    • VFXTransformFinalColor関数。
      • デバッグ用(?)の処理が入っていますが、削除し、単に入力を返すようにします。
    • VFXApplyShadowBias関数。こちらは一旦関数ごと削除で問題ないようです。
ランタイムのパス/VFXCommon.hlslの例
ランタイムのパス/VFXCommon.hlslの例
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Texture.hlsl"
#include "行列のマクロやビルドイン変数の宣言・定義などを行っている共通のhlslファイル"


float3 _LightDirection;

#ifdef VFX_VARYING_PS_INPUTS
void VFXTransformPSInputs(inout VFX_VARYING_PS_INPUTS input) {}

float4 VFXApplyPreExposure(float4 color, float exposureWeight)
{
    return color;
}

float4 VFXApplyPreExposure(float4 color, VFX_VARYING_PS_INPUTS input)
{
    return color;
}
#endif

float2 VFXGetNormalizedScreenSpaceUV(float4 clipPos)
{
    //TODO:仮。
    return clipPos.xy;
}

void VFXEncodeMotionVector(float2 velocity, out float4 outBuffer)
{
    outBuffer = float4(velocity.xy, 0, 0);
}

float4x4 VFXGetObjectToWorldMatrix()
{
    // NOTE: If using the new generation path, explicitly call the object matrix (since the particle matrix is now baked into UNITY_MATRIX_M)
    #if defined(HAVE_VFX_MODIFICATION) && !defined(SHADER_STAGE_COMPUTE)
    return GetSGVFXUnityObjectToWorld();
    #else
    return GetObjectToWorldMatrix();
    #endif
}

float4x4 VFXGetWorldToObjectMatrix()
{
    // NOTE: If using the new generation path, explicitly call the object matrix (since the particle matrix is now baked into UNITY_MATRIX_I_M)
    #if defined(HAVE_VFX_MODIFICATION) && !defined(SHADER_STAGE_COMPUTE)
    return GetSGVFXUnityWorldToObject();
    #else
    return GetWorldToObjectMatrix();
    #endif
}

float4 VFXTransformPositionWorldToClip(float3 posWS)
{
    return TransformWorldToHClip(posWS);
}

float4 VFXTransformPositionWorldToNonJitteredClip(float3 posWS)
{
    //TODO:仮。
    return mul(unity_MatrixVP, float4(posWS, 1.0f));
}

float4 VFXTransformPositionWorldToPreviousClip(float3 posWS)
{
    //TODO:仮。
    return mul(unity_MatrixVP, float4(posWS, 1.0f));
}

float4 VFXTransformPositionObjectToClip(float3 posOS)
{
    float3 posWS = mul(VFXGetObjectToWorldMatrix(), float4(posOS,1)).xyz;
    return VFXTransformPositionWorldToClip(posWS);
}

float4 VFXTransformPositionObjectToNonJitteredClip(float3 posOS)
{
    float3 posWS = mul(VFXGetObjectToWorldMatrix(), float4(posOS,1)).xyz;
    return VFXTransformPositionWorldToNonJitteredClip(posWS);
}

float3 VFXTransformPreviousObjectToWorld(float3 posOS)
{
    return mul(GetPrevObjectToWorldMatrix(), float4(posOS, 1.0)).xyz;
}

float4 VFXTransformPositionObjectToPreviousClip(float3 posOS)
{
    float3 posWS = VFXTransformPreviousObjectToWorld(posOS);
    return VFXTransformPositionWorldToPreviousClip(posWS);
}

float3 VFXTransformPositionWorldToView(float3 posWS)
{
    return TransformWorldToView(posWS);
}

float3 VFXTransformPositionWorldToCameraRelative(float3 posWS)
{
#if SHADEROPTIONS_CAMERA_RELATIVE_RENDERING
#error VFX Camera Relative rendering isn't supported in URP.
#endif
    return posWS;
}

//Compatibility functions for the common ShaderGraph integration
float4x4 ApplyCameraTranslationToMatrix(float4x4 modelMatrix)
{
    return modelMatrix;
}
float4x4 ApplyCameraTranslationToInverseMatrix(float4x4 inverseModelMatrix)
{
    return inverseModelMatrix;
}
//End of compatibility functions

float3x3 VFXGetWorldToViewRotMatrix()
{
    return (float3x3)GetWorldToViewMatrix();
}

float3 VFXGetViewWorldPosition()
{
    return _WorldSpaceCameraPos;
}

float4x4 VFXGetViewToWorldMatrix()
{
    return UNITY_MATRIX_I_V;
}

#ifdef USING_STEREO_MATRICES
float3 GetWorldStereoOffset()
{
    return unity_StereoWorldSpaceCameraPos[0].xyz - unity_StereoWorldSpaceCameraPos[1].xyz;
}

#endif

/*
void VFXApplyShadowBias(inout float4 posCS, inout float3 posWS, float3 normalWS)
{
    posWS = ApplyShadowBias(posWS, normalWS, _LightDirection);
    posCS = VFXTransformPositionWorldToClip(posWS);
}

void VFXApplyShadowBias(inout float4 posCS, inout float3 posWS)
{
    posWS = ApplyShadowBias(posWS, _LightDirection, _LightDirection);
    posCS = VFXTransformPositionWorldToClip(posWS);
}
*/

float4 VFXApplyAO(float4 color, float4 posCS)
{
#if defined(_SCREEN_SPACE_OCCLUSION) && !defined(_SURFACE_TYPE_TRANSPARENT)
    float2 normalizedScreenSpaceUV = (posCS.xy);
    AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(normalizedScreenSpaceUV);
    color.rgb *= aoFactor.directAmbientOcclusion;
#endif

    return color;
}

float4 VFXTransformFinalColor(float4 color, float4 posCS)
{
    return color;
}

float4 VFXApplyFog(float4 color,float4 posCS,float3 posWS)
{
   float4 fog = (float4)0;
   fog.rgb = unity_FogColor.rgb;

   float fogFactor = ComputeFogFactor(posCS.z * posCS.w);
   fog.a = ComputeFogIntensity(fogFactor);

#if VFX_BLENDMODE_ALPHA || IS_OPAQUE_PARTICLE
   color.rgb = lerp(fog.rgb, color.rgb, fog.a);
#elif VFX_BLENDMODE_ADD
   color.rgb *= fog.a;
#elif VFX_BLENDMODE_PREMULTIPLY
   color.rgb = lerp(fog.rgb * color.a, color.rgb, fog.a);
#endif
   return color;
}

float3 VFXGetCameraWorldDirection()
{
    return unity_CameraToWorld._m02_m12_m22;
}

これでよくやくVFXGraphのOutput Particle Unlit Quadコンテクストが描画されるようになりました!

Strip対応

OutputParticleStripUnlitコンテクスト(トレイルの表現)の描画ができない状態ですので、これから対応を行っていきます。

このコンテクストを使用例を得るには、例えば以下のHead&TrailでVFXGraphアセットを作成します。

このコンテクストはVFXQuadStripOutputというクラスで実装されていますが、使用しているテンプレートファイルはVFXPlanarPrimitiveOutputクラスと同様VFXParticlePlanarPrimitive.templateです。

VFXQuadStripOutput.cs
public override string codeGeneratorTemplate { get { return RenderPipeTemplate("VFXParticlePlanarPrimitive"); } }

つまり、stripの実装はこれまで扱ってきたテンプレートファイルに既に含まれているということです。それらの実装を有効にするために以下の対応を行う必要があります。

IsPerspectiveProjection関数の定義

以下のようなIsPerspectiveProjection関数の定義が必要です。

bool IsPerspectiveProjection()
{
    return (unity_OrthoParams.w == 0);
}

これは例えばURPではShaderVariableFunction.hlslに定義されてます。VFXGraph以外で使うこともあるかと思いますのでこのようにレンダーパイプライン側の共通のHLSLファイルに書いておくと良いかと思います。

HAS_STRIPS_DATA

次にVFXDefines.hlslに以下の記述を追加し、stripの描画時にHAS_STRIPS_DATAフラグが有効になるようにします。

VFXDefines.hlsl
#if HAS_STRIPS
#define HAS_STRIPS_DATA 1
#endif

これでstripの描画もできるようになりました。

Mesh対応

テンプレートファイルの用意

OutputParticleUnlit(Mesh)コンテクストが描画されない状態なので、対応を行います。

OutputParticleUnlitコンテクストではパーティクルの形状として使えるのはQuad、Triangle、Octagonの三択でしたが、このOutputParticleUnlit(Mesh)コンテクストでは任意のメッシュを用いることができます。
最低限の対応としては以下のテンプレートファイルを用意するだけです。

VFXParticleMeshes.template
{
	SubShader
	{
		${VFXInclude("Shaders/VFXParticleHeader.template")}
		${VFXInclude("Shaders/ParticleMeshes/PassForward.template")}
	}
}

つまりメッシュの描画もVFXGraphパッケージ側に専用の実装が用意されているので、それを使うだけで良いということですね。

PassSelectionパス対応

これでMeshでも描画されるようになりましたが、SceneViewにてGameObject選択時のオレンジ色の輪郭線の表示とクリック時の判定領域が何やらおかしいです。

これはPassSelectionパスを追加することで解消されます。

VFXParticleMeshes.template
...
${VFXInclude("Shaders/ParticleMeshes/PassSelection.template")}

VFXParticlePlanarPrimitive.templateの方にもPassSelectionパスを追加しておきましょう。
ディレクトリが先ほどと異なる点に注意が必要です。

VFXParticlePlanarPrimitive.template
...
${VFXInclude("Shaders/ParticlePlanarPrimitives/PassSelection.template")}

ファイルの対応関係まとめ

Outputコンテクスト クラス テンプレートファイル
OutputParticleUnlit VFXPlanarPrimitiveOutput VFXParticlePlanarPrimitive
OutputParticleStripUnlit VFXPlanarPrimitiveOutput VFXParticlePlanarPrimitive
OutputParticleUnlit(Mesh) VFXMeshOutput VFXParticleMeshes

少し考察

生成されたシェーダーコードを観察することで理解を深めましょう。

生成されたシェーダーを見るには

VFXGraphによって生成されたシェーダーのコードはVFXGraphアセットのインスペクタのShow Generatedボタンで確認できます。

生成されたシェーダーファイルはUnityのTempフォルダにあり、これがコードエディタで開かれます。
重要なこととして、Outputコンテクストに名前を付けていないとTempフォルダにシェーダーファイルが生成されません。

デフォルトでOutputコンテクストを作成した際には名前が空欄になってしまっているのでご注意ください。
この状態でShow Generatedボタンを押しても以下のエラーログが出て、シェーダーファイルは(存在しないので)開きません。

Cannot open file 'Temp/シェーダーファイル名.shader' for write.

生成されたシェーダーコードの観察

PlanarPrimitiveの頂点シェーダー

まず頂点入力構造体を見てみましょう。

struct vs_input
{
	VFX_DECLARE_INSTANCE_ID
};

変数がマクロになっており、どう展開されるかは場合によるようですが、概ね次のような展開になります。

uint instanceID : SV_InstanceID;

インスタンシングでお馴染みのSV_InstanceIDセマンティクスになっており、POSITIONセマンティクスを持たない点が重要です。
また、uniform変数としてattributeBufferやindirectBufferといったバッファがあります。

ByteAddressBuffer attributeBuffer;
...
StructuredBuffer<uint> indirectBuffer;

つまり、PlanerPrimitiveのシェーダーでは概ね、頂点の番号(頂点入力構造体)とさまざまな情報が入ったバッファ(uniform変数)を入力とし、そこからパーティクルのメッシュの頂点位置、UV、色、寿命などをシェーダーのロジックとして構成しているということが分かります。
attributeBufferはVFXGraphのいわゆる属性(パーティクルの寿命やら位置やらサイズやら)に関する情報が詰まったバッファです。

頂点番号から形状を構築する処理は長くて複雑ですが、その一部を抜粋します。

#if VFX_PRIMITIVE_TRIANGLE
    uint index = id / 3;
#elif VFX_PRIMITIVE_QUAD
    uint index = (id >> 2) + VFX_GET_INSTANCE_ID(i) * 2048;
#elif VFX_PRIMITIVE_OCTAGON
    uint index = (id >> 3) + VFX_GET_INSTANCE_ID(i) * 1024;
#endif

ここではOutput Particle Unlit QuadコンテクストのPrimitiveType(三角か四角か八角形か)に応じて頂点番号から形状を構築するロジックが分岐しています。このことからも頂点シェーダーにおいて頂点番号などからプロシージャルに形状の構築が行われていることがよく分かります。

https://docs.unity3d.com/Packages/com.unity.visualeffectgraph@17.2/manual/Context-OutputPrimitive.html

Meshの頂点シェーダー

任意のメッシュを使う場合にはどのようなシェーダーになっているのでしょうか。
まず頂点入力構造体を見てみましょう。

struct vs_input
{
    float3 pos : POSITION;
    float2 uv : TEXCOORD0;
    ...
	VFX_DECLARE_INSTANCE_ID
};

先ほどのPlanarPrimitiveの場合は頂点の番号(VFX_DECLARE_INSTANCE_ID)だけでしたが、こちらの任意のメッシュを使う場合は他にもたくさんのセマンティクスが見られます。
また、当然ながらTriangle/Quad/Octagonの形状を構築するようなコードもありません。

カスタムシェーダー

生成されたコードを見れば分かる通り、VFXGraphのシェーダーでは属性やバッファが複雑に絡み合うことになるため、カスタムシェーダーを作る場合に全て手書きでShaderLabとして作成するのはなかなか困難です。
以前私がURPでVFXGraphを使い始めた頃VFXGraphのカスタムシェーダーとしてShaderLabが使えない(ShaderGraphしか使えない)ことにしばしば不満を感じたものですが、その一因としてはこういった事情があるのだと思われます。

この件については以下のフォーラムでも触れられています。
https://discussions.unity.com/t/using-custom-shaders-on-vfx-graph/895635/3

In the case of VFX, the input for rendering is simply a byte address buffer and some generated code (which comes from blocks declared in VisualEffect). The sampling of byteaddressbuffer use a complex layout which relies on attribute usage in VFX. For these reasons, the usage of custom shader in VFX can’t be done easily.

なお、上記のような不満に対しフォーラムでは将来的にBlockShaderがVFXGraphに対応するということが述べられています。

On the long term, we are working on Block Shaders to streamline the workflow for authoring hand written shaders.

しかし問題の本質は、単にシェーダーを手書きできないということよりも、現状ShaderGraphでLightModeタグやマルチパスの編集ができないということではないでしょうか。
ただこの間の公式発表では、BlockShaderはShaderGraph2という新しいツールと対応関係にあるとのことでした。ツールが一新されるということは、将来的にはLightModeタグやマルチパスの変更ができるようになる可能性はあります。
とはいえそもそも今回のように自作レンダーパイプラインでVFXGraphの組み込みを自分で行うような場合であれば、こうした制約に捉われることなくもっと自由な実装が可能なはずですので、LightModeやマルチパスの構成等を自由かつ簡単に編集できるような仕組みを実装できないか試してみるつもりです。恐らくOutputコンテクストを自作するような形になるかと思います。もしうまくいったらまた記事にします。

Discussion