INDEX(各項目ごとの目次)

[HOME]  [Processing関係]  [Arduino関係]  [マテリアル関係]  [秋葉原工作マップ]

2008年用ですが、部分的に内容を更新しています(2010/06/14)。
また、[建築農業工作ゼミ2009-2010]とも連動していますので、そちらにも幾つかサンプルがあります。
:

ラベル 3D の投稿を表示しています。 すべての投稿を表示
ラベル 3D の投稿を表示しています。 すべての投稿を表示

9/14/2008

Processing 緊急モーションセンサー(Mac)

Appleの2005年以降のPowerBook/iBook/MacBook(ノート型)には、緊急モーションセンサーが内蔵されています。緊急モーションセンサーは、ハードディスクを保護するために、あやまってコンピュータを落としたときの衝撃を感知します。
Processingには、この緊急モーションセンサーから値を得る「Sudden Motion Sensorライブラリ」があります。緊急モーションセンサーでは、以前扱った「加速度センサ」のように、コンピュータ本体を傾けたり衝撃を与えたりすることによって変化する値(XYZ軸に対する3つの値)が得られます。



Sudden Motion Sensorライブラリのサイトに従えば、3つの値は以下の方法で読み込むことが可能になります。尚、ライブラリをダウンロード+インストールする必要があります。

//ライブラリを取り込む
import sms.*;

void setup() {
//画面サイズをとりあえず200角
size(200,200);
}

void draw() {
//3つの値を読み込み、配列に代入
int[] vals = Unimotion.getSMSArray();
//3つの値を出力
println(vals[0] + " " + vals[1] + " " + vals[2]);
}

上記プログラムによって出力された値は以下のようになりました(機種や状況によって多少誤差が含まれるかもしれません)。

水平時 x: 0, y: -3, z:56
左90度 x: 52, y: -3, z: 6
右90度 x:-51, y: -3, z: 5
前90度 x: 1, y: 49, z: 6
後90度 x: 1, y:-54, z: 5

xの振り幅:103(-51から52までの±51.5)
yの振り幅:103(-54から49までの±51.5)

になります。それぞれの振り幅からxとyの中点(水平時の値)を新たに求めると、

xの中点:(-51+52)/2=0.5
yの中点:(-54+49)/2=-2.5

になります。これら中点の値をオフセット値として用いることにします。つまり、計測された値からオフセット値を差し引いて角度の計算をすることになります。以前の「Arduino 加速度センサ」のときと同様に、出力値からそれぞれの角度を求める式を用意します。それぞれの角度をradX、radY、オフセット値をoffsetX、offsetY、読み取り値をx、yとすると、

//atan2()で求める場合
radX=atan2((x-offsetX),sqrt(51.5*51.5-(x-offsetX)*(x-offsetX)))
radY=atan2((y-offsetY),sqrt(51.5*51.5-(y-offsetY)*(y-offsetY)))

//またはacos()、asin()で求める場合
radX=asin((x-offsetX)/51.5)
radY=acos((y-offsetY)/51.5)

になります。式中の51.5は加速度1G(重力)の時の値です(機種によっては256くらいのときもあります)。

以上の式を使って、コンピュータ本体を傾けることでProcessing画面上の3Dモデルを動かしてみます。
3Dモデルに関しては、前回の記事「Processing 3Dモデル/OBJ Loader」のものを使うことにします。コンピュータを傾けた方向に、画面内の3Dモデルも同様に傾く内容とします。
スケッチフォルダ内にdataフォルダを作成し、3Dモデルのデータを入れておいて下さい(3Dデータは、ここからダウンロードできます/.objファイルと.mtlファイルの二つが必要です)。


(コンピュータを傾けた時の3Dモデル/前回ブログの3Dモデルを使用)


//ライブラリのインポート
import sms.*;
import saito.objloader.*;

//モデルのオブジェクトを用意
OBJModel model;

void setup() {
//3D画面サイズ設定
size(400,400,P3D);
//モデルのオブジェクトを生成
model=new OBJModel(this);
//3Dデータ読み込み
model.load("macbook.obj");
//ワイヤーフレームなし
noStroke();
}

void draw() {
//3つの値を読み込み、配列に代入
int[] vals = Unimotion.getSMSArray();
//println(vals[0] + " " + vals[1] + " " + vals[2]);

//背景描画
background(50);
//直線光の設定
directionalLight(200, 200, 200, -1, 1, -1);
//環境光の設定
ambientLight(200, 200, 200);

//3Dモデルの位置座標設定
translate(width/2,height*2/3,0);

//オフセット値
float offsetX=-2.5;
float offsetY=0.5;
//角度の計算:atan2()で求める場合
float radX=-atan2(vals[1]-offsetX,sqrt(51.5*51.5-(vals[1]-offsetX)*(vals[1]-offsetX)));
float radY=-atan2(vals[0]-offsetY,sqrt(51.5*51.5-(vals[0]-offsetY)*(vals[0]-offsetY)));
//またはacos()、asin()で求める場合
//float radX=asin((vals[1]-offsetX)/51.5);
//float radY=acos((vals[0]-offsetY)/51.5);

//回転角度
rotateX(radX+PI/2);
rotateY(radY);

//三角形分割で面を生成する
model.drawMode(TRIANGLES);
//スケール(200倍)
scale(200);
//モデル描画
model.draw();
}

