Hello, my name is pornanime.

アニメっぽいエフェクトを生成したい

クリスマスだしたまには便利な記事でも書く。



以前少し書いた、ゲームにアニメ風のエフェクトを入れたいという話。アニメ風というのは例えば

f:id:gyuque:20141224232355j:plain

f:id:gyuque:20141224232403j:plain

こういう光線や光輪を指す。ポイントとしては、わざと線の太さを不安定にしたり途切れ途切れにして躍動感を出す、という所。

さて、まず光輪からいくと、左上に(0,0)、右下に(1,1)のテクスチャ座標を指定した矩形ポリゴンを用意する。(次の画像だけ、u,vを赤と緑に割り当てている)

f:id:gyuque:20141224232926p:plain

中心が(0.5,0.5)なので、フラグメントシェーダー上でテクスチャ座標と(0.5,0.5)の距離をとって、ある閾値を超えたらdiscardすると円形になる。

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);
}

f:id:gyuque:20141224233652p:plain
まあこれは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);
}

f:id:gyuque:20141224234045p:plain
次に、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);
}

f:id:gyuque:20141224234435p:plain
さらに高周波成分を足していくと掠れたような味が出てくる。

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);
}

f:id:gyuque:20141224235446p:plain
線が大きく波打っててダサい、と思う場合は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);
}

f:id:gyuque:20141224235859p:plain
最後にアニメーションを付ける。アニメーションの時刻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);
}

ということで出来たのがこれ。
f:id:gyuque:20141225002918g:plain
まあまあかな。



さて、次は光線を。光輪では中心からの距離を使っていたが、今度は角度をパラメーターにする。ただし、光線を複数出したいので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);
}

f:id:gyuque:20141225003924p:plain
旭日旗みたいになってしまったので、外側に行くほど閾値を厳しくしてみる。

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);
}

f:id:gyuque:20141225004039p:plain
だいぶマシになったけど、上のグレンラガンのキャプチャのように、中心部分は◆形になっていてほしい。そこで、中心部分に近づくほど 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);
}

f:id:gyuque:20141225004522p:plain
かなりそれっぽくなった。さらに、閾値の増分を非線形にしたり、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);
}

f:id:gyuque:20141225005245p:plain
さらに光輪の場合と同じように閾値を揺らしてみたり……と、まあ黒魔術をいろいろやる。



そしてひとまずの結果。光線はもうちょっとなんとかならんか。
f:id:gyuque:20141225012034g:plain

作例:
f:id:gyuque:20141225013416j:plain
(注:辞職したわけではありません)

以下ソース、ただし全く最適化していないので注意。(とりあえず、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;
}