note.x

てっく煮ブログ」さんにて、

3次元空間にプロットしてくれることを期待!
各国の伝統色をHSV空間に配置してみた

という面白そうな課題が提示されてたのでトライしてみた。下手でも、とにかくやってみるのココロ。

nitoyonさんのデモは、HSVのうち「V」を使ってないとのことで、これを使って3次元に拡張するわけだ。直球で拡張したら円柱状の空間になると思うんだけど、それでいいのか調べてみたら

HSVモデルの別の視覚化方法は円錐である。この表現では、色相は色環の三次元円錐状の構造に描かれる。彩度はその円錐の円形交差部分の中央からの距離、明度は円錐の頂点からの距離で表される。
WikiPedia:HSVの視覚化

というWikiPediaの解説を見つけた。これによると円柱で表現してもいいらしい(全然関係ないけど、HSVって「HSB」が正しいんだと思ってた、どっちでもいいんだ。)
せっかくなので円錐に挑戦。HSVの各要素から座標を算出して、手始めにパーティクルでプロット。

e_jtc.jpg

j_traditional_color_lt.swf(要:FlashPlayer9)

上手くいったみたいなので、TextFieldを配置したMovieClipからMovieMaterialを生成。それを貼り付けたPlaneをビルボード表示して終わり…とか簡単に済むわけがなく、動作重すぎ。安直すぎ。

色名テキストをHSV色空間にプロット – MovieMaterial版(要:FlashPlayer9)
※かなり重いです御注意下さい。

もうちょっと現実的な速度で動かせないもんかなぁと思って、ダミーのDO3Dを色数分配置して、そのスクリーン座標からTextFieldを持つMovieClipの表示位置を決めるというフェイク感漂う方法でやってみた。

e_jtc2.jpg

色名テキストをHSV色空間にプロット – MovieClip版(要:FlashPlayer9)
※ステージクリックで、BlendModeをon/off

スクリーン座標の算出には、DO3Dのプロジェクション変換そのまんまの式を使った。

//mc = TextFieldを持つMovieClip
//DO3D = ダミーのDisplayObject3D
//centerX,centerY = 原点を画面の中心にするための補正値

var persp:Number = (camera.focus * camera.zoom) / (camera.focus + DO3D.view.n34);
mc.x = DO3D.view.n14 * persp + centerX;
mc.y = DO3D.view.n24 * persp + centerY;

MCが拡大縮小しないのでcacheAsBitmapが効く効く。MovieMaterial使った場合と比べてかなりマトモに動くようになった。パーティクルよりボリューム感があっていいな。調子に乗ってステージクリックでBlendModeをon/offできるようにしてみた。onにするとキレイなんだけど、さすがに重い。

今回みたいなものは、PV3Dに限らずFlash用3Dエンジン使わずに自前で座標変換したほうがよさげ。って、自分でやるとかえって遅くなるから3Dエンジン頼みなわけなんだけども…。精進ですな。

以下、パーティクル版を簡略化したソース

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
99
100
101
102
103
104
105
106
107
108
package
{
    import flash.display.*;
    import flash.events.*;
    import flash.net.*;
 
    import com.adobe.serialization.json.*;
    import sketchbook.colors.*;
 
    import org.papervision3d.view.BasicView;
    import org.papervision3d.objects.*;
    import org.papervision3d.objects.primitives.*;
    import org.papervision3d.materials.*;
    import org.papervision3d.core.geom.*;
    import org.papervision3d.core.geom.renderables.*;
    import org.papervision3d.materials.special.ParticleMaterial;
 
    [SWF(backgroundColor=0x000000)]
 
    public class j_traditional_color_sample extends BasicView
    {
        private var colorArray  : Array;
        private var colorChips  : Array = new Array();
        private var pChips      : Particles;
        private var rotateAngle : Number = 0;
 
        public function j_traditional_color_sample()
        {
            stage.frameRate = 30;
            stage.align = StageAlign.TOP_LEFT;
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.quality = StageQuality.LOW;
 
            super (0,0,true,true,"Target");
            initColor();
        }
 
        private function initColor():void
        {
            var jsonLoader:URLLoader = new URLLoader();
            jsonLoader.addEventListener(Event.COMPLETE, onComplete);
            jsonLoader.load(new URLRequest("asset/japan.json"));
 
            function onComplete(e:Event):void
            {
                var my_str:String = e.target.data;
                colorArray = (JSON.decode( my_str ) as Array);
                init3D();
            }
        }
 
        private function init3D():void
        {
            camera.x = 0;
            camera.y = 600;
            camera.z = 0;
            camera.fov = 30;
 
            pChips = new Particles("pChips");
            scene.addChild(pChips);
            for each(var col:Object in colorArray)
            {
                plotColorChip(col);
            }
 
            var wfmat:WireframeMaterial = new WireframeMaterial(0x333333);
            var objCone:Cone = new Cone( wfmat, 280, 600, 8, 4 );
            scene.addChild(objCone);
            objCone.pitch(180);
 
            startRendering();
        }
 
        override protected function onRenderTick(event:Event=null):void
        {
            rotateAngle += 1;
            if(rotateAngle > 360)
                rotateAngle -= 360;
 
            var radian:Number = rotateAngle * Math.PI/180;
            camera.x += ( ( Math.cos(radian) * 1800 ) - camera.x ) / 10;
            camera.z += ( -( Math.sin(radian) * 1800 ) - camera.z ) / 10;
 
            super.onRenderTick(event);
        }
 
        private function plotColorChip(col:Object):void
        {
            var RADIUS:Number = 280;
            var CONEHEIGHT:Number = 600;
 
            var hsb:Object = ColorUtil.getHSB(col.value);
            var rad:Number = (2 * Math.PI * hsb.h) / 360;
            var r:Number = RADIUS * (hsb.s / 100);
            var h:Number = CONEHEIGHT * (hsb.b / 100);
            var r2:Number = Number(h/CONEHEIGHT) * r;
 
            var pmat:ParticleMaterial = new ParticleMaterial(col.value,1);
            var tx:Number = r2 * Math.cos(rad);
            var tz:Number = r2 * Math.sin(rad);
            var ty:Number = h - CONEHEIGHT/2;
 
            var pt:Particle = new Particle(pmat, 10, tx,ty,tz)
            pChips.addParticle(pt);
        }
 
    }
}

