note.x

FrustumClipping と QuadrantRenderEngine が混在できないバグが未だにほったらかしっぽいので(Rev.942)、去年MLで流れてた対処法を紹介。とりあえず何とかしたい人向けTips。

FrustumClipping and QuadrantRenderEngine work together Demo(要:FlashPlayer9)

/core/dyn/DynamicTriangles.as の64行目以下

triangle.instance = object;
triangle.vertices = [v0, v1, v2];
triangle.uv = [uv0, uv1, uv2];
…

の部分を、以下のような感じに。

triangle.renderCommand.instance = object; //←この一行を追加
triangle.instance = object;
triangle.vertices = [v0, v1, v2];
triangle.uv = [uv0, uv1, uv2];
…

バカ正直に使うと当然負荷が大きくなるわけで、床面が真っ平らで良いならビットマップを透視投影した方が経済的だと思う。
QuadrantRenderEngine使用時は、通常のクリッピングの流れからバイパスして、視錘台の各平面と他のポリゴンとの交差をいっぺんに判定して分割、不可視部分を刈り取るようにすれば結構最適化できそうだけど、そこまでやるなら根本的なデータ構造含めて作り直したいっていう開発サイドの判断で放置されてるのかなぁ。


suumo(要:Flash Player9)

スーモ萌え。
を目指して作ったものの「スーモではない何か」が出来た。
無意味にファー表現したので、見た目のわりにポリゴン数食ってたり、負荷が増大してて非常にコストパフォーマンスが悪い。低コストな屋外の景観生成が個人的な課題。一つの方法論がドライブシミュもどきの時の構造だけど、これよりさらにコストを抑えられないもんかな。


Imitate Morphovision(要:Flash Player9)

※ステージドラッグで視点位置が変わります。

岩井俊雄さんと、NHK放送技術研究所が共同開発したモルフォビジョン
プリズムミラーで反射させたプロジェクタからの光を使ってスリットスキャンする方法で、立体物の見えを変化させるシステム。Another time, Another space の立体版って感じだけど、デジタル的な画像処理じゃない方法で摩訶不思議な現象を作り出してるのがカッコイイ。悔しいかな一度も本物を見た事がない。

んで、実機と同じような状況を作ったら、以前作ったゾートロープのようにソレっぽく見えたりしねぇかなぁーとか思ったのが2年くらい前。エライ時間がかかったけど、プロジェクションマップがそれなりに動いたので、技術的に課題だったライトマップ的なものが実装できて、ようやく動かせた。見やすくするためにブレンド使った以外は、スリット光の移動と物体の回転しかしてないんだけど、歪んで見えないことも…ない感じにはなった。スリット光のピッチが荒いのと、回転と同期がうまく取れてないあたりが課題か。でもまぁ、明らかに普通に回しただけじゃこうは見えないので、ちょっと嬉しい。

実機みたいに滑らかで有機的な振る舞いをさせるには、もっと高fps叩き出せるプラットホームじゃないと無理があるかなぁ…。それとも根気よく調整すればもう少し改善するのか? とりあえず一歩前進。なんとなく可能性が垣間見えたので、またしばらく寝かせとこう。


projectionmap

Projection map Test(要:FlashPlayer9)
※各Cubeはドラッグで動かせます。

プロジェクションマップの習作。
パフォーマンス悪いなぁ。根本的に考え直さないとダメか。


pv3dbasicbook

昨年末、yas@ClockMakerさんのご好意で「Flash3Dコンテンツ制作のためのPapervision3D入門」を献本していただき、年末年始に少しずつ読んだ。yasさんの太っ腹さに感謝。
あざーッス!

総じて懇切丁寧、全項カラーで超豪華。この本を隅々まで読み込んで実際に手を動かし、それでも理解できなきゃPV3D以前の問題なんじゃね?と、言えるくらい基礎から実践+αまで非常に盛りだくさんな内容でお腹いっぱい。PV3DのAPIを使いこなして、Flashコンテンツに3D要素をちょろっと加えたいだけならこれ一冊で十分。

ただ、説明の分かりやすさや冗長さの回避が優先されて、多少語弊のある記述もチラホラ見受けられるので(UVの解説とかシェーディング系Materialあたり)、ネット上に多数ある情報も併せて参照するとなお良い。このへんは入門書のジレンマですな。

「英語わかんね」とかいうモノグサ野郎もこれでバッチシ!PV3Dに興味のあるFlashデベロッパは、手元に置いて損は無いかと。


tkinjoさんが、wonderflに投稿されたコードを見て。

