アニメっぽいエフェクトを生成したい
クリスマスだしたまには便利な記事でも書く。
以前少し書いた、ゲームにアニメ風のエフェクトを入れたいという話。アニメ風というのは例えば
こういう光線や光輪を指す。ポイントとしては、わざと線の太さを不安定にしたり途切れ途切れにして躍動感を出す、という所。
さて、まず光輪からいくと、左上に(0,0)、右下に(1,1)のテクスチャ座標を指定した矩形ポリゴンを用意する。(次の画像だけ、u,vを赤と緑に割り当てている)
void main() { vec2 rpos = v_texCoord - vec2(0.5, 0.5); float r = length(rpos); if (r > 0.5) { discard; } gl_FragColor = vec4(1,1,1, 1); }
まあこれはcocos-2dxの中で使っていたものを拝借しただけ。次に、距離がある範囲にある場合だけ残して他はdiscard、とするとリングになる。
void main() { vec2 rpos = v_texCoord - vec2(0.5, 0.5); float r = length(rpos); float ringR = 0.4; float thresh = 0.01; if (abs(r - ringR) > thresh) { discard; } gl_FragColor = vec4(1,1,1, 1); }
次に、sinやcosを使って閾値を揺らすと線の太さを不安定にできる。
void main() { vec2 rpos = v_texCoord - vec2(0.5, 0.5); float r = length(rpos); float angle = atan(rpos.y, rpos.x); float ringR = 0.4; float thresh = 0.01 + sin(angle * 3.0) * 0.01; if (abs(r - ringR) > thresh) { discard; } gl_FragColor = vec4(1,1,1, 1); }
さらに高周波成分を足していくと掠れたような味が出てくる。
void main() { vec2 rpos = v_texCoord - vec2(0.5, 0.5); float r = length(rpos); float angle = atan(rpos.y, rpos.x); float ringR = 0.4; float thresh = 0.01 + sin(angle * 3.0) * 0.01 + sin(angle * 13.0) * 0.003 + cos(angle * 19.0) * 0.002; if (abs(r - ringR) > thresh) { discard; } gl_FragColor = vec4(1,1,1, 1); }
線が大きく波打っててダサい、と思う場合はmin()で閾値が揺れる上限を決めてやると良いかもしれない
void main() { vec2 rpos = v_texCoord - vec2(0.5, 0.5); float r = length(rpos); float angle = atan(rpos.y, rpos.x); float ringR = 0.4; float thresh = 0.01 + sin(angle * 3.0) * 0.01 + sin(angle * 13.0) * 0.006 + cos(angle * 31.0) * 0.005; if (abs(r - ringR) > min(thresh, 0.01)) { discard; } gl_FragColor = vec4(1,1,1, 1); }
最後にアニメーションを付ける。アニメーションの時刻tを格納する頂点属性が必要になるが、cocos2d-xのスプライトを使っている場合は既存の頂点属性を使うしかないので、とりあえず頂点色に押し込む。tに応じて閾値を厳しくする(下げる)と線が細くなるし、sinの中身をいじれば線の掠れ具合が変わる……ということであとは試行錯誤する。
void main() { float animation_t = v_color.r; vec2 rpos = v_texCoord - vec2(0.5, 0.5); float r = length(rpos); float angle = atan(rpos.y, rpos.x); float ringR = 0.4 * animation_t; float thresh = 0.01 + sin(angle * 3.0 + animation_t) * 0.01 + sin(angle * (11.0+animation_t*9.0)) * 0.006 + cos(angle * 31.0) * 0.005; if (abs(r - ringR) > (min(thresh, 0.01) - animation_t*0.01)) { discard; } gl_FragColor = vec4(1,1,1, 1); }
ということで出来たのがこれ。
まあまあかな。
さて、次は光線を。光輪では中心からの距離を使っていたが、今度は角度をパラメーターにする。ただし、光線を複数出したいのでsinに通して周期性を持たせる。
void main() { vec2 rpos = v_texCoord - vec2(0.5, 0.5); float angle = atan(rpos.y, rpos.x); float s = sin(angle * 4.0); float thresh = 0.9; if (s < thresh) { discard;} gl_FragColor = vec4(1,1,1, 1); }
旭日旗みたいになってしまったので、外側に行くほど閾値を厳しくしてみる。
void main() { vec2 rpos = v_texCoord - vec2(0.5, 0.5); float angle = atan(rpos.y, rpos.x); float r = length(rpos); float s = sin(angle * 4.0); float thresh = 0.9 + r * 0.2; if (s < thresh) { discard;} gl_FragColor = vec4(1,1,1, 1); }
だいぶマシになったけど、上のグレンラガンのキャプチャのように、中心部分は◆形になっていてほしい。そこで、中心部分に近づくほど s の値を底上げしてやる。
void main() { vec2 rpos = v_texCoord - vec2(0.5, 0.5); float angle = atan(rpos.y, rpos.x); float r = length(rpos); float s = sin(angle * 4.0); s += 0.0004 / (r*r*r); float thresh = 0.9 + r * 0.2; if (s < thresh) { discard;} gl_FragColor = vec4(1,1,1, 1); }
かなりそれっぽくなった。さらに、閾値の増分を非線形にしたり、sを三乗してみたり……と形を整える。
void main() { vec2 rpos = v_texCoord - vec2(0.5, 0.5); float angle = atan(rpos.y, rpos.x); float r = length(rpos); float s = sin(angle * 4.0); s += 0.0004 / (r*r*r); float thresh = -3.41 + pow(r, 0.01) * 4.48; if ((s*s*s) < thresh) { discard;} gl_FragColor = vec4(1,1,1, 1); }
さらに光輪の場合と同じように閾値を揺らしてみたり……と、まあ黒魔術をいろいろやる。
そしてひとまずの結果。光線はもうちょっとなんとかならんか。

