CityCompilerで空間をプログラミングしよう!

建物へのプロジェクションマッピングやたくさんのモニタを使用するような大きな規模のインスタレーションを作りたいと思った時、 環境やハードウェアがある程度めぐまれていないとあれこれ試行錯誤するのは難しいです。そんな時に便利なのがCityCompilerです。 CityCompilerを使えば、バーチャルな世界にカメラやディスプレイ、プロジェクタなどをあれこれ置いてシミュレーションを行うことができます。 CityComplierを使って空間のプログラミングに挑戦しましょう!

CityCompilerとは

CityCompilerは空間を使ったインタラクティブなインスタレーションを作るためのプロトタイピング環境です。 Google SketchUpで作られた現実世界の3DモデルとProcessingのソースコードを組み合わせて、 バーチャルな3D空間上でインタラクティブシステムのシミュレーションを行うことができます。

CityCompilerは慶應義塾大学 中西泰人研究室において開発されています。 CityCompilerそのものはJavaのクラスライブラリです。jar形式で提供され、Eclipseなどの統合開発環境(IDE)で利用することができます。

セットアップと動作確認

プログラミングする人もしない人も、とにかくインストールして動かしてみましょう!


1. インストールするもの

CityCompilerを使うには以下のものが必要です。

CityCompilerではJavaのプログラミングを行うため、EclipseとJDKが必要です。EclipseはJavaやC++など各種プログラミングを行うための統合開発環境です。 JDKはJavaのプログラミングを行うための開発キットです。 Windowsの場合は、日本語化されたEclipseにJDKを同梱したセット(Pleiades All in One)が配布されていますので、こちらを利用するのが便利です。 Processingはメディアアートやビジュアルデザインのためのプログラミング言語およびその統合開発環境です。v2.0を使うことができます。 まずはEclipseとJDK、Processingをインストールしてください。


2. CityCompilerのセットアップ

  1. CityCompilerの配布ページ(GitHub)にアクセスし、「ZIP」ボタンを押してzip形式でファイル一式をダウンロードします。



  2. ダウンロードしたZIPファイル(CC4p52-master.zip)を解凍します。
  3. Eclipseのワークスペース(作業用のディレクトリ)を任意の場所に作り、そこに解凍してできた「CC4p52-master」というフォルダを移動します。 ワークスペースはWindowsであれば「C:\workspace」、Macであれば「/Users/ユーザ名/Documents/workspace」などに作ります。



  4. Eclipseを起動し、ワークスペースを選択します。



  5. Project Explorerのところで右クリックし、[Import] を選択します。



  6. 出てきたダイアログでは、[General] - [Existing Projects into Workspace] を選択して [Next] を押します。



  7. [Select root directory:] という項目で、先ほど「CC4p52-master」を置いたディレクトリを指定します。 プロジェクトが検出されると、一覧のところに「CC4p52-master」が現れます。 プロジェクトが選択された状態なのを確認したら [Finish] ボタンを押します。



  8. 以下のようにプロジェクトが追加されたらOKです。



  9. Macの場合、もしエラーが出ていたら、プロジェクト(CC4p52-master)を右クリックしてメニューから[Properties]を選択し、出てきた画面で文字エンコードを「SJIS」から「UTF-8」に変更してください。


3. 既存のサンプルの実行

動作確認を兼ねて既存のサンプルプログラムをいくつか実行してみましょう。 プロジェクト・エクスプローラーで [CC4p52-master] - [src] - [simulation] と辿り、そこに並んでいるいずれかのjavaファイルをダブルクリックします。 ファイルを開いたら、実行ボタンを押してください。実行するとJMEの設定ウィンドウが表示される場合がありますが、そのまま [Ok] ボタンを押してください。


MovingCameraMirror2Simulation
動くカメラと画像処理のサンプル

DisplayGridSimulation
上空にたくさんのディスプレイ

ShadowProjection2DisneyHallSimulation
ディズニーホールにプロジェクションマッピング

ShadowProjection2BigsightSimulation
東京ビッグサイトにプロジェクションマッピング

LakeDisplaySimulation
湖畔に映り込む球体ディスプレイ

MovingProjectorSimulation
動くプロジェクタのサンプル

PendulumnDisplaySimulation
振り子状ディスプレイとそれに連動した物理演算

ExhibitionPlaningSimlation
ディスプレイを美術館風に配置

DistanceSensingObjectSimulation
距離センサと連動したオブジェ

KineticProjectionSimulation
うにうに変形する物体にプロジェクション

KineticMonitorSimulation
色を変化させながらうにうに変形する物体

DisplayLandscapeNew
任意の場所にディスプレイを配置

CityCompilerを使ったプログラミングの全体像

プログラミングをはじめる前に全体の概要について説明します。これが把握できていると以降のプログラミングもスムーズに進められるでしょう。


書くのは2種類のコード

CityComplierを使ったプログラミングでは、大きく分けて2種類のコードを書きます。 3Dバーチャル空間に関するコードと、グラフィックスに関するコードです。


CityCompilerプログラミングで書く2種類のコード


3Dバーチャル空間に関するコードでは、草原や室内などの箱庭的な空間を作り、カメラやプロジェクタをどこに置くのか、 それらがどのように動くのかを記述します。 バーチャル空間に関する処理は jMonkeyEngine(JME)というJava用のゲームエンジンライブラリとCityComplierを使って記述します。

グラフィックスに関するコードでは、ディスプレイやプロジェクタに表示する2Dまたは3Dのグラフィックスに関する処理を記述します。 このコードはProcessingを使って記述します。 グラフィックス処理の記述にProcessingを使うことのメリットは、OpenProcessingなどで共有されている既存のコードを再利用でき、 なおかつバーチャル空間と現実世界の双方で同じコードを利用することができるという点です。

これら2つのコードはいずれもJava形式のファイルです。グラフィックスに関するコードはProcessingのIDE上で書いたものをJava形式に変換して使います。 変換と言っても、Processingは内部的にはJavaとして動作しているため、元のコードとほぼ同等のコードが生成されます。


全体の処理の流れ

全体の処理構造を下図に示します。JMEによる3Dバーチャル空間の処理とProcessingのアプレットという2種類のスレッドが動作しています。


CityCompilerのプログラムにおける全体の処理の流れ

プログラムを実行すると、まずsimpleInitApp()というメソッドによってバーチャル空間の初期化が行われます。 その後、終了の合図があるまでsimpleUpdate()によってバーチャル空間の更新が繰り返し行われます。 バーチャル空間の初期化時には、カメラやディスプレイなどのオブジェクトを作成しますが、 この時、それらに対してProcessingのアプレットが紐づけられます。 Processing側では、setup()での初期化後、終了の合図があるまでdraw()による描画処理が繰り返し実行されます。

バーチャル空間内のカメラが撮影した画像をProcessing側のCaptureに渡したり、 逆にProcessing側でimage()で描画したグラフィックスをバーチャル空間のディスプレイに渡したり、という「橋渡し」をやるのがCityComplierの役目です。

ゼロからプログラムを書いてみよう

いちばん簡単なサンプルとして、バーチャル空間に1つのディスプレイを置き、そこにProcessingで作られたグラフィックスを表示する、というのに挑戦してみましょう。 これで基本的なプログラミングの手順を習得しましょう。



こんな感じのを作ります

1. Processingでコードを書こう


まずはディスプレイに表示するグラフィカルなコンテンツをProcessingで作りましょう。 以下は画面上に表示されたボールが跳ねるコードです。「BounceBall」という名前でスケッチを保存してください。


