「JavaFXの座標系と座標変換」の版間の差分

提供: tknotebook
移動: 案内検索
(カメラのシーンへ設置の仕方)
(カメラのシーンへ設置の仕方(重要!))
495行: 495行:
 
===カメラのシーンへ設置の仕方(重要!)===
 
===カメラのシーンへ設置の仕方(重要!)===
  
パラレルカメラも透視投影カメラも シーンのsetCameraメソッドでシーンに追加する必要があります。
+
パラレルカメラも透視投影カメラも '''シーンのsetCameraメソッドでシーンに追加する必要があります。'''
 
これは必須です。これに加えてカメラもノードなので、任意のノードに子ノードとして
 
これは必須です。これに加えてカメラもノードなので、任意のノードに子ノードとして
 
追加することもできます。
 
追加することもできます。

2016年4月9日 (土) 10:38時点における版

メインページ>コンピュータの部屋#JavaFX>JavaFX Tips


JavaFXに関してたくさんの解説がネットや書籍に載っていますが、意外なことに JavaFXの座標系に関する解説が少ないようです。

この記事では JavaFx の座標系と、座標変換に関して解説します。

尚、解説は、図を描くのが大変なので2Dで行ないますが、 基本的に3Dでも同じです。3D特有の話は適当に補足することにします。

JavaFXの座標系

まず、JavaFxで使用されているワールド座標を示します。 ワールド座標とは、JavaFXのコントロールや図形が置かれる 座標空間です。

JavaFX ワールド座標.png
図1 JavaFXのワールド座標

図は、3Dカメラを用いない場合の Scene で使われるワールド座標系です。 z軸も書いてあるのは、2Dグラフィックスでも画面の深さ方向の座標(Z軸)が使われることがあるからです。

Scene(ウィンドウ) の左上が座標系の原点で、そこから右方向にX軸が、下方向にY軸が伸びます。 画面に潜る方向がZ軸の正方向になります。 軸目盛の単位はピクセルです。

3Dカメラを用いない Scene では、Scene とワールド座標の関係は固定されています。 3Dカメラに関しては「変換」の説明が終わってから説明します。

ローカル座標系

Scene に登録された個々のノード(コントロールやコンテナや図形)は、 自身のローカル座標系を持ちます。

例えば、2次元図形の Rectangle は矩形の幅、高さや左上の座標を保持しますが、これらはローカル座標系の値です。

3次元図形の Box(箱型) や Sphere(球) はその中心が常に座標 (0, 0, 0) になりますが、 (0、0, 0) はローカル座標です。つまり3次元図形は自身のローカル座標の中を動くことができません。

JavaFX座標系の階層構造.png
図2 JavaFX の座標系の階層構造

ローカル座標系は親ノードのローカル座標系の中に置かれます。もし、ノードにTransform(変換)が無く、 layoutX, layoutYプロパティもゼロのままならば、ノードのローカル座標系と親ノードの ローカル座標系は一致します。

JavaFxでは、2Dでは図形はある程度ローカル座標系内でも動かすことができますが、3D図形を ローカル座標系内で動かすことはできません。

従って、JavaFXでは、特に3D では ローカル座標系はノード(図形)に固定された座標系と考えます。 親ノードに対するノード(図形)の位置や回転や大きさの指定は、親ノードのローカル座標系に対する ノードのローカル座標系の位置や傾きや大きさを指定することで行います。 図形ではなくローカル座標系を指定するという考え方に注意してください。

JavaFX のノードの Transform(変換)やlayoutX/LayoutY プロパティはノードの親のローカル座標系に対する 自身のローカル座標系を定義するためのものなのです。

Transform(変換)

それでは早速、Transform(変換)を紹介しましょう。

平行移動

親ノードのローカル座標系に対して、ノードのローカル座標系を平行移動した位置に するには、以下のようにします。

ノード.getTransforms.add(new Translate(3, 2));
JavaFX Transform平行移動.png
図3 座標変換(平行移動)

Translateのコンストラクタには三次元用もあります。

ノードのローカル座標系の原点のあたりに図形が書いてありますが、この図形はノードそのものと 考えて下さい。

ノード(図形)はローカル座標系の平行移動に従って一緒に移動します。その結果、親のノードから 見ると図形は右下の方へ移動します。

