Projection map Test(要:FlashPlayer9)
※各Cubeはドラッグで動かせます。
プロジェクションマップの習作。
パフォーマンス悪いなぁ。根本的に考え直さないとダメか。
昨年末、yas@ClockMakerさんのご好意で「Flash3Dコンテンツ制作のためのPapervision3D入門」を献本していただき、年末年始に少しずつ読んだ。yasさんの太っ腹さに感謝。
あざーッス!
総じて懇切丁寧、全項カラーで超豪華。この本を隅々まで読み込んで実際に手を動かし、それでも理解できなきゃPV3D以前の問題なんじゃね?と、言えるくらい基礎から実践+αまで非常に盛りだくさんな内容でお腹いっぱい。PV3DのAPIを使いこなして、Flashコンテンツに3D要素をちょろっと加えたいだけならこれ一冊で十分。
ただ、説明の分かりやすさや冗長さの回避が優先されて、多少語弊のある記述もチラホラ見受けられるので(UVの解説とかシェーディング系Materialあたり)、ネット上に多数ある情報も併せて参照するとなお良い。このへんは入門書のジレンマですな。
「英語わかんね」とかいうモノグサ野郎もこれでバッチシ!PV3Dに興味のあるFlashデベロッパは、手元に置いて損は無いかと。
3Dアプリでモデリング・モーション付やって、それをそのままASで扱えたらさぞかし楽しかろうと常々考えてきたにもかかわらず、一向にベストな方法が見つけられないし、出現しそうもないので、自力で色々作り始めてみることにした。
本来、.daeが正しく書き出せて、正しく読み込まれればそれが出来るはずなんだろうけど、現状3dsMAXとかMayaでも持ってない限り可能性が薄い。Unwrap3Dはwin用だし。んで、BlenderのエクスポータでもいじってみようかとColladaの仕様書とか見てみたりするんだけど、これがもーなにがなんだか。
そんなわけで.blend形式。ファイルフォーマットが公開されてるんで、こいつを手がかりに.blendローダーを作成。バイナリ最高。バージョンによるデータの差異は吸収できてないしメッシュの読み込みだけだけど、2.49bのファイルは問題ないとこまで来た。
いいかげんな3Dエンジンらしきものも自作。ジオメトリパイプラインの実装でつまずきまくって、首吊ろうかと思ったけど何とか動くようになった。まだ何かがオカシイので多分どっかの符号間違えてる。苦労した分、オレオレエンジンの成長が楽しくてしょーがない。擬人化したいぐらい。
ここまでの成果として、Blenderで編集したメッシュが.swf側に即時反映されるビューアを作ってみた。画面右がFlashPlayer。
城戸さんそのまんまなのがアレだしショボすぎて泣けてくるけど、これ、ちゃんと作ればかなり便利かも。swfで実際に表示されるまで分からない部分とかが、モデリングしながら確認できるのはメチャメチャ助かる。
当面は、プレハブみたいな実装の3Dエンジンをもうちょっとマトモなものにしつつ、なんとかしてモーションデータの再生までこぎ着けたい。とか宣言して自分を追い詰めておく。
まぁぼちぼち。
以前アナウンスがあったAway3Dの高速化に関する話が、Away3D Liteとして結実。
svnリポジトリのtrunk入りしてたのが、ようやく正式発表されましたよっと。
最大の功労者であるsleepydesign の katopzに敬礼!Greate work katopz!
Away3D Lite v1.0: fastest and smallest 3d engine in Flash.
動作が軽いのと共に機能もライト、とりあえず最低限の機能のみ提供されてる感じ。
高機能化の果てに超重量級エンジンになっちゃった本家Away3Dとは別に、パフォーマンス重視専用のライブラリを別展開するという戦略なのか、単に本家の再設計が面倒だったのかはさておき、選択肢が増えたのはありがたいことであります。
どの程度の高速化がなされているかは上記のエントリに掲載されているデモで確認できる。
手元のデータでもやってみた。
Dae Performance Test(要:FlashPlayer10)
画面クリックで増殖、減らせないので注意。
sortTypeが上手く動作せずZソートがおかしいけど、3000△ポリ超えてもそこそこ動いてる。
.mqoを扱うクラスも標準提供されているので、メタセコのデータでも試してみた。
Mqo Performance Test(要:FlashPlayer10)
※モデルデータは、もとがしさんが配布されているものをお借りしました。
今までが1000△ポリあたりで限界だった事を考えると、このくらいのローポリモデルが扱えるようになったのはスゴイな。ビルボードとかPlaneとかを大量に使いたいみたいなケースにはもってこいかも。
触ってみた感触として、
とか思った。
Matrix3Dなどのコアクラスが自前のものではなく、全部ビルトインクラスに置き換わってたりして、FP10の3D機能を体系化した3Dエンジンのひな形として参考になりそうではある。これをベースに自分好みのエンジンに調教するのもアリなんじゃねーかと。
ワーカーホリック野郎な日々を送ってる間にAway3Dがバージョンアップ。
Away3d: 2.4 & 3.4 released!
とりあえず地味な機能から試してみる。
ExplodeTest(要:FlashPlayer9)
Explodeクラスは、指定したObject3Dインスタンス内の全Faceを位置関係をそのままに、バラバラのMeshやFaceに分解してくれるジオメトリモディファイア。
Away3DはもともとFace単位でvisibleプロパティを持ってるので、表示/非表示をFace単位で行うことができていたけど、Face単位で大きさ変えたりすることはできなかった。その名のとおり、簡易的な爆発表現なんかに使えそうだし、演出面で結構使えるかもしれない。
具体的には以下のようにして使う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import away3d.geom.Explode; - 中略 - //適当なメッシュを用意 var wcMat:WireColorMaterial = new WireColorMaterial(0x0099CC, {wirecolor:0x00CCFF}); var sphere:Sphere = new Sphere({material:wcMat, radius:100, segmentsW:8, segmentsH:6}); //Explodeでメッシュを分解 var _explode:Explode = new Explode(true, true); var _explodedObj:ObjectContainer3D = _explode.apply(sphere) as ObjectContainer3D; //分解されたメッシュが束ねられたObjectContainer3Dをシーンに追加 scene.addChild(_explodedObj); |
Explodeのコンストラクタに渡す引数は以下のようになってる模様。
コンストラクタの第1引数をtrueにして、個別のMeshを生成した場合は、
for each(var item:Mesh in _explodedObj.children)
{
item.scaleX = 0.5;
}
などとして、それぞれのMeshを制御する。
理屈では、Colladaメッシュにも適用できる。
dae Explode(要:FlashPlayer9)
ただ、現状のExplode.asには、元にしたメッシュがFace単位でマテリアル指定されているような場合、マテリアルが引き継がれないという問題があって、テクスチャが無効になってしまう。そこで、Explode.asを以下のように書き換えることで暫定的に対処した。ついでに「bothsides、backの要素が引き継がれない」という問題にも対処。
Explode.as 61行目
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | for(i=0;i<loop;++i){ face = obj.faces[i]; mesh = new Mesh(); va = new Vertex(face.v0.x, face.v0.y, face.v0.z); vb = new Vertex(face.v1.x, face.v1.y, face.v1.z); vc = new Vertex(face.v2.x, face.v2.y, face.v2.z); uva = new UV(face.uv0.u, face.uv0.v); uvb = new UV(face.uv1.u, face.uv1.v); uvc = new UV(face.uv2.u, face.uv2.v); mesh.addFace(new Face(va, vb, vc, obj.material as ITriangleMaterial, uva, uvb, uvc)); _container.addChild(mesh); } |
これを、
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | for(i=0;i<loop;++i){ face = obj.faces[i]; mesh = new Mesh(); va = new Vertex(face.v0.x, face.v0.y, face.v0.z); vb = new Vertex(face.v1.x, face.v1.y, face.v1.z); vc = new Vertex(face.v2.x, face.v2.y, face.v2.z); uva = new UV(face.uv0.u, face.uv0.v); uvb = new UV(face.uv1.u, face.uv1.v); uvc = new UV(face.uv2.u, face.uv2.v); var setMaterial:ITriangleMaterial; if (obj.material) setMaterial = obj.material as ITriangleMaterial; else setMaterial = face.material as ITriangleMaterial; var fc:Face = new Face(va, vb, vc, setMaterial, uva, uvb, uvc); mesh.addFace(fc); _container.addChild(mesh); mesh.bothsides = obj.bothsides; fc.back = obj.back as ITriangleMaterial; } |
とする。
あとは、バラバラになった後のMeshが、もともとのFaceの法線方向をforwardベクトルにしてくれるといいのになぁと思ったけど面倒くさそうだったのでとりあえず保留。
また、Explodeコンストラクタの第1引数をtrueにして、FaceごとのMeshを生成すると当然負荷が増大するので、パフォーマンス重視の場合はMeshを生成せずに、Faceレベルでコントロールする方法を確立しないとダメっぽい。
しばらくの間、超多忙状態が続きそうなのでショボいデータでお茶を濁しておく。
前回のpcm同期ネタはリズムというよりSEみたいな使い方だったので、もうちょっとまともにリズム刻んでみるテスト。
(要:FlashPlayer10)
いい感じのが見つからなくて、スネアのみOPM。
途中で投げ出した感がそこはかとなく漂っておりますな。
さっさと仕事終わらせよう。
090710追記:
肝心な事を何も書いてなかった。
当初、前回のpcm同期テストと同じく、mp3ファイルを外部読み込みしようとしたところ、激しくレイテンシーが発生してしまった。mp3ファイルとかの圧縮音源はその仕様上データの前後に無音部分が出来ちゃうらしく、これが原因だった模様。この辺りの事情、恥ずかしながら今回初めて知った。前回はテンポがゆっくり目だったからなのか、全然気がつかなかった。
そこで今回は、AIFFで保存したデータをFlashに読み込んで、mp3圧縮を指定した上でswfを書き出しておき、このswfを外部読み込みして該当のSoundインスタンスを生成した。こうすることで遅延なく発音するpcmパートが完成。Flashに読み込む段階で、AIFFやWAV保存してあることがポイントみたいだ。
SiON使って、UIにSEを付けたりしてみるテスト。
(要:FlashPlayer10)
マウスイベントなんかに連動して音を鳴らす場合、今まではmp3ファイル読み込んで再生してたけど、SiONを使うことで、PSG / FM / WaveTable / PCMなどの多彩な音源で、しかもラクしてサウンド制御ができる。個人的にはOPMが鳴るってだけで抱かれてもいいと思ったけど、トータルに音の制御ができるという側面はコンテンツ制作する上でとっても有用。
例えば、下記のようなコードで音の鳴るボタンが作れる。
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 | package { import flash.display.*; import flash.events.*; import org.si.sion.*; [SWF(backgroundColor=0x000000)] public class main extends Sprite { private var driver:SiONDriver; private var sinWave:SiONVoice; public function main() { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } public function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); stage.frameRate = 60; stage.scaleMode = StageScaleMode.NO_SCALE; stage.align = StageAlign.TOP_LEFT; stage.quality = StageQuality.BEST; initSiON(); } private function initSiON():void { //SiONDriverをスタンバイ driver = new SiONDriver(); driver.play(); //音色定義(%5, @0 = sin波) sinWave = new SiONVoice(5,0); //UIを定義 var soundButton:Sprite = new Sprite(); soundButton.graphics.beginFill(0x0099cc); soundButton.graphics.drawRoundRect(0, 0, 120, 120, 4, 4); soundButton.graphics.endFill(); addChild(soundButton); soundButton.x = 20, soundButton.y = 20; soundButton.addEventListener(MouseEvent.ROLL_OVER, onrollover); soundButton.useHandCursor = true; } public function onrollover(e:MouseEvent):void { //ノート番号90番(オクターブ7の「ファ」)の音を //事前に定義した音色(sinWave)で鳴らす。 //第3引数:音長(1 = 16分音符), 第4引数:発音までのディレイ driver.noteOn(90, sinWave, 1, 0); } } } |
音色定義については、SiON MMLリファレンス「音色関連 – ‘%’,'@’の第1引数」の項を参照。
音色定義を
var sawWave:SiONVoice = new SiONVoice(5,1); // ノコギリ波 var triWave:SiONVoice = new SiONVoice(5,4); // 三角波 var squareWave:SiONVoice = new SiONVoice(5,5); // 矩形波
などと差し替えることで違う音色にできる。PSG / FM / WaveTable 音源については、使いこなせれば、別途音源データを用意せずに、コーディングだけで多彩な音を鳴らすことができる反面、音色定義自体がそれなりの知識と慣れを要求するので、UIサウンドとしてお手軽にSiONを活用することを考えた場合、swfに埋め込んだ、もしくは外部から読み込んだmp3サウンドをSiONで制御するのが良いんじゃないかと思う。例えば以下のような感じで。
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 | package { import flash.display.*; import flash.events.*; import flash.media.*; import org.si.sion.*; [SWF(backgroundColor=0x000000)] public class main extends Sprite { private var driver:SiONDriver; private var pcmsound:SiONVoice; [Embed(source='hoge.mp3')] private var hogeSound:Class; public function main() { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } public function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); stage.frameRate = 60; stage.scaleMode = StageScaleMode.NO_SCALE; stage.align = StageAlign.TOP_LEFT; stage.quality = StageQuality.BEST; initSiON(); } private function initSiON():void { //SiONDriverをスタンバイ driver = new SiONDriver(); driver.play(); //音色定義(PCMサウンド) var hoge:Sound = new hogeSound(); driver.setSamplerSound(90, hoge); //ノート番号90番に、Embedしたmp3サウンドを割り当て pcmsound = new SiONVoice(10); //UIを定義 var soundButton:Sprite = new Sprite(); soundButton.graphics.beginFill(0x0099cc); soundButton.graphics.drawRoundRect(0, 0, 120, 120, 4, 4); soundButton.graphics.endFill(); addChild(soundButton); soundButton.x = 20, soundButton.y = 20; soundButton.addEventListener(MouseEvent.ROLL_OVER, onrollover); soundButton.useHandCursor = true; } public function onrollover(e:MouseEvent):void { //ノート番号90番(mp3サウンドを割り当てた場所)の音を鳴らす。 driver.noteOn(90, pcmsound, 1, 0); } } } |
setSamplerSoundは、通常0-127までのノート番号に任意のSoundを割り当て可能。さらにバンク切り替えで128-255まで定義しておくことができる。
また、driver.noteOn()の代わりに driver.sequenceOn() を使うことで、事前に定義したMMLを再生することも可能。
driver = new SiONDriver();
var mml:SiONData = driver.compile("l16 o6 ffffre-rgrf2;l16 o6 c>bb-argrb-ra2;");
driver.play();
sinWave = new SiONVoice(5,0);
driver.sequenceOn(mml, sinWave, 0, 0, 1, 3);
driver.sequenceOn()は、別途演奏中のMMLシーケンスとタイミングを合わせて再生を乗せることができたりするあたりの仕様にニヤリとさせられた。
加えて、delay, reverb, chorus 等のエフェクトや、low/high pass filter等のフィルタも備えているため、かなり自由度の高い音作りをすることもできる(このエントリ冒頭のデモでは、reverbをon/offできるようにした)
SiONで使えるエフェクトについては、SiON MMLリファレンス「エフェクタ接続」の項を参照。
ということで、SiONはオッサンの懐古主義を満足させるためだけのものではなく(オレにとってはここも超重要なのだけど)、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で複数のアニメーションセットを定義する方法がわかんなくて一旦保留。
こうなってくると、アニメーション切り替え時にモーションブレンドしたい。それは自前で実装するしかねーだろうなー。