lecture:design_with_prototyping:動作の再構成

動きを再構成する

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

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

ではまず,github上にある github: perfume-dev からデータ一式をダウンロードします。

次にp5f_sampleフォルダの以下フォルダに次のファイルを追加します.

  1. aachan.bvh, kashiyuka.bvh, nocchi.bvhをdataフォルダに追加
  2. Perfume_globalsite_sound.wavをdataフォルダに追加

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

sample.pde
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分を利用して,一旦各配列に値を代入し直してみます. ただし,頭,手,足,のシンプルな点のみに限定します.

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の配列に代入されました.この座標データを利用して,グラフィック描画を変化させていきます.

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

例1 各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);
  }  

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

p5f_sample.pde
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.h
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.pde
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.pde
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);      
    }
  }
}

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

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

p5f_sample.pde
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();
 
  // 各3人の左手y座標の値に応じて、発音しているsin波の周波数を変更する
  // bvh[0],bvh[1],bvh[2]にはそれぞれのメンバーの座標データが保存されている。
  for ( int i  = 0; i < 3; i++ ) { 
    wave[i].setFrequency( 2*bvh[i].pos_y[1]); // 単位は[Hz]
  }
}
PBnv.pde
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));
 
    // 各種頂点座標の読み取り(頭、両手足の5つ)
    for ( BvhBone b : parser.getBones())
    {
      pushMatrix();
      translate(b.absPos.x, b.absPos.y, b.absPos.z);
      ellipse(0, 0, 2, 2);
      popMatrix();
      // 末端オブジェクトであれば、それぞれを配列に保存する
      // 例えば pos_x[0]には 頭のx座標が入っている
      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();
    }
  }
}

実行した際のスクリーンショット

練習2 記サンプルでは演者左手の高さ情報を音高情報に割り当てています.少しこのアルゴリズムをもっと 身体動作を活かしたものに変更してみます.具体的には動作量(単位フレームあたりの座標移動量)に応じて 行ってみます.まずはあーちゃんのモーションデータの内,頭(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.pde
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.pde
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)の操作等も行ってみましょう.

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

  • lecture/design_with_prototyping/動作の再構成.txt
  • 最終更新: 2019/11/07 14:06
  • by baba