TetsuakiBaba

workshop/perfume_and_bvh/main


Processing & Perfume

このページでは,Perfume Global Siteから提供されているperfumeのモーションデータを読み込み, Processingで表示させる手順を紹介します.手順は次のとおりになります.

  1. Perfume Global Siteから Processingサンプル,モーションデータ(BVH),音楽をダウンロード
  2. Processingサンプルをダウンロードしたモーションデータと音楽で実行するように修正する

Perfume Global Siteから Processingサンプル,モーションデータ(BVH),音楽をダウンロード

ではまず,Perfume Global SiteからProcessingサンプル,モーションデータ,音楽をダウンロードしましょう

次にp5f_sampleフォルダの以下フォルダに次のファイルを追加します.
  • aachan.bvh, kashiyuka.bvh, nocchi.bvhをdataフォルダに追加
  • Perfume_globalsite_sound.wavをdataフォルダに追加

Processingサンプルをダウンロードしたモーションデータと音楽で実行するように修正する

では最後にp5f_sample.pdeをProcessingで開き,最初から setup()関数の終わりまで,次のコードに置き換えます.

import ddf.minim.*;

PBvh bvh1, bvh2, bvh3;
	  
Minim minim;
AudioPlayer player;

public void setup()
{
  size( 1280, 720, P3D );
  background( 0 );
  noStroke();
  frameRate( 60 );

  bvh1 = new PBvh( loadStrings( "aachan.bvh" ) );
  bvh2 = new PBvh( loadStrings( "kashiyuka.bvh" ) );
  bvh3 = new PBvh( loadStrings( "nocchi.bvh" ) );

 minim = new Minim(this);
  player = minim.loadFile("Perfume_globalsite_sound.wav", 2048);
  player.play(); //再生
  loop();
}
ここまでできたら実行してみましょう.

モーションの頂点を取得する

ここで,各ファイルにおけるモーションデータは白丸で画面上に表示されています. そこで,この頂点位置を様々な形て表示し,Perfumeの動きを表現してみましょう. 実際にモーションを描画している部分はPBvhファイルのdraw()を参照してください.元々のサンプルでは 各頂点を拡張for分を利用して配列から取得しています,まずはこのfor分を利用して,一旦各配列に値を代入し直してみます. ただし,頭,手,足,のシンプルな点のみに限定します.

PBvh.pdeのpublic void draw()部分
public void draw()
  {
    float[] pos_x = new float[5];
    float[] pos_y = new float[5];
    float[] pos_z = new float[5];
    int count = 0;
    fill(color(255));

    for ( BvhBone b : parser.getBones())
    {
      if (!b.hasChildren()) {
        pos_x[count] = b.absEndPos.x;
        pos_y[count] = b.absEndPos.y;
        pos_z[count] = b.absEndPos.z;
        count=count+1;
      }
    }

    for ( int i = 0; i < count; i++ ) {
      pushMatrix();
      translate(pos_x[i], pos_y[i], pos_z[i]);
      ellipse(0, 0, 10, 10);
      popMatrix();
    }
  }
 

上記コードにて,各頂点がpos_x,pos_y,pos_zの配列に代入されました.この座標データを利用して,グラフィック描画を変化させていきます.

モーションデータを利用しながら,人の動きに見えないようなグラフィック表現に変更してください.

https://processing.org/reference/を参照して,Processingで利用可能な様々なグラフィック表示を適用してみましょう.例えば sphere()関数を使って各頂点を3次元オブジェクトで大きく描画してみたり,ベジェ曲線や直線等で各頂点を描画するなどするこで,同じデータが様々な表現が可能であることを確認しましょう.

各5頂点を線でつなげて表示してみました.PBvh.pdeのpublic void draw()部分を変更してください.

 public void draw()
  {
    float[] pos_x = new float[23];
    float[] pos_y = new float[23];
    float[] pos_z = new float[23];
    int count = 0;
    fill(color(255));

    for ( BvhBone b : parser.getBones())
    {
      if (!b.hasChildren()) {
        pos_x[count] = b.absEndPos.x;
        pos_y[count] = b.absEndPos.y;
        pos_z[count] = b.absEndPos.z;
        count=count+1;
      }
    }
    noFill();
    beginShape();
    for ( int i = 0; i < count; i++ ) {
      stroke(255, 255, 255, 2555);
      strokeWeight(10);
      vertex(pos_x[i], pos_y[i], pos_z[i]);
    }
    endShape(CLOSE);
  }
  

