「Arduino」「Processing」環境で「3軸加速度センサモジュール」と「MqoViewer Library for Processing」を使ってメタセコイアのモデルデータを表示・操作するまでの手順

この記事は、Arduino UNOに接続した3軸加速度センサモジュール KXM52-1050の値をシリアル通信でPCに送信し、その値を元にPC上のMqoViewer Library for ProcessingライブラリをインポートしたProcessingが表示したメタセコイアのモデルデータをカメラ操作するまでの手順をまとめている、メモです。Processingのコードの中に、OpenGLを用いた3Dプログラミングに関する基礎処理を(勉強がてら)まとめました。

1. Arduino環境の構築

Arduino – Wikipediaいわく、「Arduinoは、単純な入出力を備えた基板とProcessing/Wiring言語を実装した開発環境から構成されるシステム。」とのことです。すなわちArduinoはハードウェアとソフトウェアから構成されるシステムであり、今回私はハードウェアに「Arduino UNO」、ソフトウェアに「Arduino IDE」を選択しました。これは2010年10月時点では一般的な選択といえます。

以降、Arduino UNOの導入方法を簡単にまとめていますが、公式サイト内のArduino – Getting Startedが非常に分り易くまとめられていますので、そちらも合わせてご確認ください。

ハードウェアの準備

Arduino UNOは、2010年10月頃に「最も標準的なArduino」と銘打ち発売されました。私はスイッチサイエンスさんからAmazon経由で3200円で購入しました。当方関東地方ですが、ポチった2日後に無事到着しました。

http://www.amazon.co.jp/dp/B0044X2E5S

Arduino本体の他にUSBケーブルを別途用意する必要があります。Arduino UNOには「B端子」が搭載されているため「A to B」のUSBケーブルが必要です。パソコンに搭載されているUSB端子が「USB A端子」、外付けハードディスクに搭載されているUSB端子が「USB B端子」なので、それらを繋ぐケーブルがあれば流用可能です。

ソフトウェアの準備

arduino_scr

公式サイトのダウンロードページから開発環境であるArduino IDEをダウンロードします。私は arduino-0021.zip をダウンロードしました。20以降がAruduino UNOをサポートしています。インストールは解凍・設置するだけですが、ファイルパスに日本語が含まれていない所に設置するのが無難です。

パソコンにArduino UNOを認識させる

ここが僅かに厄介です。 単にArduino UNOとPCをUSBケーブルで接続するだけではCOMポートが利用できるようになりません。Windows 7とArduino UNOの場合、接続後に以下の手順を行ないます。

  1. 「デバイスマネージャー」を開く。(スタート → コントロールパネル → デバイスマネージャー)
  2. 「ほかのデバイス」内の「[!]Arduino Uno」の右クリックメニューから「ドライバー ソフトウェアの更新」を選択。
  3. 「コンピューターを参照してドライバー ソフトウェアを検索します」を選択。
  4. 「参照」から「C:\arduino-0021\drivers(←Arduino UNO.infファイルがあるarduinoフォルダ内のdriversフォルダを指定する)」を指定し「OK」を押す。
  5. 「このドライバー ソフトウェアをインストールします」を選択。
  6. 「このデバイスのドライバー ソフトウェアのインストールを終了しました」というメッセージを確認して閉じる。
  7. デバイスマネージャーの「ポート(COM と LPT)」内に「Arduino UNO(COM●)」と正常に表示されているかを確認。

面倒なのは1回目の接続時のみです。2度目からはドライバが自動認識されます。UNO以前のArduinoの場合は、Virtual COM Port Driversから該当するドライバをダウンロードしておき、USB接続時やデバイスマネージャからドライバを指定していたみたいです。ArduinoやOSの種類により認識手順が異なります。

サンプルプログラムを書き込み動作を確認する

  1. Arduino UNOをUSBで接続する。
  2. Arduino IDEを起動する。
  3. File → Examples → 1.Basics → Blink を選択する。(=サンプルスケッチを選択)
  4. Tools → Board → Arduino Uno を選択する。(=ボードの指定)
  5. Tools → Serial Port → COM●(←ドライバインストール時に割り当てられたCOMポート番号)を選択する。
  6. (メニューアイコンの右から2個目の)Uploadを押す。
  7. 基板上のLEDが点滅すればOKです。

