JavaFX アプリケーション

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


JavaFX 3次元グラフィックス、透視カメラ(PerspectiveCamera)の回転(Rotation)と移動(Translation)

投稿日時:

最終更新日時:

カテゴリー:

,

3次元の物体を回転させたらどう見えるのか?の基礎として、PerspectiveCamera(透視カメラ)の回転と移動について、例を通じて理解したことを述べる。作成した例は下図のような「Camera Rotation and Translation」である。これに関しては、「JavaFX 3D の座標系とカメラの向きについて」を参考にさせていただいた。

Camera Rotation and Translation
カメラの回転と移動

上図の「Camera Rotation and Translation」(向こうへを-450、カメラの方位角と仰角を40度と30度に設定してある)において、赤、緑、青軸がX、Y、Z軸で、先端に球がついている方が正の方である。また、原点に接して正の方向に立方体がある。(透視)カメラは初期に原点にあり、向こう(Z軸の正の方)を向いている。

画面の右のほうにスライダーが5個ある。下の2個はカメラの回転に関するもので、カメラの方位角と仰角を指定できる。両方とも原点を通るある軸を回転軸として指定された角度だけカメラを回転させる。上の3個はカメラの位置を右へ、下へ、向こうへ移動させる。このように方位角と仰角を指定しカメラを回転させたのち、カメラを右、下、向こうへ移動させたら、X、Y、Z軸と立方体がどのように見えるか?が分かる。(マウスよりも左右の矢印キーでスライダーの値を変化させる方が、元の値に戻すことが容易である。)今回はカメラを回転、移動させる方法を述べるが、次回は、カメラを固定し、被写体である座標軸と立方体を入れたグループを回転、移動させることによって、同じことを実現させる。

基本的なことを述べる。PerspectiveCamera(透視カメラ)を引数を true にして作成し、scene(今回の場合は subScene)にセットすると、カメラが subScene の中心に向こうを向いて設置され、group の原点が subScene の中心と一致し、X軸が右を向き、Y軸が下を向き、Z軸が向こうを向く。(透視カメラを利用しない、または、透視カメラを引数を false にして作成し、subScene にセットすると、軸の向きは同一で group の原点が subScene の左上になる。)

PerspectiveCamera camera = new PerspectiveCamera(true);
Group group = new Group();
SubScene subScene = new SubScene(group, 600, 600, true, SceneAntialiasing.BALANCED);
subScene.setCamera(camera);

このままではカメラが原点にあり被写体に近すぎるので、(後で、カメラをこちら側へ離し、)また、ある距離まではカメラで見えて欲しいので、

// camera.setNearClip(0.1); // 既定値0.1で良いので指定しない。
camera.setFarClip(1000); // 既定値は100である。

今回の主なテーマである、カメラの回転と移動の説明の前に被写体である、座標軸と立方体の作成について述べる。上で述べたように group の原点は subScene の中心と一致する。この group に被写体である、座標軸と立方体を入れる部分が

makeAxes(group);

であり、関数 makeAxes(group) は以下のようである。

