この記事はProcessing Advent Calendar 2022 4日目の記事です。
Contents
概要
2020年12月20日に、Tidal club winter solstice marathonというオンラインライブに出演しました。その時にProcessingでリアルタイムに映像を描写させて自動VJを行いました。本記事では自動VJシステムのソースコードを公開しつつ、コードを作るときに考えていたことを記述します。本番の動画はこちらです。
やったこと
- TidalCyclesでライブコーディングを行い演奏する(画面右側)
- Processingを実行してVJ映像を描写(画面左側)
- ProcessingとTidalCyclesの画面をOBSでYoutube配信
Processingで自動VJシステムを作る時に考えたこと
- 一定時間経過したら映像が自動で切り替わる
- 飽きさせないように複数の映像を用意する
- リアルタイムに処理ができるくらい軽い処理にする
- オーディオリアクティブを多用する
- 大きな動きを作る
一定時間経過したら映像が自動で切り替わる
TidalCyclesでライブコーディングを行うのは集中力が必要で、VJ映像の切り替えをする余裕がなさそうでした。そのため、VJ映像の切り替わりも全て自動にしました。単純に一定時間経過したら描写する関数を変える、というだけです。元々はframe数と曲のbpmから、何小節ごとに切り替わるような仕組みを考えていたのですが、タイミングのずれが徐々に蓄積していくことがわかったので、曲との同期は諦めました。その痕跡がソースコードに残っています。
飽きさせないように複数の映像を用意する
今回、一つの関数(function)で映像を一つ作り、時間経過したら次の映像に切り替える、というコードを書きました。つまり複数の関数(例えば円を書く関数、四角を書く関数、線を書く関数など)を作って組み合わせています。この時は10種類の関数を用意しました。
オーディオリアクティブ映像をずっと映し続けるのもいいのですが、一定以上同じ映像だとどうしても飽きてしまうので、飽きない程度の時間が経過したら映像を切り替えて、飽きさせないようにしました。
リアルタイムに処理ができるくらい軽い処理にする
ライブコーディング・VJ映像・OBSでのライブ配信を全て1台のPCで行うため、処理が重いと事故になります。それを防ぐために、なるべく処理が重くならないように気をつけました。といっても、for文を3重にしない、for文の繰り返し回数を千とか万にしない、画像ファイルのリアルタイム処理は行わない、という程度です。
オーディオリアクティブを多用する
VJ映像の醍醐味として、オーディオリアクティブがあります。オーディオに反応させて映像が動く、というのは見ていて気持ちがいいので、多用しました。minimライブラリを使用しています。
- 音量が一定以上なら線を引く(if文)
- 音量が一定以上なら背景を白くする(if文)
- 波形を表示する
- 音量に合わせてサイズを変える
改めてコードを見てみると、音量のRMSを計算したりとかも特にせず、音量情報をそのまま使用していますね。
ちなみに、Processingにオーディオ入力するときに課題になるのは、オーディオ入力ソースの指定の仕方です。やり方詳細を忘れてしまったのでこの記事では取り上げませんが、PCでオーディオを再生しながら、それをProcessingに取り込んでオーディオリアクティブにする、というのは一手間かかる場合があるのでご注意ください。(むしろ誰か記事にしてほしい)
大きな動きを作る
VJ映像は、クラブなどを想定した場合は「空間」「奥行き」を持たせると没入感が増して、かつ現実味が減って良い、というのを何かで読んだので、以下方針としました。
- 背景は黒に統一
- Processingの3Dモードで空間を演出
- 3Dモードのカメラをsinで滑らかに動かす
この「3Dモードでカメラをsinで滑らかに動かす」のがポイントで、ほぼ全てのコードで行なっています。カメラが動くだけで空間に奥行きが出ますし、単純にかっこいいです。また、ただ前に進むだけのカメラワークではなく、x/y/z方向にsinで動かすことで、複雑で意図しない動きにできました。3Dのリサージュ曲線を描くようなカメラワークになっています。一つの映像であっても視点が常に動き続けるので次が予想しにくく飽きない映像になりました。
//カメラの座標、カメラが注視する座標、カメラの上方向を設定
camera(sin(rot/4)*height,height/2, height/2, width / 4.0, height / 2.0, 0, sin(rot/2), sin(rot/2), sin(rot/2));
rot += 0.1;
ProcessingでVJシステムを作った経緯
そもそもなぜProcessingでVJ映像を作ろうとしたか、簡単に経緯を記載します。
TidalCyclesに出会ってクリエイティブコーディングというジャンルを知ったあと、コーディングで映像も作れることを知りました。その中で気軽に始められそうなのがProcessingでした。田所先生の本を購入して試したのが以下です。
比較的簡単にtwitter映えする映像が作れたので、TidalCyclesで曲を作ってアップするときに、Processingでもオーディオリアクティブな小作品を一緒に作って公開しました。
その中で、VJ的な画面切り替えを実装してみたら意外といい感じだったので、これをベースにして今回のシステムを作りました。
上記ツイートの映像のソースコードを下に掲載しました。これを見ていただければわかると思いますが、ベースとなるシステム(一定時間経過したら次の映像(関数)に遷移させる)を一旦つくっておいて、その後は一つの関数で一つの映像を作るようにしておけば、デイリーコーディングでの小作品を作るごとに自分のVJシステムをどんどん成長させていくことができます。
オンラインライブ本番に向けて、それまで作ってきた小作品を組み合わせて一つのVJシステムに組み上げることができた時は、積み重ねたものが形になったようで嬉しかったです。
以下のソースコードに別の映像の関数を追加したい場合は、以下の作業を行なってください。
- void drawの最初のif文のelse ifにtransition_num==3とかの条件分岐を追加し、追加したい関数を記載
- 関数を追加(void xxxxfunc())
- transition関数の「トランジションする数の最大値を入力」の数字を増やす
//音楽のテンポに合わせてProcessingの表示を次々に切り替える(トランジションさせる)
int transition_num=0 ; //トランジション関数関連の初期値代入
int measure_count =0; //トランジション関数関連の初期値代入
//setup関数
void setup(){
size(854,480); //画面サイズを指定。1280x720,854x480,640x360などが代表的な16:9画面
surface.setLocation(0,0); //実行画面をスクリーン上の左上に位置付ける
frameRate(30);//フレームレート設定
smooth();
background(255);
stroke(0);
strokeWeight(1);
noFill();
}
//draw関数
void draw(){
if (transition_num == 0 )
{
circlefunc(); //円を書く関数呼び出し
}
else if (transition_num == 1)
{
squarefunc(); //四角を書く関数呼び出し
}
else if (transition_num == 2)
{
linefunc(); //線を書く関数呼び出し
}
else
{
textfunc();//文字を書く関数呼び出し
}
transitionfunc(); //トランジション関数を呼び出し
}
//draw関数ここまで
//円を書く関数
void circlefunc(){
background(255);
ellipse(width/2,height/2,random(width),random(width));
ellipse(width/2,height/2,random(width),random(width));
}
//四角を書く関数
void squarefunc(){
pushMatrix(); //座標を保存
translate(width/2,height/2); //原点を中心に移動
rotate(frameCount);
rect(width/10,height/10,100,100);
popMatrix();//座標をもとに戻す
}
//線を書く関数
void linefunc(){
line(width/2,height/2,random(width),random(height));
}
//文字を書く関数
void textfunc(){
background(255);
textSize(random(width/2)); //文字サイズ指定
String str_text_left = str(second());
String str_text_right = str(frameCount);
text(str_text_left,width/5,height*4/10,1000,1000);
text(str_text_right,width*3/5,height*4/10,1000,1000);
}
//トランジション関数 テンポに合わせて小節の区切りよいところでトランジションする。frameCountを使用。
void transitionfunc (){
int bpm = 140; //bpmを入力
int measure = 0; //小節数を初期化
float measure_num =4; //トランジションする小節数を入力。例:4 例2:0.5 小数点もOK
int transition_num_max = 4; //トランジションする数の最大値を入力(繰り返すため)
int beat =4; //拍子を入力
int setframe = 30; //frameRateで設定した値を入力。Default30
float transition_frame =0; //トランジションするframeを初期化
measure = 60*setframe*beat/bpm; //1小節のframe数を計算
transition_frame = 60*setframe*beat*measure_num/bpm; //トランジションするframe数を計算
float transition_num_buffer = transition_num; //トランジションするときにbackground(255)で白紙に戻すためのtransition_num_bufferを定義
if(frameCount%int(transition_frame) == 0 ){ //frameCountとtransition_frameの余りが0だったらtransition_numを1増やす
transition_num++;
}
if(frameCount%measure == 0){ //frameCountとmeasureの余りが0だったらmeasure_countを1増やす
measure_count++;
}
if(transition_num >= transition_num_max){ //transition_numが最大値を超えたら0にクリア
transition_num = 0;
}
if(transition_num_buffer != transition_num){ //transition_numが変化したら背景をbackground(255)でクリアする
background(255);
transition_num_buffer = transition_num;
}
//トランジションの数字と小節数を表示する
fill(255);
rect (0,0,100,100);
fill(0);
String str_transition_num = str(transition_num);
String str_measure_count = str(measure_count);
textSize(15); //文字サイズ指定
text("transition",10,10,100,100);
text(str_transition_num,10,30,100,100);
text("measure",10,50,100,100);
text(str_measure_count,10,70,100,100);
noFill();
}
ライブで使ったソースコード
オンラインライブで使ったソースコードはこちらです。書き方は整理できていませんがご容赦ください。M1 Macでエラーを出しつつも動きました。音を入力しないと意図通りの映像にはならないですが、映像が切り替わる様子は見れると思います。音を入力する場合はminimライブラリ必須です。トランジション関数はやや複雑なので、シンプルにするなら消してもいいかもしれないです。
コード流用は自由に使っていただいてOKですが、一つ一つの関数についてはネット上の誰かしらのコードを流用している可能性もあるので、自己責任でお願いします。
//音楽のテンポに合わせてProcessingの表示を次々に切り替える(トランジションさせる)
int transition_num=0 ; //トランジション関数関連の初期値代入
int measure_count =0; //トランジション関数関連の初期値代入
import ddf.minim.*; //minimライブラリのインポート
Minim minim; //Minim型変数であるminimの宣言
AudioInput in; //マイク入力用の変数
float rot =0;
int n =0;
//setup関数
void setup(){
surface.setLocation(0,0); //実行画面をスクリーン上の左上に位置付ける
frameRate(30);//フレームレート設定
size(640, 720,P3D);//画面サイズを指定。1280x720,854x480,640x360などが代表的な16:9画面
minim = new Minim(this); //初期化
rectMode(CENTER);
smooth();
noFill();
//バッファ(メモリ上のスペース。この場合は512要素のfloat型の配列)を確保し、マイク入力用の変数inを設定する。
in = minim.getLineIn(Minim.STEREO, 128);
}
//draw関数
void draw(){
if (transition_num <= 0 )
{
circlefunc(); //円を書く関数呼び出し
}
else if ( transition_num > 0 && transition_num <= 1)
{
squarefunc(); //四角を書く関数呼び出し
}
else if ( transition_num > 1 && transition_num <= 2)
{
linefunc(); //線を書く関数呼び出し
}
else if ( transition_num > 2 && transition_num <= 3)
{
func1(); //func1関数呼び出し
}
else if ( transition_num > 3 && transition_num <= 4)
{
func2(); //func2関数呼び出し
}
else if ( transition_num > 4 && transition_num <= 5)
{
func3(); //func3関数呼び出し
}
else if ( transition_num > 5 && transition_num <= 6)
{
func4(); //func4関数呼び出し
}
else if ( transition_num > 6 && transition_num <= 7)
{
func5(); //func5関数呼び出し
}
else if ( transition_num > 7 && transition_num <= 8)
{
func6(); //func6関数呼び出し
}
else
{
textfunc();//3Dを書く関数呼び出し
}
transitionfunc(); //トランジション関数を呼び出し
}
//draw関数ここまで
//円を書く関数
void circlefunc(){
background(0);
stroke(255);
strokeWeight(0.5);
float x = in.left.get(0)*height;
float y = in.left.get(100)*height;
if (in.left.get(0) > 0.3){
stroke(in.left.get(0)*1000,100,100);
}
//カメラの座標、カメラが注視する座標、カメラの上方向を設定
camera(sin(rot/4)*height,height/2, height/2, width / 4.0, height / 2.0, 0, sin(rot/2), sin(rot/2), sin(rot/2));
for(int i=0; i <30;i++){
pushMatrix(); //座標を保存
ellipse(width/2,height/2,x*5,x*5);
rotate(rot/4);
ellipse(width/2+x*i,height/2+x*i,x*5,x*5);
ellipse(width/2+x*i,height/2+x,y*3,y*3);
rotate(rot);
translate(0,0,-rot*10);
popMatrix();//座標をもとに戻す
}
rot += 0.1;
}
//四角を書く関数
void squarefunc(){
if (in.left.get(0) > 0.3){
background(0);
}
stroke(150);
strokeWeight(0.5);
if (in.left.get(0) > 0.3){
stroke(in.left.get(0)*1000,100,100);
strokeWeight(0.5);
}
camera(sin(rot)*height/2,height/2, height/2, width / 4.0, height / 2.0, 0, sin(rot/2), sin(rot/2), sin(rot/3));
pushMatrix(); //座標を保存
translate(width/2,height/2); //原点を中心に移動
rotate(frameCount/(second()+1));
for (int i =0;i<100;i++){
rect(width/10+i*5,height/10+i*5,100,100);
rect(i*10,i*10,200,200);
}
popMatrix();//座標をもとに戻す
rot += 0.1;
}
//線を書く関数
void linefunc(){
float a = in.left.get(0);
if (in.left.get(0) > 0.3){
background(100);
}
else {
background(0);
}
stroke(150);
strokeWeight(0.5);
if (in.left.get(0) > 0.3){
stroke(in.left.get(0)*1000,100,100);
}
for(int i = 0;i <100;i++){
float x = random(height);
float y = random(height);
float z = random(height);
line(width/2,height/2,height/2,x,y,z);
camera(sin(rot)*height/2,height/2, height/2, width / 4.0, height / 2.0, 0, sin(rot/2), sin(rot/2), sin(rot/3));
}
rot += 0.1;
}
//func1の関数
void func1(){
camera(sin(rot)*height/2,height/2, height/2, width / 4.0, height / 2.0, 0, sin(rot/2), sin(rot/2), sin(rot/3));
//波形を描く
background(0);
for(int j = 0; j< 100;j++){
stroke(255);
strokeWeight(0.5);
for(int i = 0; i < in.bufferSize()-1; i++){
if (in.left.get(0) > 0.3){
stroke(in.left.get(0)*1000,100,100);
}
float x1 = map(i,0,in.bufferSize(),0,width*2);
float x2 = map(i+1, 0,in.bufferSize(),0,width*2);
float y1 = 0;
float y2 = height;
line( x1,y1+in.left.get(i)*300,-height+j*50,x2,y1+in.left.get(i+1)*300,-height+j*50);
line( x1,y2+in.right.get(i)*300,-height+j*50,x2,y2+in.right.get(i+1)*300,-height+j*50);
}
}
rot += 0.05;
}
//func2関数
void func2(){
background(0);
stroke(255);
camera(sin(rot/2)*height/2,cos(rot/6)*height/2, sin(rot/3)*height/2, sin(rot/2)*width / 4.0, height / 2.0, 0, sin(rot/3), cos(rot/3), cos(rot/3)); //カメラの座標、カメラが注視する座標、カメラの上方向を設定
if (in.left.get(0) > 0.3){
background(100);
}
for(int i = 0;i <100;i++){
for (int j = 0; j <30; j++){
stroke(i*20);
line(-1000,i*80,j*80,10000,i*80,j*80);
}
}
float rot1 = map(in.left.get(0),0,1,0,0.3);
rot += rot1+0.2;
}
//func3 ベジエ曲線
void func3(){
background(0);
stroke(255);
strokeWeight(0.5);
float x = in.left.get(0)*height;
float y = in.left.get(100)*height;
if (in.left.get(0) > 0.3){
stroke(in.left.get(0)*1000,100,100);
}
//カメラの座標、カメラが注視する座標、カメラの上方向を設定
camera(sin(rot/4)*height,height/2, height/2, width / 4.0, height / 2.0, 0, sin(rot/2), sin(rot/2), sin(rot/2));
for(int i=0; i <40;i++){
bezier(0,0,sin(rot)*height*i,sin(rot/2)*height,10*i,50*i,width,height);
}
for(int i=0; i <20;i++){
bezier(width/2,height/2,in.left.get(0)*300*i,in.left.get(0)*200*i,in.left.get(0)*500*i,in.left.get(0)*700*i,width*sin(rot)*10,height*sin(rot/2)*10);
}
rot += 0.1;
}
//func4
void func4(){
background(0);
translate(width / 2, height / 2, 0);
stroke(255);
strokeWeight(1);
//カメラの座標、カメラが注視する座標、カメラの上方向を設定
camera(sin(rot/4)*height,height/2, height/2, width / 4.0, height / 2.0, 0, sin(rot/2), sin(rot/2), sin(rot/2));
//四角形描画を中心を原点に
rectMode(CENTER);
//敷きつめるタイルの一片の長さ
int dim = 18;
//XY平面を正方形でタイリング
for (int i = -height / 1; i < height / 1; i += dim) {
for (int j = -width / 1; j < width / 1; j += dim) {
pushMatrix();
translate(i, j);
rotateX(radians(30)+rot);
rotateY(radians(30)+rot);
rect(0, 0, dim, dim);
popMatrix();
}
}
rot += 0.1;
}
//func5 ;球と背景色変化
void func5(){
background(0);
stroke(255);
strokeWeight(0.5);
//カメラの座標、カメラが注視する座標、カメラの上方向を設定
camera(sin(rot/4)*height,height/2, height/2, width / 4.0, height / 2.0, 0, sin(rot/2), sin(rot/2), sin(rot/2));
if (in.left.get(0) > 0.2){
stroke(in.left.get(0)*50,200,10*second()+50);
n = n +20;
background(n,n,40);
}
sphereDetail(second()*5);
sphere(in.left.get(0)*1500);
if (n >150){
n =0;
}
textSize(10); //文字サイズ指定
for(int i=0;i<30;i++){
for(int j=0;j<20;j++){
String a = str(in.left.get(i));
String b = str(in.right.get(i));
pushMatrix();
translate(i*30, j*30);
text(a,10,30,100,100);
text(b,10,70,100,100);
popMatrix();
}
}
rot+=0.1;
}
//func6 たくさんの音に反応する線
void func6(){
background(0);
stroke(255);
strokeWeight(0.5);
//カメラの座標、カメラが注視する座標、カメラの上方向を設定
camera(sin(rot/4)*height,height/2, height/2, width / 4.0, height / 2.0, 0, sin(rot/2), sin(rot/2), sin(rot/2));
for(int j = 0; j< 150;j++){
stroke(second()*3,200,200);
strokeWeight(1);
for(int i = 0; i < in.bufferSize()-1; i++){
if (in.left.get(0) > 0.2){
// stroke(in.left.get(0)*10+second()*10,0,0);
stroke(1000*in.left.get(0),80,100);
strokeWeight(3);
}
float x1 = map(i,0,in.bufferSize(),0,width*4);
float x2 = map(i+1, 0,in.bufferSize(),0,width*4);
float y1 = height / 1;
float y2 = height / 3;
line( x1,y1+in.left.get(i)*100,j*10,x2,y1+in.left.get(i+1)*100,j*10);
//line( x1,y2+in.right.get(i)*100,j*10,x2,y2+in.right.get(i+1)*100,j*10);
line( y2+in.right.get(i)*3000,x2,j*10,y2+in.right.get(i+1)*3000,x2,j*10);
}
}
rot+=0.1;
}
//3Dを書く関数
void textfunc(){
background(0);
stroke(255);
strokeWeight(0.5);
camera(sin(rot)*height/2,height/2, height/2, width / 4.0, height / 2.0, 0, sin(rot/2), sin(rot/2), sin(rot/3));
//円と四角を描く
for(int i=0;i<10;i++){
stroke(255);
strokeWeight(0.5);
if (in.left.get(0) > 0.3){
stroke(in.left.get(0)*1000,100,100);
}
noFill();
rotate(0.01);
rect(0,0,200+i*5,200+i*5);
}
if (in.left.get(0) > 0.3){
stroke(in.left.get(0)*1000,50,50);
}
sphereDetail(second()/10*5);
sphere(in.left.get(0)*1500);
//角度を更新
rot += 0.05;
//波形を描く
for(int j = 0; j< 150;j++){
stroke(second()*1.2,60,50);
strokeWeight(0.5);
for(int i = 0; i < in.bufferSize()-1; i++){
if (in.left.get(0) > 0.1){
// stroke(in.left.get(0)*10+second()*10,0,0);
stroke(255);
}
float x1 = map(i,0,in.bufferSize(),0,width*4);
float x2 = map(i+1, 0,in.bufferSize(),0,width*4);
float y1 = height / 10;
float y2 = height / 3 * 2;
line( x1,y1+in.left.get(i)*100,j*10,x2,y1+in.left.get(i+1)*100,j*10);
line( x1,y2+in.right.get(i)*100,j*10,x2,y2+in.right.get(i+1)*100,j*10);
}
}
}
//トランジション関数 テンポに合わせて小節の区切りよいところでトランジションする。frameCountを使用。
void transitionfunc (){
int bpm = 70; //bpmを入力
int measure = 0; //小節数を初期化
float measure_num =4; //トランジションする小節数を入力。例:4 例2:0.5 小数点もOK
int transition_num_max = 10; //トランジションする数の最大値を入力(繰り返すため)
int beat =4; //拍子を入力
int setframe = 30; //frameRateで設定した値を入力。Default30
float transition_frame =0; //トランジションするframeを初期化
measure = 60*setframe*beat/bpm; //1小節のframe数を計算
transition_frame = 60*setframe*beat*measure_num/bpm; //トランジションするframe数を計算
float transition_num_buffer = transition_num; //トランジションするときにbackground(255)で白紙に戻すためのtransition_num_bufferを定義
if(frameCount%int(transition_frame) == 0 ){ //frameCountとtransition_frameの余りが0だったらtransition_numを1増やす
transition_num++;
}
if(frameCount%measure == 0){ //frameCountとmeasureの余りが0だったらmeasure_countを1増やす
measure_count++;
}
if(transition_num >= transition_num_max){ //transition_numが最大値を超えたら0にクリア
transition_num = 0;
}
if(transition_num_buffer != transition_num){ //transition_numが変化したら背景をbackground(255)でクリアする
background(10);
transition_num_buffer = transition_num;
}
/*
//トランジションの数字と小節数を表示する
fill(255);
rect (0,0,100,100);
fill(0);
String str_transition_num = str(transition_num);
String str_measure_count = str(measure_count);
textSize(15); //文字サイズ指定
text("transition",10,10,100,100);
text(str_transition_num,10,30,100,100);
text("measure",10,50,100,100);
text(str_measure_count,10,70,100,100);
noFill();
*/
}
おわりに
p5.jsやProcessingで作品を作っている方に向けた提案です。最終的な作品のアウトプットの選択肢の一つとして、VJをやってみるというのはいかがでしょうか。自分の作品が音楽と共に大画面で表示されて、例えばそれをキーボードで自由に切り替えられるとか、とても楽しいと思います。
ProcessingでVJをしている例として、以下ツイートがあります。これはキーボードのボタンで映像を切り替えてVJをしているようです。
個人的な悩みとして、「コーディングでVJを本格的にやるならopenFrameworksだと処理が最適化されて重めの描写もできて理想的。でも気軽さ的にはProcessingもいいし、p5.jsはユーザー多くてライブラリ豊富。結局どれがいいのか?」という「結局どれがいいのか問題」があります。簡単に全部触ってみた経験からすると、「気軽に始めるならProcessingかp5.js、本格的にやりたくなってからopenFrameworks」というのが良いかと思いました。ただp5.jsのコミュニティが盛り上がっているのを見て、p5.jsでVJシステム作るのも楽しそうだと思うこともよくあり、悩ましいですね。
Processingに限らず、クリエイティブコーディングでのVJは面白いので、一度挑戦してみてはいかがでしょうか。
最後までお読みいただきありがとうございました。