実は

ノード.setTranslateX(3);
ノード.setTranslateY(2);

ノード.layoutX=3;
ノード.layoutY=2;

でも同じことができるのですが、意味が若干異なります。後で説明しますので、とりあえず getTransforms.add を使う方法で 先に進みましょう。

回転

親ノードのローカル座標系に対して、ノードのローカル座標系を回転させるには、以下のようにします。

ノード.getTransforms().add(new Rotate(45, 3, 2));
JavaFX Transform回転.png
図4 座標変換(回転)

Rotateのコンストラクタで指定した 45 は回転角(単位:度), 3, 2 は回転中心(ピボット)を表します。 ノードのローカル座標系は、この場合、親ノードのロ-カル座標系の (3, 2) を中心に、 時計回りで(Z軸の右ねじ方向に)45度回転します。

上に紹介したRotateのコンストラクタでは 2D 用ですが、3次元のピボットと回転方向を指定できる 3次元用の コンストラクタも用意されています。

スケール

親ノードのローカル座標系に対して、ノードのローカル座標系をスケール(拡大縮小)させるには 、以下のようにします。

ノード.getTransforms().add(new Scale(0.8, 0.7, 3, 2));


JavaFX Transformスケール.png
図5 座標変換(スケール)

Scale はコンストラクタのパラメータ 0.8, 0.7 を比率として使って、親ノードのローカル座標系に対し、 ノードのローカル座標系を拡大縮小します。0.8 は X軸方向の拡大率。0.7 は Y軸方向の拡大率です。 この場合、図形は親ノードのローカル座標系から見ると「小さく」なることに注意してください。

拡大縮小は、指定された拡大縮小中心(ピボット、この場合は (3, 2)) を中心に行います。つまり、 拡大縮小中心に対してすべての点が同じ比率で離れて(近づいて)ゆくように変換します。

Scaleのコンストラクタには3次元用もあり、その場合、拡大縮小率はX, Y, Z の3方向、 拡大縮小中心は3次元で指定できます。

スケールの場合、回転や平行移動とはことなり、座標系の目盛りの大きさが変わるので注意が 必要です。

シャーリング

シャーリングを簡単に解説しておきましょう。

あまり使わないわりに、まともに説明するのが一番大変な変換なので、ちょっと端折って、 どんなものか簡単に紹介するだけしておきます。

シャーリングは図形に斜めの傾きを与えるような変形です。 回転や平行移動とは違い、図形が歪みます。四角を平行四辺形にしたり、ひし形に変形したりするような変形です。

親ノードの座標系のX軸, Y軸の座標を x, y、ノードの座標系のX軸, Y軸の座標を x', y' とすると、座標系間の関係は