private void makeAxes(Group group) {
  PhongMaterial redPhongMaterial = new PhongMaterial(Color.RED);
		Cylinder xCylinder = new Cylinder(5, 200); // 半径5、長さ200の円柱を作成。
  xCylinder.setMaterial(redPhongMaterial); // 赤色に設定。
  xCylinder.setRotationAxis(Rotate.Z_AXIS);
  xCylinder.setRotate(90); // 円柱の中心を通り向こうを向いているZ軸(と平行な軸)を回転軸にして、90度回転し、赤色の円柱がX軸方向を向いた。
  Sphere xSphere = new Sphere(10); // 半径10の球を作成。
  xSphere.setMaterial(redPhongMaterial); // 赤色に設定。
  xSphere.setTranslateX(100); // 球を100だけ右の方(X軸方向)へ移動する。X軸を表す円柱の先端に移動した。
  group.getChildren().addAll(xCylinder, xSphere); // 赤色の円柱と球、すなわち、X軸がgroupに追加された。

  PhongMaterial greenPhongMaterial = new PhongMaterial(Color.GREEN);
  Cylinder yCylinder = new Cylinder(5, 200);
  yCylinder.setMaterial(greenPhongMaterial);
  Sphere ySphere = new Sphere(10);
  ySphere.setMaterial(greenPhongMaterial);
  ySphere.setTranslateY(100);
  group.getChildren().addAll(yCylinder, ySphere); // 緑色の円柱と球、すなわち、Y軸がgroupに追加された。

  PhongMaterial bluePhongMaterial = new PhongMaterial(Color.BLUE);
  Cylinder zCylinder = new Cylinder(5, 200);
  zCylinder.setMaterial(bluePhongMaterial);
  zCylinder.setRotationAxis(Rotate.X_AXIS);
  zCylinder.setRotate(90);
  Sphere zSphere = new Sphere(10);
  zSphere.setMaterial(bluePhongMaterial);
  zSphere.setTranslateZ(100);
  group.getChildren().addAll(zCylinder, zSphere); // 青色の円柱と球、すなわち、Z軸がgroupに追加された。
		
  PhongMaterial burlywoodPhongMaterial = new PhongMaterial(Color.BURLYWOOD);
  Box box = new Box(30, 30, 30); // 1辺の長さ30の立方体を作成。
  box.setMaterial(burlywoodPhongMaterial);
  box.setTranslateX(15);
  box.setTranslateY(15);
  box.setTranslateZ(15); // 立方体の内部が正の領域に来るように移動。
		
  group.getChildren().add(box); // 立方体がgroupに追加された。
}

円柱や球、等の3次元グラフィックスの表示位置、移動、回転については、前回の「JavaFX 3次元グラフィックスの表示位置、移動、回転(の中心)について」を参照して下さい。上記で、例えば、赤色の円柱 xCylinder は、中心を通るZ軸(と平行な軸)を回転軸にして、90度回転させれば、X軸になる。また、軸の先端に置くことにより軸の正の方向を示す赤色の球に関しては、右の方(X軸方向)へ100移動させればよい。

これで、group の原点と座標軸の意味でも、X軸、Y軸、Z軸、となるものが配置され、結果として、われわれが目標としている被写体である座標軸と立方体が、group の原点、座票軸と一致するように配置された。

カメラは原点にあり、向こう側を向いている。この時点での見え方は次の通りである。原点にある被写体であるX、Y、Z軸を表す円柱は見えないが、Z軸の正の方を表す青色の球が見える。

カメラが原点にある時
カメラが原点にある時

今回の本題のカメラの回転と移動に話を進める。上に述べたように、引数を true にしてカメラを作成し、それを subScene にセットすると、カメラが sugScene の中心に向こうを向いて置かれ、group の原点が subScene の中心と一致し、X軸が右を向き、Y軸が下を向き、Z軸が向こうを向く。カメラの原点と座標軸は group の原点と座標軸に一致する。今回の目的は、原点に置いてある被写体(上記で既に作成した、座標軸と立方体)を最初の図(この図では、既に、カメラがこちら側に移動している)で示したように、まず、(数学を勉強した時の標準である)X軸はこちらを、Y軸は右を、Z軸は上を向いているようにし、次に、方位角と仰角で示される方向から被写体のある原点を眺めるようにカメラを回転し、更に、カメラを右に、下に、向こう側に移動することである。これを実現する主な部分は

camera.getTransforms().addAll(
/*(1)*/	horizontalAxisRotate, // チェックボックスの真または偽により、原点を通る右向き水平軸を回転軸としてカメラを-90度、または、0度回転する、
/*(2)*/	verticalAxisRotate, // チェックボックスの真または偽により、原点を通る下向き鉛直軸を回転軸としてカメラを-90度、または、0度回転する。
/*(3)*/	azimuthRotate, // 方位角によるカメラの回転
/*(4)*/	elevationRotate, // 仰角によるカメラの回転
/*(5)*/	translate // カメラの右へ、下へ、向こうへ、の移動
        );

である。カメラに Transform(変換) として、(1)から(4)の回転と(5)の移動を設定している。このように設定することで、カメラに(1)から(5)の順番に変換(回転と移動)が適用される。この変換の一部としての回転は、カメラの原点を通る指定された回転軸による回転である。上記において、変数は次のように定義されている。

