musicLineの記念動画(ランキング・急上昇・殿堂入り)のデザインを紹介。
内部の実装もちょこっと紹介!の第二弾です。
ランキングの記念動画
はじめに
musicLineコミュニティでは曲(MIDI)の再生時に音の可視化映像が流れますが、ランキング等で上位に上がった曲はキラキラしたリッチな表現の映像になります。
ちなみに、前回はコンテストの記念動画を紹介しました。
今回はランキング・急上昇・殿堂入りの映像について観察し、内部実装(GLSLのコーディング)について軽く触れます。
デザインの観察
ランキングと急上昇の記念動画 1~8 位と殿堂入り含めて合計17種類あります。
各々のバリエーションを比較して、記念動画の要素を分解することでデザインを観察します。
バリエーションの比較
殿堂入りとランキング・急上昇の 1~4 位の記念動画を比較します。
各々の記念動画を比較すると
- 順位ごとにテーマカラーがある
- モチーフの図形が異なる(丸、菱形、格子)
- ランキング・殿堂入りはノイズ感がある
- ランキングはチェック模様、殿堂入り奥行きのある背景
- 3位以上はより華やなエフェクト
- 殿堂入りは色合いにグラデーションが掛かる
等の違いがあります。
以下に紹介させて頂いた曲のアプリリンクを載せています。
気になった曲はmusicLineで聴いてください ♪
要素の分解
この記念動画を分解すると、4つの要素(レイヤー)で構成されています。
名前 |
イメージ |
エフェクト |
|
情報 |
|
コンテンツ |
|
背景 |
|
この内、エフェクトと背景を変えることで記念動画のバリエーションを出しています。
エフェクトの実装
今回はランキング記念動画を単純化したピカーンエフェクトの実装を紹介します。AndroidはOpenGLESが動作するため、実装はGLSLを使いました。
なお、サンプルはWebでGLSLを実行できるエディタ** The Book of Shaders Editor **で表示して確認しました。
エフェクトの構成要素
ピカーンエフェクトは3種類のアルファアニメーションを用いて、カラーとテクスチャをブレンドすることで実現しています。
パターンアニメーション(アルファ 1)の作成
パターンアニメーションは座標変換
とアニメーション
の2ステップで解説します。
1. 座標変換
菱形を描画しやすいように、
ST座標
→ 回転ST座標
→ タイル座標
と座標を変換します。
ST座標
precision mediump float;
uniform vec2 u_resolution;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
gl_FragColor = vec4(st, 0., 1.);
}
左下原点(0, 0)
でサイズ 1 の基準となるST座標空間へ変換します。
このサンプルでは座標を可視化するため、gl_FragColor
の赤と緑の値にXとYの値を渡して色を表示しています。なので、値が大きくなる程色がつきます。例えば、座標(1, 1)
はRGB(1, 1, 0)
となり黄色になります。
回転ST座標(45度回転)
...
vec2 trans_st = (st - 0.5);
float rot = radians(45.);
mat2 rot_matrix = mat2(cos(rot), -sin(rot), sin(rot), cos(rot));
vec2 rot_st = trans_st * rot_matrix;
gl_FragColor = vec4( rot_st, 0., 1.);
菱形になるようにST座標空間を45度回転します。
回転は行列演算を用いますが、これは原点中心を起点として回転します。回転する前に原点を画面の中心に移動しておくことで、後の処理が計算しやすくなります。
タイル座標
vec2 scaled_st = rot_st * 6.;
vec2 shift_st = scaled_st + .5;
vec2 tile = fract(scaled_st);
gl_FragColor = vec4(tile, 0., 1.); return;
パターンアニメーションは中心の菱形から波紋状に周囲の菱形へ広がっていくアニメーションになります。そのため、同じ菱形が繰り返されるように、座標をタイル状に分割します。shift_st
で中央に菱形が来るように位置をずらします。
2. アニメーション
アニメーションは全体と細部の2種類あります。
タイルアニメーション
で全体の動き、スケールアニメーション
で細部の動きを制御します。
タイルアニメーション(全体)
vec2 tile_pos = floor(shift_st);
float dist = length(tile_pos);
float progress = sin(u_time - dist);
float easeIn_progress = progress * progress;
float adjust_progress = pow(easeIn_progress, 2.2) * 0.5;
gl_FragColor = vec4(vec3(adjust_progress), 1.);
floor
でタイルの座標を取得し、length
で中心からの距離を算出しています。外側に行くほどタイルの色が明るくなります。
progress
は中央からの距離と時間に応じてタイル毎のアニメーション進捗度を-1 ~ 1
で算出します。ざっくり言うと0 ~ 1
でアニメーションを行い、-1 ~ 0
でアニメーションを休憩するイメージです。
progress
のままでは単純なSin波なので、イージングでメリハリをつけたり出現時間のバランスを調整します。
ちなみに、イージングの変換数式はこちらが参考になりました。
easings.net
スケールアニメーション(細部)
float animation_rule = min(min(tile.x, 1.0 - tile.x), min(tile.y, 1.0 - tile.y));
float pattern_margin = 0.15;
float preview_progress = pow(sin(u_time), 2.) * .5;
float preview_value = smoothstep( preview_progress - 0.05, preview_progress, 0.95 * (animation_rule - pattern_margin));
gl_FragColor = vec4(vec3( preview_value), 1.); return;
アニメーションするために、ルール画像というものを作成します。ルール画像はアニメーション進捗度0 ~ 1
を明度で示した画像です。真ん中から外へ広がるように明度を指定しています。
あとはsmoothstep
を使って、ルール画像に基づき菱形が拡大縮小するような動きに制御します。
こちらはsmoothstep
関数やShaderについての解説されています。
thebookofshaders.com
コースティクスアニメーション(アルファ 2)の作成
float h12(highp vec2 p) {
return fract(sin(dot(p,vec2(32.52554,45.5634)))*12432.2355);
}
float n12(highp vec2 p)
{
vec2 i = floor(p);
vec2 f = fract(p);
f *= f * (3.-2.*f);
return mix(
mix(h12(i+vec2(0.,0.)),h12(i+vec2(1.,0.)),f.x),
mix(h12(i+vec2(0.,1.)),h12(i+vec2(1.,1.)),f.x),
f.y
);
}
float caustics(highp vec2 p, highp float t)
{
highp vec3 k = vec3(p,t);
highp float l;
mat3 m = mat3(-2,-1,2,3,-2,1,1,2,2);
float n = n12(p);
k = k*m*.5;
l = length(.5 - fract(k+n));
k = k*m*.4;
l = min(l, length(.5-fract(k+n)));
k = k*m*.3;
l = min(l, length(.5-fract(k+n)));
return pow(l,7.)*25.;
}
...
float caust = caustics(st, u_time * 0.3);
gl_FragColor = vec4(vec3(caust), 1.); return;
コースティクスは水に乱反射して映し出された光を意味し、光のCG表現で使われたりします。
こちらは処理が複雑すぎて理解できなかったのですが、乱数に上手いこと規則性を持たせているようです。この辺りは数学の深い部分に入らないとわからなそうですね。。
とりあえず、caustics
関数を使うことで表現できます。
リフレクションアニメーション(アルファ 3)の作成
float interval_time = 2.;
float gap_pos = st.x * 2.0 + st.y;
float speed = 8.0;
float line_width = 1.;
float dist_to_line = mod(gap_pos - u_time * speed, interval_time * speed);
float line = (1.0 - step(line_width, dist_to_line));
float decay_line = line - (line_width - dist_to_line) * line;
gl_FragColor = vec4(vec3(decay_line), 1.);
dist_to_line
は原点から光線までの距離ですが、光線が通る時間の間隔が大きいほど座標系を大きくします。mod
は剰余演算(割り算の余り)なので、特定の区間を繰り返すことになります。decay_line
で光線が減衰するようにしています。
カラーの作成
vec3 shift_col(vec3 RGB, vec3 shift) {
vec3 RESULT = vec3(RGB);
highp float VSU = shift.z * shift.y * cos(shift.x * 3.14159265 / 180.0);
highp float VSW = shift.z * shift.y * sin(shift.x * 3.14159265 / 180.0);
RESULT.x = (.299 * shift.z + .701 * VSU + .168 * VSW) * RGB.x
+ (.587 * shift.z - .587 * VSU + .330 * VSW) * RGB.y
+ (.114 * shift.z - .114 * VSU - .497 * VSW) * RGB.z;
RESULT.y = (.299 * shift.z - .299 * VSU - .328 * VSW) * RGB.x
+ (.587 * shift.z + .413 * VSU + .035 * VSW) * RGB.y
+ (.114 * shift.z - .114 * VSU + .292 * VSW) * RGB.z;
RESULT.z = (.299 * shift.z - .3 * VSU + 1.25 * VSW) * RGB.x
+ (.587 * shift.z - .588 * VSU - 1.05 * VSW) * RGB.y
+ (.114 * shift.z + .886 * VSU - .203 * VSW) * RGB.z;
return (RESULT);
}
...
float gap_pos0 = (st.x + st.y);
vec3 shift = vec3(200.0 * (gap_pos0 + u_time), 1.0, 1.0);
vec3 rainbow = shift_col(vec3(0.5, 0.3, 0.2), shift);
shift
は(色相、彩度、明度)をshift_col
関数に指定しますが、そのうちの色相のみ変更しています。色相はST座標と時間で決めており、時間が進むことで色が変化していきます。
テクスチャの作成
highp float random(highp vec2 st){
return fract(sin(dot(st.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
...
float sand_noise = mix(.7, 1., random(st));
gl_FragColor = vec4(vec3(sand_noise), 1.);
サンドノイズのテクスチャはrandom
関数で簡単にを作れるので、金属感を出す時などにおすすめです。最小を白と最大を黒にすると、コントラストが効きすぎるのでmix
で微調整しています。
全コードを表示
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform float u_time;
highp float random(highp vec2 st){
return fract(sin(dot(st.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
vec3 shift_col(vec3 RGB, vec3 shift) {
vec3 RESULT = vec3(RGB);
highp float VSU = shift.z * shift.y * cos(shift.x * 3.14159265 / 180.0);
highp float VSW = shift.z * shift.y * sin(shift.x * 3.14159265 / 180.0);
RESULT.x = (.299 * shift.z + .701 * VSU + .168 * VSW) * RGB.x
+ (.587 * shift.z - .587 * VSU + .330 * VSW) * RGB.y
+ (.114 * shift.z - .114 * VSU - .497 * VSW) * RGB.z;
RESULT.y = (.299 * shift.z - .299 * VSU - .328 * VSW) * RGB.x
+ (.587 * shift.z + .413 * VSU + .035 * VSW) * RGB.y
+ (.114 * shift.z - .114 * VSU + .292 * VSW) * RGB.z;
RESULT.z = (.299 * shift.z - .3 * VSU + 1.25 * VSW) * RGB.x
+ (.587 * shift.z - .588 * VSU - 1.05 * VSW) * RGB.y
+ (.114 * shift.z + .886 * VSU - .203 * VSW) * RGB.z;
return (RESULT);
}
float h12(highp vec2 p) {
return fract(sin(dot(p,vec2(32.52554,45.5634)))*12432.2355);
}
float n12(highp vec2 p)
{
vec2 i = floor(p);
vec2 f = fract(p);
f *= f * (3.-2.*f);
return mix(
mix(h12(i+vec2(0.,0.)),h12(i+vec2(1.,0.)),f.x),
mix(h12(i+vec2(0.,1.)),h12(i+vec2(1.,1.)),f.x),
f.y
);
}
float caustics(highp vec2 p, highp float t)
{
highp vec3 k = vec3(p,t);
highp float l;
mat3 m = mat3(-2,-1,2,3,-2,1,1,2,2);
float n = n12(p);
k = k*m*.5;
l = length(.5 - fract(k+n));
k = k*m*.4;
l = min(l, length(.5-fract(k+n)));
k = k*m*.3;
l = min(l, length(.5-fract(k+n)));
return pow(l,7.)*25.;
}
vec2 convertRotatedTileCoord(vec2 st, float degree, float repeat){
vec2 trans_st = (st - 0.5);
float rot = radians(degree);
mat2 rot_matrix = mat2(cos(rot), -sin(rot), sin(rot), cos(rot));
vec2 rot_st = trans_st * rot_matrix;
vec2 scaled_st = rot_st * repeat;
return scaled_st;
}
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
vec2 scaled_st = convertRotatedTileCoord(st, 45., 6.);
vec2 shift_st = scaled_st + .5;
vec2 tile = fract(shift_st);
vec2 tile_pos = floor(shift_st);
float dist = length(tile_pos);
float progress = sin(u_time - dist);
float easeIn_progress = progress * progress;
float adjust_progress = pow(easeIn_progress, 2.2) * .5;
float animation_rule = min(min(tile.x, 1.0 - tile.x), min(tile.y, 1.0 - tile.y));
float pattern_margin = 0.15;
float value = smoothstep( adjust_progress - 0.05, adjust_progress, 0.95 * (animation_rule - pattern_margin));
value *= mix(.5, 1., random(tile_pos));
float caust = caustics(st, u_time * 0.3);
float interval_time = 2.;
float gap_pos = st.x * 2.0 + st.y;
float speed = 8.0;
float line_width = 1.;
float dist_to_line = mod(gap_pos - u_time * speed, interval_time * speed);
float line = (1.0 - step(line_width, dist_to_line));
float decay_line = line - (line_width - dist_to_line) * line;
float gap_pos0 = (st.x + st.y);
vec3 shift = vec3(200.0 * (gap_pos0 + u_time), 1.0, 1.0);
vec3 rainbow = shift_col(vec3(0.5, 0.3, 0.2), shift);
float sand_noise = mix(.7, 1., random(st));
gl_FragColor = vec4(vec3(value + caust + decay_line) * rainbow * sand_noise, 1.);
}
おわりに
今回はmusicLineの記念動画(ランキング・急上昇・殿堂入り)のデザインを紹介しました。共有動画をもっと美しくしたいと軽い気持ちで手をつけましたが、良い感じにエフェクトを付けることは思ったより大変でした。orz
今回も内部処理をがっつり紹介してしまいましたが、ざっくりイメージが伝われば良いなと思います。奥深いShaderの世界を知って、少しでもプログラミングに興味を持っていただければ嬉しいです。
あと今回は省略しましたが、殿堂入りの記念動画は最後に特殊な処理が施されています。また機会があればOpenGLのFBOという機能について紹介します。
え、そんなマニアックな機能興味ない?
まあそう言わず〜 ( ◜ᴗ◝)و