note.x

e_penrose.jpg

penroseTriangle.swf(要:FlashPlayer9)※画面クリックで種明かし

PV3D2.0a Rivision 414で追加された、FrustumCamera3Dの新プロパティ「ortho」。これをtrueにするとプロジェクション変換が平行投影になる。

camera = new FrustumCamera3D(viewport, 48, 1, 5000);
camera.ortho = true;

当たり前のことなんだろうけど、通常の透視投影とは違って距離感が掴めなくなるので、カメラ位置の指定時にとまどう。クリッピングやカリングは、透視投影の時と同じように行われるので、画面になにも表示されないなんて事態になった場合はカメラとオブジェクトの位置を極端に離してみるとわかりやすいかも。カメラとオブジェクトの距離や、fovの設定では見た目の大きさが変化しないので、orthoScaleを使って調節するといいみたいだ。

ってことで、平行投影だからできるトリックでペンローズの三角形を作ってみたのが上記のデモ。エッシャーの作品「WaterFall」のモチーフになってたりする。PSPの無限回廊も、このあたりがヒントになってるんだろうな。

FrustumCamera3Dには、さらにorbit()メソッドも追加されてた。こっちは指定したDisplayObject3Dをターゲットとして、その周りを衛星のようにグルグルと回る動きを簡単に付けられるメソッドみたいだ。実際試してないけど。


e_geometory_uv.jpg

uv_change.swf(要:FlashPlayer9)

PV3Dのマテリアルは、Triangle3Dで定義されたUV座標を元にその貼り込まれ方(こんな日本語あるのか)が決定される。
このUV座標定義は任意に書き換えできるので、デモのように複数のオブジェクトを使って一枚の画像を構成しようなんていう場合にも、マテリアルを1つだけ定義して、割り当てるuv座標をオブジェクトごとに変化させれば実現できる。要は任意のFaceに、テクスチャの任意の座標を割り当てることができるって感じ。1枚のビットマップから、copyPixelsとか使って複数のマテリアルを内部で生成する方法もあるけど、せっかくUV座標が変えられるならこういう手段も知っておこうと思って調べてみた。

セグメントを切らない(ポリゴンの分割数が縦横ともに1)Planeを例にとると、

var objPlane:Plane = new Plane( bitMat, 100, 100 );

scene.addChild(objPlane);

objPlane.geometry.faces[0].uv = [new NumberUV(0.5,0),new NumberUV(1,0),new NumberUV(0.5,0.5)];

objPlane.geometry.faces[1].uv = [new NumberUV(1,0.5),new NumberUV(0.5,0.5),new NumberUV(1,0)];

っていう感じで、uv定義を変えられる。
geometry.faces.uv は、uv0、uv1、uv2(それぞれNumberUVオブジェクト)が順に格納されたArray。
なので、冗長になるけど

var objPlane:Plane = new Plane( bitMat, 100, 100 );

scene.addChild(objPlane);

objPlane.geometry.faces[0].uv0 = new NumberUV(0.5,0);
objPlane.geometry.faces[0].uv1 = new NumberUV(1,0);
objPlane.geometry.faces[0].uv2 = new NumberUV(0.5,0.5);
objPlane.geometry.faces[1].uv0 = new NumberUV(1,0.5);
objPlane.geometry.faces[1].uv1 = new NumberUV(0.5,0.5);
objPlane.geometry.faces[1].uv2 = new NumberUV(1,0);

という書き方でもオケ。
NumberUVは、テクスチャ画像の左下(ここ重要)を原点に、0〜1の値を取る正規ベクトルで定義。また、faces[0]、faces[1]とあるのは、分割数1のPlaneの場合2枚の三角ポリで構成されるため。

e_geometory_uv_rec.gif

自分自身がすぐ忘れるので、簡易デモも作ってみた。
4枚のPlaneそれぞれUV座標定義を変えて、一枚のテクスチャ画像を再構成。
最初のデモは一枚ずつ定義してたんじゃやってられないのでforループ使ったけど、原理は全く一緒。

参考用デモ(要:FlashPlayer9)

