JavaFX アプリケーション

JavaFX 11以上、Scene Builder、Eclipse を利用して


JavaFX 3次元グラフィックスの表示位置、移動、回転(の中心)について

投稿日時:

最終更新日時:

カテゴリー:

,

例えば、円柱などの JavaFX 3次元グラフィックスを表示させる場合、画面のどこに表示されるのであろうか?移動させるとはどういうことか、また、回転させる場合、回転軸はどこに設定されるのであろうか?等の基本的な疑問が生じる。具体的な例を通じてこれらを理解するのが今回の目的である。

まず、基本的な設定として、PerspectiveCamera(透視カメラ)を引数を true にして作り、scene にセットする。

PerspectiveCamera camera = new PerspectiveCamera(true);
Group groupRoot = new Group();
Scene scene = new Scene(groupRoot, 800, 800, true, SceneAntialiasing.BALANCED);
scene.setCamera(camera);

これで、カメラが scene の中心に向こうを向いて設置され、groupRoot の原点が scene の中心と一致し、X軸が右を、Y軸が下を、Z軸が向こうを向く。このままではカメラが原点にあり被写体に近すぎるので、カメラをこちら側へ離し、また、カメラがある距離までは見えて欲しいので、

// camera.setNearClip(0.1); // 既定値0.1で良いので指定しない。
camera.setFarClip(1000); // 既定値は100である。
camera.setTranslateZ(-400); // カメラをこちら側へ400だけ移動する。

とする。以上で、カメラの設定が終わった。

完成版
完成版

次に、被写体をgroupRootに加える。上図が完成版である。このために、以下で、グループ group1、group2、group21 と円柱 cylinder10、cylinder11、cylinder20、cylinder21 を作成する。ここで登場する、円柱などのグルフィックスやそれをまとめるグループ、等はノードとも呼ばれる。また、ノードの原点、中心、座標軸(の向き)、という用語に注意する。円柱などの実際の図形の中心は、この用語で普通に想定されるものが、そのものであるが、グループの中心、座行軸の向き、また、ノードの原点は、以下で述べるように、ある規則で決められる。グループの中心に関しては、最後の方で述べる。

Group group1 = new Group();
Group group2 = new Group();
Group group21 = new Group();

PhongMaterial phongMaterialRed = new PhongMaterial(Color.RED);
Cylinder cylinder10 = new Cylinder(10, 100); // 半径が10、長さが100の円柱を作成する。
cylinder10.setMaterial(phongMaterialRed); // 赤色にする。
Cylinder cylinder11 = new Cylinder(10, 100);
cylinder11.setMaterial(phongMaterialRed);

PhongMaterial phongMaterialBlue = new PhongMaterial(Color.BLUE);
Cylinder cylinder20 = new Cylinder(10, 100);
cylinder20.setMaterial(phongMaterialBlue);
Cylinder cylinder21 = new Cylinder(10, 100);
cylinder21.setMaterial(phongMaterialBlue);

上記のコードで、例えば、赤色の円柱 cylinder10 がY軸に沿って作成される。cylinder10 の原点は cylinder10 の中心となり、座標軸は、X軸が右向き、Y軸が下向き、Z軸が向こう向きに作成され、cylinder10 に固定される。また、group1 は現時点では中身がないが、group1 の原点がどこかに、座標軸はX軸が右向き、Y軸が下向き、Z軸が向こう向きとなるように作成され、group1 に固定される。

これらに親子の関係を導入する。

groupRoot.getChildren().addAll(group1, group2);
group1.getChildren().addAll(cylinder10, cylinder11);
group2.getChildren().addAll(cylinder20, group21);
group21.getChildren().add(cylinder21);

例えば、、cylinder10 を group1 に入れると、親である group1 の原点、座標軸が cylinder10 の(もし、移動や回転などの変換をしていれば、それ以前の初期の)原点、座標軸と一致するように決定される。すなわち、中身がない時点ではどこにあるか不明だった group1 の原点が決まる。また、以後、group1、group2 は移動、回転を行わないので、親である groupRoot との位置関係は変わらない。すなわち、原点と座標軸が一致する。group21 は、後で、回転のみを行うので、親である group2(結局、groupRoot)と原点が一致する。これで次のような親子関係ができた(説明のための印を付けた完成版を参照)。