画面内の3D座標は、
左:-X
右:+X
上:-Y
下:+Y
後:-Z
前:+Z
という関係になります。
緊急モーションセンサーのX軸は、Processing上のY軸に対応しているので、vals[0]の値を3DモデルのY軸回転角度へ代入し、val[1]の値は3DモデルのX軸回転角度へ代入します。表示上90度X軸に対してずれていたので、rotateX(radX+PI/2)というようにradXにPI/2(90度)足しておきました。
directionalLight()は太陽光のような直線光であり、括弧内の数値については、最初の3つがRGBで光の色を指定、最後の3つが(0,0,0)の原点を基準に光の向きを設定することになります。
ambientLight()は環境光であり、光の向きはなく、空間全体を明るくしたり暗くしたりし(あるいは色を変える)、RGBの3つの数値で指定します。


もう一つのサンプルとして、コンピュータ自体をコントローラとして傾けて、水平面上のボールを転がすプログラムをしてみます。今回は、setup(){...}内で、一度緊急モーションセンサーから値を読み込み、それらをオフセット値として使うことにします。こうすることで、コンピュータを平らな場所においてプログラムを開始したときの状態(オフセット値)を記憶させておくことができます。ellipse()で擬似的な影を地面に落とすことで立体感がでるようにします。


(コンピュータの傾きに合わせてボールが転がる)


//ライブラリのインポート
import sms.*;

//ボールの座標用変数
float xPos,yPos;

//オフセット用変数
float xOffset;
float yOffset;

void setup() {
//3D画面設定
size(400,400,P3D);
//ワイヤーフレームなし
noStroke();
//水平状態の読み込み
int[] vals = Unimotion.getSMSArray();
//読み込み値をオフセット値に設定する
xOffset=vals[0];
yOffset=vals[1];
}

void draw() {
//モーションセンサーからの読み込み
int[] vals = Unimotion.getSMSArray();
//背景色
background(220);
//画面上半分の塗色
fill(50);
//画面上半分の矩形
rect(0,0,width,height/2);

//ボールの速度の計算
float xSpeed=vals[0]-xOffset;
float ySpeed=vals[1]-yOffset;
//ボール移動量の計算
xPos+=-xSpeed;
yPos+=ySpeed;
//ボール位置の設定
translate(width/2+xPos,height*4/5,-200+yPos);

//影の塗色
fill(50);
//影の座標と大きさ
ellipse(-25,30,80,80/5);

//直線光の設定
directionalLight(255, 255, 255, -1, 1, 0);
//ボールの塗色
fill(255,100,50);
//ボール描画
sphere(30);
}

緊急モーションセンサーの読み取り値からオフセット値を差し引いた値を、そのままボールのスピードに反映させています。読み取られたY方向の値は、画面内のZ座標(前後の軸)に対応するので、yPosをtranslate()のZ軸に代入してあります。実際に動かしてみて、向きが逆であったり、座標軸がきちんと対応していない場合は、値にマイナスを掛けたり、代入先を入れ替えたりして調整してみて下さい。
ボールの位置設定のためのtranslate()は、まずX座標をwidth/2で左右中央、Y座標をheight*4/5で画面上下4/5の位置、Z座標を-200奥とした座標を基準とし、その基準の座標に変化する値となるxPosとyPosを追加して、最終的な位置を決定しています。
疑似の影として用いたellipse()は2D用の図形でありX座標とY座標しか設定できませんが、translate()以後に挿入してあるので、translate()のZ軸の値に合わせて、前後に動きつつ大きさも変化します(2D図形は、3D空間上ではZ座標値が0の位置に配置されており、translate()以後に書いた2D図形は、Z軸の値を変化させれば、見た目の大きさや位置も影響を受けて変化します)。同様に、directionalLight()に対しても、2D図形をdirectionalLight()以前に書けば、光の影響を受けませんが、directionalLight()以後に2D図形を書くと、光の影響を受けて図形自体に陰影がつきます。そのため、directionalLight()は、プログラムの冒頭の方に書かずに、ボール描画の直前(2D図形描画以降)に書いておきます。

9/11/2008

Processing 3Dモデル/OBJ Loader

