「Cathode」を参照・分析・応用してみた。
ブラウン管みたいにスキャンライン、そして色の滲みを表現するポストプロセッシングシェーダーを分析、応用してみました。
分析
MainImage
関数
MainImage
は各描画セルの腐食によるスキャンラインを含むもの、ぼかしだけをやって描画するもの、そして単にピクセルを拡大して描画するようとしてます。
それぞれの部分の境界線はsmoothstep
を使用して枠線を引いてますね。
vec3 col = phosphors(fragCoord.xy/4.0, iChannel0); if (f.y > 200.) col = phosphors(fragCoord.xy/4.0 - vec2(0,30.), iChannel1);
phosphors
名は「蛍光体」というもので、この関数は蛍光体を使用したブラウン管の画面描画や蛍光体による腐食現象を再現して描画する。iChannel0
、iChannel1
は各テクスチャバッファを示す。なので下の部分と上の部分で違ったテクスチャを描画している。fragCoord.xy
は画面のピクセル単位の座標値を示す。- もし推測が正しければ、
fragCoord
はgl_TexCoord
と同じで、の座標値を持つかもしれない。
- もし推測が正しければ、
4.0
はズーム値を示す。
phosphors
関数
vec3 col = vec3(0); p -= 0.25; p.y += mod(gl_FragCoord.x,2.)<1.?.03:-0.03; p.y += mod(gl_FragCoord.x,4.)<2.?.02:-0.02;
col
は蛍光体関数ロジックの最後に決まった色である。p -= 0.25
は、蛍光体の位置を調整する。またp.y
の位置を調整するのは、各燐光体がずらりを並ぶことになるとパターンが認識しやすいので、それを防ぐために微細に調整したものかもしれない。
for(int i=-2;i<=2;i++) for(int j=-2;j<=2;j++) { vec2 tap = floor(p) + 0.5 + vec2(i,j); vec3 rez = texture(tex, tap/iChannelResolution[0].xy).rgb; //nearest neighbor
- 今位置する蛍光体からある範囲の隣接した蛍光体の色を使用して最終の色を決める。
floor(p) + 0.5
をする理由は(確かではないが)Nearest
な色を取るためには各バッファのピクセルの中間地点が必要となるのに、p
の定数部分だけが必要となるからだと思われる。iChannelResolution[0]
は各テクスチャバッファのサイズを示す。texture
はUVとして]の座標を求む。
//center points float rd = sqd(tap, p + vec2(0.0,0.2));//distance to red dot const float xoff = .25; float gd = sqd(tap, p + vec2(xoff,.0));//distance to green dot float bd = sqd(tap, p + vec2(-xoff,.0));//distance to blue dot
- 蛍光体の各セル「G、R、B」の各距離値を得るために
sqd
という関数を呼び出す。- 一般的には「R、G、B」が普通だけど、なぜかこのシェーダーでは「G」が中央になっている。
spd
関数は以下のように最適化か余計なものを省けられる。- `xoff``は、各チャンネルの蛍光体がお互いに離れている程度を示す。
- 個人的には0.25から0.5のほうが良かった。
- もし、古すぎて色がちゃんと出せない時の効果をやらせるなら、
xoff
を2から3にすればいいと思う。
// Phosphor shape float Sqd(in vec2 a, in vec2 b) { // Range of [x-scanline, y-scanline] a = (a - b) * vec2(1.0f, 2.0f); return mix(max(abs(a.x), abs(a.y)), length(a), 0.3f); }
spd
関数のvec2
の値は、横または縦のスキャンラインを作るのに必要となる。- 値が大きくなると、腐食が深化してスキャンラインの許容範囲が大きくなる。
rez = pow(rez,vec3(1.18)) * 1.08; rez.r *= decay(rd); rez.g *= decay(gd); rez.b *= decay(bd);
- 求めた
rez
(今のイテレーションで得られた色)の各チャンネルをdecay
関数を呼び出しした結果で侵食させる。 pow
をする理由は、多分カラースペースを非線形に合わせるために調整したものだと見られる。(GammaのDecode過程でも見られそう)1.08
を外して1.18
を2.2
にしたら色が暗くなるが、濃くみえるようになる。
//Phosphor decay float decay(in float d) { return mix(exp2(-d*d*2.5-.3),0.05/(d*d*d*0.45+0.055),.65)*0.99; }
- わけわからない(MagicNumberである可能性が非常に高い)数式を使用して腐食値を決める。
応用(KodeLifeを使用)
前日実装した「ゆらゆら」に「Cathode」効果を適用させてみました。zoom
で、画面描画のズームインを出来るようにし、各蛍光体には一つの色しか出せないよう、UVを画面用とゆらゆらの描画用を一つずつ用意して各蛍光体に一つの色がまとまってレンダリングさせるようにしました。
今のコードではレンダリングパスが一つなのでちょっと複雑になりましたが、「ゆらゆら」の描画部分をテクスチャにまとめて、次のパスで「Cathode」効果を実装すればもっと良いのではと思います。
Vertex Shader
#version 150 uniform vec2 resolution; uniform mat4 mvp; uniform int zoom = 8; in vec4 a_position; in vec3 a_normal; in vec2 a_texcoord; out VertexData { vec2 v_texcoord; vec2 v_zoomCoord; } outData; varying float v_cellOffset; void main(void) { // Some drivers don't like position being written here // with the tessellation stages enabled also. // Comment next line when Tess.Eval shader is enabled. gl_Position = mvp * a_position; outData.v_texcoord = (a_texcoord * resolution) / min(resolution.x, resolution.y); outData.v_zoomCoord = (a_texcoord * resolution) / zoom; v_cellOffset = min(resolution.x, resolution.y) / zoom; }
Fragment Shader
#version 150 uniform float time; in VertexData { vec2 v_texcoord; vec2 v_zoomCoord; } inData; varying float v_cellOffset; out vec4 fragColor; const vec3 col1 = vec3(1, 0, 0.25); // off1 color (primary) const vec3 col2 = vec3(0, 0.3f, 1); // off2 color (secondary) const vec3 col3 = vec3(1, 1, 1); // off3 color (sub) vec3 Way3Color(in vec2 uv) { float speed = 0.125f; vec2 p = uv * 5.0f; for (int i = 1; i < 10; ++i) { float fi = float(i); p.x += 0.4f / fi * sin(fi * 3.0f * p.y + time * speed) + 0.005f; p.y += 0.4f / fi * sin(fi * 3.0f * p.x + time * speed) + 0.005f; } // [0, 1] float off1 = cos(p.x + p.y) * 0.5f + 0.5f; float off2 = sin(p.x + p.y) * 0.5f + 0.5f; float off3 = (2.0f - (off1 + off2)) * 0.5f; // over operator vec3 color = vec3(0); color = col1 * off1 + color * (1.0f - off1); color = col2 * off2 + color * (1.0f - off2); color = col3 * off3 + color * (1.0f - off3); return color; } // Phosphor decay float Decay(in float d) { return mix( exp2(-d * d * 2.5f - 0.3f) , 0.05f / (pow(d, 3.0f) * 0.45f + 0.055f) , 0.65f); } // Phosphor shape (Magic number?) float Sqd(in vec2 a, in vec2 b) { a -= b; // Range of [x-scanline, y-scanline] a *= vec2(1.0f, 2.0f); return mix(max(abs(a.x), abs(a.y)), length(a), 0.3f); } vec3 Phosphors(in vec2 zoomUv) { vec3 col = vec3(0); // Start phosphors cell from top to avoid patterning. zoomUv.y += mod(gl_FragCoord.x, 2.0f) < 1.0f ? 0.03f: -0.03f; zoomUv.y += mod(gl_FragCoord.x, 4.0f) < 2.0f ? 0.02f : -0.02f; //5x5 kernel (this means a given fragment can be affected by a pixel 4 game pixels away) const int range = 2; for(int i = -range; i <= range; i++) { for(int j = -range; j <= range; j++) { // Nearest neighbor vec3 rez = Way3Color(inData.v_texcoord - mod(inData.v_texcoord, 1 / v_cellOffset)); // Center points (5) [R,G,B] in each phosphor cell. const float xoff = 0.25f; // relocate gl_TexCoord (center pixel coordinate) vec2 tap = floor(zoomUv) + 0.5f + vec2(i,j); rez = pow(rez,vec3(1.18)) * 1.08; // Decode? rez.r *= Decay(Sqd(tap, zoomUv + vec2(xoff, 0.2))); // Distance to red filter rez.g *= Decay(Sqd(tap, zoomUv + vec2(0.0, 0.0))); // Distance to green filter rez.b *= Decay(Sqd(tap, zoomUv + vec2(-xoff, 0.0)));// Distance to blue filter col += rez; } } return col; } void main() { fragColor = vec4(Phosphors(inData.v_zoomCoord), 1.0f); }