NEUROMANTIC

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

「Heartfelt」を分析・KodeLifeで応用してみた。

www.shadertoy.com

分析しにくかったシェーダー効果でしたけど、なんとか分析して変数をuniformで作成してKodeLifeで実装してみました。(なお、不必要なコードも削除)

  • CHEAP_NORMALSは除きます。コメントでは2倍見にくくなるよと書いていたんですが、実際にやってみると3倍以上見にくくなります。もしかして水滴のぼかしを追加で行いたいならやっても良いかもしれませんね。
  • ハート模様の雨レンダリングの分析はしませんでした。
  • USE_POST_PROCESSINGの分析はします。
  • 元となるシェーダーは、「表示 - 非営利 - 継承 3.0 非移植」のライセンスによって営利的に利用してはなりません。ですから上のようなシェーダー効果を商用ゲームなどで出せるには一から作り直す必要があります。
  • 率直に言うと、アルゴリズム自体はものすごく感動しましたけど、素のままじゃコードが見にくいです。

分析

1. mainImage

vec2 uv = (fragCoord.xy-.5*iResolution.xy) / iResolution.y;
vec2 UV = fragCoord.xy/iResolution.xy;
vec3 M = iMouse.xyz/iResolution.xyz;
float T = iTime+M.x*2.;
    
float t = T*.2; 
float rainAmount = iMouse.z>0. ? M.y : sin(T*.05)*.3+.7;
    
float maxBlur = mix(3., 6., rainAmount);
float minBlur = 2.;
    
float zoom = -cos(T*.2);
uv *= .7+zoom*.3;
UV = (UV-.5)*(.9+zoom*.1)+.5;
  • uv:雨の水滴をレンダリングする時に必要となるUV座標。ノイズマップからテクセルを参照してレンダリングする時に必要となる。
    • uvを引数として算出されるfocus変数が最終のcolを求める時に使用される以外には水滴のノイズマップによる値獲得が主な用途。
  • UV:バックグラウンドとなるテクスチャのUV座標を示す。zoomによって拡大か縮小される。
  • M:こっちでは分析しない。雨の水滴の時間の流れによる落ちる表現を巻き戻したりする時に使われるらしい。
  • T:時間変数。オリジナルコードではMがオフセットとして作用して、調整できるようになっている。
    • tTを細く使うようにしたもの。
  • rainAmount:雨の降る量を調整する変数。
  • maxBlurminBlur:水滴のぼかしを調整する。
    • バックグラウンドのテクスチャがそんなにはっきりじゃないと、あんまり差は出てないようだ。
    • maxBlurminBlurの値によるレンダリング結果の差は応用で説明する。
  • zoom:時間によってズームイン・ズームアウトをするようにする。
#define S(a, b, t) smoothstep(a, b, t)
float staticDrops = S(-.5, 1., rainAmount)*2.;
float layer1 = S(.25, .75, rainAmount);
float layer2 = S(.0, .5, rainAmount);

smoothstepを使用して3つの変数に値を算出している。glslHLSLでのsmoothstepは以下の数式で実装されている。この式は下のグラフの図のような格好で0から1へと増加する。

 S_1(x) =3x^2 - 2x^3  \quad 0 \le x \le 1

f:id:neuliliilli:20190522203204p:plain
https://qiita.com/oishihiroaki/items/9d899cdcb9bee682531a

  • staticDrops:動的に生成されて落ちる水滴を除外した静的な水滴の生成量を示す。範囲は[0, 2]
  • layer1:雨の動的水滴レイヤー1の生成量を示す。範囲は[0, 1]。
  • layer2:雨の動的水滴レイヤー2の生成量を示す。範囲はレイヤー1と同じ。
vec2 c = Drops(uv, t, staticDrops, layer1, layer2);
vec2 e = vec2(.001, 0.);
float cx = Drops(uv+e, t, staticDrops, layer1, layer2).x;
float cy = Drops(uv+e.yx, t, staticDrops, layer1, layer2).x;
vec2 n = vec2(cx-c.x, cy-c.x); // expensive normal
  • cDrops関数で、水滴を表現するのに必要な値をx, yとして獲得する。詳しくはDrops関数で…
    • 重要なことは、CHEAP_NORMALSを使っても、そうじゃない場合にも水滴が表現される領域は同じだということである。ただし、nがどんな値を持ち、テクスチャを参照して水滴表現をさせるかが方法によって違う。
    • c.xfocusを得る時のぼかしの程度。
    • c.y:ぼかしの最大値のオフセット値。
  • eCHEAP_NORMALSを使わない時に、現在のuvから座標を微調整するためのエプシロンとして扱われる。この値が大きくなると、水滴に表現する(Transmitされて見える)部分の範囲が大きくなる。
    • でかくすると不自然に見えるので、出来るだけ範囲は[0.00001, 0.001]に収める方が良いと思う。
  • cxcydfDxdfDyの値を類似に得るための単位面積当たりの差異。これでnを得る。
  • n:水滴の各表面から、uvに足してレンダリングするオフセット座標を示す。ある水滴の左の部分は、uvより右に移動してテクスチャを取り、右はその逆でカラーを取る。