x = x' + S_x(y'-pv_x) ( 1 )
y = S_y(x'-pv_y) + y' ( 2 )

となります。

この式を見ればどのように座標系が変形されるかだいたい予想できると思いますが、 S_x, S_yは座標系を傾かせる度合いを表す係数、pv_x, pv_y は 座標系の変換で動かない点(ピボット)です。回転の回転中心や、スケールの拡大縮小中心に似たものです。

親ノードのローカル座標系に対して、ノードのローカル座標系をシャーリングさせるには S_x=0.3, S_y=0.4, pv_x=10, pv_y=5 のシャーリングの場合、

ノード.getTransforms().add(new Shera(0.3, 0.4, 10, 5));

とします。

尚、JavaFX の Shearクラスには2次元用のコンストラクタしかありません。3次元でも利用可能ですが、 ピボットを3次元で指定することはできません。3次元での使用はあまり想定していないのでしょう。

もし、3次元でシャーリングをどうしても行いたいなら、この後で説明するアフィン変換を利用してください。

アフィン変換

ここまでで紹介してきた平行移動、回転、スケール、シャーリングは全てアフィン変換という 変換で表現できます。

アフィン変換とは、親ノードのローカル座標系の座標値を (x, y, z) 、 ノードのローカル座標系の座標値を (x', y', z') とすると、座標系間の関係は

 

\begin{array} {ll}
x = mxx\cdot x' + mxy\cdot y' + mxz\cdot z' + tx \\
y = myx\cdot x' + myy\cdot y' + myz\cdot z' + ty \\
z = mzx\cdot x' + mzy\cdot y' + mzz\cdot z' + tz
\end{array}

( 3 )

この座標変換で平行移動、回転、スケール、シャーリングは全てサポート 可能なのですが、正しいパラメータを設定した正しい Transform オブジェクトを作るには 数学の詳しい知識が必要です。

Affineというクラスを使います。

そうめったには必要にはならないので、最後の手段として こういうものがあることを覚えておけばよいでしょう。

複数の変換を使う(重要!!)

ここまでは、単一の変換(Transform)を使ってきましたが、実は、ノード.getTransforms().add メソッドは その名前からお察しの通り、複数の変換(Transform)をノードに登録できます。

以下コードを実行してみましょう。ちなみに mainPanel という変数は、シーンのルートノードで Pane型です。このコードは ウィンドウに2個の矩形を表示します。結果を図6に示します。

       // 矩形を作る。幅=100, 高さ=100, 左上の点=(0, 0)
       Rectangle rec1 = new Rectangle(100, 100);
       rec1.setFill(Color.BLACK);

       // 平行移動する。(+50, +50)
       rec1.getTransforms().add(new Translate(100, 50));


       // 矩形を作る。幅=100, 高さ=100, 左上の点=(0, 0)
       Rectangle rec2 = new Rectangle(100, 100);
       rec2.setFill(Color.RED);
       rec2.setOpacity(0.5);

       // 平行移動する。(+50, +50)
       rec2.getTransforms().add(new Translate(100, 50));
       // 回転する。時計回り30度。回転中心 原点
       rec2.getTransforms().add(new Rotate(30, 0, 0));

       mainPanel.getChildren().addAll(rec1, rec2);
JavaFX連続変換.png
図6 連続変換

いかがでしょう。予想通りだったでしょうか?

このコードは2個の矩形を表示します。1個目の矩形 rec1(黒) は、変換に「平行移動」のみを使っています。 rec1 は最初は画面の左上の辺にぴったり張り付いていたはずですが、「平行移動」によって少し画面の中の方へ 移動しています。

2個目の矩形 rec2(半透明な赤) は、変換に「平行移動」「回転」を順番に使っています。 rec2のローカル座標系での座標値はrec1と同じ。 使っている「平行移動」もrec1と同じです。

rec2 はまず「平行移動」で画面のrec1の位置まで移動してきますが、その次に回転します。 回転中心座標は (0, 0) が指定されていますが、この (0, 0) はいったいどこなのでしょうか?

もし、親ノードのローカル座標系なら、矩形はもっと大きな半径で回るはずです。でも図6は rec2 が rec1 の左上の点を中心に回っていることを示しています。

何故こうなるかというと、rec2 が回転するときの中心の座標 (0, 0) は、rec2 のローカル座標系が 「平行移動」してできた新しい座標系の (0, 0) だからです。

つまり、ある座標系変換が使用する座標系は、その座標系変換の一つ前の座標系変換が作り出した座標系なのです。別の言い方をすれば、「変換」は以降の座標系変換が使用する座標系を「定義」します。

これは非常に大事です。よく覚えておいてください。

尚、「Transform(変換)」 節で説明した各種の変換は、説明の順序の都合で、 親ノードのローカル座標から ノードのローカル座標を作ると説明してきましたが、 本当は直前の変換で作られたローカル座標からさらに新しいローカル座標を作るものです。 申し訳ありませんが読み替えてください。

変換の二つ目の方法

実は簡単な変換ならノードのプロパティを変更するだけでできてしまいます。

平行移動は、getTransforms().add を使うよりかえって面倒かもしれませんが

ノード.setTranslateX(100);
ノード.setTranslateY(50);
ノード.setTranslateZ(70); // 3次元用

でできます。

回転は

ノード.setRotate(45); // 回転角
ノード.setRotationAxis(Rotate.X_AXIS); // 回転軸(3次元用)

でできます。ピボットの指定はできません。ピボットはノードの中心(layoutBoundsプロパティの表す矩形の中心)になります。

スケールは

ノード.setScaleX(1.2);
ノード.setScaleY(0.8);
ノード.setScaleZ(0.8); // 3次元用

でできます。ピボットの指定はできません。ピボットはノードの中心(layoutBoundsプロパティの表す矩形の中心)になります。

シャーリングはノードのプロパティでは行えません。

ノードの平行移動、回転、スケール用のプロパティが同時に使われると、 変換が順次適用されてローカル座標系が作られますが、 適用の順番は「平行移動」、「回転」、「スケール」の順で、順番を変えることはできません。

このように、プロパティによる「変換」はかなり機能が限定されるので、 用途に応じて使い分けましょう。プロパティによる変換で十分であることが多いです。

ノード.getTransforms().add で追加した変換と、プロパティで設定した 変換が両方ある場合、まず先にプロパティで設定した「平行移動」、「回転」、「スケール」が順に実行され、 その後ノード.getTransforms().add で追加した変換が順番に実行されます。


注意: Javadoc では、getTransforms().add で追加した変換 の後にプロパティで設定した変換が適用されると 書かれてますが、 恐らくそれは、後で述べる ローカル座標系での座標値を グローバル座標系へ変換してゆく時の「変換」の適用順を表していると思われます。

layoutX, layoutYプロパティ

ノードのlayoutX, layoutYプロパティは、本来コンテナがノードを配置するためのプロパティですが、 親ノードに対してノードを移動させるので「変換」の一種です。実際layoutX, layoutYプロパティはノードの ローカル座標を平行移動させます。驚いたことに3次元でも立派に動作します。 但し layoutZ が無いので、3次元ではあまり実用性はありません。 また、layoutX, layoutYプロパティはコンテナがレイアウトのために値を変更することがあるので、 注意が必要でしょう。

layoutX, layoutYプロパティによる「変換」は他の変換(プロパティで指定された変換、ノード.getTransforms().add で追加した変換)に先立って、最初に実行されます。つまり最優先の「変換」です。

変換の順序のまとめ

以上をまとめると、親ノードの座標系からノードの座標系を作る時の「変換」が実行される順番は

  1. ) layoutX, layoutYプロパティによる「変換」(平行移動)
  2. ) ノードのプロパティで指定した平行移動。
  3. ) ノードのプロパティで指定した回転
  4. ) ノードのプロパティで指定したスケーリング
  5. ) ノード.getTransforms().add で追加した変換が add で追加した順に実行。

