こくぶん研究室

Leap Motion で遊ぼう

はじめに

VR で「手」を使う Leap Motion

Leap Motion
Leap Motion

Leap Motion(リープ・モーション)という、手や指を認識できる小さなデバイスがあります。2012年の発売当初は「面白いけど、何に使う?」という印象でしたが、今では、各社の VR 用デバイス(いわゆる VR ゴーグル)と組み合わせて、VR 世界の中でユーザが「手」を使っていろいろインタラクションするためのデバイスとして欠かせないものになりました。

VR ゴーグルと Leap Motion
VR ゴーグルと Leap Motion

こくぶん研究室でも、VR ゴーグルに Leap Motion を取り付けて、VR 世界で直接手でインタラクションする不思議な体験を楽しんでいます。手に何も付けずに VR 空間に手を伸ばせる体験はとても楽しいものです。

Leap Motion でできること

Leap Motion のイメージ
Leap Motion のイメージ

右の写真を見れば Leap Motion でできることが想像できると思います。Leap Motion に手をかざすと、手や指を認識してくれます。手の位置、右手か左手か、各指の関節とそれらの位置などを瞬時に測れます。コントローラやキーボードやマウスを使うことなく、空中で手を動かせば、コンピュータを(VR 世界も)操作できるようになるのです。

さぁ 始めましょう

Leap Motion の国内正規品の価格は約 10,000円です(例えばこちら)。Leap の直販サイトの価格は 79.99ドルです。ただし、様々な価格がつくことがあり、まれにかなり安価で入手できる場合があります。いろいろ探してみて入手しましょう。

そしてこの Leap Motion は Processing でもプログラミングできます。さぁ、遊んでみましょう!

▲TOP

今回の内容

目標

二人(二つ)の手とその指を認識して「じゃんけんプログラム」を作ります。

今回の目標 1
今回の目標 1

また、指をつまむ(ピンチ)動作を使って空中でピアノを引く「空中ピアノ」を作ります。

今回の目標 2
今回の目標 2

使うもの

ステップ

  1. Leap Motion を使う準備
  2. プログラミング前に少し解説
  3. 手の位置を測る
  4. 手のひらの向きを測る
  5. 指の位置を測る
  6. じゃんけんプログラム
  7. 空中ピアノ

▲TOP

1. Leap Motion を使う準備

Leap Motion をパソコンで使うためのドライバと、Processing で使うためのライブラリを設定します。ドライバをインストールするので、管理者権限のあるユーザとして Windows にサインインしておきます。

ドライバのインストール

まずはこちらのサイト(Leap Motion のセットアップサイト)を開いてください。「WINDOWS 向けのダウンロードをする」をクリックするとドライバのダウンロードが始まります。かなり容量が大きいので、しばらく待ちます。

補足

Leap Motion には現状二種類のドライバがあります。最新の V3(通称 ORION)と旧版の V2 です。ここでは V3 をダウンロードします。V2 と V3 は機能に少し違いがあり、完全互換ではありません。このページでは V3 の機能も使うため、V3 をインストールしてください。なお、ダウンロードページの下の方には V2 をダウンロードするためのリンクもあります。

Leap Motion のドライバのダウンロード
Leap Motion のドライバのダウンロード

ダウンロードされた「Leap_Motion_Orion_Setup_win_3.2.0.exe」をダブルクリックします。インストーラの指示通りに進んでください。

インストールが終了したら Leap Motion をパソコンに接続してください。

ライブラリ(SDK)のダウンロードと設定

こちらのサイト(Leap Motion の開発者サイト)にアクセスしてください。

Leap Motion SDK のダウンロード
Leap Motion SDK のダウンロード

「DOWNLOAD ORION BETA」をクリックすると、ログイン画面になります。Leap Motion の SDK を使うにはユーザ登録する必要があります(無料)。もし既に登録してあればログインします。登録したことがなければ「新規登録」をクリックします。

Leap Motion 開発者サイトへのログイン画面
Leap Motion 開発者サイトへのログイン画面

「ご利用開始」という画面で、メールアドレス、設定するユーザ名、設定するパスワード、生年月日を入力して、「私は App Store サービス利用規約に同意します」にチェックを入れて、「登録する」をクリックします。

Leap Motion 開発者サイトへの登録画面
Leap Motion 開発者サイトへの登録画面

SDK の利用規約が表示されますので、「Yes, I agree with the terms」にチェックを入れて、「Accept」ボタンをクリックすると登録完了です。開発者サイトに戻って、再び「DOWNLOAD ORION BETA」をクリックしてください。すると SDK のダウンロードが始まります。かなり容量が大きいので、しばらく待ちます。

SDK の利用規約画面
SDK の利用規約画面

SDK のダウンロードを待つ間に、Processing の libraries フォルダを開いてください。一般的には C:\Users\{ユーザ名}\Documents\Processing\libraries です。その中に「LeapJava」という名前のフォルダを新規作成してください。さらにその「LeapJava」フォルダの中に「library」という名前のフォルダを新規作成してください。

ダウンロードされた SDK(LeapDeveloperKit_3.2.0+45899_win.zip)をダブルクリックして開きます(展開しないままでOK)。LeapDeveloperKit_3.2.0+45899_win -> LeapSDK -> lib とフォルダを開いていってください。「LeapJava.jar」というファイルを見つけてください。