上記のサンプルから更に,各クラスに色付けをし,背景更新をとめ,頂点座標の線描画にアルファ値を持たせることで,下記のようなグラフィックを描くことができました.

p5f_sample
  import ddf.minim.*;
PBvh bvh1, bvh2, bvh3;

Minim minim;
AudioPlayer player;

public void setup()
{
  size( 1280, 720, P3D );
  smooth();
  background( 255 );
  noStroke();
  frameRate( 60 );

  bvh1 = new PBvh( loadStrings( "aachan.bvh" ), color(255, 0, 0, 10));
  bvh2 = new PBvh( loadStrings( "kashiyuka.bvh" ), color(0, 255, 0, 10) );
  bvh3 = new PBvh( loadStrings( "nocchi.bvh" ), color(0, 0, 255, 10));

  minim = new Minim(this);
  player = minim.loadFile("Perfume_globalsite_sound.wav", 2048);
  player.play(); //再生
  loop();
}

public void draw()
{
  //camera
  float _cos = cos(millis() / 5000.f);
  float _sin = sin(millis() / 5000.f);
  camera(width/4.f + width/4.f * _cos +200, height/2.0f-100, 550 + 150 * _sin, width/2.0f, height/2.0f, -400, 0, 1, 0);

  pushMatrix();
  translate( width/2, height/2-10, 0);
  scale(-1, -1, -1);

  //model
  bvh1.update( millis() );
  bvh2.update( millis() );
  bvh3.update( millis() );
  bvh1.draw();
  bvh2.draw();
  bvh3.draw();
  popMatrix();

}

  
PBvh
  public class PBvh
{
  public BvhParser parser;  
  color c;
  public PBvh(String[] data, color _c)
  {
    parser = new BvhParser();
    parser.init();
    parser.parse( data );
    c = _c;
  }
  
  public void update( int ms )
  {
    parser.moveMsTo( ms );//30-sec loop 
    parser.update();
  }
  
 public void draw()
  {
    float[] pos_x = new float[23];
    float[] pos_y = new float[23];
    float[] pos_z = new float[23];
    int count = 0;
    fill(color(255));

    for ( BvhBone b : parser.getBones())
    {
      if (!b.hasChildren()) {
        pos_x[count] = b.absEndPos.x;
        pos_y[count] = b.absEndPos.y;
        pos_z[count] = b.absEndPos.z;
        count=count+1;
      }
    }
    noFill();
    beginShape();
    for ( int i = 0; i < count; i++ ) {
      stroke(c);
      strokeWeight(1);
      vertex(pos_x[i], pos_y[i], pos_z[i]);
     }
    endShape(CLOSE);
  }
}
  
プログラムを実行した際のスクリーンショット

応用例:楽曲のビートに合わせる

FFTを利用して,低周波成分の平均レベルを参照し,しきい値を決めて,そのタイミングで 背景色をランダム更新しています.ビートと動機して背景色が変わっていきます.

p5f_sample
BvhParser parserA = new BvhParser();
PBvh bvh1, bvh2, bvh3;
import ddf.minim.analysis.*;
import ddf.minim.*;

Minim minim;
FFT fftLin;
AudioPlayer player;

public void setup()
{
  size( 1280, 720, P3D );
  background( 255 );
  noStroke();
  stroke(0);
  fill(0);
  frameRate( 30 );

  bvh1 = new PBvh( loadStrings( "aachan.bvh"), 1 );
  bvh2 = new PBvh( loadStrings( "kashiyuka.bvh"),2 );
  bvh3 = new PBvh( loadStrings( "nocchi.bvh"),3 );


  minim = new Minim(this);
  player = minim.loadFile("Perfume_globalsite_sound.wav");
  player.play();

  fftLin = new FFT( player.bufferSize(), player.sampleRate() );
  fftLin.linAverages( 30 );

  loop();
}