BounceBall.pde
						int size = 60;       // Width of the shape
						float xpos, ypos;    // Starting position of shape    
						
						float xspeed = 2.8;  // Speed of the shape
						float yspeed = 2.2;  // Speed of the shape
						
						int xdirection = 1;  // Left or Right
						int ydirection = 1;  // Top to Bottom
						
						void setup() {
						  size(640, 200);
							frameRate(30);
						  smooth();
						  xpos = width/2;
						  ypos = height/2;
						}
						
						void draw() {
						  background(100);
						  
						  xpos = xpos + ( xspeed * xdirection );
						  ypos = ypos + ( yspeed * ydirection );
						  
						  if (xpos > width-size || xpos < 0) {
						    xdirection *= -1;
						  }
						  if (ypos > height-size || ypos < 0) {
						    ydirection *= -1;
						  }
						
						  ellipse(xpos+size/2, ypos+size/2, size, size);
						}

まずはこれをProcessingで書いて動かしてみましょう。Processingを起動してソースコードをコピペしたら、[Run]ボタンを押して実行してください。


実行するとこんな感じのウィンドウが表示されます。


ボールが飛んで画面の端で跳ね返ります


2. EclipseでProcessingのコードを動かそう


Processingで実行してみて動くのが確認できたら、Javaのコードに変換します。Processingの [File] メニューから [Export Application] を選択してください。 出てきたダイアログでいま使用しているOSにチェックが入っているのを確認したら、[Export]ボタンを押してください。


Exportすると、スケッチを保存したフォルダの中に「application.windows32」や「application.macosx」という名前のフォルダが自動的に作られ、 OSごとの実行ファイルが生成されます。そして、さらにその中にある「source」というフォルダの中にjava形式のソースコードが入っています。 ここでのお目当てはjava形式のデータです。

先ほどのコードを変換して得られるBounceBall.javaは以下のようなコードになっています。


BounceBall.java
						import processing.core.*; 
						import processing.xml.*; 
						
						import java.applet.*; 
						import java.awt.Dimension; 
						import java.awt.Frame; 
						import java.awt.event.MouseEvent; 
						import java.awt.event.KeyEvent; 
						import java.awt.event.FocusEvent; 
						import java.awt.Image; 
						import java.io.*; 
						import java.net.*; 
						import java.text.*; 
						import java.util.*; 
						import java.util.zip.*; 
						import java.util.regex.*; 
						
						public class BounceBall extends PApplet {
						
						int size = 60;       // Width of the shape
						float xpos, ypos;    // Starting position of shape    
						
						float xspeed = 2.8f;  // Speed of the shape
						float yspeed = 2.2f;  // Speed of the shape
						
						int xdirection = 1;  // Left or Right
						int ydirection = 1;  // Top to Bottom
						
						public void setup() {
						  size(640, 200);
						  frameRate(30);
						  smooth();
						  xpos = width/2;
						  ypos = height/2;
						}
						
						public void draw() {
						  background(100);
						  
						  xpos = xpos + ( xspeed * xdirection );
						  ypos = ypos + ( yspeed * ydirection );
						  
						  if (xpos > width-size || xpos < 0) {
						    xdirection *= -1;
						  }
						  if (ypos > height-size || ypos < 0) {
						    ydirection *= -1;
						  }
						
						  ellipse(xpos+size/2, ypos+size/2, size, size);
						}
						  static public void main(String args[]) {
						    PApplet.main(new String[] { "--bgcolor=#F0F0F0", "BounceBall" });
						  }
						}

Processingで書いたコードの前後にいろいろと追加されているのがわかります。 何をやっているのかというと、Javaとして動かすためにPApplet型の派生クラスにしています。 また、そのために必要なライブラリをimportし、実行できるようにエントリポイント(static public void main(String args[]))を追加しています。 すなわち、Processingは内部でこういうコードに変換してからJavaとして実行していたわけです。


ではこのJavaのコードをEclipse上で動かしてみましょう。

  1. Eclipseを起動し、CityCompilerのプロジェクトフォルダがあるワークスペースを開きます。
  2. プロジェクト [CC4p52-master] を右クリックし、[New] - [Package] を選択します。
  3. 名前に適当なパッケージ名を付けます。ここでは「cctest」とします。



  4. この操作によって、ワークスペースの中に「CC4p52-master/src/cctest」というディレクトリが自動的に作られますので、 そこに先ほど生成した BounceBall.java をコピーします。


  5. Eclipse上でプロジェクト [CC4p52-master] を右クリックし、[Refresh] を選択すると、プロジェクトに BounceBall.java が追加されます。
  6. プロジェクトに追加された BounceBall.java を開きます。
  7. パッケージが一致しないというエラーが出るので、ソースコードの1行目に package cctest; と書きます。
  8. エラーがなくなったら、メニューから [Run] - [Run As] - [Java Applet] を選択し、実行します。

実行するとこのようなウィンドウが表示されます。


ウィンドウの見た目がちょっと違いますが、表示内容は一緒です

これでEclipseでもProcessingのコードを動かすことができました。もちろん最初からEclipseでProcessingのコードを書き始めても良いですが、 慣れないうちはここで説明したようにProcessingからJavaに変換する方法がおすすめです。


3. バーチャル空間側のコードを書こう


ここからは新しくクラスを作り、バーチャル空間側のコードを書いていきます。

  1. 先ほど自分で作ったパッケージ(cctest)の上で右クリックし、[New] - [Class] を選択します。
  2. [Name] という項目で適当な名前を付けます。ここでは「DisplayTest」という名前を付けてください。
  3. [Superclass] では com.jme3.app.SimpleApplication を指定します。
  4. [Modifiers] では [public] を選択します。
  5. [public static void main(String[] args)] と[Inherited abstract methods]にチェックを入れます。
  6. 以上の設定ができたら、[Finish] ボタンをクリックします。



この作業を終えると、ベースとなる何もしないプログラムが生成されます。ここに自分でコードを書いていくことになります。

DisplayTest.java
					package cctest;
					
					import com.jme3.app.SimpleApplication;
					
					public class DisplayTest extends SimpleApplication {
					
						@Override
						public void simpleInitApp() {
							// TODO Auto-generated method stub
					
						}
					
						/**
						 * @param args
						 */
						public static void main(String[] args) {
							// TODO Auto-generated method stub
					
						}
					
					}

では以下のようにコードを書き加えてみましょう。

DisplayTest.java
					package cctest;
					
					import com.jme3.app.SimpleApplication;
					import com.jme3.util.SkyFactory;
					import net.unitedfield.cc.PAppletDisplayGeometry;
					
					public class DisplayTest extends SimpleApplication {

						public void simpleInitApp() {
					
							// 空を作成
							rootNode.attachChild(SkyFactory.createSky(assetManager, "Textures/Sky/Bright/BrightSky.dds", false));
					
							// Processingのアプレットを作成
							BounceBall applet = new BounceBall();
					
							// ディスプレイを作成し、そこにProcessingのアプレットを設定
							PAppletDisplayGeometry display = new PAppletDisplayGeometry("display", assetManager, 6, 2, applet, 640, 200, true);
							rootNode.attachChild(display);
							display.setLocalTranslation(0, 0, -3);
						}

						public void simpleUpdate(float tpf) {
							/* 毎フレーム行う処理をここに記述 */
						}

						public void destroy() {
							super.destroy();
							System.exit(0);
						}
					
						public static void main(String[] args) {
							SimpleApplication app = new DisplayTest();  // SimpleApplicationのインスタンスの生成
							app.setPauseOnLostFocus(false);             // フォーカスがロストした場合にポーズしない設定
							app.start();                                // シミュレーションのスタート
						}
					}

実行すると、以下のようになります。荒野の中にProcessingのプログラムが走っているディスプレイが鎮座しているシュールな絵になりました。 マウスの移動で首振り、ホイールで前後の移動ができます。また、キーボードのAとDで左右、WとSで前後の移動をし、方向キーで首振りができます。



4. バーチャル空間側のコードを理解しよう


このコードは、main()、simpleInitApp()、simpleUpdate()、destroy()の4つのメソッドから構成されています。 それぞれの役割を細かく見ていきましょう。


main() からスタート