となります。何度も書きますが、既に説明しましたように、 変換はその直前の変換でできた座標系上で新たな座標系を定義します。

以上の変換はグローバル座標系からルートノードのローカル座標系へ、 ルートノードのローカル座標系からその子ノードのローカル座標系へと 上の階層から下の階層に向かって順に実行されます。

あるノードのローカル座標系は、グローバル座標系に直系の全ての親ノードと自身の変換を順に適用して 作られる座標系です。

変換がノードの階層構造と同じ形の階層構造を持つ理由

例えば Group ノードがあるとします。ここに子ノードとして 玄関や屋根や窓を設置します。

こうして作った「家」は個々の家の部品の座標を変更しなくとも、Group ノードの「変換」を変更することで、 「家」全体の位置や向きや大きさを変更できます。

このように「変換」を使うと、図形をグループとして管理するのがとてもやりやすくなるのです。

座標系の変換の順序と座標値の変換の順序

ここまでで、「変換」とは新たなローカル座標系を定義することと説明してきました。

ここで、注意してほしいのは、既に、アフィン変換のところで説明しましたように、 「変換」は、変換後のローカル座標系の座標値を変換前の座標系の座標値に変換する マトリックスで定義されているということです。

これはどういうことなのかというと、ノードを描画する際、ノード上の点の描画位置を求める ということは、まずその点のローカル座標系での座標値を得、 それをより上位の座標系の座標値に変換していって、グローバル座標にまで変換する ということなのです。

つまり、ノード上の点の描画位置の計算はノードの「変換」を逆順で行うのです。


具体的には、あるノードのローカル座標系の座標値から一つ上のノードのローカル座標系の座標値を計算するには

  1. ) ノード.getTransforms().add で追加した変換を add で追加した順とは逆順に使って座標値を変換する。
  2. ) ノードのプロパティで指定したスケーリング で座標値を変換する。
  3. ) ノードのプロパティで指定した回転 で座標値を変換する。
  4. ) ノードのプロパティで指定した平行移動 で座標値を変換する。
  5. ) layoutX, layoutYプロパティによる「変換」(平行移動)で座標値を変換する。

