この記事はU-TOKYO AP Advent Calendar 2018の24日目の記事です.他の皆さんが理論に立脚した良記事を書く中で恐縮ですが,僕は理論も何もない一芸を披露します.
はじめに
皆さん今日が何の日かご存知でしょうか?そう,今日はupd8所属のバーチャルシンガーYuNiの,VARKでのVRライブ「YuNi 1st VR LIVE! 〜VeRy Merry X’mas〜」の日ですね!用意されたチケットはわずか7分で売り切れたそうです.
さて,YuNiがYouTubeに投稿している動画のうち,2018年12月23日正午時点で最も再生回数が多い動画は,「シャルル」のカバー動画です.その再生回数は443万回.聴いたことがない人は今すぐリンクを踏んで聴きましょう.「シャルル」自体聴いたことがない人も多いと思いますが,以下の話はシャルルのオリジナルPVを前提として書くので,この機会にYouTubeのページで聴いてください.この曲はバルーンさん作曲のボカロ曲で,アボガド6さんが映像を作成しています.2016年に公開され,2018年12月23日正午時点では再生回数1,079万回,バルーンさんのセルフカバー版の再生回数はなんと2,047万回にも及んでいます.僕も今夏デスマーチを繰り広げているときに後輩がやたら流すのを聴いてハマりました.
この記事では特にPVの映像表現に着目します.具体的には,1回目のサビの「夜の群れ」部分です.オリジナル動画の1:09付近ですね.YuNiのカバー動画の1:09付近でもバッチリ再現されています.「夜」という文字が背景を暗く埋め尽くす表現は,初めてPVを見たときはかなり驚きました.今回はこの表現をProcessingで実装してみます.
文字の生成
この表現は端的に,「ある文字が上から下にかけてランダムに生成されていく表現」と言えそうです.手始めにこの方針で実装してみます.
文字の位置や大きさ等といった情報を保持するクラスCharacter
を定義して,これを元に文字を生成します.この際,現在のフレーム数frameCount
を参照して,文字を生成する高さを制御します.
class Character{
String text;
PVector position;
color col;
float size;
Character(String _text, PVector _position, color _col, float _size){
text = _text;
position = _position;
col = _col;
size = _size;
}
void draw(){
textAlign(CENTER);
textSize(size);
fill(col);
text(text, position.x, position.y);
}
}
final color BACK = color(248, 242, 225);
boolean isPlaying = false;
void setup() {
size(640, 360, P2D);
background(BACK);
fill(0);
noStroke();
frameRate(240);
PFont font = createFont("HiraMinPro-W3", 160, true);
textFont(font);
}
void draw() {
if(isPlaying){
Character character = new Character("夜", new PVector(random(0, width), random(frameCount, frameCount + 160)), color(43, 65, 75), random(80, 160));
character.draw();
}
}
void mouseReleased(){
if(isPlaying){
background(BACK);
}else{
frameCount = 0;
}
isPlaying = !isPlaying;
}
着目すべきポイントは,Processingのデフォルトのフォントだと「夜」という漢字が表示されないため,文字のフォントを日本語対応のものに変えている点です.
PFont font = createFont("HiraMinPro-W3", 160, true);
textFont(font);
フォントはPFont
型で定義されます.createFont(String name, float size, boolean smooth)
関数は,name
という名前のフォントを,size
の大きさで使えるように,PFont
型のインスタンスを生成します.またsmooth
をtrue
にすると,アンチエイリアシングが働きます.ここではname
をヒラギノ明朝1に,size
を生成される可能性のある最大サイズに指定しています.そしてtextFont()
関数でテキストのフォントを指定します.
このようにすると,マウスクリックを離したときにアニメーションが始まります.実際に動かすと…
と微妙な出来栄えのものになりました.
改善策
今作ったものと原作を見比べると,次の3つのことに気が付きました.
- 今作ったものではところどころ隙間が残ってしまっているが,原作では最終的に隙間が綺麗に埋まっている.
- 原作では同じタイミングで複数個の文字を生成している.
- 原作では文字の生成範囲のうち下の方の密度はあまり高くなく,「夜」という文字が読みやすい.
これらに関して改善策を考えると,画面全体をいくつかの範囲に等分し,1フレームで1つの範囲あたりに描画する文字数を固定する.そして毎フレーム描画対象になる範囲の数を増やしていく.それでも埋まらない隙間は,少しの遅延を入れて上からベタ塗りして埋めるというなんとも夢のない結論に至りました.概念図にすると次の通りです.
この考えを元にしてコードを書き直します.
final color BACK = color(248, 242, 225);
final int NUM = 10; // 1つの領域に一度に描画する文字数
final int COUNT = 8; // 画面分割数
final int DELAY = 4; // ベタ塗りのために遅らせるフレーム数
boolean isPlaying = false;
void setup() {
size(640, 360, P2D);
background(BACK);
fill(0);
noStroke();
frameRate(10);
PFont font = createFont("HiraMinPro-W3", 160, true);
textFont(font);
}
void draw() {
if(isPlaying){
for(int i = 0; i < frameCount; i++){
for(int j = 0; j < NUM; j++){
Character character = new Character("夜", new PVector(random(0, width), random(i * (height / COUNT), i * (height / COUNT) + 160)), color(43, 65, 75), random(80, 160));
character.draw();
}
}
rect(0, (frameCount - DELAY) * (height / COUNT), width, height / COUNT);
}
}
void mouseReleased(){
if(isPlaying){
background(BACK);
}else{
frameCount = 0;
}
isPlaying = !isPlaying;
}
完成物
Processingでシャルルの「夜の群れ」を作ってみた pic.twitter.com/TY40NCZH6O
— らでぃっしゅ (@radi_bow) 2018年12月23日
GIFの方はフレームレートが落ちてしまっているのですが,動画の通り実際は原作とほぼ同じ速さで背景が塗りつぶされます.原作と完全に一致とは言えないと思いますが,それなりの完成度にはなっているのではないでしょうか.
さて,僕の卒業した計数工学科数理情報工学コースの授業でProcessingを習うことはないのですが,普段オブジェクト指向プログラミング,特にUnityでC#を使っている人であれば紹介したコードの雰囲気がすぐに掴めたと思います.こういう可視化ツールとしてのプログラミングを面白いと思った人は,是非Creative Codingの沼に足を踏み入れてみてください.
それではVeRy Merry X’mas!!
-
MacではなくWindowsを使っていてヒラギノ明朝がPCに入っていない場合は,MSP明朝等を指定するとそれっぽく見えて良いと思います. ↩