[Unity]VFXでDJを粒子にしちゃう方法(爆発付き)

# はじめに
[VR_ize](https://twitter.com/VR_ize)として活動していく中で日に日にDJの爆散需要が高まってきている気がしたので記事にしました．  
サンプルプロジェクトを用意しました！https://drive.google.com/drive/folders/1UBhTSxYqDRBe2Dg8ctSae5PQaQdPxdnx?usp=sharing (2021/05/31)
# 環境
- Unity 2020.3.9f
- HDRP 10.5.0
- Visual Effect Graph 10.5.0
- RenderPipeline Wizard設定 HDRP + DXR



# 前準備

## HDRPの導入
プロジェクト作成時にHDRPを選択してしまうと割とデカめなサンプルシーンがついてくるので，
一旦Built-In(3D)で作成してからパッケージマネージャー経由でインストールします．
VFXはこの時自動でインストールされます．

HDRPの各種設定については省きますが，RenderPipeline Wizardに従っていれば大丈夫です．
![2021-05-29-18-36-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/63649f02-2825-cb3e-ab69-1d94680b5bec.png)


## VFXの設定変更
デフォルトだとVFXの機能が全開放されていないため，`Preferences`から`Experimental Operators/Blocks`にチェックします．
![2021-05-29-18-40-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/88fb420e-6c5d-0246-88b8-9c2c862ba5ab.png)


# VFXの作成
ProjectタブでVFXを作成し，シーンにD&Dします．
そうすると自動的にコンポーネントを含んだゲームオブジェクトが生成されます．
![2021-05-29-18-46-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/a06aab12-2a3f-4e21-e519-0eb88033be7e.png)

こんな感じにシーンにパーティクルがでていればOK
![2021-05-29-18-46-48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/625d6fc0-78a3-d591-6163-01e79de2e2ba.png)

## VFXの簡単解説
作成したVFX(プロジェクトタブの)をダブルクリックしてVFXのエディタを開きます．基本的にVFXはノードベースで編集します．
![2021-05-29-18-49-56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/e6ec2926-092c-c4f7-fdb8-e5df4c8bee4b.png)

VFXでは，Block，Context,
Nodeの3つの概念が存在します．  

### Context
Contextはパーティクルの挙動そのものに関わるもので，`Spawn`,`Initialize Particle`,`Update Particle`,`Ouput ~~`が該当します．  
`Spawn`ではパーティクルの放出量の設定，`Initialize`では生成時の初期状態，`Update`では生存中に付与され続ける状態，`Output`では最終的に付与される状態及び**見た目の部分**を決定します．  
つまりInitializeで初期状態を決定し，Updateでそれを引き継ぐ，上書きする形で状態を変化させ，さらにOutputでそれらの状態を引き継ぐ，上書きする形で状態を変化させることになります．  
ので，InitializeでSet Sizeなどをしても，OutputでもSet Sizeしていた場合，Outputが優先されます.

### Block
以下のようにContext内で右クリックすることで作成できます．
![2021-05-29-19-12-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/10fe28a8-5c27-85b6-c0d1-a018133d5ad3.png)

設定したBlockによってContextの振る舞いが決定されます．

### Node
計算ロジックなどを組むためのもので，一番ノードベースっぽいやつです．
エディタ内の何もない所で右クリックで生成できます． 
ちなみに画像中のCreate Nodeの下にあるCreate Stickey Noteを選択するとメモ書きできる付箋が作れます．
![2021-05-29-19-14-41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/d014971b-d966-dc8e-e126-4c783630249f.png)


# DJを粒子にする準備
## DJをクロマキーで抜く(1)
まずはDJをUnityに顕現させる必要があります．
基本的にVR_izeではDJをグリーンバッグ背景で撮影し，それをUnity内でShaderを使いクロマキー処理することで実現しています．
クロマキー処理にはGithubで公開されているHDRP対応のものを利用します．
https://github.com/otdavies/UnityChromakey  
HDRP対応というだけでなく，機能もばっちりなのでVR_izeでは積極的に使っています．
↓HDRPでの様子(素材:[DJ FAIO](https://twitter.com/FAIO1230))
![2021-05-29-20-11-39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/291cf882-da1e-2617-1528-803fd44f598c.png)


## DJをクロマキーで抜く:VFX対応する(2)
VFXでクロマキー処理された画像や映像を使用する場合，一旦それをテクスチャに落とし込む必要があります．  
そのため，shaderを改造し，CRT(Custom Render Texture)で使えるものにします．  
↓対応済みshader

```c
Shader "Unlit/ChromaKey/CustomRenderTexture/otdaviesChromaKey"
{
  Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _KeyColor("KeyColor", Color) = (0,1,0,0)
        _TintColor("TintColor", Color) = (1,1,1,1)
        _ColorCutoff("Cutoff", Range(0, 1)) = 0.2
        _ColorFeathering("ColorFeathering", Range(0, 1)) = 0.33
        _MaskFeathering("MaskFeathering", Range(0, 1)) = 1
        _Sharpening("Sharpening", Range(0, 1)) = 0.5

        _Despill("DespillStrength", Range(0, 1)) = 1
        _DespillLuminanceAdd("DespillLuminanceAdd", Range(0, 1)) = 0.2

        _ResultColorIntensity("ResultColorIntensity", Float) = 1.0
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #include "UnityCustomRenderTexture.cginc"
            #pragma vertex CustomRenderTextureVertexShader
            #pragma fragment frag

            sampler2D _MainTex;
            float4 _MainTex_TexelSize;
            float4 _MainTex_ST;
            float4 _KeyColor;
            float4 _TintColor;
            float _ColorCutoff;
            float _ColorFeathering;
            float _MaskFeathering;
            float _Sharpening;
            float _Despill;
            float _DespillLuminanceAdd;
            float _ResultColorIntensity;
			//sampler2D _GrabTexture;
			//float4 _GrabTexture_TexelSize;

			#define GRABXYPIXEL(kernelx, kernely) tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(float4(i.globalTexcoordgrab.x + _GrabTexture_TexelSize.x * kernelx, i.globalTexcoordgrab.y + _GrabTexture_TexelSize.y * kernely, i.globalTexcoordgrab.z, i.globalTexcoordgrab.w)))



            // Utility functions -----------

            float rgb2y(float3 c)
            {
                return (0.299*c.r + 0.587*c.g + 0.114*c.b);
            }

            float rgb2cb(float3 c)
            {
                return (0.5 + -0.168736*c.r - 0.331264*c.g + 0.5*c.b);
            }

            float rgb2cr(float3 c)
            {
                return (0.5 + 0.5*c.r - 0.418688*c.g - 0.081312*c.b);
            }

            float colorclose(float Cb_p, float Cr_p, float Cb_key, float Cr_key, float tola, float tolb)
            {
                float temp = (Cb_key-Cb_p)*(Cb_key-Cb_p)+(Cr_key-Cr_p)*(Cr_key-Cr_p);
                float tola2 = tola*tola;
                float tolb2 = tolb*tolb;
                if (temp < tola2) return (0);
                if (temp < tolb2) return (temp-tola2)/(tolb2-tola2);
                return (1);
            }

            float maskedTex2D(sampler2D tex, float2 uv)
            {
                float4 color = tex2D(tex, uv);

                // Chroma key to CYK conversion
                float key_cb = rgb2cb(_KeyColor.rgb);
                float key_cr = rgb2cr(_KeyColor.rgb);
                float pix_cb = rgb2cb(color.rgb);
                float pix_cr = rgb2cr(color.rgb);

                return colorclose(pix_cb, pix_cr, key_cb, key_cr, _ColorCutoff, _ColorFeathering);
            }

            //-------------------------
            float4 frag (v2f_customrendertexture i) : SV_Target
            {
                // Get pixel width
                float2 pixelWidth = float2(1.0 / _MainTex_TexelSize.z, 0);
                float2 pixelHeight = float2(0, 1.0 / _MainTex_TexelSize.w);


				//float2 uv = i.globalTexcoord.xy;
				//half4 grab = GRABXYPIXEL(0,0);

                // Unmodified MainTex
                float4 color = tex2D(_MainTex, i.globalTexcoord);

                // Unfeathered mask
                float mask = maskedTex2D(_MainTex, i.globalTexcoord);

                // Feathering & smoothing
                float c = mask;
                float r = maskedTex2D(_MainTex, i.globalTexcoord + pixelWidth);
                float l = maskedTex2D(_MainTex, i.globalTexcoord - pixelWidth);
                float d = maskedTex2D(_MainTex, i.globalTexcoord + pixelHeight);
                float u = maskedTex2D(_MainTex, i.globalTexcoord - pixelHeight);
                float rd = maskedTex2D(_MainTex, i.globalTexcoord + pixelWidth + pixelHeight) * .707;
                float dl = maskedTex2D(_MainTex, i.globalTexcoord - pixelWidth + pixelHeight) * .707;
                float lu = maskedTex2D(_MainTex, i.globalTexcoord - pixelHeight - pixelWidth) * .707;
                float ur = maskedTex2D(_MainTex, i.globalTexcoord + pixelWidth - pixelHeight) * .707;
                float blurContribution = (r + l + d + u + rd + dl + lu + ur + c) * 0.12774655;
                float smoothedMask = smoothstep(_Sharpening, 1, lerp(c, blurContribution, _MaskFeathering));
                float4 result = color * smoothedMask;

                // Despill
                float v = (2*result.b+result.r)/4;
                if(result.g > v) result.g = lerp(result.g, v, _Despill);
                float4 dif = (color - result);
                float desaturatedDif = rgb2y(dif.xyz);
                result += lerp(0, desaturatedDif, _DespillLuminanceAdd);
                result *= _ResultColorIntensity;
                return float4(result.xyz, smoothedMask);
            }
            ENDCG
        }
    }
}
```

作成したCRTのプレビューを見るとアルファ抜きされていないように見えますがしっかり情報は保持されているので大丈夫です．
![2021-05-29-20-27-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/84ae032a-841f-b4a3-8cc4-67c0a40d29fa.png)


## PointCacheの作成
簡単にいえばメッシュの頂点位置，UVなどをキャッシュしてVFXで使用可能にするものです．  
`Window`->`Visual Effects`->`Utilities`->`Point Cache Bake Tool`からツールを開けます．
今回はDJの映像を写すスクリーンとの整合性をとるために，スクリーンのUV座標をポイントキャッシュツールを利用して出力します．Planeを利用できれば良かったのですがMeshを取得できなかったのでBlenderで作りました．
一応モデルは[こちら](https://drive.google.com/file/d/1OTQ9b_7rRePhug-XEJ9oIetm57oHsWE8/view?usp=sharing)にあります． 
***(モデルの向きがおかしいままでVFXを作ってしまったので正しく向きを整えた場合チュートリアル画像のいくつかのプロパティはそれに合わせる必要があります．  
あくまでも16:9の設定をどうするか程度なので修正しませんでした)***
![2021-05-29-20-55-32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/2c74ed9e-862b-17ca-f744-9d2d7e4dc2d0.png)  
モデルのメッシュを選択し，`Point Count`を4096×2の倍数にします(僕は4096*4にしました).
最後に`Save to pChache file...`を選択します．
![2021-05-29-22-17-52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/81b6f3fc-820e-066b-755a-b4e8f1f71ab3.png)

# VFXの編集
まずは外観からです！
恐らく文字がつぶれて見えないと思うのでこの後詳細なものを説明します．
![2021-05-30-00-36-06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/fb7ef6e5-edc1-f664-38bb-b6f1325631eb.png)

また，先にインスペクタに出す用の変数を紹介しておきます．  
![2021-05-30-01-21-30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/800dd010-25b3-9e58-0f94-4b7cb2452146.png)

Video_CRTには作成したCRTを指定します．
Screen Scaleは，パーティクルが出現するスクリーンのスケールを拡大するのに利用します．最後のExplosion Powerは，DJを爆破するための威力です．0だとそのまま表示されます．

## Initialize 
![2021-05-30-00-28-46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/fe484868-dcf2-d8e5-5b18-fcef2eb20955.png)

スポーン量などは各々の自由として，
ここでやっていることは
- パーティクルにUV
値を設定
- UV値を元にパーティクルの色を決定
- パーティクルの位置を決定
の３つに大別されます．

UV値の設定には`Set Target Posiiton from Map`というブロックを使用しています．
これは名まえの通りで，Target PositionをTextureなどを参照して決定することができるものです．  
ここで`Target Position`なるものがでてきましたが，あくまでもここで設定したものは何かパーティクルに影響することはなく，`Get Target Position`をすること以外で一切他に影響することはありません．ので，便利な仲介役とおもってくれれば大丈夫です．  
追記(2021/08/19)一部TargetPositionを参照するブロックもあるので「一切」というわけではないです．ただし基本的に他の値で代用可能なため引き続きこの方法で問題ないと思います．(もしくはカスタムアトリビュートを使う)  

`Set Color from Map`では先ほど作成したUVを`Get Attribute:targetPosition`経由で取得し，サンプリングするテクスチャとしてVideo_CRTを指定します．
こうすることで，パーティクルに映像の各テクセルの色を反映できます．  
`Set Position from Map`では，Point Cache経由でポジションを指定します．同じ1パーティクルでuv値とpositionを取得しているので，こうすることで自然とuvと位置との関係が保たれます．
また，そのままだと正方形のエリアでパーティクルが出現するので16:9の形になるようにスケールします．(恐らくメッシュのスケールに依存していない)  
最後の`Multiply Color`は単純に明るさの調整用です．
ビカビカにしたい場合はここを強くします．
**ここでMultiplyしてもUpdateで上書きされるので意味ないことに気づきました．Multiplyする場合Outputで行うことでしっかり反映されます**(2021/05/31　加筆)

## Update
![2021-05-30-01-47-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/d1e2f724-a08f-ebf6-77a7-6b919b1f7897.png)

Updateではテクスチャが毎フレーム更新されることを考慮して，毎フレームSet Colorで更新しています．
ついでにInitializeでは行わなかった`Set Alpha from Map`を行っていますが，単純にアルファ値を取得しているだけです．  
### Turbulence
DJを吹き飛ばすのに必要不可欠なブロックです．
解説は[Unity公式](https://docs.unity3d.com/Packages/com.unity.visualeffectgraph@12.0/manual/Block-Turbulence.html)にまかせるとして，
ここでは`Explosion Power`をIntensityに繋げて外から変更できるようにしています．  
ここまでで，
- パーティクルへの色付け
- パーティクルの位置決め
- パーティクルの吹き飛ばし  

までが実装できました．
さて次が最後です．

# Output
![2021-05-30-01-48-56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411974/99c3574d-87bf-3083-c86e-f50026ef8a70.png)

標準のアウトプットコンテキストから`Output Particle Lit Cube`に変更をしています．  
その後，`Use Alpha Clipping`にチェックを入れアルファ値で描画の破棄が行えるようにします．  
利用しているshaderの関係上，アルファが0・1ではなくなだらかに変化しているため，`Alpha Threshold`を0.001にしておきます．(微妙にアルファ値が下がっていたりする部分が破棄されるの防ぐため)
あとは`Set Scale`でお好みのおおきさにパーティクルを調整します．

# 最後に
無事に爆散？分散？すればおっけーです！お疲れさまでした！！
ちなみに動画は，VideoPlayerか何かでRenderTextureへ流し込み，それをCRT
のMainTextureに渡せば動きます！
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Qiita用動画(gif変換めんどｋ) <a href="https://t.co/IvSZCXlzn0">pic.twitter.com/IvSZCXlzn0</a></p>&mdash; よつば (@Yothuba3) <a href="https://twitter.com/Yothuba3/status/1398700329487634433?ref_src=twsrc%5Etfw">May 29, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
本記事は勢いのみで書き上げた部分が多いのでもしも誤字，不明点などあれば教えていただけると幸いです．