JavaFXの座標系と座標変換

提供: tknotebook
2016年3月29日 (火) 04:53時点におけるNakamuri (トーク | 投稿記録)による版

移動: 案内検索

メインページ>コンピュータの部屋#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 で追加した変換が先に順番に実行され、その後にプロパティで設定した「平行移動」、「回転」、「スケール」が順に実行されます。

layoutX, layoutYプロパティ

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

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

変換の順序のまとめ

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

3Dカメラと座標変換


鋭意作成中