public void draw()
{
  //camera
  float _cos = cos(millis() / 5000.f);
  float _sin = sin(millis() / 5000.f);
  camera(width/4.f + width/4.f * _cos +200, height/2.0f-100, 550 + 150 * _sin, width/2.0f, height/2.0f, -400, 0, 1, 0);
  pushMatrix();
  translate( width/2, height/2-10, 0);
  scale(-1, -1, -1);

  // perform a forward FFT on the samples in jingle's mix buffer
  // note that if jingle were a MONO file, this would be the same as using jingle.left or jingle.right
  fftLin.forward( player.mix );
  
  if( fftLin.getAvg(0) > 60 ){
    background(random(255), random(255), random(255));
  }
  //model
  bvh1.update( millis() );
  bvh2.update( millis() );
  bvh3.update( millis() );
  bvh1.draw();
  bvh2.draw();
  bvh3.draw();
  popMatrix();
}

PBvh
int leap = 30; //how far the point travels each iteration also controls opacity

public class PBvh
{

  public BvhParser parser;  
  public int id;
  public PBvh(String[] data, int number)
  {
    parser = new BvhParser();
    parser.init();
    parser.parse( data );
    id = number;
  }
  
  public void update( int ms )
  {
    parser.moveMsTo( ms );//30-sec loop 
    parser.update();
  }

  public void draw()
  {
    float[] pos_x = new float[5];
    float[] pos_y = new float[5];
    float[] pos_z = new float[5];
    int count = 0;
    
    fill(color(255));
     
    for( BvhBone b : parser.getBones())
    {
      pushMatrix();
      translate(b.absPos.x, b.absPos.y, b.absPos.z);
      //ellipse(0, 0, 2, 2);
      popMatrix();
      if (!b.hasChildren())
      {
        pushMatrix();
        translate( b.absEndPos.x, b.absEndPos.y, b.absEndPos.z);
        //ellipse(0, 0, 10, 10);
        popMatrix();
        pos_x[count] = b.absEndPos.x;
        pos_y[count] = b.absEndPos.y;
        pos_z[count] = b.absEndPos.z;
        count=count+1;
      }
        
      noFill();
      beginShape();
      for( int i = 0; i  < count; i++ ){
        if( id == 1 ){
         stroke(205-i, 0, 0, 5);
        }
        else if( id == 2 ){
           stroke(0,205-i,0 ,5);
        }
        else if( id == 3 ){
           stroke(0, 0, 205-i, 5);
        }        
          strokeWeight(random(leap));
        for( int j = 0; j < 30; j++ ){         
          vertex(pos_x[i]+random(-leap,leap), pos_y[i]+random(-leap, leap), pos_z[i]+random(-leap,leap));
        }
      }
      endShape(CLOSE);      
    }
  }
}

モーションデータを可聴化(Sonification)する

ここまでで,モーションデータを利用して視覚化(Visualization)することができました.デジタルデータの面白さの一つのこのような メディア変換があります.普段目にしているようなものでも,視点を変えることで,これまでとは見え方がことなることを実感できたかと 思います.では,次に同じ手法を利用して,可聴化に挑戦します.可聴化とは読んで字のごとくで,聞こえるようにするための手法のことを さします.まず最初にProcessing側で音を出力する必要があるため,その準備をします.今回はVersion.2以降に提供されているMinimという ライブラリを使用していきます. Minim自体のインストール方法などはウェブで検索してください.

下記に最初の雛形を起きます.これを実行してみてください.

p5f_sample
BvhParser parserA = new BvhParser();
PBvh bvh[] = new PBvh[3];

import ddf.minim.*;
import ddf.minim.ugens.*;

Minim       minim;
AudioOutput out;
Oscil       wave[] = new Oscil[3];

public void setup()
{
  size( 1280, 720, P3D );
  background( 255 );
  noStroke();
  stroke(0);
  fill(0);
  frameRate( 30 );

  bvh[0] = new PBvh( loadStrings( "aachan.bvh"), 1 );
  bvh[1] = new PBvh( loadStrings( "kashiyuka.bvh"), 2 );
  bvh[2] = new PBvh( loadStrings( "nocchi.bvh"), 3 );


  minim = new Minim(this);
  // use the getLineOut method of the Minim object to get an AudioOutput object
  out = minim.getLineOut();

  // create a sine wave Oscil, set to 440 Hz, at 0.5 amplitude
  for ( int i = 0; i < 3; i++ ) {
    wave[i] = new Oscil( 440, 0.2f, Waves.SINE );
    wave[i].patch( out );
  }

  loop();
}

