[Unity] DEATH STRANDINGのカイラル通信っぽいシェーダ


はじめに

配達系国道制作ゲーム、DEATH STRANDING。このゲームに登場するカイラル通信による通話(広義のホログラムのようなもの)を模倣してみました。
『DEATH STRANDING Day-1 GAMEPLAY SESSION Vol.1』PlayStation® presents LIVE SHOW “TGS2019”の53分あたりに登場。

できたもの

構成要素

Unity ShaderLabでの実装です。
おおまかな構成要素は以下
1.半透明
2.リムライト
3.vertex shaderでx軸方向にモデルの頂点をランダムに動かす
4.上方向に移動するスキャンライン
5.モデル全体にランダムノイズ
6.モデル全体に青系のオーバーレイ
7.上から下にdissolve
8.1のノイズが大きくなる(dissolve時)

実装

流れとしては、1Pass目で深度書き込み、2Pass目でアウトライン、3Pass目でその他、といった感じです。

1.半透明

Tags { "Queue"="Transparent" }
LOD 100
//先に深度を書き込む
Pass{
  ZWrite ON
  ColorMask 0
}
//透明
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha 

QueueをTransparentに設定し、描画順を不透明オブジェクトの後に設定します。
1Pass目でモデルの深度情報を書き込んでおきます。これをしないとデプス値が正しく更新されず、モデルが綺麗に表示されません。
その後はZWriteをOffにすることで深度の計算を破棄し、Blendを記述することでアルファブレンディングを有効にします。

2.リムライト

2Pass目でモデルの頂点を法線方向にオフセットすることでアウトラインを出します。

v2f vert (appdata v){
  v2f o;
  v.vertex += float4(v.normal * 0.006f, 0);   
  o.vertex = UnityObjectToClipPos(v.vertex); 
  o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
  return o;
}

3Pass目のfragment shaderで視線ベクトルと頂点の法線ベクトルの内積をとることでさらにリムライトっぽくします。

//リムライト
half rim = 1.0-saturate(dot(i.viewDir, i.worldNormal));
fixed4 rimColor = _RimColor * pow (rim, _RimPower);

3.vertex shaderでx軸方向にモデルの頂点をランダムに動かす

3Pass目のvertex shaderで計算を行い、x軸方向またはz軸方向に頂点をランダムに動かします。
ランダムに時間で区切ってオフセットさせるとパッパッと切り替わるような動きになってしまいます。これを防ぐために経過時間をfloorに入れ整数部のみを取り出すことでrandamに入れた時一定時間は同じ値を取るようにし、fracで小数部分のみを取り出しオフセットのサイズ調整に当てています。こうすることでサイズ徐々に変わるウニョウニョした動きをさせることができます。

float offset = Graph(v.vertex.y);
float t = _Time.z*_NoiseSpeed;
float offsetTF = rand(floor(t)+v.vertex.z);
if(offsetTF>_NoiseRange) v.vertex.z += offset*(1-frac(t))*_NoiseSize;

また、ランダムな値の算出は有名な関数をちょっと弄ったもの、Graphは適当にランダムな凹凸が現れるような関数を作成しました。こちらのサイトでグラフを描画して良い感じのものを採用しました。

float rand(float co){
return frac(sin(dot(co,12.9898)) * 43758.5453);
}

float rand2 (fixed2 p) { 
return frac(sin(dot(p, fixed2(12.9898,78.233))) * 43758.5453);
}

float Graph( fixed x){
return pow(_NoiseSize*abs(sin(10*x)*(-sin(x*2)+1))*0.5,2.0)*_Alpha;
}

4.上方向に移動するスキャンライン

ランダムな太さのスキャンラインを上方向に移動させる方法が思いつかなかったので、白黒のシマシマのテクスチャを作成し、uvスクロールさせる方法を取りました。これがスキャンラインA。
上のgifのようなスキャンラインBは、y座標を定数倍して小数部をアルファ値とすることで実装しています。

//スキャンラインA
//float2 scan = i.worldPos + float2 (0, _Time.x*-2.0);
//float4 scanLine = tex2D(_ScanlineTex, scan);

//スキャンラインB
float scanLine = frac(i.worldPos.y*5.0-_Time.y);

5.モデル全体にランダムノイズ

3で使用したノイズ関数を利用します。floorはタイミング調整に便利です。
noiseを減算することでザラついた感じを表現できます。

//ノイズ
float noise = rand2(i.uv+floor(_Time.y*10.0));

6.モデル全体に青系のオーバーレイ

オーバーレイは加算や乗算、スクリーンなどと異なり条件分岐が必要なので少し面倒です。

//オーバーレイ
if (col.r < 0.5){
col.r = 2.0*col.r*_OverlayColor.r;
}else{
col.r = 1.0 - 2.0 * (1.0 - col.r) * (1.0 - _OverlayColor.r);
}

if (col.g < 0.5){
col.g = 2.0*col.g*_OverlayColor.g;
}else{
col.g = 1.0 - 2.0 * (1.0 - col.g) * (1.0 - _OverlayColor.g);
}

if (col.b < 0.5){
col.b = 2.0*col.b*_OverlayColor.b;
}else{
col.b = 1.0 - 2.0 * (1.0 - col.b) * (1.0 - _OverlayColor.b);
}

7.上から下にdissolve

条件付きコンパイルを用いて実装します。
キーワードを定義しておき、

#pragma shader_feature _ Dissolve_ON

定義されたキーワードによってコンパイルする内容を変更します。
バリアントを作成する方がif文で毎回参照するよりも効率が良いので(おそらく)、できるだけこちらをつかいたいです。

#ifdef Dissolve_ON
col.a=saturate(saturate(abs(-i.worldPos.y+_ObjectSize)-_T*_DissolveSpeed)*5.0)*_Alpha;
#else
col.a = _Alpha;
#endif    

dissolveにグラデーションをかけたいので、saturateを使って値を調整します。

8.1のノイズが大きくなる(dissolve時)

実際のムービーを見ていただけると分かるのですが、disolve時はノイズのサイズが大きくなり、密度が大きくなります。C#スクリプトから7のdissolveのキーワード切り替えと同時にノイズのサイズと密度を調整します。

void Dissolve(){
        m.SetFloat("_NoiseSize", 1.5f);
        m.SetFloat("_NoiseRange",0.96f);
        m.EnableKeyword("Dissolve_ON");
        dissolve = true;
}

参考

・半透明 http://nn-hokuson.hatenablog.com/entry/2018/01/23/202530
・オーバーレイ http://sylphylunar.seesaa.net/article/390331341.html
・スキャンライン https://twitter.com/minionsart/status/899628037360234496
・シェーダのバリアント作成 http://light11.hatenadiary.com/entry/2019/01/12/232533