musicLineアプリ開発日記

作曲を誰でも楽しく簡単に♪

記念動画デザインの観察(ランキング・急上昇・殿堂入り編)

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

コースティクス
アニメーション

アルファ 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; // 45度回転
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); // 小数点のみ(0 ~ 1の間を繰り返す)
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); // -1 ~ 1 を周回
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

スケールアニメーション(細部)

// ルール画像(タイル端から中心までの距離 端 0 中心 0.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 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);
}

// shiftにより、色をずらす。
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() {
    // ST座標
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    // gl_FragColor = vec4(st, 0., 1.); return;
    
    
    // 回転ST座標
    vec2 scaled_st = convertRotatedTileCoord(st, 45., 6.);
    vec2 shift_st = scaled_st + .5;
    // gl_FragColor = vec4(shift_st, 0., 1.); return;
    
    
    // タイル座標
    vec2 tile = fract(shift_st); // 小数点のみ(0 ~ 1の間を繰り返す)
    // gl_FragColor = vec4(tile, 0., 1.); return;
      

    // タイルアニメーション(全体)
    vec2 tile_pos = floor(shift_st);  // 整数へ(小数点切り捨て)
    float dist = length(tile_pos);
    // gl_FragColor = vec4(vec3(dist * .1), 1.); return; // 中央からタイルまでの距離
      
    // アニメーション進捗の調整
    float progress = sin(u_time - dist); // -1 ~ 1 を周回
    float easeIn_progress = progress * progress; // イージング(徐々に加速)
    float adjust_progress = pow(easeIn_progress, 2.2) * .5; // 出現時間のバランスを調整
    // gl_FragColor = vec4(vec3(adjust_progress), 1.); return; 

    
    // スケールアニメーション
    // ルール画像(タイル端から中心までの距離 端 0 中心 0.5)
    float animation_rule = min(min(tile.x, 1.0 - tile.x), min(tile.y, 1.0 - tile.y));
    // float preview_progress = pow(sin(u_time), 2.) * .5;
    // float preview_value = smoothstep( preview_progress - 0.05, preview_progress, 0.95 * (animation_rule - .15));
    // gl_FragColor = vec4(vec3( preview_value), 1.); return; // スケールアニメーションのみ
    
    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));          
    // gl_FragColor = vec4(vec3(value), 1.); return;
      
    
    // コースティクスアニメーション
    float caust = caustics(st, u_time * 0.3);
    // gl_FragColor = vec4(vec3(caust), 1.); return;
    
    
    // リフレクションアニメーション
    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.); return;
    
    
    // カラーの作成(レインボーパターン)
    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);
    // gl_FragColor = vec4(rainbow, 1.); return;
    
    
    // テクスチャの作成(金属のサンドノイズ)
    float sand_noise = mix(.7, 1., random(st));
    // gl_FragColor = vec4(vec3(sand_noise), 1.); return;
    
    
     gl_FragColor = vec4(vec3(value + caust + decay_line) * rainbow * sand_noise, 1.); 
}



おわりに

今回はmusicLineの記念動画(ランキング・急上昇・殿堂入り)のデザインを紹介しました。共有動画をもっと美しくしたいと軽い気持ちで手をつけましたが、良い感じにエフェクトを付けることは思ったより大変でした。orz

今回も内部処理をがっつり紹介してしまいましたが、ざっくりイメージが伝われば良いなと思います。奥深いShaderの世界を知って、少しでもプログラミングに興味を持っていただければ嬉しいです。

あと今回は省略しましたが、殿堂入りの記念動画は最後に特殊な処理が施されています。また機会があればOpenGLのFBOという機能について紹介します。


え、そんなマニアックな機能興味ない?
まあそう言わず〜 ( ◜ᴗ◝)و