これをルートノードまで繰り返せばグローバル座標にまでたどり着けることになります。

「変換」を考えるとき、それを座標系の定義ととらえるか、座標値の変換ととらえるかで、 「変換」の適用の順番が逆になることに注意してください。 変換を変更してその効果を考えるとき、後者のとらえ方の方がわかりやすいことが多いです。

両方のとらえ方を頭に入れておくべきでしょう。

3Dカメラと座標変換

最初に説明しましたように、3Dカメラを使わない場合、グローバル座標とシーンの関係は 図1 のように 固定されています。グローバル座標を好きな位置から好きな方向から、好きな拡大率で眺められるように してくれるのがカメラというノードです。

カメラが ParalellCameraと PerspectiveCamera の2種類があります。

ParallelCamera

このカメラは平行投影(正射投影)を行うカメラです。下図を見てください。

JavaFX ParallelCamera.png
図7 ParalellCameraの座標系

図中のX-Y平面上(Z=0)にある薄い空色の面はカメラにセットされたシーン(Scene)です。 シーンはカメラのローカル座標系の中にあって、そのサイズは画面上のシーンのサイズと同じです。 ローカル座標の中を動かすことも大きさを変えることもできません。

初期状態では、パラレルカメラのローカル座標系はシーンのグローバル座標系と一致しています。

パラレルカメラは常にそのローカル座標系のZ軸のプラス方向を向いています。

パラレルカメラはグローバル座標系上のノードを、自身のローカル座標系に変換してレンダリングするため、カメラノードに「変換」(Transform)を加えると カメラの向き、回転、ズーム量を自由に変更できます。パラレルカメラは自身のローカル座標系に変換されたノードを ローカル座標系のXY平面(Z=0)に正射影で投影して表示します。


パラレルカメラは実はローカル座標系の原点に位置していますが、 シーンで見えるのはローカル座標系のX, Y ともプラスの領域です。

シーンの左上隅がカメラの存在する原点になっています。かなり奇妙な見せ方といえるでしょう。 カメラに「変換」を加えない場合、カメラ無しの見え方との互換性があるのでこのようにしたものと 思われますが、かなり使いにくいです。 例えばカメラに回転変換を加えるとシーンの左上隅を中心に回るので注意してください。

パラレルカメラはカメラのセットされたシーンの大きさで、クリップ領域の大きさを決めてしまいます。これが曲者です。

X軸方向とY軸方向のクリッピングがシーンのサイズで決まるのは合理的ですが、Z軸方向まで farClipプロパティを無視して 決めてしまうのは明らかにやりすぎです。しかも、シーンの大きさの幅または高さのサイズの大きい方÷2 より奥のノードを レンダリングしません。これはいくら何でも奥行きが浅すぎるでしょう。

解決方法は パラレルカメラのクリッピングを変更するには を見てください。

PerspectiveCamera

このカメラは透視投影を行うカメラです(以降透視投影カメラ)。

このカメラも JavaFXの Javadoc を眺めても、細かなところがよくいわからないので、 実験でカメラのローカル座標系とシーンの関係を探ってみました。

尚ここでは、透視投影カメラのプロパティ fixedEyeAtCameraZero が true の場合のみ説明します。 この値が推奨値ですので、fixedEyeAtCameraZero=false の場合に関しては Javadocなどを読んで 研究してみてください。

下図を見てください。

JavaFX PerspectiveCamera01.png
図8 透視投影カメラのローカル座標系

透視投影カメラではノードを「投影面」に写すことでシーンに表示します。ローカル座標系上の 図形上の点は、その点と「カメラの目」を結ぶ直線が投影面と交わる点に表示されます。

投影面とはシーンそのもので、シーンと同じサイズです。但し、 図からわかりますように、シーンには Z軸がその中心を 垂直に貫いており「カメラの目」はシーンの中央の後方にあります。 つまり、シーンの中央にはローカル座標の原点が見えており、パラレルカメラより使いやすい 座標系の配置になっています。

「カメラの目」はカメラのローカル座標系の原点にあり、 初期状態でローカル座標系とグローバル座標系は一致しています。