SDK の lib フォルダ内
SDK の lib フォルダ内

右クリックして「コピー」し、上で作成した「library」フォルダの中に貼り付けてください。さらに、SDK の lib フォルダにある「x64」フォルダを開いて、中にある「Leap.dll」と「LeapJava.dll」をコピーして、「library」フォルダの中に貼り付けてください。

補足

お使いのパソコンの OS が 32 bit で、Processing も 32 bit 版を使っている場合は、「x64」フォルダではなく、「x86」フォルダ内から同じ名前のファイルをコピーしてください。

まとめると、C:\Users\{ユーザ名}\Documents\Processing\libraries\LeapJava\library の中に、以下の三つのファイルを入れてください。

LeapJava の library フォルダ内
LeapJava の library フォルダ内

手間がかかりましたが、これで準備完了です。

補足

ライブラリの設定方法の出典はこちらです。

▲TOP

2. プログラミング前に少し解説

Leap Motion の SDK は少し複雑なので、プログラミングを始める前に少しだけ SDK の構造やプログラミングの流れを書いておきます。この時点で完全に理解する必要はありません。プログラムを書きながら時々思い出してもらえば構いません。

以下が Leap Motion を使うためのオブジェクト(部品)の階層構造です。もっとたくさんのオブジェクトがありますが、このページで使うオブジェクトだけ示しています。


Controller
 └─ Frame
     ├─ HandList
     │   └─ Hand
     │       └─ FingerList
     │           └─ Finger
     └─ InteractionBox

Controller オブジェクト

「Leap Motion 本体」と思っておきましょう。プログラムの最初に必ずこのオブジェクトを作ります。

Frame オブジェクト

Leap Motion はカメラのようなデバイスです。Frame はカメラの画像と思っておけば構いません。実際には Frame には画像だけでなく、その画像の中にある手や指などのあらゆる情報が入っています。繰り返しこの Frame を取得して、その中にある様々な情報(以下)を取り出します。

HandList オブジェクト

「複数の手」の情報が入っている部品です。Frame オブジェクトから取り出します。Leap Motion は複数の手を認識できるので、この HandList の中から特定の手(以下の Hand オブジェクト)を取り出します。

Hand オブジェクト

「ひとつの手」の情報が入っている部品です。上の HandList から取り出します。手の位置や向きなどの値をこの Hand から取り出して利用します。

FingerList オブジェクト

ひとつの手の「複数の指」の情報が入っている部品です。上の Hand オブジェクトから取り出します。親指を 0 ~ 小指を 4 で指定して特定の指(以下の Finger オブジェクト)を取り出します。

Finger オブジェクト

「ひとつの指」の情報が入っている部品です。上の FingerList から取り出します。指の位置や方向や状態などの値をこの Finger から取り出して利用します。

InteractionBox オブジェクト

「位置座標の変換のための部品」と思っておきましょう。Leap Motion で得られる手や指の位置のデータは、以下の図のように、Leap Motion 本体を原点にして、mm 単位で得られます(こちらから引用)。

Leap Motion の座標系
Leap Motion の座標系

例えば画面上での手の位置を知りたい場合、mm 単位の値では使いにくいです。そこで、InteractionBox オブジェクトを使って、以下のような空間(画面を模した空間)での座標値に変換します(こちらから引用)。

InteractionBox の座標系
InteractionBox の座標系

▲TOP

3. 手の位置を測る

お待たせしました。ここからがプログラミングです。まずは手の位置を測る最低限のプログラムを書いて、Leap Motion プログラミングの基本を抑えます。

Leap Motion は以下の写真のようにパソコンの手前に置いてください。USB ケーブルが左側から出ている(緑色の電源ランプが見える)向きです。

Leap Motion の置き方
Leap Motion の置き方

プログラミング

Processing に以下のコードを入力して、leap_hand.pde という名前で保存しましょう。

それぞれのコードが何をしているのかは、コード中のコメント文を見て考えながら入力していきましょう。

import com.leapmotion.leap.*;               // LeapJava ライブラリを使う

Controller leap = new Controller();         // leap という名前で Controller オブジェクトを宣言

void setup() {
  size(800, 600);                           // ウィンドウのサイズ設定
}

void draw() {
  background(0);                            // 背景を黒に
  Frame frame = leap.frame();               // Frame オブジェクトを宣言し、leap のフレームを入れる
  HandList hands = frame.hands();           // HandList オブジェクトを宣言し、frame 内の手(複数)の情報を取得
  for(int i = 0; i < hands.count(); i++) {  // 見つかった全ての手について
    Hand hand = hands.get(i);               // Hand オブジェクトを宣言し、i 番目の手を取得
    Vector palmPos = hand.palmPosition();   // Vector オブジェクトを宣言し、その手の手のひらの位置を取得
    textSize(40);                           // 文字サイズ
    text(hand.id() + palmPos.toString(), 0, 40 * (i + 1));  // その手の ID と手のひらの位置をテキストで表示
  }
}

動作確認

leap_hand.pde を実行し(▶をクリック)、Leap Motion の上に手をかざしてみましょう。

手はいくつでも認識できます。認識された順に、手の ID と、それぞれの手のひらの位置が表示されます。