int pos = 0;
public void draw()
{
  background(100);
  //camera
  float _cos = cos(millis() / 5000.f);
  float _sin = sin(millis() / 5000.f);
  //camera(width/4.f + width/4.f * _cos +200, height/2.0f-100, 550 + 150 * _sin, width/2.0f, height/2.0f, -400, 0, 1, 0);
  pos=pos-10;
  camera(width/2, height/4, -400, 
    width/2, height/2, 0, 
    0, 1, 0);

  //ground 
  fill( color( 255 ));
  stroke(127);
  line(width/2.0f, height/2.0f, -30, width/2.0f, height/2.0f, 30);
  stroke(127);
  line(width/2.0f-30, height/2.0f, 0, width/2.0f + 30, height/2.0f, 0);
  stroke(255);
  pushMatrix();
  translate( width/2, height/2-10, 0);
  scale(-1, -1, -1);

  //model
  for ( int i = 0; i < 3; i++ ) {
    bvh[i].update( millis() );
    bvh[i].draw();
  } 
  popMatrix();

  for ( int i  = 0; i < 3; i++ ) { 
    wave[i].setFrequency( 2*bvh[i].pos_y[1]);
  }
}

PBvh
int leap = 30; //how far the point travels each iteration also controls opacity

public class PBvh
{

  public BvhParser parser;  
  public int id;
  public PBvh(String[] data, int number)
  {
    parser = new BvhParser();
    parser.init();
    parser.parse( data );
    id = number;
  }

  public void update( int ms )
  {
    parser.moveMsTo( ms );//30-sec loop 
    parser.update();
  }

  public float freq;
  float[] pos_x = new float[5];
  float[] pos_y = new float[5];
  float[] pos_z = new float[5];

  public void draw()
  {
    int count = 0;
    fill(color(255));

    for ( BvhBone b : parser.getBones())
    {
      pushMatrix();
      translate(b.absPos.x, b.absPos.y, b.absPos.z);
      ellipse(0, 0, 2, 2);
      popMatrix();
      if (!b.hasChildren()) {
        pos_x[count] = b.absEndPos.x;
        pos_y[count] = b.absEndPos.y;
        pos_z[count] = b.absEndPos.z;
        count=count+1;
      }
    }

    for ( int i = 0; i < count; i++ ) {
      pushMatrix();
      translate(pos_x[i], pos_y[i], pos_z[i]);
      ellipse(0, 0, 10, 10);
      rotateZ(3.14);
      rotateY(3.14);
      text("   "+i+": "+(int)pos_x[i]+","+(int)pos_y[i]+","+(int)pos_z[i], 0, 0);
      popMatrix();
    }
  }
}

実行した際のスクリーンショット.各頂点座標が表示されています.

上記サンプルでは演者左手の高さ情報を音高情報に割り当てています.少しこのアルゴリズムをもっと 身体動作を活かしたものに変更してみます.具体的には動作量(単位フレームあたりの座標移動量)に応じて 行ってみます.まずはあーちゃんのモーションデータの内,頭(n=0)・左手(n=1)・右手(n=2)・左足(n=3)・右足(n=4)の各座標を$\vec{P}_n$とします. ただし, $\vec{P}_n = (x_n, y_n, z_n) $です.その時に次の式であーちゃんのモーションデータからfrequencyを計算し,音を鳴らしてみましょう. \[ frequency = \sum_{k=0}^4 | \vec{P}_k |  \\ ただし |\vec{P}_k| = \sqrt{(\frac{dx_k}{df})^2+(\frac{dy_k}{df})^2+(\frac{dz_k}{df})^2 }  とし, dfは単位フレーム(delta frame)のことを指す \] 上記に基づきfrequency をクラスのメンバ変数であるfreqに代入し,ダンスモーションで音を鳴らしてください.

実際に動作させてみると,

p5f_sample
BvhParser parserA = new BvhParser();
PBvh bvh[] = new PBvh[3];

import ddf.minim.*;
import ddf.minim.ugens.*;

Minim       minim;
AudioOutput out;
Oscil       wave[] = new Oscil[3];