2. Processing環境の構築

processing_scr

Processing – Wikipediaいわく、「Processingは、電子アートとビジュアルデザインのためのプログラミング言語であり、統合開発環境である。視覚的なフィードバックが即座に得られるため、初心者がプログラミングを学習するのに適しており、電子スケッチブックの基盤としても利用できる。」とのことです。

シリアル通信を用いてArduinoとProcessingを連動させることにより、Arduinoと接続したセンサ値をPC側のProcessingで処理したり、PC側のProcessingからArduinoと接続したアクチュエータを制御したりと、いわゆるフィジカルコンピューティングが容易に実現できます。

Processingのインストール

公式サイトのダウンロードページからダウンロードします。私は processing-1.2.1.zip をダウンロードしました。インストールは解凍・設置するだけですが、ファイルパスに日本語が含まれていない所に設置するのが無難です。

3. Arduinoの実装

必要な部品

  • Arduino UNO … 1個
  • 3軸加速度センサモジュール KXM52-1050 1個(1000円@秋月)
  • ブレッドボード … 1個
  • ジャンパー線 … 複数本

配線図

breadboard

ちなみに、この配線図はFritzingというフリーの基板デザインツールで作成しました。図中のKXM52-1050というパーツ図は自作です。パワーポイントで適当に作成したKXM52-1050の画像をFritzingのパーツの新規作成から読み込み、コネクタを設定しています。

Arduinoのスケッチ

//初期化処理
void setup(){
  //シリアル通信の転送速度は9600bps
  Serial.begin(9600);
}

//ループ処理
void loop(){

  //Processingからデータを受信するまで待機
  if(Serial.available()>0){

    //加速度センサの値(0~1023)を4で割ることにより0~255の範囲で送信する
    Serial.print(analogRead(2)/4,BYTE); //アナログ入力の0番ピン(に入力されたX軸加速度)の値を送信
    Serial.print(analogRead(1)/4,BYTE); //アナログ入力の1番ピン(に入力されたY軸加速度)の値を送信
    Serial.print(analogRead(0)/4,BYTE); //アナログ入力の2番ピン(に入力されたZ軸加速度)の値を送信

    //Processingのデータを受信
    Serial.read();
  }
}

メモ

  • ((1024/2)*(1.65/2.5))/4=84.48

4. Processingの実装

準備

  • Sketch → Add File… → MQOLoader.jar
  • Tools → Create Font…

Processingのスケッチ

//----------------------------------------------------------------------------------------------------
// ライブラリのインポート
//----------------------------------------------------------------------------------------------------

//OpenGLライブラリ
import javax.media.opengl.*;
import processing.opengl.*;
//MqoViewer Library for Processingライブラリ
import jp.nyatla.kGLModel.*;
import jp.nyatla.kGLModel.contentprovider.*;
//シリアル通信ライブラリ
import processing.serial.*;

//----------------------------------------------------------------------------------------------------
// 変数
//----------------------------------------------------------------------------------------------------

//モデルデータ
KGLModelData object_data; //モデル本体
KGLModelData shadow_data; //モデルの影
KGLModelData racket_data; //卓球ラケット
//読み込み用
ContentProvider content_provider_object;
ContentProvider content_provider_shadow;
ContentProvider content_provider_racket;

//シリアル通信変数
Serial myPort;
//フォント変数
PFont myFont;

//カメラ回転角
float cameraThe;
float cameraPhi;
//カメラ座標
float cameraXpos;
float cameraYpos;
float cameraZpos;
//光源の回転角
float lightThe;
float lightPhi;
//光源の座標
float lightXpos;
float lightYpos;
float lightZpos;

int aaa;
int bbb;