leap_hand.pde の実行結果
leap_hand.pde の実行結果

手のひらの位置の値は (X, Y, Z) の順で、以下の座標軸での値(単位は mm)です。上の図の ID 6 の手を例にとると、Leap Motion 本体の中心から、X軸方向に -107.8mm(つまり左)、Y軸方向(上)に 167.3mm、Z軸方向(手前)に 23.1mm の位置に手のひらの中心がある、というデータです。

Leap Motion の座標系
Leap Motion の座標系

解説

プログラムの冒頭で Controller オブジェクト(Leap Motion 本体と思えばよかったんでしたね)を作っています。

draw() 関数の中で、繰り返し Frame オブジェクトを取得しています。この frame の中に、認識された複数の手の情報が入ってきます。

次に HandList オブジェクトを作って、frame の中にある複数の手の情報を hands() で取り出しています。

HandList の中にある手の数は hands.count() で分かります。また、ひとつひとつの手の情報は hands.get(i) のようにして取り出せます。取り出した手の情報は Hand オブジェクトに入れています。

手のひらの位置は hand.palmPosition() で取り出せます。parmPosition() は (X, Y, Z) のように三つの値を持った Vector という形式で取り出します。

このように、Controller -> Frame -> HandList -> Hand のように順番に部品を取り出していき、具体的な値を取り出していく流れが基本です。

▲TOP

4. 手のひらの向きを測る

上の例では値を文字で表示しただけで、あまり面白くありませんね。次は、もう少し様々な手の情報を取り出して、CG でビジュアルに表示してみましょう。

今から作るもの
今から作るもの

プログラミング

Processing に以下のコードを入力して leap_palm.pde という名前で保存しましょう。さきほどの leap_hand.pde を「名前をつけて保存」して、書き加えたり書き換えていくと効率的でしょう。

それぞれのコードが何をしているのか、さっきと何が違うか、コード中のコメント文を見て考えながら入力していきましょう。

import com.leapmotion.leap.*;               // LeapJava ライブラリを使う

Controller leap = new Controller();         // leap という名前で Controller オブジェクトを宣言
InteractionBox iBox;                        // ★ InteractionBox オブジェクト(座標変換などをする)を宣言

void setup() {
  size(800, 600, P3D);                      // ★ P3D モードでウィンドウを作る
}

void draw() {
  background(0);                            // 背景を黒に
  Frame frame = leap.frame();               // Frame オブジェクトを宣言し、leap のフレームを入れる
  HandList hands = frame.hands();           // HandList オブジェクトを宣言し、frame 内の手(複数)の情報を取得
  iBox = frame.interactionBox();            // ★ InteractionBox を初期化
  for(int i = 0; i < hands.count(); i++) {  // 見つかった全ての手について
    Hand hand = hands.get(i);               // Hand オブジェクトを宣言し、i 番目の手を取得
    drawPalm(hand);                         // ★ drawPalm 関数を呼ぶ
  }
}

// ★ 手のひらを描画する drawPalm 関数(Hand オブジェクトを引数に取る)
void drawPalm(Hand hand) {
  Vector palmPos = hand.palmPosition();     // 手のひらの位置を取得
  Vector palmPosNorm = iBox.normalizePoint(palmPos, false); // 標準化された座標値に変換
  pushMatrix();                             // 描画座標の変換開始
    translate(palmPosNorm.getX() * width,   // 標準化された座標値にウィンドウの幅を掛けて画面内の座標に変換
              (1 - palmPosNorm.getY()) * height,  // ウィンドウの高さを掛けて画面内の座標に変換
              palmPosNorm.getZ() * 50);     // Z軸(奥行方向)は適当に 50 を掛けた
    rotateX(-1 * hand.direction().pitch()); // X軸まわりに座標を回転(手の pitch 角度ぶん)
    rotateY(-1 * hand.direction().yaw());   // Y軸まわりに座標を回転(手の yaw 角度ぶん)
    rotateZ(-1 * hand.palmNormal().roll()); // Z軸まわりに座標を回転(手の roll 角度ぶん)
    noStroke();                             // 線は描かない
    fill(255);                              // 白く塗りつぶす
    lights();                               // 3D シーン内に照明を置く
    box(80, 10, 100);                       // 手のひらを示す箱を描く
    stroke(255);                            // 線を描く(白)
    line(0, 0, 0, 50);                      // 箱の中心から Y 軸方向に 50ピクセル
  popMatrix();                              // 描画座標の変換終了
}

動作確認

leap_hand.pde を実行し(▶をクリック)、Leap Motion の上に手をかざしてみましょう。

手はいくつでも認識できます。手のひらが薄い板のような CG で表示されて、手のひらの位置に加えて、手のひらの角度も反映されています。また、手のひらの方向に線が描かれているので、手のひらの向きも分かります。試しに手を裏返したりもしてみましょう。Leap Motion がかなり素早く正確に手の情報を認識してくれることを感じられると思います。

leap_palm.pde の実行結果
leap_palm.pde の実行結果

解説

プログラムの冒頭で InteractionBox オブジェクト(座標変換をする部品でしたね)を作っています。

