[Unity]VFXでDJを粒子にして爆発させる方法

12 min read読了の目安(約10800字

はじめに

VR_izeとして活動していく中で日に日にDJの爆散需要が高まってきている気がしたので記事にしました.
サンプルプロジェクトを用意しました

https://drive.google.com/drive/folders/1UBhTSxYqDRBe2Dg8ctSae5PQaQdPxdnx?usp=sharing

環境

  • 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に従っていれば大丈夫です.

VFXの設定変更

デフォルトだとVFXの機能が全開放されていないため,PreferencesからExperimental Operators/Blocksにチェックします.

VFXの作成

ProjectタブでVFXを作成し,シーンにD&Dします.
そうすると自動的にコンポーネントを含んだゲームオブジェクトが生成されます.

こんな感じにシーンにパーティクルがでていればOK

VFXの簡単解説

作成したVFX(プロジェクトタブの)をダブルクリックしてVFXのエディタを開きます.基本的にVFXはノードベースで編集します.

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内で右クリックすることで作成できます.

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

Node

計算ロジックなどを組むためのもので,一番ノードベースっぽいやつです.
エディタ内の何もない所で右クリックで生成できます.
ちなみに画像中のCreate Nodeの下にあるCreate Stickey Noteを選択するとメモ書きできる付箋が作れます.

DJを粒子にする準備

DJをクロマキーで抜く(1)

まずはDJをUnityに顕現させる必要があります.
基本的にVR_izeではDJをグリーンバッグ背景で撮影し,それをUnity内でShaderを使いクロマキー処理することで実現しています.
クロマキー処理にはGithubで公開されているHDRP対応のものを利用します.

https://github.com/otdavies/UnityChromakey
HDRP対応というだけでなく,機能もばっちりなのでVR_izeでは積極的に使っています.
↓HDRPでの様子(素材:DJ FAIO)

DJをクロマキーで抜く:VFX対応する(2)

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

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のプレビューを見るとアルファ抜きされていないように見えますがしっかり情報は保持されているので大丈夫です.

PointCacheの作成

簡単にいえばメッシュの頂点位置,UVなどをキャッシュしてVFXで使用可能にするものです.
Window->Visual Effects->Utilities->Point Cache Bake Toolからツールを開けます.
今回はDJの映像を写すスクリーンとの整合性をとるために,スクリーンのUV座標をポイントキャッシュツールを利用して出力します.Planeを利用できれば良かったのですがMeshを取得できなかったのでBlenderで作りました.
一応モデルはこちらにあります.
(モデルの向きがおかしいままでVFXを作ってしまったので正しく向きを整えた場合チュートリアル画像のいくつかのプロパティはそれに合わせる必要があります.
あくまでも16:9の設定をどうするか程度なので修正しませんでした)


モデルのメッシュを選択し,Point Countを4096×2の倍数にします(僕は4096*4にしました).
最後にSave to pChache file...を選択します.

VFXの編集

まずは外観からです!
恐らく文字がつぶれて見えないと思うのでこの後詳細なものを説明します.

また,先にインスペクタに出す用の変数を紹介しておきます.

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

Initialize

スポーン量などは各々の自由として,
ここでやっていることは

  • パーティクルにUV
    値を設定
  • UV値を元にパーティクルの色を決定
  • パーティクルの位置を決定
    の3つに大別されます.

UV値の設定にはSet Target Posiiton from Mapというブロックを使用しています.
これは名まえの通りで,Target PositionをTextureなどを参照して決定することができるものです.
ここでTarget Positionなるものがでてきましたが,あくまでもここで設定したものは何かパーティクルに影響することはなく,Get Target Positionをすること以外で一切他に影響することはありません.ので,便利な仲介役とおもってくれれば大丈夫です.
Set Color from Mapでは先ほど作成したUVをGet Attribute:targetPosition経由で取得し,サンプリングするテクスチャとしてVideo_CRTを指定します.
こうすることで,パーティクルに映像の各テクセルの色を反映できます.
Set Position from Mapでは,Point Cache経由でポジションを指定します.同じ1パーティクルでuv値とpositionを取得しているので,こうすることで自然とuvと位置との関係が保たれます.
また,そのままだと正方形のエリアでパーティクルが出現するので16:9の形になるようにスケールします.(恐らくメッシュのスケールに依存していない)
最後のMultiply Colorは単純に明るさの調整用です.
ビカビカにしたい場合はここを強くします.

Update

Updateではテクスチャが毎フレーム更新されることを考慮して,毎フレームSet Colorで更新しています.
ついでにInitializeでは行わなかったSet Alpha from Mapを行っていますが,単純にアルファ値を取得しているだけです.

Turbulence

DJを吹き飛ばすのに必要不可欠なブロックです.
解説はUnity公式にまかせるとして,
ここではExplosion PowerをIntensityに繋げて外から変更できるようにしています.
ここまでで,

  • パーティクルへの色付け
  • パーティクルの位置決め
  • パーティクルの吹き飛ばし

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

Output

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

最後に

無事に爆散?分散?すればおっけーです!お疲れさまでした!!
ちなみに動画は,VideoPlayerか何かでRenderTextureへ流し込み,それをCRT
のMainTextureに渡せば動きます!

本記事は勢いのみで書き上げた部分が多いのでもしも誤字,不明点などあれば教えていただけると幸いです.