-1

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();
    }
}
  • And now, I'm reading this other answer (https://stackoverflow.com/questions/23737119/javafx-2d-shapes-in-3d-space) which seems discouraging like I'm not getting it at all. I mean... I thought the whole primary basis of JavaFX is that it builds the 3D thing onto the 2D classes... isn't that why there's no useful 2D primitives in the 3D package? You need to have (2D) lines and circles and the like in 3D space all the time, and aren't the 3D components just built up from 2D objects rotated around? That's certainly how it seems with most of the early instructional info on JFX... – Joshua Chambers Sep 25 '21 at 19:39
  • 1
    You may want to look at `SubScene`, seen [here](https://stackoverflow.com/a/69156091/230513), to overlay or add nearby controls. See also [_Picking_](https://docs.oracle.com/javase/8/javafx/graphics-tutorial/picking.htm#CJAHFAHF), seen [here](https://stackoverflow.com/search?tab=votes&q=%5bjavafx%5d%20PickResult), but I haven't used it; I defer to the [top users](https://stackoverflow.com/tags/javafx-3d/topusers) on [tag:javafx-3d]. IIUC, [tag:javafx] uses OPenGL on Mac, as discussed [here](https://docs.oracle.com/javase/8/javafx/get-started-tutorial/jfx-architecture.htm#A1106308). – trashgod Sep 25 '21 at 20:37
  • "aren't the 3D components just built up from 2D objects rotated around? That's certainly how it seems with most of the early instructional info on JFX" -> well yes, for early JavaFX applications which didn't have [Shape3D](https://openjfx.io/javadoc/17/javafx.graphics/javafx/scene/shape/Shape3D.html) nodes. But now, for most 3D tasks, you would probably want to use Shape3D nodes, which were added to the platform some time back. For example: "You need to have (2D) lines and circles and the like in 3D space all the time" -> you can use Cylinders. or a texture on a MeshView. – jewelsea Sep 25 '21 at 23:57
  • 1
    I'm not quite sure what your actual concrete question is here? What would you expect to be in an answer? "I am yet unable to get modify the navigation to work the way I want" -> what is it that you want? (I mean specifically, with regards to the threejs sample you linked there is a lot going on there, so that would seem far too general for somebody to answer here). – jewelsea Sep 26 '21 at 00:07
  • Have you studied the Oracle [JavaFX 3D tutorials](https://docs.oracle.com/javase/8/javafx/graphics-tutorial/javafx-3d-graphics.htm) in depth? Have you followed along with their tutorial [for building a sample 3D app](https://docs.oracle.com/javase/8/javafx/graphics-tutorial/sampleapp3d.htm#CJAHFAF) , building step-by-step? Have you studied the [transformation tutorials](https://docs.oracle.com/javase/8/javafx/visual-effects-tutorial/transforms.htm#JFXTE139)? – jewelsea Sep 26 '21 at 00:26
  • 1
    Well, one very specific question that has been driving me crazy for days which I ask in code comments near the top is about why I can't manage to get the camera to look "down" at the model from "above". In the commented out code you can see I `translateY` ("up"), then set my pivot point to origin (this should be local, so at my personal center, right?) then `rotate 90 degrees` on the X axis, which should be like spinning forward so I'm facing "down" back at my model, no? – Joshua Chambers Sep 26 '21 at 00:28
  • But I guess the core of my question can be summed up (and I will edit to reflect): "why, when I do a simple `translateZ` (or X or Y), do I get "parent" movement, but when I rotate, I get "local" movement? Is that correct? I thought both would act in "local", but then when I try and translate local to parent, it doesn't work! – Joshua Chambers Sep 26 '21 at 00:45
  • Yes, I have studied that old Oracle tutorial very much. I'm very confused by that `Xform` class, why is it so very complicated, and why is it needed? – Joshua Chambers Sep 26 '21 at 00:53
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/237499/discussion-between-joshua-chambers-and-jewelsea). – Joshua Chambers Sep 26 '21 at 01:04
  • I had a look through your code. I think there are likely quite a few mistakes and misconceptions there. Posting it on StackOverflow and expecting useful help fixing it is I believe, unrealistic. I really think you are trying to do too much too quickly. I don’t think you should be trying to create such a sophisticated class at this time. – jewelsea Sep 26 '21 at 05:49
  • As a suggestion, and I don’t know if this is feasible or even reasonable for your app dev purposes, just use the camera implementation from the Oracle tutorial, unmodified, with no object picking. That will give you basic navigation around a 3D world with panning, rotation, translation and scaling, which is an awful lot really. Then focus on building your 3D world or model collection or whatever else you want your app to accomplish. If you make good progress on other things and your understanding of 3D graphics improves, you can return and implement your custom camera control functions. – jewelsea Sep 26 '21 at 05:56
  • 1
    Below, I've shown one way _to get the camera to look "down" at the model from "above"._ – trashgod Sep 26 '21 at 23:22

1 Answers1

1

To get oriented, start from this example. Note that the x, y and z axes are red, green and blue, respectively; as shown here, positive x points to the right, positive y points down, and positive z points into the screen. The camera has been translated to -2000 along the z axis, so one sees the blue z axis on end.

Z-2000.X-0

Now, rotate the camera around the x axis -90° and translate it along the y axis to -2000, so one is looking straight down on the green y axis.

private final Rotate xRotate = new Rotate(-90, Rotate.X_AXIS);
camera.setTranslateY(-2000);

Y-2000.X-90

Finally, adjust the keyboard and mouse handlers accordingly. For example, use the arrow keys to move the axis group along the y axis.

case UP:
    g.setTranslateY(g.getTranslateY() - 100);
    break;
case DOWN:
    g.setTranslateY(g.getTranslateY() + 100);
    break;
case HOME:
    g.xRotate.setAngle(-90);

See also Rotating perspective camera around an object in javaFX, which illustrates rotating the camera around a pivot; unlike this, it both sets the camera on the scene and adds the camera to a group with which the camera rotates.

As your goal is to model an orrery, consider a PathTransition along a suitable Shape as illustrated here; as long as you construct the ecliptic to coincide with the xy plane, you can rotate the group as usual.

As tested:

import javafx.application.Application;
import javafx.scene.Camera;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;

/**
 * @see https://stackoverflow.com/a/69339586/230513
 * @see https://stackoverflow.com/a/69260181/230513
 */
public class RotateCameraExample extends Application {

    private static class RotateCamera extends Group {

        private final Camera camera;
        private final Rotate xRotate = new Rotate(-90, Rotate.X_AXIS);
        private final Rotate yRotate = new Rotate(0, Rotate.Y_AXIS);
        private final Rotate zRotate = new Rotate(0, Rotate.Z_AXIS);

        public RotateCamera() {
            buildAxes();
            camera = new PerspectiveCamera(true);
            camera.setFarClip(6000);
            camera.setNearClip(0.01);
            camera.setTranslateY(-2000);
            camera.getTransforms().addAll(xRotate, yRotate, zRotate);
        }

        private void buildAxes() {
            final Box xAxis = new Box(1200, 10, 10);
            final Box yAxis = new Box(10, 1200, 10);
            final Box zAxis = new Box(10, 10, 1200);

            xAxis.setMaterial(new PhongMaterial(Color.RED));
            yAxis.setMaterial(new PhongMaterial(Color.GREEN));
            zAxis.setMaterial(new PhongMaterial(Color.BLUE));

            Group axisGroup = new Group();
            axisGroup.getChildren().addAll(xAxis, yAxis, zAxis);
            this.getChildren().add(axisGroup);
        }
    }

    @Override
    public void start(Stage stage) {
        RotateCamera g = new RotateCamera();
        Scene scene = new Scene(g, 800, 800, Color.BLACK);
        scene.setCamera(g.camera);
        stage.setScene(scene);
        stage.show();
        scene.setOnScroll((final ScrollEvent e) -> {
            g.xRotate.setAngle(g.xRotate.getAngle() + e.getDeltaY() / 10);
            g.yRotate.setAngle(g.yRotate.getAngle() - e.getDeltaX() / 10);
            g.setTranslateX(g.getTranslateX() + e.getDeltaX());
            g.setTranslateY(g.getTranslateY() + e.getDeltaY());
        });
        scene.setOnKeyPressed((KeyEvent e) -> {
            System.out.println(g.getTranslateY());
            KeyCode code = e.getCode();
            switch (code) {
                case LEFT:
                    g.zRotate.setAngle(g.zRotate.getAngle() + 10);
                    break;
                case RIGHT:
                    g.zRotate.setAngle(g.zRotate.getAngle() - 10);
                    break;
                case UP:
                    g.setTranslateY(g.getTranslateY() - 100);
                    break;
                case DOWN:
                    g.setTranslateY(g.getTranslateY() + 100);
                    break;
                case HOME:
                    g.xRotate.setAngle(-90);
                    g.yRotate.setAngle(0);
                    g.zRotate.setAngle(0);
                    g.setTranslateX(0);
                    g.setTranslateY(0);
                    g.setTranslateZ(0);
                    break;
                default:
                    break;
            }
        });
    }

    public static void main(String[] args) {
        launch();
    }
}

trashgod
  • 203,806
  • 29
  • 246
  • 1,045