drawPalm() 関数の中で hand.palmPosition() で手のひらの位置を取得しているところまでは leap_hand.pde と同じです。でもこの palmPosition() で得られるのは mm 単位の値でした。この座標値を、InteractionBox オブジェクト(iBoxと名づけた)の normalizePoint() という機能を使って変換しています。

InteractionBox の座標系
InteractionBox の座標系

normalizePoint() で得られる値は、上の図の赤い箱の左奥の下が原点で、Xは右方向、Yは上方向、Zは手前方向がプラスで、それぞれ 最小 0 ~ 最大 1 の値が返ってきます。相対的な値ですが、この値がわかれば、画面のサイズ(幅や高さや距離)などをこれらの値に掛け算すれば、任意の大きさの座標系にできます。と、言葉で書くと分かりにくいので、プログラム中の translate() のあたりを書き換えながら実験してみましょう。

また、hand.direction().pitch() や hand.direction().yaw() という機能を使って、手のひらの傾きを取得しています。pitch は前後の回転(自動車が加速したりブレーキをかけたときの動き)、yaw は左右方向の回転(ゆっくりハンドルをきったときに自動車が向きを変える動き)、roll は左右方向の傾き(急ハンドルをきると自動車や体が傾く動き)です。これらの値の単位は「ラジアン」(180度が π (3.14))です。この値を rotateX、rotateY、rotateZ に入れて手のひらの CG を回転させています。

▲TOP

5. 指の位置を測る

次は「手」だけでなく「指」の情報まで測ってみましょう。

今から作るもの
今から作るもの

プログラミング

leap_hand.pde を「名前をつけて保存」で leap_finger.pde という名前で保存します。そこに以下のコードを入力しましょう。それぞれのコードが何をしているのか、さっきと何が違うか、コード中のコメント文を見て考えながら入力していきましょう。

import com.leapmotion.leap.*;               // LeapJava ライブラリを使う

Controller leap = new Controller();         // leap という名前で Controller オブジェクトを宣言
InteractionBox iBox;                        // InteractionBox オブジェクト(座標変換などをする)を宣言

void setup() {
  size(800, 600, P3D);                      // P3D モードでウィンドウを作る
}

void draw() {
  background(0);                            // 背景を黒に
  Frame frame = leap.frame();               // Frame オブジェクトを宣言し、leap のフレームを入れる
  HandList hands = frame.hands();           // HandList オブジェクトを宣言し、frame 内の手(複数)の情報を取得
  iBox = frame.interactionBox();            // InteractionBox を初期化
  for(int i = 0; i < hands.count(); i++) {  // 見つかった全ての手について
    Hand hand = hands.get(i);               // Hand オブジェクトを宣言し、i 番目の手を取得
    drawPalm(hand);                         // drawPalm 関数を呼ぶ
    drawFingerTip(hand);                    // ★ drawFingerTip 関数を呼ぶ
  }
}

// 手のひらを描画する drawPalm 関数(Hand オブジェクトを引数に取る)
void drawPalm(Hand hand) {
  Vector palmPos = hand.palmPosition();     // 手のひらの位置を取得
  Vector palmPosNorm = iBox.normalizePoint(palmPos, false); // 標準化された座標値に変換
  pushMatrix();                             // 描画座標の変換開始
    translate(palmPosNorm.getX() * width,   // 標準化された座標値にウィンドウの幅を掛けて画面内の座標に変換
              (1 - palmPosNorm.getY()) * height,  // ウィンドウの高さを掛けて画面内の座標に変換
              palmPosNorm.getZ() * 50);     // Z軸(奥行方向)は適当に 50 を掛けた
    rotateX(-1 * hand.direction().pitch()); // X軸まわりに座標を回転(手の pitch 角度ぶん)
    rotateY(-1 * hand.direction().yaw());   // Y軸まわりに座標を回転(手の yaw 角度ぶん)
    rotateZ(-1 * hand.palmNormal().roll()); // Z軸まわりに座標を回転(手の roll 角度ぶん)
    noStroke();                             // 線は描かない
    fill(255);                              // 白く塗りつぶす
    lights();                               // 3D シーン内に照明を置く
    box(80, 10, 100);                       // 手のひらを示す箱を描く
    stroke(255);                            // 線を描く(白)
    line(0, 0, 0, 50);                      // 箱の中心から Y 軸方向に 50ピクセル
  popMatrix();                              // 描画座標の変換終了
}

// ★ 指先を描画する drawFingerTip 関数(Hand オブジェクトを引数に取る)
void drawFingerTip(Hand hand) {
  FingerList fingers = hand.fingers();        // FingerList オブジェクトに見つかった指の情報(複数)を入れる
  for(int i = 0; i < fingers.count(); i++) {  // 見つかった全ての指について
    Finger finger = fingers.get(i);           // 指 i を取得(0:親指, 1:人差し指, 2:中指, 3:薬指, 4:小指)
    Vector tipPos = finger.tipPosition();     // その指の指先(tip)の位置を取得
    Vector tipPosNorm = iBox.normalizePoint(tipPos, false);   // 標準化された座標値に変換
    if(finger.isExtended() == true) {         // その指が伸びて(isExtended)いたら
      fill(255, 0, 0);                        // 塗りつぶし色を赤に
    } else {                                  // そうでなければ(伸びていなければ)
      fill(255, 255, 255);                    // 塗りつぶし色を白に
    }
    pushMatrix();                           // 描画座標の変換開始
      translate(tipPosNorm.getX() * width,  // 標準化された座標値にウィンドウの幅を掛けて画面内の座標に変換
                (1 - tipPosNorm.getY()) * height,   // ウィンドウの高さを掛けて画面内の座標に変換
                tipPosNorm.getZ() * 50);    // Z軸(奥行方向)は適当に 50 を掛けた
      noStroke();                           // 線は描かない
      lights();                             // 3D シーン内に照明を置く
      sphere(20);                           // 球を描く
    popMatrix();                            // 描画座標の変換終了
  }
}