ローカル座標系上の投影面(シーン)より先にある図形は元のサイズより縮小されて表示され、投影面(シーン)より手前にある 図形は元のサイズより拡大されて表示されます。

投影面のZ座標値(カメラの目から投影面までの距離)は fieldOfView プロパティとverticalFieldOfViewプロパティとシーンの大きさで 決まります。

fieldOfView プロパティとはカメラの「視野角」の広さを表します。既定では縦30度(上へ15度から下へ15度)の範囲が丁度 シーンの高さと合うように投影面の位置が決められます。

verticalFieldOfViewプロパティとは、視野角の向きを表します。verticalFieldOfView=true なら視野角(fieldOfView)は 縦方向の視野の広さを表します。verticalFieldOfView=false なら横方向の視野の広さを表します。

下の図を見てください。

JavaFX PerspectiveCamera02.png
図9 透視投影カメラの投影面の位置

verticalFieldOfViewプロパティが true の場合(既定値)、図は X軸のプラス位置からX軸のマイナス方向を見た際の (つまりローカル座標を横から見た際の) カメラのローカル座標を表しています。

「カメラの目」から角度が fieldOfView で交わる2本の直線がZ軸方向に延びています。ここに、図のように 投影面(シーン)の「高さ」がちょうどはまる位置に投影面が置かれます。

投影面の Z座標は、シーンの画面上での高さを HEIGHT、投影面(シーン)の Z座標をScene_z とすると



Scene_z = \frac{\frac{HEIGHT}{2}}{\tan\frac{fieldOfView}{2}}

 

verticalFieldOfViewプロパティが false の場合(既定値)、図は Y軸マイナスの位置からY軸のプラス方向を見た際の (つまりローカル座標を上から見た際の) カメラのローカル座標を表しています。

「カメラの目」から角度が fieldOfView で交わる2本の直線がZ軸方向に延びています。ここに、図のように 投影面(シーン)の「幅」がちょうどはまる位置に投影面が置かれます。

投影面の Z座標は、シーンの画面上での幅を WIDTH、投影面(シーン)の Z座標をScene_z とすると



Scene_z = \frac{\frac{WIDTH}{2}}{\tan\frac{fieldOfView}{2}}

 


以上のように透視投影カメラでは、シーンに指定された視野の範囲が丁度の大きさで表示されるため、 シーンの大きさを変えるとシーンのZ座標も変わり、 表示される図形がそれに追随して拡大/縮小します。これは パラレルカメラには無い、透視投影カメラの特徴です。

つまり、verticalFieldOfViewプロパティが true の場合、シーンの高さを大きくすると表示される図形も大きくなります。 シーンの幅を変更してもシーンから見える範囲が変わるだけでズーム率は変わりません。

verticalFieldOfViewプロパティが false の場合、シーンの幅を大きくすると表示される図形も大きくなります。 シーンの高さを変更してもシーンから見える範囲が変わるだけでズーム率は変わりません。

このカメラのローカル座標系に「変換」を加えると、パラレルカメラと同様に、グローバル座標系上の図形を眺める カメラの位置、方向、スケーリイングを自由に変更できます。カメラの視点がシーンの中央に向いているため 使いやすいカメラになっています。

カメラのシーンへ設置の仕方(重要!)

パラレルカメラも透視投影カメラも シーンのsetCameraメソッドでシーンに追加する必要があります。 これは必須です。これに加えてカメラもノードなので、任意のノードに子ノードとして 追加することもできます。

実はこれには重要な意味があります。

■カメラをノードの子ノードにしない場合
カメラのローカル座標系は、グローバル座標系にカメラの「変換」を加えたものになります。
■カメラをノードの子ノードにする場合
通常のノードと同様、カメラのローカル座標系は、グローバル座標系にカメラの直系の親ノードの「変換」およびカメラ自身の「変換」を加えたものになります。

つまりカメラを子ノードにしないということは、カメラが図形群とは独立であることになります。 その一方、カメラが子ノードになる場合、カメラが図形のグループの一員になります。

例えば、自動車ノード全体を表すノードがあり、その子ノードとして自動車のドアやタイヤなどと共に カメラも積めば、自動車の方向を変えるとカメラもその方向へ向きます。 つまり車載カメラのように動くわけです。