main()が最初に処理が実行される場所です。 ここではSimpleApplicationのインスタンスを生成し、app.start()によってシミュレーションをスタートさせます。 また、必要に応じてウィンドウの挙動に関するオプションを設定します。 ここでは画面からフォーカスが失われた場合でもポーズせずに描画処理を続行する設定を行っています。


main()の中身
					public static void main(String[] args) {
							SimpleApplication app = new DisplayTest();  // SimpleApplicationのインスタンスの生成
							app.setPauseOnLostFocus(false);             // フォーカスがロストした場合にポーズしない設定
							app.start();                                // シミュレーションのスタート
					}

simpleInitApp() で空間の構成要素を設定

simpleInitApp()がバーチャル空間の初期化時に実行されるメソッドです。ここに「何をどこ置くのか」に関する処理を記述します。 上のコードでは、「空」と「ディスプレイ」を空間に追加しています。 空間にオブジェクトを追加する処理を行っているのがrootNode.attachChild()という部分です。 空を作っては空間に追加、ディスプレイを作っては空間に追加、という感じでいろんな要素を空間に追加していきます。


simpleInitApp()
						public void simpleInitApp() {
					
							// 空を作成
							rootNode.attachChild(SkyFactory.createSky(assetManager, "Textures/Sky/Bright/BrightSky.dds", false));
					
							// Processingのアプレットを作成
							BounceBall applet = new BounceBall();
					
							// ディスプレイを作成し、そこにProcessingのアプレットを設定
							PAppletDisplayGeometry display = new PAppletDisplayGeometry("display", assetManager, 6, 2, applet, 640, 200, true);
							rootNode.attachChild(display);
							display.setLocalTranslation(0, 0, -3);
						}
					

ここでいちばん大事なのが、Processingの処理結果を表示するディスプレイを作成しているところです。 まず、BounceBall applet = new BounceBall()とやってProcessingのアプレットのインスタンスを作成します。 次に、PAppletDisplayGeometryによってディスプレイを作成します。コンストラクタの第2・第3引数が空間中での物理サイズです。 物理サイズの単位はメートルです。第4引数にProessingのアプレットを指定し、第5・第6引数にProcessing側でsize()で指定している値を与えます。 これらの設定によってディスプレイに対してProcessingのアプレットが紐づけられます。 作ったディスプレイはrootNode.attachChild()によって空間に追加します。そして最後に、display.setLocalTranslation()によって 空間中の位置を設定します。


PAppletDisplayGeometryのコンストラクタの引数
						PAppletDisplayGeometry( String       name,         // 名前
						                        AssetManager assetmanager, // assetManager
						                        float        width,        // バーチャル空間内におけるディスプレイの横幅
						                        float        height,       // バーチャル空間内におけるィスプレイの高さ
						                        PApplet      applet,       // Processingのアプレット
						                        int          appletWidth,  // Processingのアプレットの画面の横幅
						                        int          appletHeight, // Processingのアプレットの画面の高さ
						                        boolean      frameVisible  // 別ウィンドウでアプレットの実行結果を表示するか
						                      );

simpleUpdate() には毎フレームの処理を記述

このプログラムでは特に処理を記述していませんが、simpleUpdate()にはバーチャル空間が更新されるタイミングで実行したい処理を記述します。 例えば、ディスプレイやカメラが空間中を移動するアニメーションを作りたい場合、ここにその動き方を記述します。 引数の tpf は time per frame の略でフレーム間の経過時間を意味します。 描画にかかる時間は変動しますので、物体を一定の速度で動かしたければ、tpfに比例した移動量を与えてください。


simpleUpdate()
						public void simpleUpdate(float tpf) {
							/* 毎フレーム行う処理をここに記述 */
						}

destroy() で終了処理

終了処理はdestroy()という名前のメソッドを作ってそこに記述します。このメソッドが終了時に自動的に呼ばれます。 慣例的にsuper.destroy()とSystem.exit(0)の2つをやると覚えてしまってOKです。


destroy()
					public void destroy() {
						super.destroy();
						System.exit(0);
					}

5. うまく動きましたか?


以上がCityCompilerを使ったプログラミングの基本的な流れです。 これがわかっていれば、既存のサンプルコードを切ったり貼ったりして新しいものが作れるはずです。 既存のコードのパッケージ構成は以下のようになっています。


最初のうちは、simulationやtest.ccに入っているコードで呼ばれているProcessingのアプレットを他のものに差し替えたり、 simpleUpdate()の中を適当に書き変えてディスプレイやプロジェクタの動き方を変更したり、いろいろやってみましょう。


カメラを使ってみよう

バーチャル空間にカメラとディスプレイを置いて、カメラで撮影した結果をそのままディスプレイに表示させるのをやってみましょう。




バーチャル空間側のコード

さきほど紹介したディスプレイを置くだけのサンプルにカメラを追加しています。また、simpleUpdate()でカメラを回転させています。


CameraTest.java
					package cctest;
					
					import com.jme3.app.SimpleApplication;
					import com.jme3.util.SkyFactory;
					import net.unitedfield.cc.PAppletDisplayGeometry;
					import net.unitedfield.cc.CaptureCameraNode;
					
					
					public class CameraTest extends SimpleApplication {
					
						private CaptureCameraNode captureCameraNode; // カメラ
					
						// 初期化
						public void simpleInitApp() {
					
							// 空を作る
							rootNode.attachChild(SkyFactory.createSky(assetManager, "Textures/Sky/Bright/BrightSky.dds", false));
					
							// Processingのアプレット
							CameraPApplet applet = new CameraPApplet();
					
							// ディスプレイを作る
							PAppletDisplayGeometry display = new PAppletDisplayGeometry("display", assetManager, 4, 3, applet, 320, 240, true);
							rootNode.attachChild(display);
							display.setLocalTranslation(0, 0, -3);
					
							// カメラを作る
					    	captureCameraNode = new CaptureCameraNode("cameraNode", 320, 240, assetManager, renderManager, renderer, rootNode);
					    	rootNode.attachChild(captureCameraNode);
					    	captureCameraNode.setLocalTranslation(0.0f, 0.5f, 8.0f); // カメラの位置を設定
					    	if (applet.realDeployment==false) {
					    		applet.setCapture(captureCameraNode.getCapture()); // Processingのキャプチャを設定
					    	}
						}
					
						// 更新処理
						public void simpleUpdate(float tpf) {
							captureCameraNode.rotate(0f, 0.2f*tpf, 0f); // カメラの回転
					    }
					
						// 終了処理
						public void destroy() {
							super.destroy();
							System.exit(0);
						}
					
						// メイン
						public static void main(String[] args) {
							SimpleApplication app = new CameraTest();
							app.start();
						}
					
					}


グラフィックス側のコード

このコードは、バーチャル空間に置かれるバーチャルカメラと、PCに接続されているリアルカメラの両方を扱うコードになっています。 realDeploymentという変数で本物を使うかどうかを切り変えていて、realDeployment = trueのときにリアルカメラを使います。 リアルカメラの場合は new Capture(this, width, height) によってカメラを初期化し、video.startによってキャプチャをスタートさせます。 バーチャルカメラの場合は、setCapture()という自前のメソッドで外部(バーチャル空間側)から与えられるキャプチャを設定します。


CameraPApplet.java
					package cctest;
					
					import processing.core.*;
					import processing.video.*;
					
					public class CameraPApplet extends PApplet {
					
						public boolean realDeployment = false;
						Capture video = null;
						PImage videoImage = null;
					
						public void setup() {
							size(320, 240, P2D);
					
							if (realDeployment) {
								video = new Capture(this, width, height);
								video.start();
							}
					
							videoImage = new PImage(width,height);
						}
					
						public	void setCapture(Capture capture){
							this.video = capture;
						}
					
						public void draw() {
							  video.read();

							  // カメラ画像の表示
							  //this.image(video, 0, 0);
							  video.loadPixels();
							  arrayCopy( video.pixels, videoImage.pixels);
							  videoImage.updatePixels();
							  image(videoImage,0,0);
					
							  // 白い太枠を表示
						      strokeWeight(15);
						      stroke(255);
						      noFill();
						      rect(0,0,width,height);
						}
					
						public static void main(String[] args){
							PApplet.main(new String[] { "--bgcolor=#c0c0c0", "CameraApplet" });
						}
					}