動作確認

leap_finger.pde を実行し(▶をクリック)、Leap Motion の上に手をかざしてみましょう。

leap_finger.pde の実行結果
leap_finger.pde の実行結果

さきほどの手のひらに加えて、指先の位置に球が表示されていて、指を伸ばすとその球が赤くなります。もちろん、複数の手を認識できます。

解説

新しく加わったのは主に drawFingerTip() 関数です。drawPalm() 関数は leap_palm.pde のまま変更ありません。

drawFingerTip() 関数の中では、Hand オブジェクトの中から FingerList オブジェクト(認識した手の中の複数の指の情報)を取得しています。指 Finger は、親指が fingers.get(0) ~ 小指が fingers.get(4) で取得できます

Leap Motion は指の全ての関節の位置を認識できますが、ここでは「指先」のデータだけ使っています。finger.tipPosition() で各指の指先の位置を取得できます。leap_palm.pde と同じように、InteractionBox を使って座標系を変換しています。

finger.isExtended() は、その指が「伸びているか否か」を true/false で返してくれます。ここでは、指が伸びていたら赤い sphere で表示しています。

▲TOP

6. じゃんけんプログラム

指まで認識できるようになりましたから、次は、じゃんけんのプログラムを作ってみます。

今から作るもの
今から作るもの

プログラミング

leap_finger.pde を「名前をつけて保存」で leap_rps.pde という名前で保存します。じゃんけんは英語で「Rock-Paper-Scissors」と言われますから、頭文字をとって「rps」です。そこに以下のコードを入力しましょう。それぞれのコードが何をしているのか、さっきと何が違うか、コード中のコメント文を見て考えながら入力していきましょう。

import com.leapmotion.leap.*;               // LeapJava ライブラリを使う

Controller leap = new Controller();         // leap という名前で Controller オブジェクトを宣言
InteractionBox iBox;                        // InteractionBox オブジェクト(座標変換などをする)を宣言

void setup() {
  size(800, 600, P3D);                      // P3D モードでウィンドウを作る
}

void draw() {
  background(0);                            // 背景を黒に
  Frame frame = leap.frame();               // Frame オブジェクトを宣言し、leap のフレームを入れる
  HandList hands = frame.hands();           // HandList オブジェクトを宣言し、frame 内の手(複数)の情報を取得
  iBox = frame.interactionBox();            // InteractionBox を初期化
  int[] choice = new int[hands.count()];    // ★ 出したじゃんけんの手を入れる変数
  for(int i = 0; i < hands.count(); i++) {  // 見つかった全ての手について
    Hand hand = hands.get(i);               // Hand オブジェクトを宣言し、i 番目の手を取得
    drawPalm(hand);                         // drawPalm 関数を呼ぶ
    drawFingerTip(hand);                    // drawFingerTip 関数を呼ぶ
    choice[i] = getChoice(hand);            // ★ getChoice 関数でじゃんけんの手を認識
  }
  if(hands.count() == 2) {                  // ★ 見つかった手が2個なら
    judge(hands, choice);                   // ★ judge 関数で勝敗を判定
  }
}

// 手のひらを描画する drawPalm 関数(Hand オブジェクトを引数に取る)
void drawPalm(Hand hand) {
  Vector palmPos = hand.palmPosition();     // 手のひらの位置を取得
  Vector palmPosNorm = iBox.normalizePoint(palmPos, false); // 標準化された座標値に変換
  pushMatrix();                             // 描画座標の変換開始
    translate(palmPosNorm.getX() * width,   // 標準化された座標値にウィンドウの幅を掛けて画面内の座標に変換
              (1 - palmPosNorm.getY()) * height,  // ウィンドウの高さを掛けて画面内の座標に変換
              palmPosNorm.getZ() * 50);     // Z軸(奥行方向)は適当に 50 を掛けた
    rotateX(-1 * hand.direction().pitch()); // X軸まわりに座標を回転(手の pitch 角度ぶん)
    rotateY(-1 * hand.direction().yaw());   // Y軸まわりに座標を回転(手の yaw 角度ぶん)
    rotateZ(-1 * hand.palmNormal().roll()); // Z軸まわりに座標を回転(手の roll 角度ぶん)
    noStroke();                             // 線は描かない
    fill(255);                              // 白く塗りつぶす
    lights();                               // 3D シーン内に照明を置く
    box(80, 10, 100);                       // 手のひらを示す箱を描く
    stroke(255);                            // 線を描く(白)
    line(0, 0, 0, 50);                      // 箱の中心から Y 軸方向に 50ピクセル
  popMatrix();                              // 描画座標の変換終了
}

