So now, thanks to help from my earlier question, and the very helpful answer from the user "trashgod" (more appropriately pronounced "javafxgod", in my case), I have been able to make much progress on the actual model I was trying to build with JavaFX, and have gotten much more comfortable with its function... however, despite being now able to roughly move around my model, I am still utterly puzzled by the behavior of the camera, and though the code that was helpfully provided works, I am yet unable to get modify the navigation to work the way I want.
It is really sad that after so many years of JavaFX, with what seems to me to be a really impressive foundation (the whole way they intermingle support for 3D and 2D in particular seems cool), there still really aren't basic utilities and libraries like standard orbit navigation (for example, like this, which is roughly what I am trying to achieve in terms of camera navigation, done with threejs and it's OrbitNavigation
class).
I have at least nicely organized it (nicely bundled into a Group
), I think, so that when I do finally get it properly nice and polished, others can at least benefit from that, as this is something literally everyone who does any 3D modeling in JFX needs, and really, good examples (that aren't all muddled up) don't seem to be easy to find.
I think the point I am most baffled about (despite a lot of research and a very thorough reading of the Node
documentation) is when the local coordinates vs. the parent coordinates will be used. For example, in my previous question, the provided code seems to use the parent coordinates, when I used the setTranslateX(getTranslateX() + 10)
or the like as shown, those seem to be interpereted as parent coordinates, since when I'm rotated I see myself moving along the axis marker.
However, when I try to convert those parent coordinates to local coordinates, I get behavior that I don't understand, and usually lose my model entirely. Please oh please someone explain what the hell is going on here, several different attempts and questions shown in comments:
import javafx.geometry.Point3D;
import javafx.scene.*;
import javafx.scene.input.*;
import javafx.scene.transform.Rotate;
public class NavigableCamera extends Group {
private final Camera camera;
private final Rotate xRotate = new Rotate(0, Rotate.X_AXIS);
private final Rotate yRotate = new Rotate(0, Rotate.Y_AXIS);
private final Rotate zRotate = new Rotate(0, Rotate.Z_AXIS);
private double x, y, z, angleX, angleY;
private Node pickedNode;
public NavigableCamera(Scene scene) {
camera = new PerspectiveCamera(true);
camera.setFarClip(6000);
camera.setNearClip(0.01);
getChildren().add(camera);
//okay, sure this works and the camera ends up pointing at the origin, but "level" with it.
setTranslateZ(-500);
//but why oh why doesn't this work to end with the camera looking "down" at the origin from above?
// setTranslateY(-500);
// Point3D pivot = Point3D.ZERO;
// Rotate pointDown = new Rotate(90, pivot.getX(), pivot.getY(), pivot.getZ(), Rotate.X_AXIS);
// getTransforms().add(pointDown);
getTransforms().addAll(xRotate, yRotate, zRotate);
initKeyboardControl(scene);
initMouseControl(scene);
initTouchControls(scene);
scene.setCamera(camera);
}
private void initMouseControl(Scene scene) {
scene.setOnMousePressed(event -> {
x = event.getSceneX();
y = event.getSceneY();
angleX = xRotate.getAngle();
angleY = yRotate.getAngle();
PickResult pickResult = event.getPickResult();
pickedNode = pickResult.getIntersectedNode();
double x, y, z;
if(pickedNode != null) {
x = parentToLocal(pickedNode.getBoundsInParent()).getCenterX();
y = parentToLocal(pickedNode.getBoundsInParent()).getCenterY();
z = parentToLocal(pickedNode.getBoundsInParent()).getCenterZ();
} else {
Point3D pivotPoint = pickResult.getIntersectedPoint();
x = pivotPoint.getX();
y = pivotPoint.getY();
z = pivotPoint.getZ();
}
xRotate.setPivotX(x);
xRotate.setPivotY(y);
xRotate.setPivotZ(z);
yRotate.setPivotX(x);
yRotate.setPivotY(y);
yRotate.setPivotZ(z);
});
scene.setOnMouseDragged(event -> {
xRotate.setAngle(angleX - (x - event.getSceneY()));
yRotate.setAngle(angleY + x - event.getSceneX());
});
scene.setOnMouseDragReleased(event -> {
xRotate.setPivotX(0);
xRotate.setPivotY(0);
xRotate.setPivotZ(0);
});
scene.setOnScroll(event -> {
xRotate.setAngle(xRotate.getAngle() + event.getDeltaY() / 10);
yRotate.setAngle(yRotate.getAngle() - event.getDeltaX() / 10);
setTranslateX(getTranslateX() + event.getDeltaX());
setTranslateY(getTranslateY() + event.getDeltaY());
});
}
private void initKeyboardControl(Scene scene) {
scene.setOnKeyPressed(event -> {
KeyCode code = event.getCode();
switch (code) {
case SPACE:
moveLocalZ(+10);
break;
case BACK_SPACE:
moveLocalZ(-10);
break;
case RIGHT:
// this commented out setTranslate, as well as all the others seem to operate on a "PARENT" coordinate system, but when I try to get local coordinates instead, it doesn't work!
// setTranslateX(getTranslateX() + 20);
moveLocalX(+10);
break;
case LEFT:
// setTranslateX(getTranslateX() - 20);
moveLocalX(-10);
break;
case UP:
moveLocalY(-10);
break;
case DOWN:
moveLocalY(+10);
break;
lateZ() - 30);
break;
}
});
}
private void moveLocalX(double amount) {
Point3D p = localToParent(getTranslateX() + amount, getTranslateY(), getTranslateZ());
moveToParentPoint(p);
}
private void moveLocalY(double amount) {
Point3D p = localToParent(getTranslateX(), getTranslateY() + amount, getTranslateZ());
moveToParentPoint(p);
}
private void moveLocalZ(double amount) {
Point3D p = localToParent(getTranslateX(), getTranslateY(), getTranslateZ() + amount);
moveToParentPoint(p);
}
private void moveToParentPoint(Point3D p) {
setTranslateX(p.getX());
setTranslateY(p.getY());
setTranslateZ(p.getZ());
}
private void initTouchControls(Scene scene) {
// on an entirely side note, I really don't get why this can't just be here, but it can be a member of the class?
// double z;
scene.setOnZoomStarted(event -> {
z = event.getZ();
});
// frankly I can't even visualize what a "scale" means in the context of a "camera"
// what I want for "zoom" is to move the camera "forwards" or "backwards" as in towards the
// direction it's facing and away from it, but when I have tried this, I end up moving just
// along the Z axis, wherever I'm facing!
scene.setOnZoom(event -> {
if (z > 0) {
setScaleZ(getScaleZ() + event.getZoomFactor());
} else {
setScaleZ(getScaleZ() - event.getZoomFactor());
}
});
}
}
I'm rather happy with this cute little AxesMarker
class which at first I was trying to do (based on some online example) out of 3D blocks, but this 2D approach is much cleaner, and a good example of the power of JavaFX:
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.transform.Rotate;
public class AxesMarker extends Group {
Line x, y, z;
public AxesMarker(double length) {
x = new Line(0, 0, length, 0);
y = new Line(0, 0, 0, length);
z = new Line(0, 0, length, 0);
z.getTransforms().add(new Rotate(90, 0, 0, 0, Rotate.Y_AXIS));
x.setStroke(Color.RED);
y.setStroke(Color.GREEN);
z.setStroke(Color.BLUE);
getChildren().addAll(x, y, z);
}
}
And here just enough Application
to demonstrate the usage of the NavigableCamera
:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class CameraTest extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Group root = new Group();
root.getChildren().add(new AxesMarker(200));
Scene scene = new Scene(root, 800, 800, Color.BLACK);
NavigableCamera camera = new NavigableCamera(scene);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch();
}
}