Processingのライブラリには、3Dモデリングソフトで制作した3Dモデルを読み込むOBJ Loaderライブラリがあります。OBJ Loaderでは、拡張子が「.obj」の3Dモデルを扱うことができます。3Dモデリングソフトがあれば、制作した3Dモデルを「.obj」フォーマットで書き出して、3DデータをOBJ Loaderで読み込みます。読み込みに使用する3Dデータは、スケッチフォルダ内に入れておきます。
また、モデリングが面倒であれば、インターネットからフリーの3Dデータを検索しダウンロードして利用する方法もあります。この場合「.obj」フォーマットのデータでなければならないのですが、それ以外のフォーマットであっても、一旦3Dモデリングソフトで読み込んで「.obj」フォーマットにして書き出せば利用可能です。最近はフリーの3Dモデリングソフトも多く存在するので、ダウンロード/インストールしてすぐに使うことができるはずです(ブログページ右側にもフリーのモデリングソフトのリストがあります)。

参考3Dモデリングソフト(フリーウェア):
Blender (Win,Mac)
Maya 2010体験版(30日)(Win,Mac)
Rhino (Mac用ベータ版/要登録)
Metasequoia/LE (Win)
DoGA (Win)
CB Model Pro (Win,Mac)
SketchUp (Win,Mac)
SketchyPhysics (Win):SketchUp物理演算プラグイン
trueSpace7 (Win)
CoCreate (Win/要登録)

今回はインターネット上から無料の3Dモデルのデータをダウンロードし、Processing上に表示してみたいと思います。Turbo Squidという3Dモデルのデータライブラリのサイトから無料の3Dモデルを探し出し、以下の3Dモデルを利用してみたいと思います(データをダウンロードするにはTurbo Squidに登録する必要があります)。

http://www.turbosquid.com/3d-models/max-apple-macbook/391534

このモデルは拡張子が「.3DS」なので3ds Max用のデータです。フリーウェアのBlenderという3Dモデリングソフトで、この「.3DS」フォーマットのデータを読み込み(File>Import>.3ds)、「.obj」フォーマットで書き出して(File>Export.obj)利用してみたいと思います(「.obj」に変換したファイルのリンクはこの記事の最後にあります)。

はじめてのBlender (I・O BOOKS)
山崎 聡
工学社
売り上げランキング: 6131


そのまま読み込むと、各パーツの位置が少しずれていたので、修正して「.obj」フォーマットで書き出すことにしました。「.obj」フォーマットを選択して書き出すと、「~.obj」と「~.mtl」という二つのファイルが出来上がります。「~.obj」はポリゴンの座標についてのデータであり、「~.mtl」は色や材質、テクスチャなどのデータが含まれています。以下のプログラムで、3Dデータを読み込んでみます(「~.obj」ファイルと「~.mtl」ファイルは、スケッチフォルダ内に入れておいてください)。

//ライブラリのインポート
import saito.objloader.*;

//モデルのオブジェクトを用意
OBJModel model;

void setup() {
//3D用の画面設定
size(400,400,P3D);
//モデルのオブジェクトを生成
model=new OBJModel(this);
//objファイルの読み込み
model.load("macbook.obj");
//ワイヤーフレームなし
noStroke();
}

void draw(){
//背景描画
background(50);
//直線光の設定
directionalLight(200, 200, 200, -1, 1, -1);
//環境光の設定
ambientLight(200, 200, 200);

//3Dモデルの位置設定
translate(width/2,height/2,0);
//スケール設定(200倍)
scale(200);

//マウス入力で回転させる
rotateX(PI*mouseY/height);

//三角形分割で面を生成する
model.drawMode(TRIANGLES);
//3Dモデルの描画
model.draw();
}


Blenderでは、細かな出力設定はせずに、そのまま「.3DS」データを読み込み(File>Import>.3ds)、「.obj」フォーマットで書き出す(File>Export>.obj)ことにします(書き出す際には、予め画面上で3DモデルをSelectしておく必要があります)。表示されるスケールが小さすぎたので、scale()で200倍の大きさに変換しています。そのまま表示させると、3Dモデルの色が真っ黒のままだったので、多少の修正が必要になります。



色などの内容を確かめるために「.mtl」ファイルをテキストエディタなどで開くと、以下のような内容が記述されていることが分かります。

# Blender3D MTL File: macbook.blend
# Material Count: 2
newmtl Screen
Ns 96.078431
Ka 0.000000 0.000000 0.000000
Kd 0.084706 0.084706 0.084706
Ks 0.449020 0.449020 0.449020
Ni 1.000000
d 1.000000
illum 2

newmtl Plastic
Ns 96.078431
Ka 0.000000 0.000000 0.000000
Kd 0.800000 0.800000 0.800000
Ks 0.449020 0.449020 0.449020
Ni 1.000000
d 1.000000
illum 2