private Rotate horizontalAxisRotate = new Rotate(0, Rotate.X_AXIS);
private Rotate verticalAxisRotate = new Rotate(0, Rotate.Y_AXIS);
private double azimuthRotateAngle = 0.0;
private double elevationRotateAngle = 0.0;
private double right = 0.0;
private double down = 0.0;
private double forward = 0.0;
private Rotate azimuthRotate = new Rotate(-azimuthRotateAngle, Rotate.Y_AXIS);
private Rotate elevationRotate = new Rotate(-elevationRotateAngle, Rotate.X_AXIS);
private Translate translate = new Translate(right, down, forward);

(1)から(5)までをもう少し詳しく説明する。(1)と(2)は初期に右、下、向こう、を向いている、X軸、Y軸、Z軸を、こちら向き、右向き、上向き、に回転によって変更する部分である。これらの効果を観察可能なように、上図右上にある2個のチェックボックスを設けた。チェックされていない場合は(0度の回転、すなわち、)回転しない。チェックされている場合を説明する。

(1)
チェックボックス「Rotate(-90, X_AXIS);」がチェックされたら、

horizontalAxisRotate.setAngle(-90);

を実行する。コードは「Rotate.X_AXIS を回転軸として、カメラを-90度回転する」であるが、「右を向いた水平軸を回転軸として、カメラを-90度回転する」と解釈した方が理解しやすい。この結果、カメラはY軸の負の方から正の方を見るようになる。カメラは、何時も自動で、(subScene の中心に、)向こう側を見るように置き直されるので、X軸は右を向いたまま、Y軸が向こうを向き、Z軸が上を向く。カメラは原点にあるので、Y軸の正の方を表す緑色の球だけが見える(次の図を参照)。

カメラがY軸の正の方向を向いた時
カメラがY軸の正の方向を向いた時

(2)
チェックボックス「Rotate(-90, Y_AXIS);」がチェックされたら、

verticalAxisRotate.setAngle(-90);

を実行する。コードは「Rotate.Y_AXIS を回転軸として、カメラを-90度回転する」であるが、「下を向いた鉛直軸を回転軸として、カメラを-90度回転する」と解釈した方が理解しやすい。この結果、カメラはX軸の正の方から負の方を見るようになる。カメラは、何時も自動で、(subScene の中心に、)向こう側を見るように置き直されるので、X軸がこちらを向き、Y軸が右を向き、Z軸は上を向いたままである。カメラは原点にあり、X軸の負の方を見ているので、何も見えない(次の図を参照)。

カメラがX軸の負の方向を向いた時
カメラがX軸の負の方向を向いた時

次に、方位角と仰角の回転に移る。(下図では方位角と仰角が何かを理解しやすいように、カメラは既に、(5)で説明するこちら側に移動している。)

方位角と仰角
方位角と仰角

(3)
方位角を表すスライダーが更新され、その値を azimuthRotateAngle とする時、原点を通る下向き鉛直軸を回転軸として、カメラを -azimuthRotateAngle だけ回転させればよい。既に、変数 azimuthRotate の回転軸として下向き鉛直軸(Rotate.Y_AXIS)を設定しているので、

azimuthRotate.setAngle(-azimuthRotateAngle);

を実行すればよい。カメラは、何時も自動で、(subScene の中心に、)向こう側を見るように置き直されるので、求める結果が得られる。

(4)
仰角を表すスライダーが更新され、その値を elevationRotateAngle とする時、原点を通る右向き水平軸を回転軸として、カメラを -elevationRotateAngle だけ回転させればよい。既に、変数 elevationRotate の回転軸として右向き水平軸(Rotate.X_AXIS)を設定しているので、

elevationRotate.setAngle(-elevationRotateAngle);

を実行すればよい。カメラは、何時も自動で、(subScene の中心に、)向こう側を見るように置き直されるので、求める結果が得られる。

(5)
最後に、カメラを右へ、下へ、向こうへ移動させることに移る。(別々に処理してもよいが一緒に処理することにした。)これら3つのスライダーが更新され、それぞれの値を right、down、forward とする時、

translate.setX(right);
translate.setY(down);
translate.setZ(forward);

を実行すればよい。カメラは、何時も自動で、subScene の中心に、(向こう側を見るように)置き直されるので、求める結果が得られる。

上記は、カメラを変換対象にしたが、下記のように、被写体を入れる groupAxes とカメラを入れる groupCamera を作成し、groupCamera に同じ変換を行うことによって同一の結果を得ることが出来る。

