ShaderToyで「Nier:Automata」の後処理っぽくレンダリングしてみた。
はじめに
今回はスクエア・エニックスさんのNier Automataの様々な後処理効果をShaderToyで再現してみようかとします。上のリンクから提供するチュートリアルを見ながら、HLSLコードをShaderToyが支援するGLSLに移してみたいと思います。
実装
共通となるコード
const int fps = 15; const float delta = 1.0f / float(fps);
fps
:効果を更新する間隔です。60
なら1秒に60回更新します。- `delta' : 効果更新の間の時間秒です。
float Random(float t) { return fract( sin( dot(vec2(t, t), vec2(12.9898, 78.233)) )* 43758.5453123 ); }
ShaderToyではnoise1
などのランダム関数が支援できないため、そのかわりに使う類似乱数生成関数です。
float getTickedTime() { float time = iTime; float garbage = mod(time, delta); return time - garbage; }
ShaderToyの秒単位のタイムを返します。しかし単純にiTime
を呼ぶこととは違って上のfps
に合わせた時間秒を返します。(ミリ秒は小数点として返します。)
Pixelization
- 画面のピクセル化のポストプロセッシングです。縦は関係せず、横でピクセル化が起こることがわかります。
- 処理前の画面と混ざって一部だけピクセル化が現れることがわかります。
コード
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord/iResolution.xy; float glitchStep = mix(4.0f, 32.0f, Random(getTickedTime())); vec4 screenColor = texture(iChannel0, uv); uv.x = round(uv.x * glitchStep ) / glitchStep; vec4 glitchColor = texture(iChannel0, uv); fragColor = mix(screenColor, glitchColor, vec4(0.3f)); }
mix
は線形補間をする関数です。横に対してランダムにピクセル化のグリッチglitchStep
を起こすためにランダム関数(0 ~ 1)からの値をしようしました。- そして元来の色を取り出します。
- グリッチした場合のピクセル化する色に対しグリッチのステップにを使用して
uv
座標を取り出します。 - グリッチした場合の色を取り出します。そして
mix
で2つの色を線形に補間します。
結果
- Pixelizationを行うには
Step
値とそしてround
などの関数を使用して一定の数値だけを結果値として出すことが肝心ですね。
Color Separation
- 色の分離を実装するにはNoise Mapを使用し、そして一定のアルゴリズムでテクスチャから取り出した値を用います。
- そして多分
R
とG
とB
に対する歪曲した色の値をテクスチャから取り出して、普通の画面に加えます。
コード
const float damage = 1.0f;
damage
:ダメージが大きれば大きいほどに色の分離強度が強まります。(今はdamage
に流動的に値が操作出来ませんので使ってはいません。)
float GetNoiseTexture(const vec2 uv) { return texture(iChannel1, fract(uv + Random(getTickedTime(), 0.25f) * 10.0f) * 0.75f).r; } float GetNoiseTexture2(const vec2 uv) { return texture(iChannel1, fract(uv + Random(getTickedTime(), 0.78f) * 10.0f) * 0.5f).r; }
GetNoiseTexture
:iChannel1
にあるノイズマップからランダムに定まったuv
座標にあるr
値を取り出します。(今使っているノイズマップはr
しか情報を持ちません。)- そしてもっとランダム性を増すためにアルゴリズムが違う
_2
バージョンも用意します。全部使います。
float GetDamage(const vec2 uv, const float dmg) { float chrOffset = step(0.5f * (GetNoiseTexture(uv) + GetNoiseTexture2(uv)), 0.5f); return (2.0f * chrOffset + 1.0f) * 0.005f * dmg; }
GetDamage
:ダメージと今レンダリングする所のuv
座標を入れて、歪曲用として遷移するuv
座標の値を返します。- ここでランダムアルゴリズムである
GetNoiseTexture
を使用します。時間によって数値がランダムになりますので、ある場所に同じ歪曲効果が出ません。
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord/iResolution.xy; float damage_ = clamp(2.0f * sin(getTickedTime()), 0.0f, 10.0f); float chrOffset = GetDamage(uv, damage_); vec4 screenColor = texture(iChannel0, uv); float chrColR = texture(iChannel0, vec2(uv.x + chrOffset, uv.y)).r; float chrColB = texture(iChannel0, vec2(uv.x - chrOffset, uv.y)).b; fragColor = vec4(chrColR, screenColor.g, chrColB, 1.0f); }
chrOffset
:色分離の効果に使う、歪曲された座標を計算するために使うuv
オフセットです。- このアルゴリズムでは
R
とB
だけを取って色分離を行います。赤色は左へと分離されて、青色は右へと分離されます。(コードでは反対になります。) - そして元に色も取ってから
G
だけを残して結果色として返します。
結果
- 上の画像では
damage
が0から2.0になるまでの効果の様相をsin
関数として具現した現しています。 - ノイズのテクスチャは出来るかぎり解像度が小さいものが望ましいです。(8x8, 4x8)
- ノイズから値を取り出すためのランダムアルゴリズムも重要になります。
- ダメージ値を調整する関数も重要となります。(ジグザグとかですね)
Color Corruption
- 昔のテレビを見るように時々横線が発生する。
- 横線が発生するところには色の歪みが起きています。
コード
float GetNoise1(const vec2 uv) { float t = getTickedTime(); float n = Random(t, 0.25f) * 10.0f; return texture(iChannel1, fract(vec2(mod(uv.x + n, 0.4f) * 0.2f, uv.y + n)) ).r; } float GetNoise2(const vec2 uv) { float t = getTickedTime(); float n = Random(t, 0.25f) * 7.0f; return texture(iChannel1, fract(vec2(mod(uv.x + n, 0.2f) * 0.35f, uv.y + n)) ).r; }
- 上のコードから名前と中身の動作をちょっと変えました。
- ノイズのテクスチャは8x8になっているが、横線を作るために
uv
のu
軸だけ狭い領域だけをピックアップするようにしている。
float GetCorruptionOffset(const float i, const float v) { return step(i, v) * uCorruption; }
- 色の歪みを行うための色オフセットを用意します。
uCorruption
が強いと色の歪みが激しくなります。
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord/iResolution.xy; float n1 = GetNoise1(uv); float n2 = GetNoise2(uv); vec4 screenColor = texture(iChannel0, uv); vec4 distortedColor1 = texture(iChannel0, vec2(uv.x + step(n1, 0.4f) * 0.005f, uv.y)); vec4 distortedColor2 = texture(iChannel0, vec2(uv.x - step(n2, 0.2f) * 0.005f, uv.y)); float rbOffset = GetCorruptionOffset(n2, 0.1f); float gOffset = GetCorruptionOffset(n1, 0.2f); fragColor.r = mix(distortedColor1.r, distortedColor2.r, 0.5f) - rbOffset; fragColor.g = screenColor.g + gOffset; fragColor.b = mix(distortedColor1.b, distortedColor2.b, 0.5f) - rbOffset; }
Color Separation
でやったように歪んだ位置の色1と色2を取り出します。(distortedColor
シリーズ)- 歪んでいる部分には「緑」を強調して、他の色を落としたかったですので
rbOffset
とgOffset
を求めて色に足します。
結果
- ゲームの効果に比べてはちょっと違うが、ややリアルっぽくなっています。
- 横線の縦幅が短いのが惜しいところでした。
- この効果もノイズテクスチャのサイズや密度が重要になる気がします。
Lens Distortion
- 画面が一応グレイ化して、そしてフレームの縁に近付いているほどに
R
とB
の色が漏れます。 - そして外に行くほどもっとボケます。
コード
float ToGrayColor(const vec3 color) { return dot(color, vec3(0.3f, 0.59f, 0.11f)); } vec2 UvConvertToNgPs(const vec2 uv) { return uv * 2.0f - 1.0f; } vec2 UvConvertToZeOn(const vec2 uv) { return (uv + 1.0f) / 2.0f; }
ToGrayColor
:RGBカラーをグレースケールに変換します。UvConvertToNgPs
:のuv
座標をに変換します。UvConvertToZeOn
:のuv
座標をに変換します。
const float uDistortion = 0.5f; void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord/iResolution.xy; // Color Corruptionのコード。そして出た歪んだ色をglitchedColorという変数に入れる。 vec2 aUv = UvConvertToNgPs(uv); float aberration = pow(((length(aUv))), 2.0f); vec3 lensBlur = texture(iChannel0, uv - aUv * aberration * 0.00625).rgb; vec3 lensBlur2 = texture(iChannel0, uv - aUv * aberration * 0.0125).rgb; vec3 lensBlur3 = texture(iChannel0, uv - aUv * aberration * 0.025).rgb; vec3 compositeColor = mix(screenColor.rgb, glitchedColor, uDistortion); vec3 desaturatedCol = vec3(ToGrayColor((screenColor.rgb + lensBlur + lensBlur2) / 3.0f)); desaturatedCol += vec3(ToGrayColor(screenColor.rgb - lensBlur3) * 2.0f, vec2(0)); fragColor = vec4(mix(compositeColor, desaturatedCol, uDistortion), 1.0f); }
uDistortion
:歪みの強度を決めます。lensBlur
lensBlur2
lensBlur3
:中心から離れれば離れるほどaberration
によってuv
が調整され、放射状に色を拾います。- そして
compositeColor
に線形補間をかけ、ほがしをしたdesaturatedCol
にまた線形補間を掛けて最終色を出します。
結果
- 画面の中央から離れれば離れるほどにほがしが出て、そして色が分離されることがわかります。
まとめ
私が開発するゲームなどにいつかこういう効果を実装するかもしれませんし、そしてどうやって実装されていたかを確かめる時間になりました。次にはもっと様々なポストプロセッシング効果をShaderToyで実装していですね。