Papervision3DとWiiFlashの接続のまとめ

要旨

PV3DとWiiFlashをつなげて、Wiiリモコンの傾きに応じて箱の表示を傾けることができたので、そのまとめ。

環境設定

環境はだいたい以下のような感じ。今回はあまり詳しく説明しない。

概要

Wiiリモコンの加速度の値を取り、デバッグとして左上に表示。
同じく加速度によって、リモコンっぽい形状の直方体を動かす。

解説

つなぐ際の問題になりそうなのは「加速度の軸の方向」と、「どうやって直方体を回転させるか」あたり。「加速度の軸の方向」はコードを見てもらえばわかるので、「どうやって直方体を回転させるか」だけ説明。


加速度はベクトルであり3次元の値である。これでは「方向」はわかっても「姿勢」はわからない。例えば「指差してる方向」はわかっても「親指がどっちの方向にあるか(手の回転角度)」まではわからない。
Wiiリモコンから取れるのも加速度のみなので、ここから姿勢は一意には決まらない。例えばWiiリモコンを床に置いた場合の加速度は一定であり、そのリモコンが北を向いてるか東を向いてるかまではわからないということ。
なので、こちらで回転の仕方を制限してやる必要がある。


ここでやっているのは「"Y軸"と"リモコンの上方向の軸"によって形成される平面状の回転」。簡単に言うと「もともとの上方向をリモコンの上方向に持っていく」というだけのこと。その回転軸を「Y軸」と「リモコンの加速度の逆方向」から求めている。角度の求め方まで含めて、結構ここらへんは適当にやってしまっている。


(説明が上手くまとまらなかったので、あとで書き直すかも。書き直さないかも。)


(あと、YouTubeとかに動画を上げた方がわかりやすそう)

コード