カメラの使い方


カメラはCaptureCameraNodeで作ります。注意すべきポイントは、第2・第3引数で設定されるカメラの画像サイズです。 これは必ずProcessing側で設定しているサイズと同じにしてください(ここでは320,240)。 カメラを作ったらrootNode.attachChild()で空間に追加し、.setLocalTranslation()で位置を決めます。最後に、 バーチャル空間にあるカメラのデータをProcessing側で処理できるようにするために、.getCapture()によってCaptureを取得し、 setCapture()でProcessing側に渡します。


カメラを作るコード
							// カメラを作る
					    	captureCameraNode = new CaptureCameraNode("cameraNode", 320, 240, assetManager, renderManager, renderer, rootNode);
					    	rootNode.attachChild(captureCameraNode);
					    	captureCameraNode.setLocalTranslation(0.0f, 0.5f, 8.0f); // カメラの位置を設定
					    	if (applet.realDeployment==false) {
					    		applet.setCapture(captureCameraNode.getCapture()); // Processingのキャプチャを設定
					    	}

このサンプルでは、カメラの映像をそのままディスプレイに出しています。 ディスプレイとカメラが向かい合わせになった時に起こる「合わせ鏡」現象もしっかり再現されます。


ディスプレイとカメラの「合わせ鏡」現象!

もちろんなんらかの画像処理を行った結果を表示することもできます。 画像処理をやりたいときは MovingCameraMirror2Simulation というサンプルが参考になります。 このサンプルで呼び出されているProcessingのアプレットのコードはMirror2PAppletです。



カメラの回転と画像処理を行うサンプル(MovingCameraMirror2Simulation)

プロジェクタを使ってみよう

プロジェクタがあればプロジェクションマッピングが作れるようになります。 また、単純にディスプレイをプロジェクタに置き換えるだけでもいきなり楽しくなります。 ここではプロジェクタ、床、物体、人のモデルなどを出す方法を紹介します。




バーチャル空間側のコード
ProjectorTest.java
package cctest;
 
import processing.core.PApplet;
import test.p5.ColorBarsPApplet;
import net.unitedfield.cc.PAppletProjectorNode;
import net.unitedfield.cc.PAppletProjectorShadowNode;

import com.jme3.app.SimpleApplication;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.post.TextureProjectorRenderer;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;


public class ProjectorTest extends SimpleApplication  {
 
    // 初期化
    public void simpleInitApp() {

    	// Processingのアプレットの作成
        PApplet applet = new ColorBarsPApplet();

        // プロジェクタの設定(通常版)
        PAppletProjectorNode projector = new PAppletProjectorNode("projector0", assetManager, applet, 200, 200, false);        
        rootNode.attachChild(projector);                          // 空間にプロジェクタを追加
        rootNode.attachChild(projector.getFrustmMdel());          // プロジェクタの視野角を表示
        projector.setLocalTranslation(new Vector3f(0,6,0));       // プロジェクタの位置
        projector.lookAt(new Vector3f(0, 0, 0), Vector3f.UNIT_X); // プロジェクタの注視点
        TextureProjectorRenderer ptr = new TextureProjectorRenderer(assetManager); // このプロジェクタ用のレンダリングの設定
        ptr.getTextureProjectors().add(projector.getProjector());
        viewPort.addProcessor(ptr);

        /*
        // プロジェクタの設定(Shadow版)
        PAppletProjectorShadowNode projector = new PAppletProjectorShadowNode("Projector0", viewPort, assetManager, 1024, 1024, applet, 200, 200, false);
        rootNode.attachChild(projector);                          // 空間にプロジェクタを追加
        projector.setLocalTranslation(new Vector3f(0,6,0));       // プロジェクタの位置
        projector.lookAt(new Vector3f(0, 0, 0), Vector3f.UNIT_X); // プロジェクタの注視点
        */		
		
        // 照明
        DirectionalLight dl = new DirectionalLight();
        dl.setDirection(new Vector3f(-0.1f, -1f, -1).normalizeLocal());
        rootNode.addLight(dl);
 
        // 床面
        Material textureMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        textureMat.setTexture("ColorMap", assetManager.loadTexture("myAssets/Textures/woodFloor.jpg"));
        Box floor = new Box(Vector3f.ZERO, 5.0f, 0.01f, 5.0f);
        Geometry floorGeom = new Geometry("Floor", floor);
        floorGeom.setMaterial(textureMat);
        rootNode.attachChild(floorGeom);
 
        // 球
        Material whitemat = assetManager.loadMaterial("Common/Materials/WhiteColor.j3m");
        Sphere sp = new Sphere(64, 64, 1.0f);
        Geometry sphereGeom = new Geometry("Sphere", sp);
        sphereGeom.updateModelBound();
        sphereGeom.setMaterial(whitemat);
        sphereGeom.setLocalTranslation(0, 1.3f, 0);
        rootNode.attachChild(sphereGeom);

        // 女性
        Spatial girl = assetManager.loadModel("myAssets/Models/WalkingGirl/WalkingGirl.obj");
        girl.rotate(0, (float)(Math.PI)*1.3f, 0);
        girl.setLocalTranslation(1f, 0, 1f);
        this.rootNode.attachChild(girl);
 
        // 影の設定
        girl.setShadowMode(ShadowMode.CastAndReceive);
        floorGeom.setShadowMode(ShadowMode.CastAndReceive);
        sphereGeom.setShadowMode(ShadowMode.CastAndReceive);
 
        // 視点
        cam.setLocation(new Vector3f(0, 1.7f, 6));
    }
 
    // 更新処理
    public void simpleUpdate(float tpf) {
    	/* プロジェクタや物体、人などを動かしたいときはここにその処理を書きます */
    }
 
    // 終了処理
    public void destroy() {
        super.destroy();
        System.exit(0);
    }
 
    // メイン
    public static void main(String[] args){
        SimpleApplication app = new ProjectorTest();
        app.start();
    }
}


グラフィックス側のコード

虹模様のカラーバーが動くアプレットです。これはあくまで一例ですので、写真や動画、幾何学模様のアニメーションなどいろいろ試してみてください。



カラーバーのアニメーション

ColorBarsPApplet.java
					package cctest;

					import processing.core.PApplet;
					
					public class ColorBarsPApplet extends PApplet {
					
						int BAR_NUM = 100;
						float[] x = new float[BAR_NUM];
						float[] xSpeed = new float[BAR_NUM];
						float[] bWidth = new float[BAR_NUM];
						int[] bColor = new int[BAR_NUM];
					
						public void setup() {
							size(200, 200);
							frameRate(30);
							smooth();
							colorMode(HSB, 360, 100, 100, 100);
							noStroke();
							for (int i=0; i<BAR_NUM; i++) {
								x[i] = random(width);
								xSpeed[i] = random(-1, 1);
								bWidth[i] = random(2, 200);
								bColor[i] = color(random(360), random(90, 100), random(50, 100), 50);
							}
						}
					
						public void draw() {
							background(0);
							for (int i=0; i<BAR_NUM; i++) {
								fill(bColor[i]);
								rect(x[i], 0, bWidth[i], height);
								x[i] += xSpeed[i];
								if (x[i] > width || x[i] < -bWidth[i]) {
									xSpeed[i] *= -1;
								}
							}
						}
					}

プロジェクタ・照明・物体の作り方と影の設定


いろいろな処理をやっていてさも複雑そうに見えますが、内容ごとに処理がきれいにまとまっているのでプログラムの構造は単純です。 一行一行無理して理解しようとせず、「あー この物体を入れたいときはこの数行を入れればいいのか」ぐらいの理解で大丈夫です。 個別に見ていきましょう。