このデータには、ScreenとPlasticという名前の二つの材質についての数値が含まれており、以下のような内訳になります。
# コメント
newmtl 材料名
Ns 輝度
Ka 環境色
Kd 拡散色
Ks 反射色
Ni 光の屈折率
d  アルファ値
illum 0:照明なし、1:反射ハイライトなし、2:Ksの値で反射ハイライトあり

恐らくKaの値がそれぞれ0.000000, 0.000000, 0.000000なので真っ黒な状態になったのでしょう。それぞれの値を以下のように変えます。

ScreenのKaの値を
Ka 0.1 0.01 0.01

PlasticのKaの値を
Ka 0.7 0.7 0.7

上記三つの値はRGB(赤,緑,青)に対応しています。
それぞれの値を変更すると、ScreenのKaは少し赤みがかった黒、PlasticのKaは白に近いグレーになります。
先ほどのプログラムで再度描画させれば、以下のようにかたちが認識できるようになるはずです。


マウス上下で3Dモデルも上下に回転します。
見えない場合はこちらへ


Processingでは、box()sphere()vertex()などの基本的な3D描画のコマンドはありますが、複雑な3Dモデルを制作する際には、モデリングソフトを利用して取り込んだ方がいいでしょう。あるいは、3Dモデルのデータもインターネット上に数多く存在しているので、検索すれば相応しいものが見つかるかもしれません。
尚、上記プログラムで使用したmacbook.objとmacbook.mtlのファイルは、ここからダウンロードできます。


3Dキャラクタアニメーション Blender(DVD付)
トニー・マレン
アスキー
売り上げランキング: 118200

6/16/2008

Arduino 加速度センサ

今回は秋月電子で購入した「KXM52-1050」という3軸加速度センサモジュールを使い、重力方向に対する傾斜角を読み取ります。このセンサでは、XYZ軸の3軸ありますが、XとY軸だけでも三次元的な傾斜角を計測することができます。一応、センサのXYZの3つの出力端子をArduinoのアナログ入力端子にそれぞれ接続することにしますが、実際使うのはXとYの出力値とします。データシートをみながらセンサの端子を以下のように接続します。

1:5V(Arduino5V端子と共有)
2:5V(Arduino5V端子と共有)
3:GND(ArduinoGND端子と共有)
4:無接続
5:GND(ArduinoGND端子と共有)
6:X軸(Arduinoアナログ入力0番ピン)
7:Y軸(Arduinoアナログ入力1番ピン)
8:Z軸(Arduinoアナログ入力2番ピン)



加速度センサを水平なところにおけば、X軸とY軸は重力方向に対して直角なので0Gとなります。5V電源の場合、0Gは2.5Vとして出力されるとデータシートには書いてあります。ArduinoのanalogRead()の1024段階(10ビット)であれば511になるはずですが、さまざまな条件で多少の誤差を含みます。実際に使用する前に、念のためArduinoの「Serial Monitor」で加速度センサの出力値をモニタリングしてみます(Arduinoのモニタリング方法については「Arduino 圧電スピーカ」を参照」。

Arduino (Serial Monitor)のプログラム:

void setup(){
//シリアル通信開始
Serial.begin(9600);
}

void loop(){
//3つの値をアナログ入力で読み込む
int x=analogRead(0);
int y=analogRead(1);
int z=analogRead(2);

//Xの値を出力(十進数)
Serial.print(x,DEC);
//値と値の間に区切りを入れる
Serial.print(",");
//Yの値を出力
Serial.print(y,DEC);
//値と値の間に区切りを入れる
Serial.print(",");
//Zの値を出力し改行する
Serial.println(z,DEC);
delay(100);
}
}

3つの値を一行で出力する際に、Arduinoの出力画面上で読みやすいようにそれぞれの値の間に「","」の区切りの記号(コンマ)をいれます。この区切り記号は何でもいいのですが、これがないとそれぞれの数値同士が隣り合わせになって読みにくくなります(また、3つの数値をそれぞれ改行して出力すると、どれがX軸の値でどれがY軸の値なのか分かりにくくなるので、3つ出力してから改行しています)。
固定した角度で計測しても数値が安定しないので、100個の値をサンプリングして平均値を求めたいと思います。平均値のプログラムを付け加えます。100個分の値の合計となると、数字も大きくなるので「int」型の整数ではなく、より大きい値が扱える「long」型の整数を変数として使います。


//加算用の変数
long x_sum, ysum, z_sum;
//回数の変数
int count=0;

void setup(){
Serial.begin(9600);
}