以下ソース。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package
{
    import flash.display.*;
    import flash.events.*;
 
    import caurina.transitions.Tweener;
 
    import org.papervision3d.view.BasicView;
    import org.papervision3d.core.proto.*;
    import org.papervision3d.core.math.*;
    import org.papervision3d.objects.DisplayObject3D;
    import org.papervision3d.objects.primitives.*;
    import org.papervision3d.materials.*;
 
    [SWF(backgroundColor=0x000000)]
 
    public class uv_change_test extends BasicView
    {
        private var planes:DisplayObject3D;
 
        public function uv_change_test()
        {
            stage.frameRate = 60;
            stage.align = StageAlign.TOP_LEFT;
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.quality = StageQuality.MEDIUM;
 
            //viewportの定義とカメラタイプ定義
            super (0,0,true,true,"Target");
 
            //カメラ設定
            camera.x = 0;
            camera.y = 0;
            camera.z = -1400;
            camera.fov = 30;
 
            planes = new DisplayObject3D();
            scene.addChild(planes);
 
            buildPlane();
 
            //レンダリング開始
            startRendering();
        }
 
 
        override protected function onRenderTick(event:Event=null):void
        {
            planes.yaw(-1);
            super.onRenderTick(event);
        }
 
 
        private function buildPlane():void
        {
            //マテリアル設定
            var bitMat:BitmapFileMaterial = new BitmapFileMaterial( "asset/finder.jpg" );
            bitMat.doubleSided = true;
 
            //Plane生成(左下)
            var objPlane:Plane = new Plane( bitMat, 100, 100 );
            planes.addChild(objPlane);
            objPlane.x = -60;
            objPlane.y = -60;
            objPlane.z = 0;
            objPlane.geometry.faces[0].uv = [new NumberUV(0,0),new NumberUV(0.5,0),new NumberUV(0,0.5)];
            objPlane.geometry.faces[1].uv = [new NumberUV(0.5,0.5),new NumberUV(0,0.5),new NumberUV(0.5,0)];
 
            //Plane生成(右下)
            var objPlane2:Plane = new Plane( bitMat, 100, 100 );
            planes.addChild(objPlane2);
            objPlane2.x = 60;
            objPlane2.y = -60;
            objPlane2.z = 0;
            objPlane2.geometry.faces[0].uv = [new NumberUV(0.5,0),new NumberUV(1,0),new NumberUV(0.5,0.5)];
            objPlane2.geometry.faces[1].uv = [new NumberUV(1,0.5),new NumberUV(0.5,0.5),new NumberUV(1,0)];
 
            //Plane生成(左上)
            var objPlane3:Plane = new Plane( bitMat, 100, 100 );
            planes.addChild(objPlane3);
            objPlane3.x = -60;
            objPlane3.y = 60;
            objPlane3.z = 0;
            objPlane3.geometry.faces[0].uv = [new NumberUV(0,0.5),new NumberUV(0.5,0.5),new NumberUV(0,1)];
            objPlane3.geometry.faces[1].uv = [new NumberUV(0.5,1),new NumberUV(0,1),new NumberUV(0.5,0.5)];
 
            //Plane生成(右上)
            var objPlane4:Plane = new Plane( bitMat, 100, 100 );
            planes.addChild(objPlane4);
            objPlane4.x = 60;
            objPlane4.y = 60;
            objPlane4.z = 0;
            objPlane4.geometry.faces[0].uv = [new NumberUV(0.5,0.5),new NumberUV(1,0.5),new NumberUV(0.5,1)];
            objPlane4.geometry.faces[1].uv = [new NumberUV(1,1),new NumberUV(0.5,1),new NumberUV(1,0.5)];
        }
 
    }
}

前回作った、マウスのクリック位置に応じてオブジェクトが移動するデモを応用して、平面上じゃなく球面上をオブジェクトが移動するものを作ってみた。ヨユーで出来たみたいな物言いだけど、オレには難易度高すぎな課題だった…orz。

e_quaternion.jpg

InteractiveFacelevelTest3.swf(要:FlashPlayer9)

平面上を移動させたい場合は、変化する座標が2軸だけなので2Dと同じ感覚で処理すれば、まぁ問題無い感じで動く。球面上を移動させたい場合は、この考え方で実装すると始点と終点の角度が大きくなるにつれ、移動するオブジェクトが球体にメリ込んじゃう。

失敗例(要:FlashPlayer9)

Tweenerのベジェ補間とかを上手く使えばソレっぽいものが出来そうな気もしたんだけど、いい加減3Dプログラミングにおける数学的知識も蓄えていかないといかんなぁと思って、クォータニオンで座標変換することに挑戦してみた。はっきり言って自分でも全く理解できてないので備忘録としてメモっとく。

PV3D2.0には core.math.Quaternion っていうクラスが用意されてるのでこれを使った。Matrix3DにもQuaternionを扱えるメソッドがあるけど、core.math.Quaternionには、SLERP (Spherical Linear intERPolation 球面線形補間)のためのメソッドがあるし、せっかくなのでこっちで。

位置の移動については以下のようなことをした。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//回転させたい点の初期位置(始点)のクォータニオン生成
var qStart:Quaternion = new Quaternion(obj.x,obj.y,obj.z,0);
 
//マウスクリック位置(終点)のベクトル
var clickVec:Number3D = new Number3D(click.x,click.y,click.z);
 
//始点と終点(マウスをクリックした座標)のベクトルの外積から法線ベクトル(回転軸)生成
var startVec:Number3D = new Number3D(obj.x, obj.y, obj.z);
startVec.normalize();
clickVec.normalize();
var vcross:Number3D = Number3D.cross(startVec,clickVec);
 
//始点と終点のベクトルの内積から回転角度(ラジアン)生成
var rot:Number = Math.acos(Number3D.dot(startVec,clickVec));
 
//指定軸回りの回転クォータニオンqを生成
var q:Quaternion = Quaternion.createFromAxisAngle(vcross.x,vcross.y,vcross.z,rot);
 
//qの共役クォータニオン(ベクトル部の符号が反転したもの)rを算出
var r:Quaternion = Quaternion.conjugate(q);
 