// 指先を描画する drawFingerTip 関数(Hand オブジェクトを引数に取る)
void drawFingerTip(Hand hand) {
  FingerList fingers = hand.fingers();        // FingerList オブジェクトに見つかった指の情報(複数)を入れる
  for(int i = 0; i < fingers.count(); i++) {  // 見つかった全ての指について
    Finger finger = fingers.get(i);           // 指 i を取得(0:親指, 1:人差し指, 2:中指, 3:薬指, 4:小指)
    Vector tipPos = finger.tipPosition();     // その指の指先(tip)の位置を取得
    Vector tipPosNorm = iBox.normalizePoint(tipPos, false);   // 標準化された座標値に変換
    if(finger.isExtended() == true) {         // その指が伸びて(isExtended)いたら
      fill(255, 0, 0);                        // 塗りつぶし色を赤に
    } else {                                  // そうでなければ(伸びていなければ)
      fill(255, 255, 255);                    // 塗りつぶし色を白に
    }
    pushMatrix();                           // 描画座標の変換開始
      translate(tipPosNorm.getX() * width,  // 標準化された座標値にウィンドウの幅を掛けて画面内の座標に変換
                (1 - tipPosNorm.getY()) * height,   // ウィンドウの高さを掛けて画面内の座標に変換
                tipPosNorm.getZ() * 50);    // Z軸(奥行方向)は適当に 50 を掛けた
      noStroke();                           // 線は描かない
      lights();                             // 3D シーン内に照明を置く
      sphere(20);                           // 球を描く
    popMatrix();                            // 描画座標の変換終了
  }
}

// ★ じゃんけんの手を認識する getChoice 関数(Hand オブジェクトを取り、整数を返す)
int getChoice(Hand hand) {
  FingerList fingers = hand.fingers();      // FingerList オブジェクトに見つかった指の情報(複数)を入れる
  int extendedFingers = 0;                  // 伸びている指の数をカウントする変数
  for(int i = 0; i < fingers.count(); i++) {    // 見つかった全ての指について
    Finger finger = fingers.get(i);         // 指 i を取得(0:親指, 1:人差し指, 2:中指, 3:薬指, 4:小指)
    if(finger.isExtended() == true) {       // その指が伸びて(isExtended)いたら
      extendedFingers += 1;                 // 伸びている指の数を増やす
    }
  }
  int choice = 0;                           // じゃんけんの手を入れる変数
  if(extendedFingers < 2) {                 // 伸びている指の数が2本未満なら
    choice = 0;                             // 0: Rock(グー)
  } else if(extendedFingers == 2) {         // 伸びている指の数が2本なら
    choice = 1;                             // 1: Scissors(チョキ)
  } else {                                  // それ以外の場合
    choice = 2;                             // 2: Paper(パー)
  }
  String[] choiceName = {"Rock", "Scissors", "Paper"};        // じゃんけんの手の名前を定義
  Vector palmPos = hand.palmPosition();                       // 手のひらの位置を取得
  Vector palmPosNorm = iBox.normalizePoint(palmPos, false);   // 標準化された座標値に変換
  fill(255);                                                  // 白く塗りつぶす
  textSize(24);                                               // 文字サイズ
  text(choiceName[choice], palmPosNorm.getX() * width, 60);   // じゃんけんの手を手のひらの位置の上方に表示
  return choice;                            // じゃんけんの手の値(0/1/2)を返す
}

// ★ じゃんけんの勝敗を判定する judge 関数(対戦している二つの手と、それぞれのじゃんけんの手を引数に取る)
void judge(HandList hands, int[] choice) {
  textSize(36);                             // 文字サイズ
  if(choice[0] == choice[1]) {              // じゃんけんの手が同じなら(あいこなら)
    fill(255);                              // 白く塗りつぶす
    text("Draw", width / 2, 120);           // Draw と表示
    return;                                 // これ以降は実行せずに関数を抜ける
  }
  Hand winner;                              // 勝者の手の上方を入れる Hand オブジェクト
  if(((choice[0] + 1) % 3) == choice[1]) {  // じゃんけんの勝敗の判定式(詳細は割愛)
    winner = hands.get(0);                  // 勝者は手[0]
  } else {                                  // 判定式が成立しなければ
    winner = hands.get(1);                  // 勝者は手[1]
  }
  Vector winnerPos = winner.palmPosition();         // 勝者の手の手のひらの位置を取得
  Vector winnerPosNorm = iBox.normalizePoint(winnerPos, false);   // 標準化された座標値に変換
  fill(0, 255, 0);                                  // 赤く塗りつぶす
  text("Win!", winnerPosNorm.getX() * width, 120);  // 勝者の手のひらの位置の上方に Win! と表示 
}

動作確認

leap_rps.pde を実行し(▶をクリック)、Leap Motion の上に両手をかざしてみましょう。そこで自分でじゃんけんしてみてください。

leap_rps.pde の実行結果
leap_rps.pde の実行結果

じゃんけんの手(グーは Rock、チョキは Scissors、パー は Paper)が表示されます。同じ手なら Draw、それ以外は勝ったほうに Win! が表示されます。

解説

新しく加わったのは主に、伸びた指の本数からじゃんけんの手を認識する getChoice() 関数と、二人のじゃんけんの手から勝敗を決定する judge() 関数です。