//----------------------------------------------------------------------------------------------------
// 初期化処理
//----------------------------------------------------------------------------------------------------
void setup() {
  //ウィンドウの初期化
  size(800, 600, OPENGL);

  //モデルデータの読み込み(mqoファイルをフルパス指定(HTTP指定やZIP指定も別メソッドを使えば可能)
  content_provider_object = new LocalContentProvider(this, "C:\\Users\\yuichiro\\Documents\\Processing\\mikukurukuru\\model\\miku.mqo");
  content_provider_shadow = new LocalContentProvider(this, "C:\\Users\\yuichiro\\Documents\\Processing\\mikukurukuru\\model\\shadow.mqo");
  content_provider_racket = new LocalContentProvider(this, "C:\\Users\\yuichiro\\Documents\\Processing\\mikukurukuru\\model\\racket.mqo");
  //OpenGLハンドルの取得
  PGraphicsOpenGL pgl = (PGraphicsOpenGL) g;
  GL gl = pgl.beginGL();
  //モデルデータに記述したOpenGLハンドルを与える(最後部の引数でモデルの座標軸を指定(true:左手系(Processing標準))
  object_data = KGLModelData.createGLModelPs(this, gl,null,this.content_provider_object,0.015f, KGLExtensionCheck.IsExtensionSupported(gl,"GL_ARB_vertex_buffer_object"),true);
  shadow_data = KGLModelData.createGLModelPs(this, gl,null,this.content_provider_shadow,0.015f, KGLExtensionCheck.IsExtensionSupported(gl,"GL_ARB_vertex_buffer_object"),true);
  racket_data = KGLModelData.createGLModelPs(this, gl,null,this.content_provider_racket,0.015f, KGLExtensionCheck.IsExtensionSupported(gl,"GL_ARB_vertex_buffer_object"),true);
  pgl.endGL();

  //シリアル通信の開始
  myPort = new Serial(this,Serial.list()[0], 9600);

  //フォントデータの読み込み
  myFont = loadFont("CarbonBlock-48.vlw");

  //変数の初期化
  cameraThe = 90 * PI/180;
  cameraPhi = 30 * PI/180;
  lightThe = 0;
  lightPhi = 100;
}

//----------------------------------------------------------------------------------------------------
// シリアル通信発生イベント
//----------------------------------------------------------------------------------------------------
void serialEvent(Serial p){
  //Arduino側からの受信が完了するまで待機
  if(myPort.available()>2){
    cameraThe = ((myPort.read() + (90-((1024/2)*(1.65/2.5))/4)) * PI/180); //加速度センサのX軸加速度の値でカメラ回転角シータを変更(0Gで90度になるように調整)
    cameraPhi = ((myPort.read() - (((1024/2)*(1.65/2.5))/4-45)) * PI/180); //加速度センサのY軸加速度の値でカメラ回転角ファイを変更(0Gで45度になるように調整)
    int temp = myPort.read(); //今回は加速度センサのZ軸加速度の値は使わないため受け流す
    myPort.write(100); //受信が完了したことをArduinoに通知する
  }
}

//----------------------------------------------------------------------------------------------------
// マウスが押されたら通信開始
//----------------------------------------------------------------------------------------------------
void mousePressed(){
  //バッファを空に
  myPort.clear();
  //通信開始の合図を送信
  myPort.write(100);
}

//----------------------------------------------------------------------------------------------------
// マウスドラッグイベント(開発時のテスト用)
//----------------------------------------------------------------------------------------------------
//void mouseDragged()
//{
//  cameraThe += (mouseX - pmouseX) * 0.01;
//  cameraPhi += (mouseY - pmouseY) * 0.01;
//}