説明のための印を付けた完成版
印付き完成版
  • groupRoot // groupRoot の原点は図の中心、黒い円内にある。X座標は右を、Y座標は下を、Z座標は向こうを向いている。
    • group1 // group1 の原点と座標軸は groupRoot のそれらと一致する。特に原点は黒い円内にある。
      • cylinder10
      • cylinder11
    • group2 // group2 の原点と座標軸は groupRoot のそれらと一致する。特に原点は黒い円内にある。
      • cylinder20
      • group21 // group21 の原点は group2 の、従って、groupRoot のそれと一致する。特に原点は黒い円内にある。
        • cylinder21

以上をまとめると、円柱やグループ、等はノードとも呼ばれるのであった。子を持たない円柱のようなノードの原点はノードの中心に、座標軸は、X軸が右向き、Y軸が下向き、Z軸が向こう向きに、作成される。子を持つ、すなわち、親であるグループのようなノードの原点、座標軸は、その子(複数あれば全ての子)の(もし、移動や回転などの変換をしていれば、それ以前の初期の)原点、座標軸に一致するように決定される。これらのノードの原点と座標軸はノードに固定され以後変わらない。ノードの変換(移動や回転)で変化するのは、その親におけるそのノードの位置と向きである。

現時点では、4個全ての円柱が重なって scene の中央に、上下方向に沿って立っている。cylinder10 と cylinder20 は cylinder11 と cylinder21 の回転直前の状態を表示させるためのものなので、以下では説明を省略する。完成させるために、(上図を参考にして)赤色の cylinder11 を右へ50だけ移動させ、図の白い円内(これは cylinder11 の中心でもある)を通るZ軸を回転軸として90度回転させる。青色の cylinder21 を左へ50だけ移動させ、黒い円内を通るZ軸を回転軸として90度回転させる。これらのために必要なことを述べる。

まず、Transform(変換)、特に、Translation(移動)と Rotation(回転)の基本的なことを述べる。
円柱やグループ、等はノードとも呼ばれるのであった。ノード node をX軸方向へ50だけ移動させるには、例えば、

node.setTranslateX(50);

または、

node.getTransforms().add(new Translate(50, 0, 0));

とすれば良い。また、node を node の原点を通るZ軸を回転軸として90度回転させるには、

node.getTransforms().add(new Rotate(90, Rotate.Z_AXIS));

また、nodeをnodeの中心を通るZ軸を回転軸として90度回転させるには、

node.setRotationAxis(Rotate.Z_AXIS);
node.setRotate(90);

とすれば良い。今回のように node が cylinder11 の場合、node の原点と中心が一致するので、どちらの回転方法を利用しても良い。ここで、回転角は正でも負でも指定できるが、正の角度の回転とは、右ねじを回した時にねじが進む方向が回転軸の正の方に一致する回転のことである。この基礎知識を基に、cylinder11 の方は

cylinder10.setTranslateX(50);
cylinder11.setTranslateX(50);
if (isRotateInTransforms) {
	cylinder11.getTransforms().add(new Rotate(90, Rotate.Z_AXIS));
} else {
	cylinder11.setRotationAxis(Rotate.Z_AXIS);
	cylinder11.setRotate(90);
}

とした。ここで、boolean 変数である isRotateInTransforms が真であれ偽であれ同一の結果となることを確認出来るように、このアプリケーションの引数の有無で isRotateInTransforms の真偽を設定できるようにしておいた。

(繰返しになるが)既に述べた通り、cylinder11 自身の原点は初期に固定され移動の前後で変わらず、その中心に一致するので、上述の2通りの回転方法の結果は一致する。

次に、cylinder21 を黒い円内を通るZ軸を回転軸として90度回転させるにはどうするか?である。

cylinder20.setTranslateX(-50);
cylinder21.setTranslateX(-50);
group21.getTransforms().add(new Rotate(90, Rotate.Z_AXIS));

(group21 を作成し、それに cylinder21 を入れてあったので、)group21 をその原点を通るZ軸を回転軸として90度回転させればよい。何故なら、group21 の原点は cylinder21 を作成した時点(移動前)での cylinder21 の中心であり、それは黒い円内であり、group21 を回転させれば、その子である cylinder21 も回転するからである。

