こんにちは、エンジニアの成田です。今回もUnityのシェーダで遊んでみます。
今回はタイトルの通り「VRっぽい」シェーダを作ってみました。VRのシェーダと言っても最近流行りのヘッドマウントディスプレイOculus等の視差シェーダみたいなものではなく、あくまで「演出上のVR」、よくアクションゲームなんかに登場する空間のソナーみたいな効果を作ってみました。説明が難しいですけど、なんというか、SF映画のトロンみたいなグリッドの表現です。
注意点として今回の実装はあくまで私流のやり方で試行錯誤したものですので、もっと美しく処理も軽い方法があるかもしれませんが、ご了承ください。というか良い方法教えて下さい……
また、今回作成するシェーダはピクセルシェーダの負荷が結構高いと思われますので、実用は厳しいかもしれません。
1.方針
とりあえずシーン内で多様なサイズのオブジェクト同士での整合性を取るために、グリッドのテクスチャを貼ることは考えず(グリッドがオブジェクト同士を跨いでも問題無いようにUVを指定していくのは非現実的)、ワールド座標とカメラ座標を使ってピクセルシェーダでグリッドの線を引くことにします。
2.実装
まず、空間座標に対して矩形波とノコギリ波みたいな波形を出力する、次のような関数を作成しました。
a.矩形波
fixed3 rule(Input IN, float period, float width) { float modX = abs(fmod(IN.worldPos.x, period)); float modY = abs(fmod(IN.worldPos.y, period)); float modZ = abs(fmod(IN.worldPos.z, period)); float minBorder = width*.5f; float maxBorder = period - width*.5f; fixed x = max(-1 * sign(minBorder - modX) * sign(modX - maxBorder), 0); fixed y = max(-1 * sign(minBorder - modY) * sign(modY - maxBorder), 0); fixed z = max(-1 * sign(minBorder - modZ) * sign(modZ - maxBorder), 0); fixed v = saturate(x+y+z); return fixed3(v,v,v); }
periodは波の周期、widthは「波の1になる部分」の幅です。これをグラフで表すとこんな感じになります。
数学関数を一杯かませた無理やり感…。これを使って空間にグリッド線を引きます。
b.ノコギリ波
fixed3 radar(Input IN, float period, float fade) { float distFromCam = length(IN.worldPos - _WorldSpaceCameraPos); float x = distFromCam / period; float sawtooth = (x - floor(x)) * period; fixed v = saturate((sawtooth - (period - fade)) / fade); return fixed3(v,v,v); }
periodは波の周期、fadeは減衰の幅です。カメラ座標を減算しているので原点はカメラの位置になります。グラフはこんな感じ。
このノコギリ波は先ほどの矩形波と積算してグリッドのグラデーションに使ったり、ソナー波の演出に使います。
ではテストしてみます。このようなシーンを用意してみました。
このシーンに先ほどの矩形波を適用した結果がこれ。
ノコギリ波を適用した結果がこれ。
うーん、ノコギリ波の方の結果は想定通りですが、矩形波の方はオブジェクトに関してはグリッドが引かれていますが、床や壁のPlaneが真っ白になってしまっています。
これには理由があって、y=0の平面など罫線が引かれる軸に平行な面を持つポリゴンは、ポリゴン全体が罫線上に存在する時にすべてのピクセル出力が1となり真っ白になってしまう可能性があります。そこでちょっとした小細工を行います。
ポリゴンの法線ベクトルをワールド座標系に変換して、これがxyzいずれかの軸に平行ならその軸方向の罫線をそのポリゴンでは描かなくします。例えば、法線がワールド+y方向のポリゴンがあれば、xz軸の罫線だけ引くという感じです。
c.矩形波の改造
fixed3 rule(Input IN, float3 worldNormal, float period, float width) { float modX = abs(fmod(IN.worldPos.x, period)); float modY = abs(fmod(IN.worldPos.y, period)); float modZ = abs(fmod(IN.worldPos.z, period)); float minBorder = width*.5f; float maxBorder = period - width*.5f; fixed factorX = 1-abs(dot(worldNormal, float3(1,0,0))); fixed factorY = 1-abs(dot(worldNormal, float3(0,1,0))); fixed factorZ = 1-abs(dot(worldNormal, float3(0,0,1))); fixed x = factorX * max(-1 * sign(minBorder - modX) * sign(modX - maxBorder), 0); fixed y = factorY * max(-1 * sign(minBorder - modY) * sign(modY - maxBorder), 0); fixed z = factorZ * max(-1 * sign(minBorder - modZ) * sign(modZ - maxBorder), 0); fixed v = saturate(x+y+z); return fixed3(v,v,v); }
factorX/Y/Zは描画の係数になります。法線ベクトルが各軸に対して平行だと0、直交していると1に近くなります。この実行結果はこうなります。
d.ノコギリ波の改造
ノコギリ波の出力は正しかったのですが、ソナー波なのでアニメーションしてほしい。あと遠くなるにつれて全体として減衰してほしい。というわけで、
fixed3 radar(Input IN, float period, float fade) { float distFromCam = length(IN.worldPos - _WorldSpaceCameraPos); float x = distFromCam / period - _Time.y / 4; float sawtooth = (x - floor(x)) * period; fixed v = saturate((sawtooth - (period - fade)) / fade) / (1+distFromCam); return fixed3(v,v,v); }
カメラからの距離から_Time.y / 4を減算しているのでグラフで言えば各軸の-方向へアニメーションすることになります。
また、最終結果を1+distFromCamで除算することで減衰っぽくしています。
ここまで来たら必要な要素はほぼ完成しています。あとは下のように組み合わせてお目当ての演出の見た目を作ります。
細い線だけでグリッドを構成しても良いんですが、今回は見た目を良くするために矩形波を2つ使って太い線(A)と細い線(B)を加算してグリッド(C)を作ります。
上で作成したグリッド(C)にフェード幅を大きめにしたノコギリ波(D)を積算します。これでグリッドにグラデーションがかかりソナーっぽい見た目になります(E)。
上のグラデーション付きグリッド(E)にソナー波の衝撃面の演出を乗せます。フェード幅を小さくしたノコギリ波(F)を加算します(G)。
これでソナー波は完成です。あとは掛け&足しあわせの係数(強さ)を微調整します。
以下が、今回作ったサーフェイスシェーダの全体です。
Shader "Custom/VRSurface" { Properties { _Color ("Color", Color) = (1,1,1,1) _Color2 ("Color2", Color) = (1,1,1,1) } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert #pragma target 3.0 struct Input { float3 worldPos; }; fixed4 _Color; fixed4 _Color2; fixed3 rule(Input IN, float3 worldNormal, float period, float width) { float modX = abs(fmod(IN.worldPos.x, period)); float modY = abs(fmod(IN.worldPos.y, period)); float modZ = abs(fmod(IN.worldPos.z, period)); float minBorder = width*.5f; float maxBorder = period - width*.5f; fixed factorX = 1-abs(dot(worldNormal, float3(1,0,0))); fixed factorY = 1-abs(dot(worldNormal, float3(0,1,0))); fixed factorZ = 1-abs(dot(worldNormal, float3(0,0,1))); fixed x = factorX * max(-1 * sign(minBorder - modX) * sign(modX - maxBorder), 0); fixed y = factorY * max(-1 * sign(minBorder - modY) * sign(modY - maxBorder), 0); fixed z = factorZ * max(-1 * sign(minBorder - modZ) * sign(modZ - maxBorder), 0); fixed v = saturate(x+y+z); return fixed3(v,v,v); } fixed3 radar(Input IN, float period, float fade) { float distFromCam = length(IN.worldPos - _WorldSpaceCameraPos); float x = distFromCam / period - _Time.y / 4; float sawtooth = (x - floor(x)) * period; fixed v = saturate((sawtooth - (period - fade)) / fade) / (1+distFromCam); return fixed3(v,v,v); } void surf (Input IN, inout SurfaceOutput o) { float3 worldNormal = WorldNormalVector (IN, o.Normal); fixed3 thick = rule(IN, worldNormal, 1.0f, 0.04f); fixed3 thin = rule(IN, worldNormal, 0.2f, 0.02f); fixed3 strength = radar(IN, 8.0f, 7.0f); fixed3 ruleResult = _Color * (4.0f * thick * thick + 4.0f * thin * thin) * strength; fixed3 radarResult = _Color2 * radar(IN, 8.0f, 1.0f); o.Emission = ruleResult + radarResult; o.Alpha = 1; } ENDCG } FallBack "Diffuse" }
法線ベクトルの処理の部分はピクセルシェーダでやるよりバーテックスシェーダでやるべきなんでしょうけど、とりあえず、ということで……。
最後に、味付けとしてポストエフェクトをかけましょう。SF映画と言えば光の効果、光の効果と言えばSF映画(多分)。
HDRとブルーム効果を使って眩しい感じの画面にします。これに備えて実は上のシェーダでは最終出力が[0.0,1.0]よりも広い範囲を返しています。
ではカメラのHDRをオンにして、Bloomのコンポーネントを追加します。パラメータを次のように設定してみました。