ソースとJSONファイル一式ダウンロード

JSONのパースに、as3corelib、RGBからHSVへの変換にfladdict様謹製のsketchbook(これものすごい便利)を使ってるのでコンパイルする場合は、よしなに対応願います。


「FlashやWebにまつわるいろいろなこと」さんで、

カメラの設定値からオブジェクトが100%で見える位置の算出ってどうやればいいんだろうか

Papervision3D 2D上で100%に見える位置は

っていうエントリを拝見。
そういややったことねーなと思って、既出かもしれないけどやってみた。

上記エントリによると、camera.focus:100、camera.zoom:11のとき、「ピクセル等倍で表示したい面」と camara の距離が1000離れていればいいと。これを汎用化するために、任意のfocus、zoomの場合にピクセル等倍になる距離を求められればいいのか。

大概こういう処理をする場合複雑に考えすぎて失敗するので、算数的な発想(むしろ勘)で

ピクセル等倍になる距離 = camera.focus * camera.zoom - camera.focus

という式をあてはめたら、なんか上手くいった模様(笑)

ただこれだと、camera.zoomが「1」のとき、ピクセル等倍になる距離が「0」になっちゃうので、camera.zoomを1にしないことが前提。また、ビューポートのアスペクト比で若干誤差が出るみたいなので、ブラウザのウインドウサイズによって1ピクセルくらいズレることがある。それと、FrustumCamera3Dはプロジェクション変換の方法が違うのでCamera3D、FreeCamera3Dを使ってることも前提条件になる。

そんなこんなで試しに作ったデモが以下。

e_tarot.jpg

tarot.swf(要:FlashPlayer9)
※カードをクリックすると、ピクセル等倍表示します。

カメラ位置の算出は下記のようなことをやってる。

var targetobj:DisplayObject3D = new DisplayObject3D();
targetobj.copyTransform( targetPlane );
targetobj.moveBackward( camera.focus * camera.zoom - camera.focus );

って感じで、targetobjっていうダミーのDO3Dを用意しておき、クリックしたPlane(targetPlane)の同次座標をコピーして、ピクセル等倍になる距離分、moveBackwardメソッドで平行移動させた位置がカメラを置くべき場所。(対象になるオブジェクトがPlane以外の場合、さらに位置をオフセットしないとダメ。)ここまでできれば、

targetobj.x
targetobj.y
targetobj.z

で、要素別に座標が取れるのでTweener使うなりなんなり。

ほんとにこれでいいのかは全く自信がないので、もっときちんとした方法があったら知りたいところ。

08/09/01追記:
Rev.639以降、FrustumCamera3Dとして定義されてたものがCamera3Dに変更、これまでのCamera3D、TargetCamera3Dは一部その性質が受け継がれたもののクラス自体は削除されちゃった。結果、上記の方法はRev.638以前のものに限定されます。
Rev.639以降で同様のことをするには、

ピクセル等倍になる距離 = camera.focus * camera.zoom - camera.focus + camera.focus

にするとよさげ。なんでこれで上手くいくのかはわからんのですが(えー)泥臭く試した結果、上手く行ってるようなので、とりあえずお茶を濁しておこうかと。

08/11/22追記
上の式、激しく余計な意味不明演算してた。

ピクセル等倍になる距離 = camera.focus * camera.zoom

これだけでオッケーです。超ハズカスィー。