float focus = mix(maxBlur-c.y, minBlur, S(.1, .2, c.x));
vec3 col = textureLod(iChannel0, UV+n, focus).rgb;
  • focus:どれだけぼかしされたテクスチャのカラーを得るかを決めるための変数。これをLOD値として使える。
  • colを得る時に、UV + nで、バックグラウンドテクスチャのちょっと移動した座標でのピクセルカラーを取る。

f:id:neuliliilli:20190522230518g:plain
`c`値をvec4(c, 0, 1)としてスクリーンに描画した結果

f:id:neuliliilli:20190522230717g:plain
`cx`、`cy`をvec4(cx, cy, 0, 1)としてスクリーンに描画した結果

2. Drops関数

vec2 Drops(vec2 uv, float t, float l0, float l1, float l2) 
{
    float s = StaticDrops(uv, t)* l0;
    vec2 m1 = DropLayer2(uv, t) * l1;
    vec2 m2 = DropLayer2(uv*1.85, t)*l2;
    
    float c = s+m1.x+m2.x;
    c = smoothstep(.3, 1., c);
    
    return vec2(c, max(m1.y*l0, m2.y*l1));
}
  • s:落ちない水滴を表現する。ある地点で現る・消えるを反復するけど、これがないと水滴のパターンが見えてしまう。
  • m1:落ちる水滴の跡のWeight値をいう。この値によって、大きめの水滴の滴と落ちる時の跡が残れる。
  • m2:落ちる水滴の跡のWeight値をいう。m1とはほぼ同じが、小さめの水滴を表現する。

    • 小さめの水滴を表現させるため、DropLayer2関数の第一引数に掛け算をしている。ノイズマップを参照するためのuvがLeapすることによってサンプリング数値が下がり、小さめの水滴が表現出来るようになるようだ。
  • c:上の3つの水滴Weight値のx値は、合計されて一つの水滴の領域として表現できる。現実で一つの水滴がとある一つの水滴に合うと、大きめの水滴になることを連想すればいいだろう。この値をリターン値のxとして扱う。

    • 一言で言えば、スクリーン上のピクセルの水滴のWeight値を示す。

3. StaticDrops関数&DropLayer2関数

f:id:neuliliilli:20190522225804g:plain
StaticDropsが返す値をvec4(x, 0, 0, 1)にして描画した結果

  • StaticDrops関数は水滴の表現値(Weight値)だけを返す。中身ではN13という関数を使用して(類似ノイズ?)水滴の表現ノイズを決めるらしい。

f:id:neuliliilli:20190522225551g:plain
DropLayer2関数の値をvec4(return, 0, 1)にして描画した結果

  • DropLayer2は落ちる水滴の滴(赤)、水跡(緑)の値を返す。N13以外にNoise、Saw関数を使う。
    • N13、Saw、ノイズ関数に関しては直接コードを見ること

5. ポストプロセッシング(USE_POST_PROCESSING

ポストプロセッシングでは3つのポストプロセッシングを実装している。

  1. 時間の流れによる雷効果
  2. 明暗のVignette効果&色調繊維(ColorShift)効果
  3. 明暗のフェード効果

共通となるコードは以下である。

t = (T+3.)*.5; // make time sync with first light noising
float fade = S(0., 10., T); // fade in at the start
float colFade = sin(t*.2)*.5+.5+story;
col *= mix(vec3(1.), vec3(.8, .9, 1.3), colFade); // subtle color shift
  • vec3(.8, .9, 1.3)は変わる色調のベース色を示す。元コードではstoryという変数によってFade数値が変わるが、応用のコードではuBlueScreenという、Weight値を別途に用意して調節するようにした。
float lightning = sin(t*sin(t*10.)); // lighting flicker
lightning *= pow(max(0., sin(t+sin(t))), 10.); // lightning flash
col *= 1.+lightning*fade*mix(1., .1, story*story); // composite lightning
  • lightning:雷効果のためのWeight値を表す。t(時間変数)によって値が下のような図のように変わる。そしてこの値が1.+lightningで画面が光ったりする。

f:id:neuliliilli:20190522233954p:plain
雷効果のWeight値の分布

col *= 1.-dot(UV-=.5, UV); // vignette 
col *= fade; // composite start and end fade
  • ビネットとフェード効果を実装する。

応用

f:id:neuliliilli:20190522234613g:plain
応用例

KodeLifeで実装したときには、雨効果に関わる値はuniformとして取り出して調節するようにしました。

f:id:neuliliilli:20190522235013p:plain
KodeLifeのUniform変数のリスト

ですけど、ライセンスのため商用では使えませんね。 しかしこのシェーダーから細部的な効果らをなんとか参考にして新しい効果を作り出せると思います。

  1. ノイズマップの生成
  2. 雨の類似効果(最初から直接つくること)
  3. 雷の効果
  4. ビネット・色調調節効果

参照

en.wikipedia.org