プロジェクタ

プロジェクタは映像出力装置なので、ディスプレイと似たような扱いです。 最初にProcessingのアプレットを作り、プロジェクタを作るときに紐づけます。 プロジェクタを作ったら空間に追加し、必要に応じてプロジェクタの位置と向き(注視点)を設定します。


通常版プロジェクタ
        // プロジェクタの設定(通常版)
        PAppletProjectorNode projector = new PAppletProjectorNode("projector0", assetManager, applet, 200, 200, false);        
        rootNode.attachChild(projector);                          // 空間にプロジェクタを追加
        rootNode.attachChild(projector.getFrustmMdel());          // プロジェクタの視野角を表示
        projector.setLocalTranslation(new Vector3f(0,6,0));       // プロジェクタの位置
        projector.lookAt(new Vector3f(0, 0, 0), Vector3f.UNIT_X); // プロジェクタの注視点
        TextureProjectorRenderer ptr = new TextureProjectorRenderer(assetManager); // このプロジェクタ用のレンダリングの設定
        ptr.getTextureProjectors().add(projector.getProjector());
        viewPort.addProcessor(ptr);

プロジェクタには「PAppletProjectorNode」と「PAppletProjectorShadowNode」との2種類があります。 「Shadow」と付くほうは、物体によって生じる影を考慮した投影が行われます。 Shadow版プロジェクタを使いたいときは、上記の通常版プロジェクタに関する処理をコメントアウトし、 代わりにShadow版プロジェクタに関する以下の処理を有効にしてください。


Shadow版プロジェクタ
        // プロジェクタの設定(Shadow版)
        PAppletProjectorShadowNode projector = new PAppletProjectorShadowNode("Projector0", viewPort, assetManager, 1024, 1024, applet, 200, 200, false);
        rootNode.attachChild(projector);                          // 空間にプロジェクタを追加
        projector.setLocalTranslation(new Vector3f(0,6,0));       // プロジェクタの位置
        projector.lookAt(new Vector3f(0, 0, 0), Vector3f.UNIT_X); // プロジェクタの注視点


Shadow版プロジェクタでの実行結果

通常版のほうがShadow版に比べて物体表面での映像の映りがきれいですが、影がつかないので状況に応じて使い分けてください。


照明

照明にはいろいろな種類がありますが、ここで扱っているのは平行光源です。太陽光と同じだと考えてください。 平行光源はDirectionalLightによって作成します。 平行光源では、ある方向から空間全体に光が降り注ぐため、位置情報は持っていません。 ここでは.setDirection()で方向を設定しています。 空間への照明の追加はrootNode.addLight()という点に気を付けてください。


照明の作り方
			// 照明
			DirectionalLight dl = new DirectionalLight();
			dl.setDirection(new Vector3f(-0.1f, -1f, -1).normalizeLocal());
			rootNode.addLight(dl);

物体

ここでは床面、球、女性という3種類のCG物体が登場します。それぞれ数行のコードで空間に追加することができます。


床のCGを追加する
        // 床面
        Material textureMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        textureMat.setTexture("ColorMap", assetManager.loadTexture("myAssets/Textures/woodFloor.jpg"));
        Box floor = new Box(Vector3f.ZERO, 5.0f, 0.01f, 5.0f);
        Geometry floorGeom = new Geometry("Floor", floor);
        floorGeom.setMaterial(textureMat);
        rootNode.attachChild(floorGeom);
球のCGを追加する
        // 球
        Material whitemat = assetManager.loadMaterial("Common/Materials/WhiteColor.j3m");
        Sphere sp = new Sphere(64, 64, 1.0f);
        Geometry sphereGeom = new Geometry("Sphere", sp);
        sphereGeom.updateModelBound();
        sphereGeom.setMaterial(whitemat);
        sphereGeom.setLocalTranslation(0, 1.3f, 0);
        rootNode.attachChild(sphereGeom);
女性のCGを追加する
		// 女性
		Spatial girl = assetManager.loadModel("myAssets/Models/WalkingGirl/WalkingGirl.obj");
		girl.rotate(0, (float)(Math.PI)*1.3f, 0);
		girl.setLocalTranslation(1f, 0, 1f);
		this.rootNode.attachChild(girl);

それぞれ作り方が微妙に異なっています。床と球では基礎形状(BoxおよびSphere)を作成してそこに材質を与える処理を行っているのに対し、 女性のCGではあらかじめ材質情報を持っているOBJ形式のCGデータをロードしています。 人や建物などの複雑な形状を持つものに関しては、なんらかのモデリングツール(例えばGoogleSketchUp)でOBJ形式のCGデータを作成し、それを読み込むのが手軽です。

CityCompilerのセットの中にはいくつか建物のCGデータが同梱されています。 例えば、同梱されている東京駅のモデルを出すには以下のようなコードを書きます。

東京駅のCGを追加する
			Spatial model = assetManager.loadModel("myAssets/Models/TokyoStation/TokyoStation.obj");
			rootNode.attachChild(model);

影の設定

GeometryやSpatialで作られた物体に対して、それぞれどのように影が映るかを設定する必要があります。 これには .setShadowMode() というメソッドを使います。通常は引数に ShadowMode.CastAndReceive(影を出す&受ける)を指定してください。


物体ごとに影の映り方を決める
			// 影の設定
			girl.setShadowMode(ShadowMode.CastAndReceive);
			floorGeom.setShadowMode(ShadowMode.CastAndReceive);
			sphereGeom.setShadowMode(ShadowMode.CastAndReceive);

GUIからディスプレイやカメラを移動させよう

ディスプレイやプロジェクタ、カメラなどをバーチャル空間に置いてみた後に、位置や向きをいろいろと変えてみたくなる場合があります。GUIウィンドウを使ってそれぞれの物体を動かすことができるSpatialUtilクラスが用意されています。

次のサンプルは、その上からプロジェクターが木の床の上に置かれた樹に静止画を投影し、その横にアプレットを表示する平面のディスプレイと球体のディスプレイが置かれている、というサンプルです。



HelloSpatiaInspectorの実行画面