完成時の親子の関係をまとめると:

  • groupRoot // groupRoot の原点は図の中心、黒い円内にある。X座標は右を、Y座標は下を、Z座標は向こうを向いている。
    • group1 // group1 の原点と座標軸は groupRoot のそれらと一致する。
      • cylinder10 // cylinder10 の原点と座標軸は、作成された時に cylinder10 に固定される。すなわち、cylinder10 の中心であり、X座標は右を、Y座標は下を、Z座標は向こうを向いている。また、円柱はY軸に沿って立っている。しかし、その親である group1 の原点からX軸方向へ50の所に cylinder10 の原点がある。
      • cylinder11 // cylinder11 の原点と座標軸は、作成された時に cylinder11 に固定される。すなわち、cylinder11 の中心であり、作成時には、X座標は右を、Y座標は下を、Z座標は向こうを向いていた。また、円柱はY軸に沿って立っていた。しかし、その親である group1 の原点からX軸方向へ50の所に cylinder11 の原点があり、(図中の白い円内にある)この原点(中心と一致する)を通るZ軸を回転軸として90度回転するので、円柱は水平方向を向き、それに応じて、cylinder11 の座標軸も90度回転する。
    • group2 // group2 の原点と座標軸は groupRoot のそれらと一致する。
      • cylinder20 // cylinder20 の原点と座標軸は、作成された時に cylinder20 に固定される。すなわち、cylinder20 の中心であり、X座標は右を、Y座標は下を、Z座標は向こうを向いている。また、円柱はY軸に沿って立っている。しかし、その親である group2 の原点からX軸方向へ-50の所に cylinder20 の原点がある。
      • group21 // group21 の原点は group2 の、従って、groupRoot のそれと一致する。座標軸は異なる。(group21 の回転については cylinder21 の所で述べる。)
        • cylinder21 // cylinder21 の原点と座標軸は、作成された時に cylinder21 に固定される。すなわち、cylinder21 の中心であり、作成時には、X座標は右を、Y座標は下を、Z座標は向こうを向いていた。また、円柱はY軸に沿って立っていた。しかし、この親である group21 の原点からX軸方向へ-50の所に cylinder21 の原点がある。group21 を(図中の黒い円内にある)その原点を通るZ軸を回転軸として90度回転するので、group21 に入っている円柱は水平方向を向き、それに応じて、group21 の座標軸も90度回転する。

下図に、完成時の cylinder11 と group21 の原点と座標軸を描いた。赤色がX軸(下を向いている)、緑色がY軸(左を向いている)、青色(向こうを向いている)がZ軸である。青色の円柱が入っている group21 の原点と座標軸は赤色の円柱の左にある。どちらの座標軸も作成時と比べ90度回転している。

完成時の cylinder11 と group21 の原点と座標軸
cylinder11group21

最後に、グループの中心はどこになるのか?を調べる。そのために、

group2.setRotationAxis(Rotate.Z_AXIS);
group2.setRotate(90);

のコードを追加して青色の2つの円柱を入れた group2 を group2 の中心を通るZ軸を回転軸として90度回転してみた。group2 の回転前の座標(groupRoot のそれと一致)で測ると、group2 の中心$(x_{center}, y_{center}, z_{center})$は

\[ x_{center} = (x_{min} + x_{max}) / 2 \\ y_{center} = (y_{min} + y_{max}) / 2 \\ z_{center} = (z_{min} + z_{max}) / 2 \]

で与えられると予想できる。ただし、例えば、$x_{min}$と$x_{max}$は group2 に含まれる図形のX座標の最小値と最大値である。具体的に計算すると、$x_{min} = -60$、$x_{max} = 50$、なので、$x_{center} = -5$、同様に、$y_{center} = -5$、$z_{center} = 0$ となる。この$(-5, -5, 0)$を通るZ軸に平行な軸を回転軸として group2 を90度回転させると、(以後の座標は groupRoot の座標である。)$(-5, -5, 0)$より右へ$45$の所、すなわち、$(40, -5, 0)$、を鉛直方向に青い円柱の中心線が通り、$(-5, -5, 0)$より上へ$45$の所、すなわち、$(-5, -50, 0)$、を水平方向に青い円柱の中心線が通る。下図に group2 の中心(黒い円内)と回転前の青色の円柱(薄くしてある)と回転後の青色の円柱(と赤色の円柱)を示した。この図により、上記の予想は正しいと判断できる。

group2 の中心
centerOfGroup2

なお、基本事項を理解するために、「JavaFX 3Dを理解する」と「JavaFXの座標系と座標変換」を参考にさせていただいた。

Eclispe プロジェクトの src フォルダの zip ファイルはこちら。実行ファイルに関してはこちら