Group groupAxes = new Group();
Group groupCamera = new Group();

makeAxes(groupAxes);

group.getChildren().add(groupAxes);
group.getChildren().add(groupCamera);
groupCamera.getChildren().add(camera);

groupCamera.getTransforms().addAll(
  horizontalAxisRotate,
  verticalAxisRotate,
  azimuthRotate,
  elevationRotate,
  translate
  );

今回はカメラ(または、カメラを入れたグループ)を回転、移動させる方法を述べたが、次回は、カメラを固定し、被写体である座標軸と立方体を入れたグループを回転、移動させることによって、同じことを実現させる。これら2つの方法を対比すると、次のことに留意することが重要になる。

カメラ(または、カメラを入れたグループ)の回転に関して:

各時点での、Rotate.X_AXIS は右向き水平軸、Rotate.Y_AXIS は下向き鉛直軸、Rotate.Z_AXIS は向こう向きの軸、を意味し、その時点でのX軸、Y軸、Z軸ではないことに注意が必要である。また、変換の都度、カメラは自動で、subScene の中心に、向こう側を見るように置き直される、ことにも注意が必要である。

次に、右側に配置された、スライダー群、等を入れた VBox を含む部分に関して概要を述べる。コードは次の通りである。

VBox controlVBox = new VBox();
makeControlVBox(controlVBox);

HBox hBox = new HBox();
hBox.setAlignment(Pos.CENTER);
hBox.getChildren().addAll(subScene, controlVBox);

Scene scene = new Scene(hBox);

スライダー群を含むcontrolVBoxを作成し(makeControlVBox(controlVBox))、subScene とこの controlVBox を入れた hBox を作成し、それを scene に入れる。現時点での、この部分に関する注意事項として、本来ならこの複雑な構造を持つ controlVBox を含む hBox を SceneBuilder で作成したいのだが、SceneBuilder で作成し、実行しても(理由は不明であるが、)正常に作動しなかった。

以下では、右へと方位角と仰角のスライダーの処理の部分のみを説明する。

controlVBox における、現在の方位角と仰角の値を表示する Text とそれらを設定する Slider を次のように定義する。(各スライダーの最小値、最大値、初期値、等は makeControlVBox(controlVBox) で設定している。)

private Text textTranslateToRight = new Text("0.0");
private Text textAzimuth = new Text("0.0");
private Text textElevation = new Text("0.0");
private Slider sliderTranslateToRight = new Slider();
private Slider sliderAzimuth = new Slider();
private Slider sliderElevation = new Slider();

スライダー群の処理を記述する、次の関数 handleSliders() を start(Stage) 関数で実行する。

private void handleSliders() {
  sliderTranslateToRight.valueProperty().addListener(new ChangeListener<Number>() {
  
    @Override
    public void changed(ObservableValue<? extends Number> ov, Number old_val, Number new_val) {
      right = (double)new_val;
      setTranslates();
    }

  });

  // 他のスライダーの処理は省略する。
  sliderAzimuth.valueProperty().addListener(new ChangeListener<Number>() {

    @Override
    public void changed(ObservableValue<? extends Number> ov, Number old_val, Number new_val) {
      if (new_val != null) {
        azimuthRotateAngle = (double)new_val;
        setAzimuthRotate();
      }
    }
  });

 sliderElevation.valueProperty().addListener((ObservableValue<? extends Number> ov, Number old_val, Number new_val) -> {
    if (new_val != null) {
      elevationRotateAngle = (double)new_val;
      setElevationRotate();
    }
  });

}

ここで、sliderAzimuth の方は旧来の匿名クラス、sliderElevation の方はラムダ式、を利用して Listener を記述している。また、set*** は

private void setTranslates() { // right、down、forwardを別々に扱わず、一緒に扱った。
  translate.setX(right);
  translate.setY(down);
  translate.setZ(forward);
  textTranslateToRight.setText(""+right);
  textTranslateToDown.setText(""+down);
  textTranslateToForward.setText(""+forward);
}

private void setAzimuthRotate( ) {
  azimuthRotate.setAngle(-azimuthRotateAngle);
  textAzimuth.setText(""+azimuthRotateAngle);
}

private void setElevationRotate() {
  elevationRotate.setAngle(-elevationRotateAngle);
  textElevation.setText(""+elevationRotateAngle);
}

である。