HelloSpatialInspector.java
			package test.cc;
			
			import net.unitedfield.cc.PAppletDisplayGeometry;
			import net.unitedfield.cc.PAppletProjectorShadowNode;
			import net.unitedfield.cc.util.SpatialInspector;
			import processing.core.PApplet;
			import test.p5.ColorBarsPApplet;
			
			import com.jme3.app.SimpleApplication;
			import com.jme3.light.AmbientLight;
			import com.jme3.light.DirectionalLight;
			import com.jme3.material.Material;
			import com.jme3.math.ColorRGBA;
			import com.jme3.math.Vector3f;
			import com.jme3.renderer.queue.RenderQueue.Bucket;
			import com.jme3.renderer.queue.RenderQueue.ShadowMode;
			import com.jme3.scene.Geometry;
			import com.jme3.scene.Mesh;
			import com.jme3.scene.Spatial;
			import com.jme3.scene.shape.Box;
			import com.jme3.scene.shape.Sphere;
			import com.jme3.texture.Texture2D;
			import com.jme3.util.SkyFactory;
			
			public class HelloSpatiaInspector extends SimpleApplication {
				
				@Override
				public void simpleInitApp() {
					//cam
					cam.setLocation(Vector3f.UNIT_XYZ.mult(15.0f));   // camera moves to 15, 15, 15 
					cam.lookAt(new Vector3f(0,5,0), Vector3f.UNIT_Y); // and looks at 0,0,0. 
					flyCam.setMoveSpeed(10);
					flyCam.setDragToRotate(true);
					        
					// light
					DirectionalLight dl = new DirectionalLight();
					dl.setDirection(new Vector3f(-0.1f, -1f, -1).normalizeLocal());
					dl.setColor(ColorRGBA.Orange);
					rootNode.addLight(dl);
					// floor             	
					Material textureMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
					textureMat.setTexture("ColorMap", assetManager.loadTexture("myAssets/Textures/woodFloor.jpg"));		
					Box floor = new Box(Vector3f.ZERO, 20.0f, 0.01f, 20.0f);
					Geometry floorGeom = new Geometry("Floor", floor);
					floorGeom.setMaterial(textureMat);
					rootNode.attachChild(floorGeom);        
					// tree 
					Spatial tree = assetManager.loadModel("Models/Tree/Tree.mesh.j3o");
					tree.setQueueBucket(Bucket.Transparent);
					rootNode.attachChild(tree);
			        		       
					// ProjectorShadowNode
					PAppletProjectorShadowNode ppg = new PAppletProjectorShadowNode("Projector",assetManager,viewPort, 200,200, (Texture2D)assetManager.loadTexture("Interface/Logo/Monkey.png"));		
					rootNode.attachChild(ppg);
					rootNode.attachChild(ppg.getFrustmModel());       
					ppg.setLocalTranslation(new Vector3f(0,10,0));		 
					ppg.lookAt(new Vector3f(0, 0, 0), Vector3f.UNIT_Y);
					//projector is a kind of Shadow, and following processes are necessary for Shadow Rendering. 		
					floorGeom.setShadowMode(ShadowMode.Receive);
					tree.setShadowMode(ShadowMode.CastAndReceive); // tree makes and receives shadow
					
					// PApplet and PAppletDisplayGeometry
					// flat display
					PApplet applet0 = new ColorBarsPApplet();
					PAppletDisplayGeometry flatDisplay = new PAppletDisplayGeometry("FlatDisplay", assetManager, 4, 4, applet0, 200, 200, false);
					rootNode.attachChild(flatDisplay);		
					flatDisplay.setLocalTranslation(-10, 4, 0);
					flatDisplay.rotate(0, (float)Math.PI/2, 0);		
					// sphere display
					PApplet applet1 = new ColorBarsPApplet();
					Mesh sphere = new Sphere(20, 20, 0.8f);
					PAppletDisplayGeometry sphereDisplay = new PAppletDisplayGeometry("SphereDisplay", sphere, assetManager, applet1, 200, 200, false);
					rootNode.attachChild(sphereDisplay);
					sphereDisplay.setLocalTranslation(8, 5, 0);		
					
					/*
					 * Get a instance of SpatialInspector, and add it to each object as control.
					 */
					SpatialInspector spatialInspector = SpatialInspector.getInstance();
					ppg.addControl(spatialInspector);
					tree.addControl(spatialInspector);
					flatDisplay.addControl(spatialInspector);
					sphereDisplay.addControl(spatialInspector);
					spatialInspector.show();
					this.setPauseOnLostFocus(false);
				}
				
				public void destroy() {
					super.destroy();
					System.exit(0);	// we should terminate the thread of PApplet.
				}
				
				public static void main(String[] args) {
					SimpleApplication app = new HelloSpatiaInspector();		
					app.start();
				}
			}


1. SpatialInspectorにオブジェクトを追加しよう

まずはそれぞれのインスタンスをnewして配置します。simpleInitApp()の後の所でそれぞれのオブジェクトをGUIから動かせるようにSpatialInspectorのインスタンスを取得します。このGUIはひとつのシミュレーションにひとつあれば十分なので、Singletonパターンで実装しています。

このサンプルでは生成したオブジェクトのうち、樹とプロジェクタ、平面ディスプレイと球体ディスプレイの位置や向きをGUIから変えられるよう、それぞれのオブジェクトに対してaddControl(spatialInspector);とメソッドを呼んでいます。このaddControlというメソッドですが、もともとはゲームの中のサブキャラや敵キャラを決まった動きをさせるために準備されているクラス群がcom.jme3.scene.control.*にあります。メインのオブジェクトを動かしたい場合にはsimpleUpdate()の中等に場所や向きを変えるようにしますが、このクラス群はそこにサブキャラや敵キャラのコードが沢山書かれないようにするためのものです(詳しくはこちら)。

そしてspatialInspector.show();とするとインスペクタが表示されます。気を付けておくべきことは、this.setPauseOnLostFocus(false);もあわせて呼んでおくことです。これによって、インスペクタを操作している間にjMEがポーズされなくなります。そうしないと、ボタンを押して移動したのに反映されない…でもウィンドウをクリックしたらすごく動いていた!ということが起きます。


2. GUIからオブジェクトの位置や向きを変えてみよう

addControlを呼び出されたオブジェクトはGUIのコンボボックスに登録されているので、位置/向きを変えたいオブジェクトをその中から選びます。


位置/向きを変えたいオブジェクトを選び、そのオブジェクトの位置や向きを変える


上半分が位置を変更するためのラジオボタンおよびボタン、今の座標を示すテキストフィールドです。下半分が向きを変えるためのスライダーと今の角度を示すテキストフィールドです。位置を変える際には、ラジオボタンでX/Y/Z座標のどれを変えるか・ボタンを押す度に移動する量(0.1mか1mか10mか)を選び、UP/DOWNのボタンで座標の値を変更します。向きを変える場合には、上からピッチ:X/ヨー:Y/ロール:Zをスライダーで変更します。位置と向きの現在の値がテキストフィールドに表示されるので、位置と向きが決まったらソースコードにコピペしておくと良いでしょう。



FlatDisplayを選び、角度を変えてからX座標を1.0ずつ増やしているところ



ディスプレイ2つとプロジェクタの位置と向きも変えてみました。
球体ディスプレイと平面ディスプレイからは影が落ちない設定になっているので、木の影だけが床に落ちています。


距離センサーを使ってみよう

Processingはマウスやキーボード、ディスプレイといったGUIの入出力機器を取り扱うだけでなく、ProcessingとArduinoを組み合わせることでセンサやモータといった入出力機器を使ったシステムを作ることができます。

具体的な使い方は
yoppa.org
 ArduinoとProcessingの連携1 : センサの情報を視覚化する
 ArduinoとProcessingの連携2:大きな値を送信する、データの流れを視覚化する
 ArduinoとProcessingの連携3:「植物シンセ」を作る
橋本 直ARプログラミング Processingでつくる拡張現実感のレシピ オーム社
などがとても参考になると思います。

ProcessingとArduinoを組み合わせたシステムには、PC1台+Arduino1台+入出力機器(センサやモータ)というシステムが多いのですが、CityCompilerを使うとそうしたシステムを何組も使って空間的に配置したシステムへと進化させやすくなります。ここではProcessingにArduino+距離センサを組み合わせたシンプルな例を紹介し、それを仮想空間に表示されているProcessingの入力に仮想距離センサを組み合わせた例にしていく過程を紹介します。


1. ProcessingとArduinoをつなぐFirmata

USB(シリアルポート)につながっているArduinoのセンサからの値をPC側のProcessingに送ったり、PC側のProcessingからArduinoにつながっているモータを動かすためには、シリアルポート通信でデータをやりとりする必要があります。こうしたやり取りは何時誰がやっても同じ処理を書くことになるので、Firmataというライブラリが準備されています。

Arduinoにはあらかじめ準備されたこのやり取りのためのプログラムが最初からExamplesに入っています。Arduino IDEでFile->Examples->Firmata->Standard Firmataを選択して、それをUSBにつないだArduinoにUploadしておきます。



Aduino IDEでFile->Examples->Firmata->Standard Firmata

FirmataのProcessingのライブラリはArduinoのサイトのArduino and Processingからダウンロードできますが、Processing2.0bではエラーが出てしまいます。 githubに置いてあるCityCompilerでは、このArduinoのサイトで配布されているArduino.javaをProcessing2.0bで動作するように修正したものをcc.arduinoパッケージに同梱してあります(2013/01現在)。Arduinoのサイトで対応版が公開されたら、そちらを使うことをおススメします。

