3Dコントロールではまった話
C++BuilderXE3 update2での話。他のバージョンは試してない。
しばらく見ないうちにBuilderがVCL以外にFireMonkeyってのを積んできていたので「アプリの見た目を簡単にリッチっぽくできるしちょっと遊んでみるか」と思っていじってたんだけど、例によってはまってしまった…
分かったことをここに整理しておこうと思う。
TLightMaterialSourceでアプリ起動中にcrash
update1では起きなかったんだけど、update2にしたら発生するようになってしまった。
TFormにTLightMaterialSourceを張り付けてビルドして実行するとフォームが表示される前にアクセス違反で落ちる。詳細に解析はしてないので原因は不明。
とりあえずいったんcleanしてから再ビルドすると落ちない。
ただし、ソースやプロパティを変更するなどしてビルドするときにもいったんクリーンナップを実行しないと落ちてしまう。
毎回クリーンナップする分には問題ないんだけど、全然RADじゃない件…*1
[2013/09/10 11:30追記]
XE4 update1で確認したらこの件は直っているようだった。良かった。
TMesh.Data.VertexBuffer.Vertices[]
こいつはTPoint3D型の値を代入しないとダメっぽい。X、Y、Zメンバを順番に設定したらポリゴンがぐちゃぐちゃになっちゃう。
// OK Mesh1->Data->VertexBuffer->Vertices[i] = TPoint3D( cube[i].X * width + x, cube[i].Y * height + y, cube[i].Z * depth + z ); // ダメ。見るも無残な姿に… Mesh1->Data->VertexBuffer->Vertices[i].X = cube[i].X * width + x; Mesh1->Data->VertexBuffer->Vertices[i].Y = cube[i].Y * height + y; Mesh1->Data->VertexBuffer->Vertices[i].Z = cube[i].Z * depth + z;
頂点データの範囲
で、このVerticesのX、Y、Zメンバの値は-0.5〜0.5の範囲に収める必要があるっポイ。
大きさを変更するときには頂点データではなく、Width/Height/Depthを変更するか、Scaleプロパティを1.0より大きくする。
繰り返す。頂点データは-0.5〜0.5の範囲を超えてはならない。
え?範囲を超えたデータを入れるとどうなるかって?
RayCastIntersect()を呼び出す処理、すなわちMouseMoveなどのマウス判定処理がおかしくなる。
具体的にはTMeshData.RayCastIntersect()からRayCastCuboidIntersect()を呼び出すときに-0.5〜0.5の範囲しか検査しないので見た目より小さな範囲でしかマウスに反応しなくなる。*2
ヘルプには何も書いてないような気がするけど、TCubeとかのTCustomMeshを親にしてるコンポーネントが-0.5〜0.5で頂点を定義しているようにソース上は見えるので、おそらくそういう仕様なのだろう。なんだかなぁ…
てか、この件の解析で数日飛んだんだが…
マウスイベントの当たり判定
3Dコントロールしか試してないけど、いろいろ重ねて表示しているとマウスイベントが効かない場合がある。
マウスイベントはForm上の子供のうち、ObjectAtPoint()で最初に引っかかったやつへわたされるっぽい。*3
一方、TControl3D.ObjectAtPoint()やTCustomForm3D.ObjectAtPoint()はループからExitしてないので最後に見つかったやつになる模様。
で、VisibleかつHitTestなやつのところへ配送されてしまうようだ。
ということで、もともとマウスに反応するつもりのないコンポーネントは他の邪魔にならないようにHitTest = falseとしてしまうのが良いようだ。
TMouseMoveEvent3D
マウス位置のX、Y以外にTVector3DのRayPos、RayDirが引数で渡されてくる。値を表示して内容を確認した感じだと、RayPosがカメラ位置、RayDirが原点からマウス位置に向かう単位ベクトル(方向ベクトル)のようだ。
それで、TControl3D.RayCastIntersect()にわたすとマウスとコントロールの接触点が求まる模様。
void __fastcall Form1::FormMouseMove(TObject *Sender, TShiftState Shift, float X, float Y, TVector3D &RayPos, TVector3D &RayDir) { TVector3D intersect_abs; // ワールド座標 TVector3D intersect_loc; // ローカル座標 // TMeshとの接触点を求める Mesh1->RayCastIntersect(RayPos, RayDir, intersect_abs); // ローカル座標にする intersect_loc = Mesh1->AbsoluteToLocalVector(intersect_abs); //... }
実際に呼び出すとローカル座標としてWidth/Height/Depth倍された値が見えた。頂点データの-0.5〜0.5の数値ではなかった。
ただ、実際にアプリ上でマウスをぐりぐり動かすと微妙に座標がずれてるような、カメラ位置によってその辺りの感じが変化したりと微妙な挙動を示した。
厳密な動作をさせようとすると微調整が必要なのかもしれない。
というか、ソースを見たらRayCastTriangleIntersect()で誰もIntersectionを更新してないような気がする…RayCastPlaneIntersect()で求めたPを何かしてIntersectionへ戻さないとダメなんじゃね?
とか考えて「RayCastTriangleIntersect Intersection」でググったら普通にQualityCentralでバグ報告されて*4た。
どうやらXE4なら直ってるらしい。そしてPはそのままIntersectionへ戻せばよいらしい。
ただ、TControl3D.RayCastIntersect()相当の処理を自前でもう一度実装するのは面倒なので、素直にXE4へ上げたほうが早そうだね。*5
[2013/09/08 23:00 追記]
XE4 updaate1のソースコードを見てるんだけど、なんかこのあたりのソースコードってXE3の頃と差分がないんだよね…本当にこの不具合直ってるの??
TExtrudedShape3Dのサブコンポーネント
TText3Dは大量に作ると重いって話を何かググったときに見た気がするけど、TRectangle3Dを自分で100個以上表示させてみたらそこそこカクカクする感じでプチ重いって感じだった。
- TText3D
- TRectangle3D
- TEllipse3D
- TPath3D
あたりがTExtrudedShape3Dを継承しているけど、こいつらのソースを見てみるとTCustomMeshは継承していない。
TCustomMeshでDataプロパティ(VertexBufferとか)が導入されるけどそれがTExtrudedShape3D配下には含まれていない。Context.FillPolygon()などを使って直接?描画しているようだ。
で、このContext.FillPolygon()はどうやらTContextHelper.FillPolygon()っぽい。中身は…
Ver := TVertexBuffer.Create(...
( ゚д゚) ・・・
(つд⊂)ゴシゴシ
(;゚д゚) ・・・
(つд⊂)ゴシゴシゴシ
(;゚ Д゚) …!?
なんでRender()内で動的にバッファこしらえてるんじゃーいっ(#ノー_ー)ノ彡┻━┻
ちなみにTCustomMeshから派生してるサブコンポーネントはこちら
- TCube
- TPlane
- TDisk
- TSphere
- TCylinder
- TRoundCube
- TCone
- TMesh
こいつらは内部でprotectedでDataプロパティを持ってる。(TMeshはpublished)ただ、試しにTCubeを100個くらい作ってみたら結構重かった。*6
結局大量に表示する際には自前で1つのTMeshにまとめるのがよさそうな感触。
で、これで全部かと思いきや…
せんせー!仲間はずれがいます!
- TGrid3D
- TStrokeCube
この2つはTControl3Dから直接派生してる。そしてRender()内ではContext.DrawLine()、Context.DrawCube()を呼んでいる。これも大量に表示すると重いかも…(ソース上で動的にバッファをCreate()してるのは確認済み)
あと、TModel3DはまさかのTDummyから派生。どうなってんの?これ。Render()も見つからないし…