sceneの原点(0, 0, 0)にターゲット指定したカメラを x=0, y=1000, z=0 のように、ワールド座標のY軸線上に配置すると何も表示されなくなる。
回避策として、tkinjoさんも採用している x, y座標を0にしないってのが一番ラクチン。
x=0, y=1000, z=-0.1 とか x=0.01, y=1000, z=-0.05 とかを指定する方法。
ただし、x軸、z軸のどっちを使ってY軸からずらすかによって結果が異なる。また数値の±によっても結果が異なる点に注意。大抵はz軸にマイナスの値を入れるといいんじゃないかと。

別の方法として、カメラのターゲット指定をせずに

camera.target = null;
camera.x = 0;
camera.y = 1000;
camera.z = 0;
camera.rotationX = 90;

という設定で同じ状況にした場合も、問題を回避できる。

にしても、なんでなんだぜ?
今まで何が原因なのかをちゃんと調べてなかったので、この機会に調べてみた。
多分、随分前に遭遇したこのトラブルとも関連がありそう。

カメラにターゲット指定しなけりゃ問題ねーってことなんで、lookAt メソッドが怪しいと睨んで DisplayObject3D.lookAt の該当部分を見てみると、

[1]まず、目標にカメラを向けた時のz軸ベクトルを、ターゲットの位置ベクトルからカメラの位置ベクトルから減算して正規化。

_zAxis.copyFrom(_lookatTarget);
_zAxis.minusEq(_position);
_zAxis.normalize();

[2]1で求めたカメラの正規化されたz軸ベクトルと、カメラのupperベクトル(通常は Number3D(0, 1, 0) )との外積をとって、x軸ベクトルを求める。