Firmataを使うことでnew Arduino();としてインスタンスを生成することができます。そのインスタンスにdigitalRead(), digitalWrite(), analogRead(),analogWrite()といったメソッドを呼び出してArduinoとデータをやり取りすることができます。次のコードは、マウスをクリックしたらArduinoのボードにあるLEDをオンするという一番シンプルなProcessingのサンプルです。


FirmataTest.pde
			import cc.arduino.*;
			import processing.serial.*;
			
			Arduino arduino;
			int pin = 13;
			
			void setup(){
				println(Arduino.list());
				arduino = new Arduino(this, Arduino.list()[0], 57600);
				arduino.pinMode(pin, Arduino.OUTPUT);
			}
			
			void draw(){
				if(mousePressed) {
					arduino.digitalWrite(pin, Arduino.HIGH);
				}else{
					arduino.digitalWrite(pin, Arduino.LOW);
				}
			}

これをエクポートしたものをtest.p5パッケージのFirmataPApplet.javaとしてありますので、ArduinoやFirmataの動作確認に使って下さい。


2. ProcessingからArduinoにつないだ距離センサを使う

ここからは、ProcessingのExamples->Topics->Fractals and L-Systemsの中のTreeというサンプルを使っていきます。このPAppletはマウスのX座標を使って、関数を再帰的に呼び出して木のカタチを描きます。JavaにエクスポートしてEclipseに取り込んで、マウスのX座標でカタチを変えている部分をArduinoにつないだ距離センサ(Sharp 2Y0A21)からの値で変えるように変更します。



ProcessingのTree.pdeをエクスポート

距離センサをオブジェクトとしてnewできるようにDistanceSensorFirmataというクラスをnet.unitedfield.ccパッケージの中に用意しています。 test.p5パッケージにTreeWithDistanceSensorFirmata.javaというコードがありますが、これはTree.pdeをJavaに書き出したものをDistanceSensorFirmataを使うように変更したものです。


TreeWithDistanceSensorFirmata.java
			package test.p5;
			
			import net.unitedfield.cc.DistanceSensorFirmata;
			import processing.core.PApplet;
			
			public class TreeWithDistanceSensorFirmata extends PApplet {
			
			/**
			 * Recursive Tree
			 * by Daniel Shiffman.  
			 * 
			 * Renders a simple tree-like structure via recursion. 
			 * The branching angle is calculated as a function of 
			 * the horizontal mouse location. Move the mouse left
			 * and right to change the angle.
			 */
			 
			float theta;   
			DistanceSensorFirmata distanceSensor; // instance of DistanceSensorFirmata
			
				public void setup() {
				  size(640, 360);
				  distanceSensor = new DistanceSensorFirmata(0);
				  distanceSensor.setup();
				}
			
				public void draw() {
				  background(0);
				  frameRate(30);
				  stroke(255);
				  
				  // Let's pick an angle 0 to 90 degrees based on the mouse position or DistanceSensorFirmata
				  //float a = (mouseX / (float) width) * 90f; // original code in Tree.java
				  /* using DistanceSensorFirmata */
				  float a = (distanceSensor.getDistance()/distanceSensor.getSenseMax()) *90f;
				  
				  // Convert it to radians
				  theta = radians(a);
				  // Start the tree from the bottom of the screen
				  translate(width/2,height);
				  // Draw a line 120 pixels
				  line(0,0,0,-120);
				  // Move to the end of that line
				  translate(0,-120);
				  // Start the recursive branching!
				  branch(120);
				}
			
				public void branch(float h) {} // same as in Tree.java
			}
			


TreeWithDistanceSensorFirmataの実行画面


元のTree.pdeをエクポートしたTree.javaでは
float a = (mouseX / (float) width) * 90f;
として木のパラメータを変更していたところを
float a = (distanceSensor.getDistance()/distanceSensor.getSenseMax()) *90f;
と変更しました。 またPAppletのsetup()の中でDistanceSensorFirmataのインスタンスをnewした後にsetup()を呼び出しています。
distanceSensor = new DistanceSensorFirmata(0);
distanceSensor.setup();
これはDistanceSensorFirmataもPAppletのサブクラスであるためです。Firmataライブラリを使ってArduinoクラスのインスタンスをnewする際にはその引数としてPAppletを渡しますが、そのためにDistanceSensorFirmataはPAppletのサブクラスとなっています。

コンストラクタに渡す引数は距離センサが接続されているArduinoのピン番号です。現在は1つのArduinoにひとつの距離センサが繋がっている状態を想定して実装しています。複数の距離センサが1つのArduinoに接続している場合や複数の異なるセンサが1つのArduinoにつながっている場合はまた別の実装をする必要があるため、今後対応してゆく予定です。


3. 仮想空間に表示したProcessingからArduinoにつないだ距離センサを使う

このPAppletをnewして仮想ディスプレイに表示すれば、距離センサと連動したコンテンツを表示するディスプレイのサンプルが出来上がります。

CityCompilerでは仮想空間と模型空間を行ったり来たりしながらプロトタイピングを進めることがあります。 そうした進め方がやり易いよう、jMEの仮想空間の中で動作する仮想距離センサもDistanceSensorNodeも用意しました。 リアルな距離センサDistanceSensorFirmataと仮想の距離センサDistanceSensorNodeで距離を計る時には同じ名前のメソッドを呼び出せるよう実装しています。

Processingでカメラを使う時はnew Capture();としますが、このCaptureもクラスではなくインタフェースです。そのためにハードウェアのカメラにアクセスするライブラリとしてQuickTimeを使ったりGStreamerを使ったりとVideo Libraryを切り替えられる柔軟性がProcessingには備わっています。この柔軟性を活用して、CityCompilerにおける仮想カメラもこのCaptureインタフェースを実装したクラスとして作り、リアルカメラと仮想カメラの切り替えを実現しています。
リアル距離センサであるDistanseSensorFirmataと仮想距離センサであるDistanseSensorNodeはどちらもDistanseSensorというインタフェースをimplementsしているので、同じような仕組みでリアルセンサと仮想センサの切替えができます。

ここではさらに、仮想距離センサにも対応できるようPAppletを少し修正してみしょう。カメラを使ったPAppletと同じようにまず boolean realDeployment; という変数を定義します。これがtrueならArduino+Firmata経由でリアルな距離センサを使うようにして、これがfalseなら仮想距離センサを使うことにします。

TreeWithDistanceSensor.java
			package test.p5;
			
			import net.unitedfield.cc.DistanceSensor;
			import net.unitedfield.cc.DistanceSensorFirmata;
			import processing.core.PApplet;
			
			public class TreeWithDistanceSensor extends PApplet {
			
			float theta;
			DistanceSensor	distanceSensor = null;
			boolean		realDeployment = false;
			
				public void setup() {
					size(640, 360);
					if(realDeployment == true){
						distanceSensor = new DistanceSensorFirmata(0);
						((DistanceSensorFirmata)distanceSensor).setup();
					}
				}
				
				public	void setDistanceSensor(DistanceSensor sensor){
						this.distanceSensor = sensor;
				}
			
				public void draw() {
					background(0);
					frameRate(30);
					stroke(255);
					
					// Let's pick an angle 0 to 90 degrees based on the mouse position or DistanceSensorFirmata
					//float a = (mouseX / (float) width) * 90f;
					float a =0;		
					if(distanceSensor != null)
						a = (distanceSensor.getDistance()/distanceSensor.getSenseMax()) *90f;
					
					theta = radians(a);
					translate(width/2,height);
					line(0,0,0,-120);
					translate(0,-120);
					branch(120);
				}
			
				public void branch(float h) {} // same as in Tree.java
			}
			
DistanceSensorFirmataのインスタンスとして定義していたインスタンス変数はDistanceSensorをimplmentしたオブジェクトとすることで、リアル距離センサと仮想距離センサのどちらも使えるようにしておきます。