//r・qStart(始点)・qと掛け算回転後のクォータニオンを生成
var ans:Quaternion = Quaternion.multiply(r,qStart);
ans = Quaternion.multiply(ans,q);
 
//クォータニオンから行列に変換
var m:Matrix3D = Matrix3D.translationMatrix(ans.x,ans.y,ans.z);
 
//行列を適用
obj.copyTransform(m);

上のコードをそのまま実装すると、クリック直後にオブジェクトがワープするので実際には回転クォータニオンを生成する際の角度の値を時間経過にしたがって変化させる。

クォータニオンをどう扱うのかについては、IKDさんの「マルペケつくろーどっとコム」内の、
クォータニオンを学んでみよう!
クォータニオンを学んで見よう:サンプルプログラム
が大変参考になりました。始点のクォータニオンと回転クォータニオンから、終点クォータニオンを得るあたりは完全に丸写し;

オブジェクト自身の姿勢制御については、以下のように

//最初の姿勢クォータニオン生成
var pos1:Quaternion = Quaternion.createFromOrthoMatrix(obj.transform);
//最終的な姿勢クォータニオン生成
var pos2:Quaternion = Quaternion.createFromOrthoMatrix(marker.transform);
pos1.normalize();
pos2.normalize();

//球面線形補間(slerp)を使ってオブジェクトの姿勢を補間(t = 0〜1)
var gpos:Quaternion = Quaternion.slerp( pos1, pos2, t );
gpos.normalize();

slerpメソッドを使うことで2つのクォータニオンから時間tにおけるクォータニオンが得られるらしい。とにかく初期状態と変化後のクォータニオン作って、tを0から1に変化させれば、中間の状態が取れるということで自分を納得させた(笑)このクォータニオンを行列に変換して、前述した移動のための行列とかけ算すれば(Matrix3D.multiply(m,gpos);)移動しつつ姿勢が変化していく行列が得られると。

あと、slerpを使う場合、初期値のクォータニオンと変化後のクォータニオンの内積が0以上じゃないと、遠回りな補間しちゃうので、この条件に該当する場合は初期値のクォータニオンのベクトル部の符号を反転させるという、かなり素人考えな方法で対処した;きっといい方法があるんだろうなぁ。

出来上がったものは微妙に挙動がおかしいし、ときどきオブジェクトがとんでもない座標に移動しちゃったりするし、クォータニオンって結局何なのよって感じだけど概ねイメージ通りのものができた。ベクトルも行列もクォータニオンも理論はサッパリだけど、道具としての扱い方は解ってきたので、ようやく3Dプログラミングの入り口付近にたどり着けたのかなと。


前回の InteractiveScene3DEvent に加えて Mouse3D を有効化することで、メッシュのクリック位置が取れる。これも1.7の時点で実装されていたものだけど、あちこちに分散した各種プロパティを操作しないと使えるようにならなかったりして、すんげーややこしかった。InteractiveScene3DEventと同様に、これも比較的シンプルに扱えるようになったかなぁと。以下利用例。

e_mouse3d.jpg

InteractiveFacelevelTest.swf(要:FlashPlayer9)
※チェック模様のPlaneをクリックした位置にConeが移動。

仕込みの手順は以下のような感じ。

  1. ビューポート定義時にinteractiveプロパティをtrueに。
  2. Mouse3D の enabled プロパティをtrueに。
  3. イベント取りたいDisplayObject3Dインスタンスに与えるマテリアルのinteractiveプロパティをtrueに。

まとめると、

viewport = new Viewport3D(0,0,true,ture);
Mouse3D.enabled = true;

var flmat:FlatShadeMaterial = new FlatShadeMaterial(light, 0x00ccff );
flmat.interactive = true;
var objPlane:Plane = new Plane( flmat, 250, 250);

これで仕込みはオッケーで、

objPlane.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, mouseClick);

private function mouseClick(event:InteractiveScene3DEvent):void
{
    log(viewport.interactiveSceneManager.mouse3D.x);
}

みたいにイベント登録すると、DisplayObject3Dインスタンスをクリックした時点でのマウス座標を、対象になるDisplayObject3Dのローカル座標(たぶん)ワールド座標に変換して返してくれる。上の例で言えばobjPlaneをクリックした時のマウス座標をobjPlaneのローカル座標系ワールド座標系に変換したx座標の値が返ってくると。

謎なのは、viewport.interactiveSceneManagerがMouse3Dオブジェクトを生成してるのに、Mouse3Dを有効化する際

viewport.interactiveSceneManager.mouse3D.enabled = true;

だとダメなこと。このへんよくわかんね。

また、ISM(interactiveSceneManager)にはvirtualMouseってのもあって、

viewport.interactiveSceneManager.virtualMouse.x
viewport.interactiveSceneManager.virtualMouse.y

で、uv座標が返ってくる。これは以前に取り上げたhelloMouse3D.asみたいなことをする場合に使う。helloMouse3DはPV3D1.7のサンプルだけど、mouse3DやvirtualMouseの扱いに関しては参考になるなぁ。今でもsvn経由で取得可能。