void loop(){
int x=analogRead(0);
int y=analogRead(1);
int z=analogRead(2);

//それぞれに値を足していく(合計数)
x_sum+=x;
y_sum+=y;
z_sum+=z;

//回数を+1する(カウントアップ)
count++;

//100回カウントしたら
if(count>99){
//合計数を100で割って平均値を出す
Serial.print(x_sum/100,DEC);
Serial.print(",");
Serial.print(y_sum/100,DEC);
Serial.print(",");
Serial.println(z_sum/100,DEC);
//カウントを0に戻す
count=0;
//合計数を0に戻す
x_sum=0;
y_sum=0;
z_sum=0;
}
}

まずは、X軸について計測することにします。水平状態(0G)に対して定規などを用いて−90度傾けて−1Gの値、90度傾けて+1Gの値を上記プログラムを用いて計測することにします。
プログラム上では、xの値をx_sumに足していき、変数countで何回足したかを数えておきます(1ループで一回足されます)。countが100になったら、100回分の合計数であるx_sumを100で割り、その値を出力します(yについても同様に計測します)。
0Gの値については、水平に置いて計測してもいいのですが、今回は−1Gの時の値と+1Gの時の値の中点を用いることにします。よって、以下のような計測結果になります。

  角度:   重力:X軸平均値:Y軸平均値
-90度:  -1G:  316:  271
中点0度:   0G:  536:  491
+90度:  +1G:  756:  711 

これらの値は、今回使用した加速度センサと計測状況において求められた値なので、各自で似たような方法で計測してください。

それでは、この計測結果をもとに、Processingにセンサからの出力値をシリアル通信し、Processing上の3D立体を動かしてみたいと思います。センサを傾ければ、同様に3D立体も同じ角度で傾くようにします。シリアル通信は、1024段階の値を文字列で送ることにします(「Arduino-Processing シリアル通信5」を参照)。このプログラムでは、X軸とY軸だけを読み取ることにします。

Arduinoのプログラム:

void setup(){
//シリアル通信開始
Serial.begin(9600);
}

void loop(){
//2つの値をアナログ入力で読み込む
int x=analogRead(0);
int y=analogRead(1);

if(Serial.available()>0){
//Xの値を出力
Serial.print(x,DEC);
//値と値の間に区切りを入れる
Serial.print(",");
//Yの値を改行して出力
Serial.println(y,DEC);
//合図用データを読み込みバッファを空にする
Serial.read();
}
}

センサから読み取ったXとY軸の値をそのままArduinoから送信します。
Processingでは、受け取った値を角度に変換する計算が必要になります。まず0Gを基準にして、水平時の値が0になるようにオフセット値(X軸の場合:536、Y軸の場合:491)を設けて差し引いておきます。そうすれば、

  角度:   重力:   X軸:   Y軸
-90度:  -1G: -220: -220
  中点:   0G:    0:    0
+90度:  +1G: +220: +220

となります。振り幅は0Gを基準にプラスマイナス220となります。
次に角度の計算ですが単位はラジアンを用います。−90度から+90度までの範囲なので、ラジアンでいうと−PI/2から+PI/2になります(PIは円周率のπです)。X軸の値が110であれば、振り幅である220(1G)の半分なので0.5Gになります。角度については90度の半分なので45度になりそうですが、実際は30度になります。−45度の場合は、以下の図のように約−156になります。



この計算方法は以下のようにして求められます。

acos()、asin()を用いる場合:
まず、Arduinoから送られて来たX軸の値をx、オフセット値をx_offset(今回のオフセット値は536)、オフセット調整した値をx0とすると、

x0=x-x_offset;

になり、角度をradX(ラジアン)とすると

sin(radX)=x0/220;

という関係になります。例えば、x0=110を代入すればsin(radX)=1/2なので、radXは30度となります。
Processingにはasin()acos()の関数があるので、それを利用すると

radX=asin(sin(radX));

という関係になり、sin(radX)にx0/220を代入し

radX=asin(x0/220);

となることで角度radXが求まります。
Y軸についてはacos()で求めると、

radY=acos(y0/220);

になります。

atan2()を用いる場合:
また、この関係をタンジェントで表せば、

tan(radX)=x0/sqrt(220*220-x0*x0)

となります。sqrt()は平方根(ルート)を求める関数です。
角度を求めるには、atan2()という関数を用いて、

radX=atan2(x0,sqrt(220*220-x0*x0));

とします。そうすると角度radXが求められます。

加速度センサのX軸プラス方向をProcessingの3D空間のX軸マイナス方向に対応させるために-radXに変換します。加速度センサのY軸方向を3D空間のZ軸方向に対応させて、

rotateX(-radX)
rotateZ(radY)

となります。
もし、加速度センサの回転方向と、3D立体の回転方向が逆になってしまうときは、値にマイナスを掛けます。また、90度ずれているときはPI/2を足します。実際にセンサを動かして、同じように3D立体が動くか確かめて下さい。

Processingのプログラム:

import processing.serial.*;
Serial port;

//読み取り値の変数
int x,y;