//wiimotedemo.as
package{
  //label
  import flash.display.*;
  import flash.text.*;
  import flash.events.*;
  import flash.ui.*;
  import flash.utils.*;

  //PV3D
  import org.papervision3d.core.*;
  import org.papervision3d.core.proto.*;
  import org.papervision3d.scenes.*;
  import org.papervision3d.objects.*;
  import org.papervision3d.cameras.*;
  import org.papervision3d.materials.*;
  import mx.core.BitmapAsset;//Material

  //WiiFlash
  import org.wiiflash.Wiimote;
  import org.wiiflash.events.ButtonEvent;

  //Class
  public class wiimotedemo extends Sprite
  {
    //WiiFlash
    private var wiimote:Wiimote = new Wiimote();

    //PV3D
    private var container : Sprite;
    private var scene     : Scene3D;
    private var camera    : Camera3D;
    private var rootNode  : DisplayObject3D;

    private var obj:Array = new Array();

    //Label
    private var label:TextField;


    //Constructor
    public function wiimotedemo():void
    {
      //Init Common
      {
        //リサイズ対応
        stage.addEventListener(Event.RESIZE, onStageResize);

        //毎フレーム呼ばせる関数の登録
        addEventListener(Event.ENTER_FRAME, myLoopEvent);
      }

      //Init WiiFlash
      {
        //サーバに接続したときのリスナ
        wiimote.addEventListener(Event.CONNECT, handleConnect);
        //Aボタンが押されたときのリスナ
        wiimote.addEventListener(ButtonEvent.A_PRESS, handleAPress);

        //接続開始
        wiimote.connect();
	  }

      //Init PV3D
      {
        //表示用のSpriteオブジェクトを生成
        container = new Sprite();
        container.x = 400 / 2; // at center : swf width  = 400
        container.y = 400 / 2; // at center : swf height = 400
        addChild(container);

        //シーンオブジェクトを作成
        scene = new Scene3D(container);

        //カメラオブジェクトを作成
        camera = new Camera3D();
        camera.x = 0;
        camera.y = 0;
        camera.z = -100;//リモコンに見立てた直方体を後ろから見る
        camera.focus = 500;
        camera.zoom = 1;

        //ルートノードを作成
        rootNode = new DisplayObject3D();
        scene.addChild(rootNode);

        //オブジェクトの作成
        obj.push(createCube());

        //オブジェクトをルートノードに追加
        for(var i:int; i<obj.length; i++){
          rootNode.addChild(obj[i]);
        }
      }

      //Init Debug Label
      {
        //デバッグ表示用のラベルの作成
        label = makeLabel("");
        addChild(label);
      }
    }

    //=WiiFlash=

    //WiiFlash Serverに接続したときのリスナ
    private function handleConnect(e:Event):void
    {
      trace("Connect");
    }

    //WiiリモコンのAボタンが押されたときのリスナ
    private function handleAPress(e:Event):void
    {
      trace("A Press");
      wiimote.rumbleTimeout = 500;
      wiimote.rumble = true; //Wiiリモコンを振動させる
    }


    //=PV3D=
    //Create Material
    private function CreateMaterial():MaterialsList
    {
      //前後左右の区別のため、各面の色を分ける
      var materialList:MaterialsList = new MaterialsList();
      materialList.addMaterial(new ColorMaterial(0x000000), "front");
      materialList.addMaterial(new ColorMaterial(0xFFFFFF), "back");
      materialList.addMaterial(new ColorMaterial(0xFF0000), "right");
      materialList.addMaterial(new ColorMaterial(0x00FF00), "left");
      materialList.addMaterial(new ColorMaterial(0x0000FF), "top");
      materialList.addMaterial(new ColorMaterial(0xFFFF00), "bottom");
      return materialList;
    }

    //Create Cube
    private function createCube():DisplayObject3D
    {
      var material:MaterialsList = CreateMaterial();

      var width:Number = 80;
      var depth:Number = 40;
      var height:Number = 30;
      var cube:Cube = new Cube(material, width, depth, height);
//      cube.x = 0;//50;//位置はループ内で指定
//      cube.y = 0;//-100;
//      cube.z = 0;//-100
      return cube;
    }


    //=Common=

    //Main Loop
    private function myLoopEvent( event:Event ):void
    {
      //Update
      for(var i:int; i<obj.length; i++){
//      wiimote.sensorY;//横持ち時のXの値
//      wiimote.sensorZ;//横持ち時のYの値
//      wiimote.sensorX;//横持ち時のZの値
        var axisY:Number3D = new Number3D(0, 1, 0);//直方体の上方向
        var nrm:Number3D = new Number3D(wiimote.sensorY, wiimote.sensorZ, -wiimote.sensorX);//リモコンの上方向
        //nrmを長さ1に収める(加速度なので、1より大きかったり0だったりしうる)
        nrm.normalize();
        if(nrm.modulo < 0.01){
          nrm = new Number3D(0, 1, 0);
        }

        //axisYとnrmによって作られる平面上で回転させることにする
        var axis:Number3D = Number3D.cross(axisY, nrm);//なので、その平面での回転軸を求める
        if(axis.modulo < 0.01){//平面が形成できない=axisYとnrmがほぼ同じ軸=じゃあ適当にZ軸でよい
          axis = new Number3D(0, 0, 1);
        }else{
          axis.normalize();
        }

        //回転する角度をaxisYとnrmから求める
        var angle:Number = Math.acos(Number3D.dot(axisY, nrm));
        if(Number3D.dot(Number3D.cross(axis, axisY), nrm) < 0){
          angle = -angle;
        }

        //実際に回転を適用
        obj[i].transform = Matrix3D.rotationMatrix( axis.x, axis.y, axis.z, angle );
        //位置はそのまま原点
      }

      //Render
      scene.renderCamera(camera);

      //Debug
      label.text = ""+wiimote.sensorX+"\n"+wiimote.sensorY+"\n"+wiimote.sensorZ+"\n";
    }

    //Resize
    private function onStageResize(event:Event):void
    {
      container.x = stage.stageWidth / 2;
      container.y = stage.stageHeight / 2;
    }


    //=Debug=
    //Create Label
    private function makeLabel(text:String):TextField
    {
        var label:TextField=new TextField();
        label.text      =text;
        label.autoSize  =TextFieldAutoSize.LEFT;
        label.selectable=false;
        return label;
    }
  }
}