はじめに
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に従っていれば大丈夫です.
VFXの設定変更
デフォルトだとVFXの機能が全開放されていないため,Preferences
からExperimental Operators/Blocks
にチェックします.
VFXの作成
ProjectタブでVFXを作成し,シーンにD&Dします.
そうすると自動的にコンポーネントを含んだゲームオブジェクトが生成されます.
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
をすること以外で一切他に影響することはありません.ので,便利な仲介役とおもってくれれば大丈夫です.
追記(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
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に渡せば動きます!
本記事は勢いのみで書き上げた部分が多いのでもしも誤字,不明点などあれば教えていただけると幸いです.Qiita用動画(gif変換めんどk) pic.twitter.com/IvSZCXlzn0
— よつば (@Yothuba3) May 29, 2021