//X軸-1G時316、+1G時756であることから
//X軸のオフセット値
int x_offset=536;
//X軸の振り幅(-1G〜0G又は0G〜+1G)
int x_range=220;

//Y軸-1G時271、+1G時711であることから
//Y軸のオフセット値
int y_offset=496;
//Y軸の振り幅(-1G〜0G又は0G〜+1G)
int y_range=220;

//角度(ラジアン)の変数
float radX,radY;

void setup(){
//3D画面サイズ400×400
size(400,400,P3D);
//シリアルポート設定
port = new Serial(this,"/dev/tty.usbserial-A50019vD",9600);
//念のためバッファを空にする
port.clear();
//「10」(ラインフィード)が来る度にserialEvent()作動
port.bufferUntil(10);
//図形塗り面なし(ワイヤフレーム描画)
noFill();
}

void draw(){
//背景色を白
background(255);

//3D立体の座標を画面中央、-100奥に配置
translate(width/2,height/2,-100);

//オフセット調整(最小値-220、最大値220)
int x0=constrain(x-x_offset,-220,220);
int y0=constrain(y-y_offset,-220,220);

//角度の計算(ラジアン)
radX=asin(x0/x_range);//asin()で求める
radY=acos(y0/y_range);//acos()で求める
//radX=atan2( x0,sqrt(x_range*x_range-x0*x0) );//atan2()で求める場合
//radY=atan2( y0,sqrt(y_range*y_range-y0*y0) );

//センサX軸の角度は3D立体のX軸の角度に対応
//センサY軸の角度は3D立体のZ軸の角度に対応
//角度をそれぞれ代入
rotateX(-radX);
rotateZ(radY);

//直方体を描画
box(200,30,100);
}

//シリアル通信
void serialEvent(Serial p){
//文字列用変数を用意し、
//「10」(ラインフィード)が来るまで読み込む
String stringData=port.readStringUntil(10);

//データが空でないとき
if(stringData!=null){
//改行記号を取り除く
stringData=trim(stringData);
//コンマで区切ってデータを分解、整数化
int data[]=int(split(stringData,','));

//データ数が2個のとき
if(data.length==2){
//データの値を代入
x=data[0];
y=data[1];
//合図用データ送信
port.write(65);
}
}
}

//マウスボタンを押して通信開始
void mousePressed(){
//合図用データ送信
port.write(65);
}

void draw(){...}内の「オフセット調整」箇所の

int x0=constrain(x-x_offset,-220,220)

は、constrain()を用いて、読み取った値xからオフセット値であるx_offsetを差引き、最小値−220から最大値220までの値になるように制限しています。

ノイズのせいか、動きがぎこちない場合はフィルターのプログラムを挿入し滑らかにします。そのためには、radX、radYと同様にプログラムの冒頭でフィルター用の変数:
float filterX,filterY;

を用意しておき、void draw(){...}内の最後の角度を求める箇所を以下のように変更してください。
radX=asin(x0/x_range);//変更なし
radY=acos(y0/y_range);//変更なし

//フィルターの式
filterX+=(radX-filterX)*0.3;//新たに挿入
filterY+=(radY-filterY)*0.3;//新たに挿入

rotateX(-filterX);//変更
rotateZ(filterY); //変更

フィルターの式の「0.3」は係数であり、1.0に近づくほどフィルターの効果はなくなります。逆に0.1のように係数の値を小さくすれば、滑らかになりつつ反応が鈍く動くようになります。適度に調整してみてください。


尚、もっと簡単に加速度センサを扱いたい場合は(あまり正確な角度にこだわらないのであれば)、
//オフセット調整(最小値-220、最大値220)
int x0=constrain(x-x_offset,-220,220);
int y0=constrain(y-y_offset,-220,220);
//角度の計算(ラジアン)
radX=asin(x0/x_range);
radY=acos(y0/y_range);

の部分を、
radX=2.0*x*PI/1023;
radY=2.0*y*PI/1023;

に置き換えてもセンサを傾けた方向に3D立体が傾きます。この計算では、読み取った直接の値に比例して角度も変わります(比率が多少ずれてしまいます)。この場合は、モニタリングで調べた最小値/最大値/オフセット値などの設定もする必要はありません。式の中の「2.0」というのは係数であり、大きくすれば傾きも大きくなるので画面で確認しながら調整してください。

−90度や+90度付近では、出力値の変化が微妙になるので、きちんとした角度が出ない場合があります。出力値補正のためにZ軸の出力も利用すれば、計算は少し複雑になるかもしれませんが、±90付近まで計測可能になります。

5/27/2008

Arduino-Processing シリアル通信2


【変更】以下はArduino1.0まで対応したプログラム内容です。
特にシリアル通信においては、Arduino2.0使用の際、バイト送信する場合、
Serial.print(value,BYTE);
のかわりに、
Serial.write(value);
を使用してください。