if( _zAxis.modulo > 0.1 )
    {
        _xAxis = Number3D.cross( _zAxis, upAxis || UP, _xAxis );
        _xAxis.normalize();

[3]1で求めたz軸ベクトルと、2で求めたx軸ベクトルの外積をとって、y軸のベクトルを求める。

        _yAxis = Number3D.cross( _zAxis, _xAxis, _yAxis );
        _yAxis.normalize();

こんな手順でカメラとターゲットとの位置関係を元に、カメラの姿勢を決めてる模様。

この方法だと、x=0, y=1000, z=0 にカメラがある場合、_zAxisが Number3D (0, 1, 0) になる。
こうなると _xAxisを求める際に外積を取るための、カメラのupperベクトル:Number3D (0, 1, 0) と平行になる。平行なベクトルの外積の大きさはゼロなので、_xAxis は Number3D (0, 0, 0)。
最後の_yAxis は _xAxis との外積だから、_yAxisも Number3D (0, 0, 0)。
って感じでz軸以外がゼロベクトルになっちゃって、カメラの姿勢が定まらない。結果、何も表示されないということみたいだ。これっていわゆるジンバルロックなのか?
むー。PV3Dサイドとしては、これは仕様だということにしてるんだろうか?

なんかアホっぽい方法で回避できそうな気がしたのでやってみた。
DisplayObject3Dの1213行目辺りから

            _zAxis.copyFrom(_lookatTarget);
            _zAxis.minusEq(_position);
            _zAxis.normalize();

            if( _zAxis.modulo > 0.1 )
            {
                _xAxis = Number3D.cross( _zAxis, upAxis || UP, _xAxis );
                _xAxis.normalize();

                _yAxis = Number3D.cross( _zAxis, _xAxis, _yAxis );
                _yAxis.normalize();

という部分を以下に書き換え。

            _zAxis.copyFrom(_lookatTarget);
            _zAxis.minusEq(_position);
           _zAxis.normalize();

           if( _zAxis.modulo > 0.1 )
            {
                _xAxis = Number3D.cross( _zAxis, upAxis || UP, _xAxis );
                _xAxis.normalize();

                if(_xAxis.x == 0 && _xAxis.y == 0 && _xAxis.z == 0)
                {
                    _yAxis = Number3D.cross( _zAxis, RIGHT, _yAxis );
                    _yAxis.normalize();

                    var tempYAxis:Number3D = new Number3D(-_yAxis.x, -_yAxis.y, -_yAxis.z);
                    _xAxis = Number3D.cross( _zAxis, tempYAxis, _xAxis );
                    _xAxis.normalize();
                }
                else
                {
                    _yAxis = Number3D.cross( _zAxis, _xAxis, _yAxis );
                    _yAxis.normalize();
                }

特異点でlookAtする場合のみ、_yAxisを先に求めるようにしたので、カメラが x=0, y=1000, z=0に配置された場合もそれっぽく表示されるようになる。強制的にLEFTベクトルとの外積を取るようにしちゃったので、カメラがアニメーションしてる場合、おかしな挙動になるケースもあるかもしれない力業。
それでも何にも表示されないよりはマシかなぁ。もうひとひねりしてクォータニオン使う方法も考えてみたい。

09/06/15追記:tkinjoさんのご指摘を受けてコード修正。
やっつけ気味なのは変わらず;


daeAnimation(要:FlashPlayer9)

[↑] … 前進(move forward)
[←] [→] … 方向変える(turn left/right)

Papervision2.1 Alpha 公開で、鬼門だったDAE周りが改善されたということで使ってみた。
http://techblog.floorplanner.com/2009/05/26/papervision3d-21-alpha/

BlenderでArmature使ったスキンメッシュをColladaでエクスポートしたものがようやく動くようになった。3dsMAXとかMaya使わない限り不可能かと思ってたけど、どうにか使えそう。Away3Dでもなんとか動いてたけど、ボーンのモーションにtranslateが含まれてると破綻したりして不完全な感じだったのが、PV3D2.1のほうが格段にマトモだ。

上のデモは、KYUCON*BLOGさんで紹介されているAnimationClip3Dを使う方法で静止状態と、歩行中のアニメーションを切り替えて再生してる。スタート・エンドポイントの指定がフレーム単位でもできればいいのになぁ…なんか歩行モーションのループ部分がぎこちない。Blender上だと綺麗につながるんだけどな。

Colladaの仕様的には、animationChannelを定義できるはずなので、Blenderでアニメーションセットを複数定義した上で正常に書き出しができればもっと楽ができそうな、ほのかで甘酸っぱい期待を持ったんだけど、Blenderで複数のアニメーションセットを定義する方法がわかんなくて一旦保留。

こうなってくると、アニメーション切り替え時にモーションブレンドしたい。それは自前で実装するしかねーだろうなー。


imitatorIntro(要:FlashPlayer10)※音入り注意

SiON と PV3D を使って Executable Music な Intro モドキで、デモシーンごっこ(いわゆる現実逃避)。
かなりCPUパワー使うので、比較的新しめのマシンでないとツラいかも。
なるべく低域の出る環境で音を出してもらえると少しはマシに聴けます。

NoteOnイベント使って、キックとクラッシュシンバルに同期させて画面に変化つけてるわけですが、こういうのがお気楽にできちゃうSiONはやっぱり偉大ですよ。ノート番号も取得できるので、メロディに同期して歌詞が出るとかもできそう。
トンネルエフェクトは、Pixeleroのコード

しかしまぁ、己のセンスの古さに打ちひしがれる思いですな。
んでも、作っててかなり面白かった。
ActionScriptでscenerのマネゴトするのは趣味としていいかもなー。


attitude Control(要:FlashPlayer9)

haramakotoさんのエントリー「方向ベクトルからMatrix回転への変換」を読んで。

ある方向に形状の任意軸ベクトルを揃えることで、姿勢を制御するというお題。
haramakotoさんはクォータニオンで挑戦されているようなので、オレは行列でやってみた。正直言ってオレもよくわかってないんだけど、なんとなく上手くいったので書いておく。

例えば、ある形状のY軸を適当な方向に向けたい場合の手順は

  1. 姿勢をコントロールしたいオブジェクトのY軸ベクトルをゲット — [1]元の姿勢
  2. ゲットしたY軸のベクトルに目標座標のベクトルを加算して新しいベクトルをゲット — [2]変形後の姿勢
  3. [1]と[2]の外積をとって、法線ベクトル(回転軸)をゲット — [3]
  4. 二つのベクトルの角度をゲット — [4]
  5. [3]のベクトルと、[4]の角度でもって回転行列を作る
  6. 姿勢をコントロールしたいオブジェクトのtransformと合成

って感じで上手くいった。

姿勢をコントロールしたいオブジェクトのY軸ベクトルは、DisplayObject3Dのtransformプロパティを参照すれば、調べることができる。

n11 n12 n13 n14
n21 n22 n23 n24
n31 n32 n33 n34
n41 n42 n43 n44

transformプロパティ(Matrix3D)の各列、
|n11,n21,n31|、|n12,n22,n32|、|n13,n23,n33|は、形状のX,Y,Z軸の単位ベクトルを示してる。なので、ある形状Obj(DisplayObject3D)の各軸のベクトルは、

X軸:Xvec:Number3D = new Number3D(Obj.transform.n11, Obj.transform.n21, Obj.transform.n31);
Y軸:Yvec:Number3D = new Number3D(Obj.transform.n12, Obj.transform.n22, Obj.transform.n32);
Z軸:Zvec:Number3D = new Number3D(Obj.transform.n13, Obj.transform.n23, Obj.transform.n33);

として取り出すことができる。これで、ある時点での形状の姿勢がわかる。ここまではyamasvさんの「回転行列の列ベクトル」の解説が詳しい。

「ゲットしたY軸のベクトルに目標座標のベクトルを加算」ってのは、自分も理解できてない。感覚的にこうじゃねーの?とかいうドンブリ勘定実装したら上手くいっちゃったので説明できない。わっはっは。…申し訳ございません。最初は目標座標のベクトルを加算せず、目標座標のベクトルをそのまま使ったんだけど、それだとリボン状にPlaneが並ばなかった。

残りの手順は、[Papervision3D2.0] Quaternionと同じ要領で、最終的に姿勢をコントロールするコードは以下のような感じになった。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//ある方向に向けたい軸
var StartVec:Number3D = new Number3D( obj.transform.n12, obj.transform.n22, obj.transform.n32 );
 
//目標になる座標
var TargetPoint:Number3D = new Number3D( 目標のX座標, 目標のY座標, 目標のZ座標 );
 
//元のベクトルを揃わせたい最終的なベクトル
var EndVec:Number3D = Number3D.add( StartVec, TargetPoint );
StartVec.normalize();
EndVec.normalize();
 
//2つのベクトルの法線ベクトル(回転軸)
var vcross:Number3D = Number3D.cross( EndVec, StartVec );
vcross.normalize();
//2つのベクトルから回転角度(ラジアン)算出
var rot:Number = Math.acos(Number3D.dot( StartVec, EndVec));
 
//ベクトルを合わせるための回転軸と角度から回転行列作成
var mat:Matrix3D = Matrix3D.rotationMatrix(vcross.x, vcross.y, vcross.z, rot);
 
//行列を適用
obj.transform = Matrix3D.multiply3x3(mat, obj.transform);

あと、wonderflに上がってたharamakotoさんのコードをforkして、行列での姿勢制御版を作ってみた。
ここらへん、ちゃんと理解したいなぁ。


Paint to DAE mash(要:FlashPlayer9)

WakaZさんのエントリー「PV3D | DAEファイルの読込み」

DAEのオブジェクトにはVirtualMouseは効かないようで(あたりまえ)・・・。

を読んで。

これは確かに解りにくいなー。
例えば、

var obj_dae:DAE = new DAE();
obj_dae.load("hoge.dae");
scene.addChild(obj_dae);
obj_dae.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, mouseClick);

だとイベントが取れない。なんでか?

PV3Dとかだと、3Dアプリケーションで作った形状データを取り込む目的でCollada形式を使ってるけど、本来、ColladaってシーングラフをまるごとXMLで記述しようっていうフォーマット(だと思う)なので、メッシュ以外にもカメラとかライトなど、3Dアプリ上で定義した全要素を定義できる。いってみれば、

var daeObj:DAE = new DAE();
daeObj.load("hoge.dae");

で生成されたdaeObjってPV3Dのシーンみたいなもんで、ツリー構造なDisplayObject3Dの塊。なので、daeObj内にある目的のメッシュを明示的に参照しないとダメなのですな。あと、InteractiveScene3DEventを有効にする場合のお約束、マテリアルのinteractiveプロパティをtrueにするのを忘れずに。

具体的には、

vMouse = viewport.interactiveSceneManager.virtualMouse;
Mouse3D.enabled = true;

-- 中略 --

obj_dae = new DAE();
obj_dae.load( "hoge.dae" );
obj_dae.addEventListener( FileLoadEvent.LOAD_COMPLETE, compCollada );
scene.addChild(obj_dae);

function compCollada(event:Event):void
{
    //対象となるマテリアルのinteractiveプロパティをtrueに
    obj_dae.materials.getMaterialByName("ターゲットのマテリアル").interactive = true;

    //対象メッシュの参照
    var dae_rootNode:DisplayObject3D = obj_dae.getChildByName("COLLADA_Scene");
    var targetMesh:DisplayObject3D = dae_rootNode.getChildByName("ターゲットの名前");

    //対象メッシュにリスナ設定
    targetMesh.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, mouseClick);
}

private function mouseClick(e:InteractiveScene3DEvent):void
{
    var axis:Point = new Point(vMouse.x, vMouse.y);
    trace(axis.x + " / " + axis.y);
}

って感じで。

上記で言うところの obj_dae内部では「COLLADA_Scene」って名前のDisplayObject3Dをルートとした階層構造(シーングラフ)ができあがってるってのを意識することが重要と。

getChildByNameで指定する名前は、ルートである「COLLADA_Scene」は固定、COLLADA_Scene以下の各メッシュは3Dアプリで付けた名前がエクスポートされてるはず。自分で作ったdaeファイルじゃない場合は、

var dae_rootNode:DisplayObject3D = obj_dae.getChildByName("COLLADA_Scene");
dae_rootNode.childrenList();

で一覧が取れる。

ということで出来上がったのが上記のデモ。
DAEでもプリミティブと同じようにペイントできるよ。