NEUROMANTIC

自分でC/C++/UE4/Graphics/ゲームなどをやったことをメモするブログ

ShaderToyで「Nier:Automata」の後処理っぽくレンダリングしてみた。

はじめに

sawcegames.com

今回はスクエア・エニックスさんの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

f:id:neuliliilli:20181207185924g:plain
Nier automataのピクセル化効果(from sawcegames)
  • 画面のピクセル化のポストプロセッシングです。縦は関係せず、横でピクセル化が起こることがわかります。
  • 処理前の画面と混ざって一部だけピクセル化が現れることがわかります。

コード

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つの色を線形に補間します。

結果

f:id:neuliliilli:20181207191252g:plain:w600
ShaderToyでPixelizationした結果
  • Pixelizationを行うにはStep値とそしてroundなどの関数を使用して一定の数値だけを結果値として出すことが肝心ですね。

Color Separation

f:id:neuliliilli:20181207192259g:plain
Nier automataの色分離効果(from sawcegames)
  • 色の分離を実装するにはNoise Mapを使用し、そして一定のアルゴリズムでテクスチャから取り出した値を用います。
  • そして多分RGBに対する歪曲した色の値をテクスチャから取り出して、普通の画面に加えます。

コード

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;
}
  • GetNoiseTextureiChannel1にあるノイズマップからランダムに定まった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オフセットです。
  • このアルゴリズムではRBだけを取って色分離を行います。赤色は左へと分離されて、青色は右へと分離されます。(コードでは反対になります。)
  • そして元に色も取ってからGだけを残して結果色として返します。

結果

f:id:neuliliilli:20181207192035g:plain:w600
ShaderToyでColor Seperationした結果
  • 上の画像ではdamageが0から2.0になるまでの効果の様相をsin関数として具現した現しています。
  • ノイズのテクスチャは出来るかぎり解像度が小さいものが望ましいです。(8x8, 4x8)
  • ノイズから値を取り出すためのランダムアルゴリズムも重要になります。
  • ダメージ値を調整する関数も重要となります。(ジグザグとかですね)

Color Corruption

f:id:neuliliilli:20181207210652g:plain
Nier automataのColor Corruption効果(from sawcegames)
  • 昔のテレビを見るように時々横線が発生する。
  • 横線が発生するところには色の歪みが起きています。

コード

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になっているが、横線を作るためにuvu軸だけ狭い領域だけをピックアップするようにしている。
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シリーズ)
  • 歪んでいる部分には「緑」を強調して、他の色を落としたかったですのでrbOffsetgOffsetを求めて色に足します。

結果

f:id:neuliliilli:20181207212546g:plain:w600
ShaderToyでColor Corruption効果を実装。
  • ゲームの効果に比べてはちょっと違うが、ややリアルっぽくなっています。
  • 横線の縦幅が短いのが惜しいところでした。
  • この効果もノイズテクスチャのサイズや密度が重要になる気がします。

Lens Distortion

f:id:neuliliilli:20181207221226g:plain
Nier automataのレンズの歪み効果(from sawcegames)
  • 画面が一応グレイ化して、そしてフレームの縁に近付いているほどにRBの色が漏れます。
  • そして外に行くほどもっとボケます。

コード

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{(0, 1)}uv座標を{(-1, 1)}に変換します。
  • UvConvertToZeOn{(-1, 1)}uv座標を{(0, 1)}に変換します。
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にまた線形補間を掛けて最終色を出します。

結果

f:id:neuliliilli:20181207223439p:plain:w600
ShaderToyでLens Distortion効果を実装。
  • 画面の中央から離れれば離れるほどにほがしが出て、そして色が分離されることがわかります。

まとめ

私が開発するゲームなどにいつかこういう効果を実装するかもしれませんし、そしてどうやって実装されていたかを確かめる時間になりました。次にはもっと様々なポストプロセッシング効果をShaderToyで実装していですね。