//----------------------------------------------------------------------------------------------------
// 描写処理
//----------------------------------------------------------------------------------------------------
void draw() {

  //背景色
  background(24, 66, 90); //※メタセコイアの背景色と同じ色

  //カメラ回転角からカメラ座標を算出し,カメラを設定
  int cameraRadius = 450;
  cameraXpos = cameraRadius * cos(cameraThe) * cos(cameraPhi);
  cameraYpos = cameraRadius * sin(cameraPhi);
  cameraZpos = cameraRadius * sin(cameraThe) * cos(cameraPhi);
  camera(cameraXpos,cameraYpos,cameraZpos, 0,200,0, 0,-1,0); //視点,注視点,天井方向の定義

  //カメラ座標表示ボードの描写
  fill(0,0,0); //背景色
  stroke(255,255,255); //枠色
  strokeWeight(4); //枠の太さ
  rect(-290,100,200,100); //描写
  noStroke();

  //テキスト(カメラ座標,モデル達の注釈)の描写
  translate(0,0,1); //1ピクセル前面にずらした位置に描写
  rotateZ(PI); //座標系がずれるので調整
  fill(255,255,255); //文字色
  textAlign(LEFT); //左揃え
  textFont(myFont, 28); //フォントサイズ28
  text("cameraXpos: ", 100, -170, 0);
  text("cameraYpos: ", 100, -140, 0);
  text("cameraZpos: ", 100, -110, 0);
  textFont(myFont, 32);
  text("Miku Hatsune", -200, -325, 0);
  textFont(myFont, 12);
  text("(C) innoce.nobody.jp", -200, -310, 0);
  textFont(myFont, 16);
  text("Penholder L8efty", 145, -293, 15);
  textFont(myFont, 8);
  text("(C) www003.upp.so-net.ne.jp/kakomiki", 145, -283, 15);
  textAlign(RIGHT); //右揃え
  textFont(myFont, 28);
  text((int)cameraXpos, 280, -170, 0);
  text((int)cameraYpos, 280, -140, 0);
  text((int)cameraZpos, 280, -110, 0);
  rotateZ(-PI); //ずらした座標系を元に戻す
  translate(0,0,-1); //ずらした座標を元に戻す

  //モデル達の注釈矢印の描写
  stroke(255,255,255); //線の色
  strokeWeight(2); //線の太さ
  line(50,320,0, 205,320,0);
  line(30,300,0, 50,320,0);
  translate(30,300,0);
  sphere(2); //矢印先端の球
  translate(-30,-300,0);

  stroke(255,255,255); //線の色
  strokeWeight(2); //線の太さ
  line(-140,290,15, -260,290,15);
  line(-125,270,15, -140,290,15);
  translate(-125,270,15);
  sphere(2); //矢印先端の球
  translate(125,-270,-15);

  //OpenGLハンドルの取得
  PGraphicsOpenGL pgl = (PGraphicsOpenGL) g;
  GL gl = pgl.beginGL();

  //gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_MODULATE); //テクスチャ設定
  //gl.glEnable(GL.GL_CULL_FACE);
  //gl.glCullFace(GL.GL_FRONT);

  //フォグ設定
  float fog[]={0.022f*5, 0.066f*5, 0.090f*5, 0.5f};
  //float fog[]={0.022f*5, 0.066f*5, 0.090f*5, 0.5f};
  gl.glFogfv(GL.GL_FOG_COLOR,fog,0);
  gl.glFogi(GL.GL_FOG_MODE,GL.GL_LINEAR);
  gl.glFogf(GL.GL_FOG_DENSITY,0.003f);
  gl.glFogf(GL.GL_FOG_START,500f);
  gl.glFogf(GL.GL_FOG_END,1000f);
  gl.glEnable(GL.GL_FOG);

  //--------------------------------------------------
  // フィールドの描写
  //--------------------------------------------------
  gl.glPushMatrix();

  //グリッド(床と重ならないように2ピクセル浮かして描写)
  gl.glLineWidth(1f);
  gl.glEnable(GL.GL_LINE_STIPPLE); //破線モードを有効に
  gl.glLineStipple(1,(short)0xF0F0); //破線パターンの設定
  gl.glBegin(GL.GL_LINES);
  gl.glColor4f(1f,1f,1f,0.2f); //色
  int gridNumber = 20;
  int gridInterval = 50;
  int gridArea = gridNumber * gridInterval;
  for(int i=-gridNumber;i<gridNumber;i++){
    gl.glVertex3f(-gridArea, 2 ,i*gridInterval);
    gl.glVertex3f(+gridArea, 2 ,i*gridInterval);
    gl.glVertex3f(i*gridInterval, 2 ,-gridArea);
    gl.glVertex3f(i*gridInterval, 2 ,+gridArea);
  }
  gl.glEnd();
  gl.glDisable(GL.GL_LINE_STIPPLE); //破線モードを無効に

  //床
  gl.glEnable(GL.GL_CULL_FACE);//裏面のみを描くように変更
  gl.glCullFace(GL.GL_FRONT);
  gl.glBegin(GL.GL_QUADS);
  gl.glColor4f(1f,1f,1f,0.1f); //色
  //gl.glColor4f(1 , 1 , 0, 1);
  gl.glVertex3f(+gridArea, 0, +gridArea);
  //gl.glColor4f(1 , 0 , 0, 1);
  gl.glVertex3f(-gridArea, 0, +gridArea);
  //gl.glColor4f(0 , 1 , 0, 1);
  gl.glVertex3f(-gridArea, 0, -gridArea);
  //gl.glColor4f(0 , 0 , 1, 1);
  gl.glVertex3f(+gridArea, 0, -gridArea);
  gl.glEnd();
  gl.glDisable(GL.GL_CULL_FACE); //両面を描くように元に戻す

  gl.glPopMatrix();
  //--------------------------------------------------
  // モデル本体の描写
  //--------------------------------------------------
  gl.glPushMatrix();

  gl.glRotatef(180, 0, 1, 0); //座標系の関係でモデルが後ろを向くので回転
  gl.glTranslatef(0, 273, 0); //地面に立つように移動
  //裏面のみを描くように変更
  gl.glEnable(GL.GL_CULL_FACE);
  gl.glCullFace(GL.GL_FRONT);

  //モデルデータ
  object_data.enables(30.0f); //スケール
  object_data.draw(); //描写
  object_data.disables(); //破棄
  gl.glDisable(GL.GL_CULL_FACE); //両面を描くように元に戻す

  gl.glPopMatrix();
  //--------------------------------------------------
  // モデルの影の描写
  //--------------------------------------------------
  gl.glPushMatrix();

  //影行列
  float matrix[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};

  //カメラ回転角からカメラ座標を算出し,カメラを設定
  int lightRadius = 500;
  lightXpos = lightRadius * cos(lightThe) * cos(lightPhi);
  lightYpos = lightRadius * sin(lightPhi);
  lightZpos = lightRadius * sin(lightThe) * cos(lightPhi);

  //影行列の計算
  matrix[0]  = 1;
  matrix[5]  = 0;
  matrix[4]  = -lightXpos/lightYpos;
  matrix[6]  = -lightZpos/lightYpos;
  matrix[10] = 1;
  matrix[15] = 1;

  gl.glRotatef(180, 0, 1, 0); //座標系の関係でモデルが後ろを向くので回転
  gl.glTranslatef(0, 1, 0); //1ピクセル浮かした位置に描写

  gl.glMultMatrixf(matrix, 0); //影行列を適用
  gl.glTranslatef(0, 273, 0); //本体の足元から伸びるように移動

  //モデルデータ
  shadow_data.enables(30.0f); //スケール
  shadow_data.draw(); //描写
  shadow_data.disables() ; //破棄

  gl.glPopMatrix();
  //--------------------------------------------------
  // 卓球ラケットの描写
  //--------------------------------------------------
  gl.glPushMatrix();

  gl.glRotatef(180, 0, 1, 1); //回転
  gl.glTranslatef(125, 0, 260); //移動
  racket_data.enables(140.0f); //スケール
  racket_data.draw(); //描写
  racket_data.disables(); //破棄

  gl.glPopMatrix();

  //--------------------------------------------------
  // 描写終了
  //--------------------------------------------------
  pgl.endGL();

  //--------------------------------------------------
  // 変数の更新
  //--------------------------------------------------

  lightThe += 0.005; //光源の移動
}

今回使わせていただいたモデルデータ

5. 動作確認

の動画を載せようかと思ったのですがそれほど面白い動きではなかったので割愛です…。

6. おまけ(3Dプログラミング技術メモ)

カメラの回転

camera

影行列

shadow

7. 参考サイト