前回のシリアル通信では、ひとつの値をArduinoからProcessingへ一方的に送り続ける内容でした。今回は、Arduino側から三つの可変抵抗器を使って、三つの異なる値をProcessingへ送りたいと思います。

例えば、Processing上の3DモデルのX座標、Y座標、Z座標の値を送り、それぞれのツマミで三次元的に立体を動かすことができるプログラムになるということです。
Arduinoから3つの値を送るには、
Serial.write(x);
Serial.write(y);
Serial.write(z);

となります。
3つの値がXYZの順番で送られる場合、XYZ XYZ XYZ・・・と繰り返されますが、Processing側の読み取りを開始するタイミングがずれると、最初の幾つかの値をスキップしてしまい、YZX YZX YZX・・・、あるいはZXY ZXY ZXY・・・となってしまいます。このように、順番がずれないようにするためには通信上の工夫が必要となります。今回の方法では、3つの値を一方的に送るのではなく、受取確認をしながらお互いに通信します。以下にプログラムを書きます。

Arduino側のプログラム:
//x,y,zの3つの変数を用意し、初期値を0とする
int x=0;
int y=0;
int z=0;

void setup(){
  //シリアル通信開始
  Serial.begin(9600);
}

void loop(){
  //アナログ入力ピン0,1,2を
  //それぞれx,y,zに対応させる
  //値を4で割って最大値255にする
  x=analogRead(0)/4;
  y=analogRead(1)/4;
  z=analogRead(2)/4;

  //Processingから合図のデータが
  //一つ送られてきたらという条件
  if(Serial.available()>0){
    //x,y,zの順番で値を送る
    Serial.write(x);
    Serial.write(y);
    Serial.write(z);
    //先ほどのProcessingからの
    //合図のデータを読み込む
    Serial.read();
  }
}

Arduinoプログラム上のSerial.available()は、外部(この場合Processing)から1バイト分のデータが何個送られてきているかを数えてくれます。送られてくるデータは、Arduinoであれ、Processingであれ、それぞれのメモリ上(バッファ領域)に一旦貯められます。その後、Serial.read()によって、貯められているデータから順番にひとつずつ読み込む処理をします。もし、読み込む処理をしなければ、送られたデータは次々とバッファ領域に溜まり続けます(設定した限界をこえるとそれ以上貯めることはできなくなります)。if(Serial.available()>0){...}という条件は、データが1個以上貯まったら(0個より多い場合)、{...}内の処理をするという意味です。

このプログラムの場合の手順は以下のようになります。

(1)まず、Processingから合図用のデータが送られてくる。
(2)合図用のデータが、一旦Arduinoのバッファ領域に貯められる。
(3)Serial.available()で、現在何個データが貯まっているか数える。
(4)合図用のデータが1個以上あれば、(5)以下を実行する。
(5)Serial.write(x)で、新たなxの値を送信する。
(6)Serial.write(y)で、新たなyの値を送信する。
(7)Serial.write(z)で、新たなzの値を送信する。
(8)Serial.read()で、バッファ領域内の合図用のデータを読み込む。
(9)結果、バッファ領域が空になる(データの個数が0個になる)。
(10)Processingからの合図用のデータを待つ。

その後は(1)に戻り、同様の処理を繰り返します。要するに、Processingからの合図のデータが毎回1個送られて、それを確認後、Arduinoはx、y、zの値を送り返すという手順を踏みます。

次にProcessing側のプログラムを書きます。Arduinoのプログラムで書いたように、Processingからは、合図用のデータを1個送る必要があります。そうすれば、Arduinoから3個のデータが送られてくるので、Processingのバッファ領域にデータが3個貯まったときに、読み込む処理をさせればいいということになります。今回はx、y、zの3個の値なので3Dの図形を動かすプログラムにします。

Processing側のプログラム:
//シリアルライブラリを取り込む
import processing.serial.*;
//シリアルのオブジェクトmyPortを用意
Serial myPort;

//x,y,zの3個の変数を用意
int x=0;
int y=0;
int z=0;

void setup(){
  //3D用の画面サイズとして設定
  size(255,255,P3D);
  //シリアルポートの設定(「A4001Kjl」は基盤により異なる)
  //Windowsの場合は、"COM5"などとなる(前回ブログを参照)
  myPort=new Serial(this,"/dev/tty.usbserial-A4001Kjl",9600);
  //3D図形の塗りは無し(ワイヤーフレーム描画)
  noFill();
}

void draw(){
  //背景色を白に設定
  background(255);
  //3Dの位置座標にx,y,z入れる
  translate(x,y,z);
  //一辺50のボックス(立方体)を描画
  box(50);
}