作例:
(注:辞職したわけではありません)
以下ソース、ただし全く最適化していないので注意。(とりあえず、VSでできる計算はVSで済ませてFSに送った方がいい)
#ifdef GL_ES precision lowp float; #endif // Input varying vec4 v_color; varying vec2 v_texCoord; // declarations bool determine_on_ring(float animation_t, float r, float angle); bool determine_on_radiation(float animation_t, float r, float angle); float sq_tween(float t); void main() { float animation_t = v_color.r; vec2 rpos = v_texCoord - vec2(0.5, 0.5); float angle = atan(rpos.y, rpos.x); float r = length(rpos); bool on_ring = determine_on_ring(animation_t, r, angle); bool on_radiation = determine_on_radiation(sq_tween(animation_t)*1.4, r, angle - 0.4 - animation_t); if (!on_ring && !on_radiation) { discard; } gl_FragColor = vec4(1,1,1, 1); } float sq_tween(float t) { return 1.0 - (1.0 - t) * (1.0 - t); } bool determine_on_ring(float animation_t, float r, float angle) { if (animation_t < 0.25) {return false;} float s = r -= 0.3 * sq_tween(animation_t); float thresh_v = sin((angle+animation_t) * 3.0) + cos((animation_t+angle) * 15.0)*0.5 + cos((angle+0.1) * 31.0)*0.6; float thresh = min(0.009 + thresh_v*0.01, 0.009 - animation_t*0.01); return (abs(s) < thresh); } bool determine_on_radiation(float animation_t, float r, float angle) { if (animation_t > 1.0) { return false; } float s = sin(angle * 4.0); float thresh = -3.41 + pow(r, 0.01) * 4.48; s += (0.00005*sin(animation_t*4.2-0.1)) / (r*r*r); thresh += (sin(sin(r-animation_t) * 40.0) + sin((r-animation_t) * 31.0) + 1.0)*0.01 - sin(animation_t*5.2 - 0.5)*0.1 + animation_t*0.2; return s*s*s > thresh; }