リアル距離センサを使いたい時は、realDeploymentをtrueにしておき、setup()の中でDistanceSensorFirmataのインスタンスをnewします。DistanceSensorFirmataのsetup()も呼び出さないといけないので、キャストをして呼び出します。

仮想距離センサを使いたい時は、jMEのコードの中でこのアプレットをnewしてから、仮想距離センサのオブジェクトをこのアプレットに接続することになります。なのでそのためのメソッドとして、void setDistanceSensor(DistanceSensor sensor)を追加しました。アプレットはnewされたけど距離センサがまだ渡されていない状態でもNullPointerExceptionが起きないよう、変数distanceSensorを定義するところではnullにしておき、nullでなければdistanceSensor.getDistance()を実行するようにします。これでPAppletがリアル距離センサと仮想距離センサの両方を使えるようになりました。

リアルカメラと仮想カメラを使う場合もPAppletでrealDeploymentという変数を定義した上でsetCapture(Capture capture)というメソッドを準備してありますが、ここでの作り方と同じになっています。

4. 仮想空間に表示したProcessingから仮想距離センサを使う

次はこのアプレットに仮想距離センサを接続し、アプレットの表示を仮想ディスプレイにしてみます。今回は複数のセンサとディスプレイを表示するサンプルにしてみましょう。センサとディスプレイの組を3セット用意することにして、特にそのうちの距離センサ1つはリアルセンサにしてみます。

DistanceDisplaysSimulationの実行画面。左と真ん中の距離センサは仮想距離センサ、右の距離センサはリアル距離センサ(Arduinoの先に距離センサがつながっているので、リアル距離センサのカタチはjMEの中にはない)。

DistanceDisplaysSimulation.java
			
			package simulation.cc;
			
			public class DistanceDisplaysSimulation extends SimpleApplication {
				Node senseTarget;
				Spatial girl;
				
				public void simpleInitApp() {
					cam.setLocation(new Vector3f(-1f, 1.5f, -3f));
					cam.lookAt(new Vector3f(0, 1.8f, 5f), Vector3f.UNIT_Y);
					flyCam.setDragToRotate(true);
					
					senseTarget = new Node();
					setupEnvironment();		
					setupDistanceDisplays();
					setupGirl();		
				}
				
				private void setupDistanceDisplays() {	
					DistanceSensor		sensors[]	= new DistanceSensor[3];
					TreeWithDistanceSensor	applets[]	= new TreeWithDistanceSensor[3];
					PAppletDisplayGeometry	displays[]	= new PAppletDisplayGeometry[3];
					
					SpatialInspector spatialInspector = SpatialInspector.getInstance();
					for(int i=0; i<3 i++){
						if(i<2){
							DistanceSensorNode sensorNodeV = new DistanceSensorNode("sensor"+i, 5f, assetManager, senseTarget);
							sensorNodeV.setLocalTranslation(new Vector3f(4-4*i, 1.5f, 9.4f));
							sensorNodeV.rotate(0, FastMath.PI, 0);				
							rootNode.attachChild(sensorNodeV);
							sensorNodeV.addControl(spatialInspector);
							sensors[i] = sensorNodeV;
						}else{
							DistanceSensorFirmata sensorNodeR = new DistanceSensorFirmata(0);
							sensorNodeR.setup();
							sensors[i] = sensorNodeR;
						}
						applets[i] = new TreeWithDistanceSensor();
						applets[i].setDistanceSensor(sensors[i]);
						displays[i] = new PAppletDisplayGeometry("display"+i,assetManager,4,3,applets[i],640,360,false);
						displays[i].setLocalTranslation(new Vector3f(4-4*i, 1.5f, 9.4f));
						rootNode.attachChild(displays[i]);
						displays[i].addControl(spatialInspector);
					}		
					spatialInspector.show();
					this.setPauseOnLostFocus(false);
				}
				
				private void setupEnvironment() {
				(省略)
				}
			
				private	void setupGirl(){
				(省略)
				}
				
				// イベントリスナー
				private AnalogListener analogListener = new AnalogListener() {
				(省略)
				};
			
				public void destroy() {
				(省略)
				}
					
				public static void main(String[] args) {
					SimpleApplication app = new DistanceDisplaysSimulation();
					app.setPauseOnLostFocus(false);
					app.start();
				}
			}
			
setupDistanceDisplays();の中で距離センサは2つを仮想距離センサ、1つをリアル距離センサをnewしています。どちらもDistanceSensorインタフェースをimplementしているので、DistanceSensor sensors[];という配列で3つのセンサを保持します。
またPAppletに距離センサを渡す部分では、距離センサが仮想であるかリアルであるかを気にする必要もありません。setup()が呼ばれるタイミングよりも後にsetDistanceSensor()で距離センサをPAppletに渡せるよう、TreeWithDistanceSensor.javaではrealDeployment = false;としてあります。

こうしてサンプルを作ってみると、距離センサは線的な広がりしかないようなセンサではなく、面的な広がりがあるレンジセンサのようなものが欲しくなってきます。その時には、このサンプルやカメラ(仮想/リアル)を参考にしながら仮想レンジセンサとリアルレンジセンサの両方を実装してみて下さい。

細かいノウハウ集

JMEの設定画面を非表示にするには?

実行時に表示されるJMEの設定画面を非表示にするには、app.setShowSettings(false) を使います。 また、実行画面のサイズを任意に設定したい場合は、AppSettings型のデータを作成した後、app.setSettings() で設定します。


設定画面の非表示と画面サイズの設定
					public static void main(String[] args) {
						SimpleApplication app = new DisplayTest();
				
						// 設定画面を非表示にする
						app.setShowSettings(false); 
				
						// 画面サイズの設定
						AppSettings s = new AppSettings(true);
						s.setWidth(1024);
						s.setHeight(768);
						app.setSettings(s);

						app.start();
					}

ステータス表示をOFFにするには?

左下に表示されるステータス文字列の表示をなくしたい場合は、app.setDisplayStatView(false)とします。 また、FPSの表示をOFFにするにはapp.setDisplayFps(false)とします。


ステータス表示とFPS表示のOFF
					public static void main(String[] args) {
						SimpleApplication app = new DisplayTest();

						app.setDisplayStatView(false);  // ステータスの表示をOFF
						app.setDisplayFps(false);       // FPSの表示をOFF

						app.start();
					}

画面をドラッグしたときだけ視点が動くようにするには?

通常simpleApplicationでは画面上でマウスカーソルを動かすだけで視点が動きますが、これをドラッグしたときだけ動くようにするにはsimpleInitApp()の中で flyCam.setDragToRotate(true); とやります。


ドラッグしたときだけ視点が動くようにする
						public void simpleInitApp() {
							/*
							 * 他の処理
							 */
							flyCam.setDragToRotate(true);
						}

アプレットへのマウス入力等がすぐに仮想空間に反映されるようにするには?

マウスを使った入力をするためにアプレットのウィンドウにアクティブにすると、何も設定せずにいるとJMEのウィンドウでは描画が止まってしまいます。アプレットとJMEの両方が常に描画されるようにするには、JMEのSimpleApplicationのメソッドsetPauseOnLostFocus(false);とやります。


マウスが別のウィンドウに移ってもJMEの描画を止めないようにする
			public static void main(String[] args) {
				SimpleApplication app = new DisplayTest();
			
				app.setDisplayStatView(false);  // ステータスの表示をOFF
				app.setDisplayFps(false);       // FPSの表示をOFF
				app.setPauseOnLostFocus(false); // JMEのウィンドウが一番前でなくても描画を止めない
				app.start();
			}
			
とやってもいいですし、

マウスが別のウィンドウに移ってもJMEの描画を止めないようにする
			public void simpleInitApp() {
				/* 他の処理*/
				PApplet applet = new DynamicParticlesRetained(); //マウスを使うアプレットを使う
				this.setPauseOnLostFocus(false); // JMEのウィンドウが一番前でなくても描画を止めない
				/* 他の処理*/
			
			}
			
とやってもいいです。