public void setup()
{
  size( 1280, 720, P3D );
  background( 255 );
  noStroke();
  stroke(0);
  fill(0);
  frameRate( 30 );

  bvh[0] = new PBvh( loadStrings( "aachan.bvh"), 1 );
  bvh[1] = new PBvh( loadStrings( "kashiyuka.bvh"), 2 );
  bvh[2] = new PBvh( loadStrings( "nocchi.bvh"), 3 );


  minim = new Minim(this);
  // use the getLineOut method of the Minim object to get an AudioOutput object
  out = minim.getLineOut();

  // create a sine wave Oscil, set to 440 Hz, at 0.5 amplitude
  for ( int i = 0; i < 3; i++ ) {
    wave[i] = new Oscil( 0, 0.2f, Waves.SINE );
    wave[i].patch( out );
  }

  loop();
}

int pos = 0;
public void draw()
{
  background(100);
  //camera
  float _cos = cos(millis() / 5000.f);
  float _sin = sin(millis() / 5000.f);
  //camera(width/4.f + width/4.f * _cos +200, height/2.0f-100, 550 + 150 * _sin, width/2.0f, height/2.0f, -400, 0, 1, 0);
  pos=pos-10;
  camera(width/2, height/4, -400, 
    width/2, height/2, 0, 
    0, 1, 0);

  //ground 
  fill( color( 255 ));
  stroke(127);
  line(width/2.0f, height/2.0f, -30, width/2.0f, height/2.0f, 30);
  stroke(127);
  line(width/2.0f-30, height/2.0f, 0, width/2.0f + 30, height/2.0f, 0);
  stroke(255);
  pushMatrix();
  translate( width/2, height/2-10, 0);
  scale(-1, -1, -1);

  //model
  for ( int i = 0; i < 3; i++ ) {
    bvh[i].update( millis() );
    bvh[i].draw();
  } 
  popMatrix();

  for ( int i  = 0; i < 3; i++ ) { 
    wave[i].setFrequency( 10*bvh[i].freq);
  }
}

PBvh
int leap = 30; //how far the point travels each iteration also controls opacity

public class PBvh
{

  public BvhParser parser;  
  public int id;
  public PBvh(String[] data, int number)
  {
    parser = new BvhParser();
    parser.init();
    parser.parse( data );
    id = number;
  }

  public void update( int ms )
  {
    parser.moveMsTo( ms );//30-sec loop 
    parser.update();
  }

  public float freq;
  float[] pos_x = new float[5];
  float[] pos_y = new float[5];
  float[] pos_z = new float[5];
  float[] pos_x_old = new float[5];
  float[] pos_y_old = new float[5];
  float[] pos_z_old = new float[5];
  float[] d_pos_x = new float[5];
  float[] d_pos_y = new float[5];
  float[] d_pos_z = new float[5];

  public void draw()
  {
    int count = 0;
    fill(color(255));
    
    freq = 0.0;
    for ( BvhBone b : parser.getBones())
    {
      pushMatrix();
      translate(b.absPos.x, b.absPos.y, b.absPos.z);
      ellipse(0, 0, 2, 2);
      popMatrix();
      if (!b.hasChildren()) {
        pos_x_old[count] = pos_x[count];
        pos_y_old[count] = pos_y[count];
        pos_z_old[count] = pos_z[count];

        pos_x[count] = b.absEndPos.x;
        pos_y[count] = b.absEndPos.y;
        pos_z[count] = b.absEndPos.z;

        d_pos_x[count] = abs(pos_x[count]-pos_x_old[count]);
        d_pos_y[count] = abs(pos_y[count]-pos_y_old[count]);
        d_pos_z[count] = abs(pos_z[count]-pos_z_old[count]);
        
        freq = freq + sqrt(pow(d_pos_x[count], 2)+pow(d_pos_y[count],2)+pow(d_pos_z[count],2));
        
        count=count+1;
      }
    }

    for ( int i = 0; i < count; i++ ) {
      pushMatrix();
      translate(pos_x[i], pos_y[i], pos_z[i]);
      ellipse(0, 0, 10, 10);
      rotateZ(3.14);
      rotateY(3.14);
      text("   "+i+": "+(int)pos_x[i]+","+(int)pos_y[i]+","+(int)pos_z[i], 0, 0);
      popMatrix();
    }
  }
}

動作させた後,各自でプログラムを修正し,周波数だけでなく,音量(amp)の操作等も行ってみましょう.

ここまでは,動きの差分(微分)をそのままサイン波の音高(周波数)としてパラメータを指定しました.では次に同じ計算結果を トリガーとして用いてみます.具体的には,有る一定の動作量を検出したら,予め用意しておいた音声データを再生するというものを 作成してみます.