じゃんけんの手を認識する方法は、伸びた指が2本未満ならグー(Rock)、2本ならチョキ(Scissors)、それ以外はパー(Paper)という簡単な方式です。3本でもパーになりますし、Leap Motion の認識精度しだいではうまく認識しませんが、そこは気にしないでおきましょう。

勝敗を判定する方法についてはいくつかのアルゴリズムが知られています。ここでは、グー(Rock)を 0、チョキ(Scissors)を 1、パー(Paper)を 3 にしたときの勝敗の関係の分析をもとに

((choice[0] + 1) % 3) == choice[1]

という判定式で勝敗を決めています。「%」という演算子は「割った余り(剰余)」を求めています。どうしてそれで勝敗が決定できるかは自身で調べたり、図表に書いて考えてみてください。

あとは、勝者を決めて、勝者の手(winner)のあたりに Win! と表示させています。

▲TOP

7. 空中ピアノ

手や指の座標値で遊べましたので、次は、その座標値をもとにコンピュータとなにかインタラクションしてみましょう。世の中には、VR 世界のなかでボタンを押したり、ジェスチャでコンピュータを操作するようなデモがあります。それらはけっこう「応用編」なので、ここでは単純に、親指で位置を決めて、ピンチ(親指と人差し指でつまむ)動作をすると音が鳴る、という単純な例を紹介します。

今から作るもの
今から作るもの

プログラミング

leap_finger.pde を「名前をつけて保存」で leap_piano.pde という名前で保存して書き始めると楽にてきると思います(けっこう書き直しますが)。それぞれのコードが何をしているのか、さっきと何が違うか、コード中のコメント文を見て考えながら入力していきましょう。

import com.leapmotion.leap.*;               // LeapJava ライブラリを使う
import processing.sound.*;                  // ★ sound ライブラリを使う

Controller leap = new Controller();         // leap という名前で Controller オブジェクトを宣言
InteractionBox iBox;                        // InteractionBox オブジェクト(座標変換などをする)を宣言
boolean pinched[] = new boolean[2];         // ★ 指がピンチ(親指と人差指でつまむ動作)か否かを入れる変数(二人プレイまで対応)
int noteNum = 8;                            // ★ 音程の数
String[] note = {"C", "D", "E", "F", "G", "A", "B", "C"}; // ★ 音名の文字列
SoundFile[] tone = new SoundFile[noteNum];  // ★ 鳴らす音を入れる SoundFile オブジェクト(配列)

void setup() {
  size(800, 600);                           // ★ このプログラムでは2次元で
  for(int i = 0; i < noteNum; i++) {        // ★ 鳴らす音の読み込み
    tone[i] = new SoundFile(this, i + ".wav");  // ★ 0:ド(C) ~ 7:ド(C) .wav
  }
}

void draw() {
  background(0);                            // 背景を黒に
  Frame frame = leap.frame();               // Frame オブジェクトを宣言し、leap のフレームを入れる
  HandList hands = frame.hands();           // HandList オブジェクトを宣言し、frame 内の手(複数)の情報を取得
  iBox = frame.interactionBox();            // InteractionBox を初期化
  drawKeys();                               // ★ drawKeys 関数で鍵盤を描く
  for(int i = 0; i < hands.count(); i++) {  // 見つかった全ての手について
    Hand hand = hands.get(i);               // Hand オブジェクトを宣言し、i 番目の手を取得
    drawThumb(hand, i);                     // ★ drawThumb 関数で親指の位置を描く
  }
}

// ★ 鍵盤を描く drawKeys 関数
void drawKeys() {
  for(int i = 0; i < noteNum; i++) {        // 音程の数ぶん
    stroke(255);                            // 白い線で
    noFill();                               // 塗りつぶしなし
    rect(0, height / noteNum * i, width, height / noteNum);         // 画面の上から矩形を描く
    textSize(40);                                                   // 文字サイズ
    text(note[(noteNum - 1) - i], 10, height / noteNum * (i + 1));  // 音名を表示
  }
}

// ★ 親指の位置を描く drawThumb 関数(手の情報と、その手のidを引数に取る)
void drawThumb(Hand hand, int id) {
  FingerList fingers = hand.fingers();      // FingerList オブジェクトに見つかった指の情報(複数)を入れる
  Finger thumb = fingers.get(0);            // 親指を取得
  Vector thumbPos = thumb.tipPosition();    // 親指の位置を取得
  Vector thumbPosNorm = iBox.normalizePoint(thumbPos, false);   // 標準化された座標値に変換
  noStroke();                               // 線は描かない
  fill(255);                                // 白く塗りつぶす
  ellipse(thumbPosNorm.getX() * width,      // 親指の位置に円を描く
          (1 - thumbPosNorm.getY()) * height,
          30, 30);
  if(hand.pinchDistance() < 20) {           // ピンチ距離が 20mm 未満なら
    playTone((1 - thumbPosNorm.getY()) * height, id);   // playTone 関数で音を鳴らす
    pinched[id] = true;                     // ピンチ状態を true にする
  } else {                                  // ピンチ距離が 20mm 以上なら
    pinched[id] = false;                    // ピンチ状態を false にする
  }
}