e_rin_run.jpg

rin_run.swf(要:FlashPlayer9)

DAEのアニメーションをただ再生するだけじゃなくて、動的にコントロールできないもんかと思って調べてたら、Scene3D定義時にアニメーション有効にすると生成される、AnimationEngine を経由してホゲホゲすればイケそうじゃんと思って格闘してみたものの、結局任意のキーフレームを再生したりする方法が掴めず。さらにボーンを埋め込んだメッシュを移動する場合、3Dアプリケーションの感覚でいくとボーン自体を移動しないとメッシュが移動しないはずで、この「ボーンを移動する」ってのがPV3Dだと出来るのか出来ないのかすらわかんね。

キーボード操作で かぐぁみね を動かしたかったんだけど、どうにもならんので一旦あきらめ。せっかくモーション作ったし、とりあえず逃げのデモ。モーション付けが詰めきれてないのと、ループ時の変形処理が妙な挙動をしてて、足をひきずってるように見える。う〜ん難しいのぉ。

そんな傷心のオレに追い討ちをかけるかのように、3/16あたりから怒濤のリビジョンアップで、アニメーション関連のクラスが全面書き直され始めてる。かなりの仕様変更で、なんかまともに動いてないっぽいし詳しく見てないけど、こりゃしばらくDAEのアニメーション実験は凍結だわ。


e_rubikscube.jpg

rubikscube.swf(要:FlashPlayer9)

クソ忙しいにもかかわらず、絶賛現実逃避中。仕事だけやってると3D忘れちまう。
PV3Dで遊び始めて1年経った。行列もベクトルもAS3.0さえ何も解ってない状態から手探りでやってきて、1年前に出来なかった課題に再挑戦。ルービックキューブはありがちなネタだけど、知識ゼロだと相当難しいと思う。実際1年前はお手上げだった。
なんとなく思った通りに動いたんで、オレ的満足度高し。地味な積み上げが報われた気がして素直にうれしいわこりゃ。

回転行列を使った座標変換の典型なので、特筆するようなことは無いけど、Matrix3D の multiply() メソッドが、実際の演算は calculateMultiply() で行い、演算後の行列を返すためだけに、一次オブジェクトを生成してるのが気になったんで、直接calculateMultiply() を使うことにしたっていうくらい。

あと、使ったこと無かったんで、レンダーステータスの表示に StatsView を使ってみた。

import org.papervision3d.view.stats.StatsView;

statusPanel = new StatsView(renderer);
addChild(statusPanel);
statusPanel.x = 0;
statusPanel.y = 0;

で、renderSessionData.renderStatistics の各種プロパティを表示してくれる。なんとなくスゴそうに見えて楽しげ。Away3Dの豪華さに比べると極端にシンプルだけど、これで充分。


e_geometory_uv.jpg

rin_anim.swf(要:FlashPlayer9)
これまで同様、ガテン系ボーカロイドのモデルデータは、ズサさんが作成されたものを、三次元CG@七葉から拝借し、Blenderにコンバートしたものを加工しました。毎度お世話になっております。

DAE.as使ってBlenderで作成したモーションデータの読み込みを模索中。
かれこれ2週間くらい、仕事の合間をかい潜ってチマチマ調査をしてみたんだけど、どうにもBlenderを使ったモーションデータを効率良くColladaへ変換する決め手になる方法が見つからない。Collada plug-in for Blenderがかなり信用ならないのと、おそらくBlenderでは動作確認してないであろう、ASColladaの挙動に振り回されて、PythonのコードとAS3.0のコードを行ったり来たりしつつ、相当遠回りしてどうにか形になったのが上記のデモ。

一番参ったのは、1つめから7つめまでのキーフレームが無視されるというASColladaの謎挙動。とりあえずダミーのキーフレームを7つ打っておくというバカバカしい方法で対処。Collada plug-in for Blenderが吐くdaeが悪い可能性が高いけど、現象を把握するのにかなり苦労した。あと、キーフレーム間の補間が実装されたのはスバラシイんだけど、少ないキーフレームで大きい動きを付けようとすると妙な補間をするので、様子を見ながらキーフレームの間隔を微調整。他にもルートボーンが複数あるとダメとか、いろいろあった気がするんだけど全然整理できてない。

ただでさえ労力のかかるモーション付けを、PV3Dで扱える条件を考慮しながらやるのはかなりキツい。効率を要求される場合、現時点では「FlexSDK + Blender」という最強低コスト環境では太刀打ちできそうにない。だからといってハイエンド3Dソフトを使うのが最適解ってのも釈然としないのでもうちょっとがんばってみたい。ShockWave3Dが作り手に普及しなかったのは間違いなくこの部分だと思うし。そういやDirector11出るんだなぁ。Directorだけの出費で済むなら買うのに…w3d形式以外の選択肢があればなぁ。とか思う今日この頃。