leapjs-pluginsとThree.jsでLeap Motionデータの表示を行う.

## はじめに

three.jsを利用して、Leapで検知した手のモデルを単純に描画することを考える.

大枠の処理の流れは下のようになるはずである.

1. three.jsのsceneを初期化する.
1. Leapで手を検出し、three.jsのsceneへmeshを追加.
1. Leapで手や指の位置情報を取得し、meshの位置情報へ反映.
1. Leapで手が消失したら、three.jsのsceneからmeshを削除.
1. 2.〜4.を繰り返す.

leap.jsを生で使うと、上記の2.や4.が発生するタイミングを検知するために、 

    「frame.handsの要素毎に`hand.id`を取得し、前回frameで保持しておいた`hand.id`と比較して一致しなかったら...」

のような実装をすることになる. また、管理している`hand.id`と作成したメッシュの紐付け関係を保持したり、等の処理が必要となるが、一々これを自分で書くのは意外と手間である.

[leapjs-plugins](https://github.com/leapmotion/leapjs-plugins)を利用すると、すこし楽ができるので紹介しておく.

なお、このエントリを書くにあたり、[Leap本家のExample](https://developer.leapmotion.com/gallery/bone-hands)を参考にした.

## leapjs-pluginsの機能

### 入手方法

[Leapのサイト](https://developer.leapmotion.com/leapjs/plugins)から落とすか、bowerを利用してinstallするのがよい.

```bash
$ bower install leapjs-plugins
```

### handEntry

handEntryはLeapのControllerにhandオブジェクトの検出・消失イベントを追加する機能である.
利用方法は下記のイメージ.

```js

var controller = new Leap.Controller();

// プラグインの利用はuseを利用.
controller.use('handEntry');

controller.on('handFound', function(hand){
//手が検出されたフレームのイベントリスナ
});

controller.on('handLost', function(hand){
//手が消失したフレームのイベントリスナ
});

controller.connect();

```

### handHold

handHoldは、Leapの各Model(HandやFinger, Pointable)にframeを跨ぐデータの保持機構を提供する.
jQueryの`$.fn.data()`と利用方法は似ており、以下の用に利用することが出来る.

```js:データの格納
frame.hands.forEach(function(hand){
  var mesh = new TREE.mesh(...);
  hand.data('palmMesh', mesh);
});
```

```js:データの取得
frame.hands.forEach(function(hand){
  mesh = hand.data('palmMesh');
});
```

handHoldプラグインも、利用時はConrollerに対して`use('handHold')`を実行する必要がある.

handEntry, handHoldを利用すると、LeapのモデルとThree.jsのメッシュのライフサイクルを簡単に管理出来る.

 1. `.on('handFound')`にて、meshを作成、`.data`でモデルと紐付ける
 2. requestAnimationFrameの中で、`.data`でモデルにからmeshを取り出し、位置の更新処理を行う
 3. `.on('handLost')でmeshを消滅させ、モデルからも`.data`にnullをセット

### その他の機能
他にも、leapjs-pluginsにはLeapのframeを記録・再生するためのPlayBackプラグイン等も含まれている. 機会があればそのうち。

## コード

実際に作成したコードは下記.

- JSの取得

```bash
bower install jquery threejs stats leapjs-plugins --save
```

- JavaScript

```js:scripts/main.js
'use strict';

(function($, Leap, THREE, Stats){

	var baseBoneRotation = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI / 2, 0, 0));

	// threejs関連Objectの定義
	var renderer, scene, camera;

	// FPS計測用
	var stats;

	var $msg = $('#output');

	/**
	 *
	 * Leapのコントローラ定義.
	 *
	 **/
	var leapController = new Leap.Controller();
	leapController.use('handHold').use('handEntry').on('handFound', function(hand){
		//手が検出されたフレームのイベントリスナ.
		//meshを作成してhand.dataで保持.
		hand.fingers.forEach(function (finger) {

			var boneMeshes = [];
			var jointMeshes = [];

			finger.bones.forEach(function(bone) {

				// create joints

				// CylinderGeometry(radiusTop, radiusBottom, height, radiusSegments, heightSegments, openEnded)
				var boneMesh = new THREE.Mesh(
					new THREE.CylinderGeometry(3, 3, bone.length),
					new THREE.MeshBasicMaterial({
						color: 0xff0000,
						wireframe: true
					})
				);

				//boneMesh.material.color.setHex(0xffffff);
				scene.add(boneMesh);
				boneMeshes.push(boneMesh);
			});

			for (var i = 0; i < finger.bones.length + 1; i++) {

				var jointMesh = new THREE.Mesh(
					new THREE.SphereGeometry(7, 6, 4),
					//new THREE.MeshPhongMaterial()
					new THREE.MeshBasicMaterial({
						color: 0xff0000,
						wireframe: true
					})
				);
				//jointMesh.material.color.setHex(0x0088ce);
				scene.add(jointMesh);
				jointMeshes.push(jointMesh);
			}
			finger.data('boneMeshes', boneMeshes);
			finger.data('jointMeshes', jointMeshes);
		});
	}).on('handLost', function(hand){

		//手が消失したフレームのイベントリスナ.
		//holdしているmeshデータをthree.jsから削除.
		hand.fingers.forEach(function (finger) {

			var boneMeshes = finger.data('boneMeshes');
			var jointMeshes = finger.data('jointMeshes');

			boneMeshes.forEach(function(mesh){
				scene.remove(mesh);
			});

			jointMeshes.forEach(function(mesh){
				scene.remove(mesh);
			});

			finger.data({
				boneMeshes: null
			});
		});

		//renderer.render(scene, camera);

	}).on('frame', function(frame){
		var msg = [];

		msg.push('hands: ' + frame.hands.length);

		frame.hands.forEach(function(hand){

			//updateHand(hand);

			msg.push('id: ' + hand.id);
			hand.fingers.forEach(function(finger){
				msg.push('fingerId: ' + finger.id);
				msg.push('jointMeshes Length: ' + finger.data('jointMeshes').length);
			});
		});

		$msg.text(msg.join());
	});

	var updateHand = function(hand){

		hand.fingers.forEach(function (finger) {

			finger.data('boneMeshes').forEach(function(mesh, i){
				var bone = finger.bones[i];

				mesh.position.fromArray(bone.center());

				mesh.setRotationFromMatrix(new THREE.Matrix4().fromArray( bone.matrix() ));

				mesh.quaternion.multiply(baseBoneRotation);
			});

			finger.data('jointMeshes').forEach(function(mesh, i){
				var bone = finger.bones[i];

				if (bone) {
					mesh.position.fromArray(bone.prevJoint);
				}else{
					bone = finger.bones[i-1];
					mesh.position.fromArray(bone.nextJoint);
				}

			});

		});

		//renderer.render(scene, camera);
	};

	/**
	 *
	 * three.jsの初期化
	 *
	 **/
	var initScene = function(){

		var width = $('#render_area').parent().innerWidth();
		var height = window.innerHeight - $('#render_area').offset().top - 20;

		renderer = new THREE.CanvasRenderer();
		renderer.setSize(width, height);
		$('#render_area')[0].appendChild(renderer.domElement);

		camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000);
		camera.position.y = 160;
		camera.position.z = 800;

		// カメラをマウス&キーボードで操作.
		// 要threejs-exampleのOrbitControls.js
		new THREE.OrbitControls(camera, renderer.domElement);

		scene = new THREE.Scene();

		scene.add(new THREE.AxisHelper(400));
		scene.add(new THREE.GridHelper(400, 10));

		// FPS計測器の初期化.
		stats = new Stats();
		$('#stats_wrap').append($(stats.domElement));

		return scene;

	};

	/**
	 *
	 * Leap型の直方体を追加する
	 *
	 **/
	var addLeapCube = function(scene){
		var geometry, material, mesh;

		geometry = new THREE.CubeGeometry(80, 11.5, 30);
		material = new THREE.MeshBasicMaterial({
			color: 0xff0000,
			wireframe: true
		});

		mesh = new THREE.Mesh(geometry, material);
		scene.add(mesh);
	};

	/**
	 *
	 * three.jsのフレーム描画.
	 *
	 **/
	var animate = function(){
		window.requestAnimationFrame(animate);
		renderer.render(scene, camera);
		leapController.frame(0).hands.forEach(updateHand);
		stats.update();
	};


	initScene();
	addLeapCube(scene);
	leapController.connect();
	animate();

})(jQuery, Leap, THREE, Stats);
```

- html

```html:index.html
<!doctype html>
<head> </head>
<body>
<div class="container">
          
  <div class="row">
    <div class="col-md-2">
      <div id="stats_wrap"></div>
    </div>  
    <div class="col-md10" style="height:100px;">
      <p id="output">hadnds:</p>
    </div>  
  </div>  
          
  <div class="row">
    <div class="col-md-12">
      <div id="render_area">
      </div>  
    </div>  
  </div>  
</div>  
        
<script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/threejs/build/three.js"></script>
<script src="bower_components/leapjs/leap-0.6.0.js"></script>
        
<script src="bower_components/leapjs-plugins/main/leap-plugins-0.1.6.js"></script>
<script src="bower_components/stats.js/src/Stats.js"></script>
<script src="bower_components/threejs-examples/examples/js/controls/OrbitControls.js"></script>
<script src="scripts/main.js"></script>
        
</body> 
</html> 

```
## 参考

 - http://leapmotion.github.io/leapjs-plugins/docs/