// ★ 音を鳴らす playTone 関数(親指の位置と、その手のidを引数に取る)
void playTone(float thumbPos, int id) {
  if(pinched[id] == true) {                 // そのidの手が既にピンチ状態なら
    return;                                 // 何もしないで抜ける(一度のピンチで何度も音が鳴らないように)
  }
  for(int i = 0; i < noteNum; i++) {        // ピンチされた位置から音程を判定
    if((thumbPos > height / noteNum * i) &&
       (thumbPos < height / noteNum * (i + 1))) {
      tone[(noteNum - 1) - i].play();       // 指定の音程の音を鳴らす
      return;                               // 抜ける
    }
  }
}

実行の前に

実行するには 8個の音源ファイル(*.wav)が必要です。8個別々の音ならなんでも構いませんが、例えばこちらのサイトから、ピアノの「ド・レ・ミ・ファ・ソ・ラ・シ・ド」の8個の wav ファイルをダウンロードしましょう。ダウンロードしたら、下の音から順に 0.wav, 1.wav, ... , 7.wav という名前にファイル名を変更しましょう。さらに、leap_piano.pde があるフォルダの中に data という名前のフォルダを作り、8個の音源ファイルをその中に入れましょう。

また、Processing で音を鳴らすには、「Sound」というライブラリを入れる必要があります。Processing で「スケッチ」メニュー -> ライブラリをインポート -> ライブラリを追加 を選びます。

ライブラリを追加
ライブラリを追加

出てきた「Contribution Manager」というウィンドウの「Libraries」タブの左上のテキストボックスに「sound」と入力すると、ライブラリがいくつか表示されます。その中の「Sound | Sound library for Processing」を選択して、右下の「Install」をクリックします。するとダウンロードされ、自動的にインストールされます。

Sound ライブラリのインストール
Sound ライブラリのインストール

動作確認

leap_piano.pde を実行し(▶をクリック)、Leap Motion の上に手をかざしてみましょう。親指の位置に白い円が描かれます。鳴らしたい音のところに親指を移動させて、そこでピンチ(親指と人差し指をあわせる)動作をすると、音が鳴ります。

leap_piano.pde の実行結果
leap_piano.pde の実行結果

二つの手(二人の手)で別々の音を鳴らすこともできます。一人で和音を鳴らしたり、二人で連弾してみましょう。

なお、このプログラムでは使える手は二つだけです。また、ピンチ動作として認識されるのは、親指と人差し指の組み合わせだけです(親指と中指、親指と薬指は認識できません)。

解説

ほとんどはここまでに使ってきた機能と、あとは アルゴリズムの工夫だけです。新しいのは hand.pinchDistance() くらいです。pinchDistance() を使うと、親指と人差し指の距離を mm 単位で取得できます。これが 20mm 未満になったら音を鳴らしています。

Sound ライブラリの使い方については、プログラムを読めばだいたい分かると思います。

あとは、詳しい解説は割愛します。自分で解読してみましょう。

▲TOP

まとめ

Leap Motion のプログラミング、いかがでしたか?

私たちは普段、「手」や「指」を使っていろいろなことをしています。その手指の位置や動きを測れる Leap Motion の可能性は無限大です。

例えば Leap Motion のサイトには、アイデアをくすぐる数多くのデモンストレーションが公開されています。正直、このページで学んだことだけではできないことも多いですが、あとはアルゴリズムの工夫や組み合わせです。

特に Leap Motion は「小さい」という特徴があります。VR ゴーグルに限らず様々な場所に設置して、手指の動きで様々なコンピュータやネットとインタラクションできるでしょう。例えば Kinect は全身の動きが測れますが、手指の動きは測れません。組み合わせたらもっともっとユーザの意図を汲み取れるでしょう。

普段、自分が手を使ってやっていることを観察してみて、それを Leap Motion で測ったら面白いことができないか考えたりして、ぜひいろんなアイデアを試してみましょう。

補足

上で作ったコードはすべてこちら(GitHub)に置いてありますから、参考にしてください。

Leap Motion の SDK については詳しくはこちらです。Processing の場合は Java の解説を参考にしてください。また、ダウンロードした SDK の zip ファイルの中の docs フォルダにも同じリファレンスが入っています。今回使わなかったオブジェクトについても詳しい説明があります。英語ですが、難しい言葉は使われていませんから、読み解いてみましょう。様々な応用のヒントが見つかると思います。また、他の言語での使い方も分かります。

今回は Leap Motion 公式の SDK を使って Processing でプログラミングしました。実は公式の SDK を使う以外にも Processing で Leap Motion で遊ぶ方法があります。Processing の「ライブラリの追加」で「Leap Motion for Processing」というライブラリをインストールして、そのライブラリの仕様に沿って作る方法です。実はそちらのほうが準備は簡単です。しかし、Leap Motion のドライバが旧版の V2 である必要があります。V2 ドライバはいつサポートされなくなる(ダウンロードできなくなる)か分かりません。また、公式 SDK は Processing だけでなく、C++, C#, Objective-C, Java, JavaScript, Python, Unity などとても多くの開発言語・環境に対応しています。つまり、このページの内容は Processing 以外にも応用が効きます。そのような理由からこのページでは公式 SDK を使う方法を解説しました。ちなみに、どちらを使っても、プログラミング自体の難易度やコード量はほとんど変わりません。

▲TOP