//シリアル通信処理
void serialEvent(Serial p){
  //Arduinoから送られてきたデータが
  //3個(2より多い)の場合
  if(myPort.available()>2){
    //x,y,zの順番でデータを読み込む
    x=myPort.read();
    y=myPort.read();
    z=myPort.read();
    //読み込み後、合図データ送信
    myPort.write(65);
  }
}

//マウスが押されたら通信開始とする
void mousePressed(){
  //念のためバッファ領域を空にする
  myPort.clear();
  //とりあえず65という合図用データを送る
  myPort.write(65);
}

以上が、Processing側のプログラムです。3Dの場合は、size()の括弧内に画面幅、高さ以外に「P3D」を書き足します。ワイヤーフレームで描画した方が、今回の場合分かりやすいと思うので、noFill()をつかって塗り面を無しにしました。
void draw(){...}内のtranslate()は、その後に描画される3D図形の座標値をいれます。translate()に含まれる値が変化することで、3D図形は移動します。box()は、3Dの直方体を描画します。box(50,100,80)というように3つ値を入れれば、幅、高さ、奥行きをそれぞれ定義できます。box(50)の場合は、各辺が50の立方体になります。
シリアル通信の部分は、まずmousePressed()でマウスを押したときに、myPort.clear()でProcessingのバッファ領域内に貯まっているデータをとりあえず空にします(初期化)。そして通信を開始するきっかけとなる合図のデータをmyPort.write(65)で送ります。65という値を送っていますが、0〜255の値であれば何でも構いません。Arduino側は送られてくるデータの個数を数えるのであって、データの中身の値については、何でもいいことになります。つまり、合図用に何らかのデータを1個送ればいいということです。一旦合図用のデータがProcessingから送られれば、次にArduinoがそのデータを受取り、そして3個のデータを送り返してきます。void serialEvent(Serial p){...}内のif(myPort.available()>2){...}内では、Arduinoから送られて来たデータがProcessingのバッファ領域に3個貯まったら(2個より多くなったら)myPort.read()で3回読み込み、x,y,zの3個の変数にそれぞれ値を入れていき、それらのデータは、box()の3D座標になるtranslate()に代入され、box()が動きます。



もう一度、手順をはじめから書くと、

(1)Arduinoの電源がオンになり、Arduinoのプログラムが開始。
(2)Processingのプログラムを立ち上げる。
(3)Arduinoは、Processingからの合図用データを待つ。
(4)Processing側でマウスを押す(通信開始)。
(5)Processingのバッファ領域を一旦空にする(初期化)。
(6)Processingから合図用データが1個送られる。
 *以下、Arduino上での処理
(7)Arduinoのバッファ領域に合図用データが一旦貯められる。
(8)Arduinoのバッファ領域内のデータの個数を数える。
(9)データの個数が1個以上のとき、以下の処理を実行。
(10)Arduinoから、新たなxの値を送信する。
(11)Arduinoから、新たなyの値を送信する。
(12)Arduinoから、新たなzの値を送信する。
(13)バッファ領域内の合図用のデータを読み込む。
(14)読み込んだ結果、Arduinoのバッファ領域が空になる。
(15)次の合図用データがバッファ領域に貯まるまで待機。
 *以下、Processing上での処理
(16)Processingのバッファ領域にデータが貯められる。
(17)Processingのバッファ領域内のデータの個数を数える。
(18)データの個数が3個になったら、以下の処理を実行。
(19)xの値として1番目のデータを読み込む。
(20)yの値として2番目のデータを読み込む。
(21)zの値として3番目のデータを読み込む。
(22)その結果、Processingのバッファ領域が空になる。
(23)Processingから合図用データが1個送られる。
(24)次の3個のデータがバッファ領域に貯まるまで待機。
 *その後は(7)へ戻り処理を繰り返します。

Serial.available()
myPort.available()を使うことで、バッファ領域内に貯められているデータの個数を数え、その個数をもとにデータを送受信するタイミングを制御することができます。これ以外にも、送られる複数のデータの先頭部分や最後の部分に「.」(ピリオド)などの特定のデータを付け加えて送ることで、読み込み開始地点や読み込み終了地点を知らせる方法もあります。
シリアル通信を使うことで、コンピュータの内側と外側の世界をつなぐことができます。今後も様々な表現に応じて「シリアル通信」の技術は、繰り返し登場してきます。今回一気に理解できなくても、徐々に使いこなしていくことで、身についていくと思います。


関連:
Arduino-Processing シリアル通信1」(一つの値を送る/非同期通信)
Arduino-Processing シリアル通信3」(大きな値を複数送る)
Processing-Arduino シリアル通信4」(ProcessingからArduinoを制御する)
Arduino-Processing シリアル通信5」(複数の値を文字列で送信する)
Arduino-Processing シリアル通信6」 (2台のArduinoとProcessingを通信させる)


[目次:Processing関係]  [HOMEへ戻る]  [目次:Arduino関係]