3

I am trying to create a first person camera in JavaFX based on bindings. The camera and the actual position both work perfectly. The only problem is that they don’t match! As you can see in the picture, the actual position (red box) is in the middle of the circle, but the camera is outside. How can I change that? What did I do wrong? enter image description here

The Player class handles the PerspectiveCamera.

package game;

import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.PerspectiveCamera;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.robot.Robot;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;

public class Player extends Character {

    private static final Robot ROBOT = new Robot();
    private DoubleProperty relativeCenterX = new SimpleDoubleProperty();
    private DoubleProperty relativeCenterY = new SimpleDoubleProperty();

    protected PerspectiveCamera camera = new PerspectiveCamera();
    protected Rotate xAxis = new Rotate(0, 250, 0, 0, Rotate.Y_AXIS);
    protected Rotate yAxis = new Rotate(0, 0, 250, 0, Rotate.X_AXIS);
    protected Translate translate = new Translate();

    protected DoubleProperty centerX = new SimpleDoubleProperty();
    protected DoubleProperty centerY = new SimpleDoubleProperty();

    @SuppressWarnings("exports")
    public Player(Stage stage) {
        camera.getTransforms().addAll(xAxis, yAxis);

        centerX.bind(stage.widthProperty().divide(2));
        centerY.bind(stage.heightProperty().divide(2));

        relativeCenterX.bind(stage.xProperty().add(centerX));
        relativeCenterY.bind(stage.yProperty().add(centerY));

        camera.translateXProperty().bind(posX.subtract(centerX));
        camera.translateYProperty().bind(posZ);
        camera.translateZProperty().bind(posY.subtract(centerY));

        xAxis.angleProperty().bind(viewX.subtract(90));
        yAxis.angleProperty().bind(viewY);

        translate.xProperty().bind(posX);
        translate.zProperty().bind(posY);
        translate.yProperty().bind(posZ);
    }

    @SuppressWarnings("exports")
    public EventHandler<KeyEvent> getKeyHandle() {
        return e -> {
            switch (e.getCode()) {
            case A:
                view(-1, 0);
                break;
            case D:
                view(1, 0);
                break;
            case W:
                move(1, 1, 0);
                break;
            case S:
                move(-1, -1, 0);
                break;
            case SPACE:
                move(0, 0, 10);
                break;
            case F:
                move(0, 0, -10);
                break;
            default:
                break;
            }
        };
    }

    @SuppressWarnings("exports")
    public EventHandler<MouseEvent> getMouseHandle() {
        return e -> {
            view(e.getSceneX() - centerX.doubleValue(), centerY.doubleValue() - e.getSceneY());
            Platform.runLater(() -> {
                ROBOT.mouseMove(relativeCenterX.intValue(), relativeCenterY.intValue());
            });
        };
    }

    @SuppressWarnings("exports")
    public PerspectiveCamera getPespectiveCamera() {
        return camera;
    }
}

The Character class calculates position and view.

package game;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public abstract class Character {

    protected DoubleProperty posX = new SimpleDoubleProperty();
    protected DoubleProperty posY = new SimpleDoubleProperty();
    protected DoubleProperty posZ = new SimpleDoubleProperty();

    protected DoubleProperty viewX = new SimpleDoubleProperty();
    protected DoubleProperty viewY = new SimpleDoubleProperty();

    protected DoubleProperty speed = new SimpleDoubleProperty(10);

    public void move(double x, double y, double z) {

        double fX = Math.cos(Math.toRadians(viewX.get()));
        double fY = -Math.sin(Math.toRadians(viewX.get()));
        double fZ = 1;

        posX.set(posX.get() + fX * x * speed.get());
        posY.set(posY.get() + fY * y * speed.get());
        posZ.set(posZ.get() + fZ * z);
    }

    public void view(double x, double y) {
        viewX.set(viewX.get() + x);
        viewY.set(viewY.get() + y);
    }

    @SuppressWarnings("exports")
    public DoubleProperty posXPorperty() {
        return posX;
    }

    @SuppressWarnings("exports")
    public DoubleProperty posYPorperty() {
        return posY;
    }

    @SuppressWarnings("exports")
    public DoubleProperty posZPorperty() {
        return posZ;
    }

    @SuppressWarnings("exports")
    public DoubleProperty viewXPorperty() {
        return viewX;
    }

    @SuppressWarnings("exports")
    public DoubleProperty viewYPorperty() {
        return viewY;
    }
}

My Application, which shows the total graphical content.

package graphics;

import game.Player;
import javafx.application.Application;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class GameStage extends Application implements Runnable {

    @Override
    public void run() {
        launch();
    }

    @SuppressWarnings("exports")
    @Override
    public void start(Stage stage) throws Exception {

        BorderPane pane = new BorderPane();
        Scene scene = new Scene(pane, 500, 500);

        Group content = new Group(), map = new Group();
        ContentScene subscene = new ContentScene(content, map, 500, 500);
        subscene.widthProperty().bind(scene.widthProperty());
        subscene.heightProperty().bind(scene.heightProperty());
        pane.getChildren().add(subscene);
        pane.setBottom(map);

        Player player = new Player(stage);
        Box box = new Box(50, 50, 50);
        box.translateXProperty().bind(player.posXPorperty());
        box.translateYProperty().bind(player.posZPorperty());
        box.translateZProperty().bind(player.posYPorperty());
        box.rotateProperty().bind(player.viewXPorperty());
        box.setMaterial(new PhongMaterial(Color.RED));
        content.getChildren().add(box);

        Rectangle rectangle = new Rectangle(5, 5);
        rectangle.translateXProperty().bind(player.posXPorperty().divide(10));
        rectangle.translateYProperty().bind(player.posYPorperty().divide(10));
        rectangle.setFill(Color.RED);
        map.getChildren().add(rectangle);

        subscene.setCamera(player.getPespectiveCamera());
        scene.addEventHandler(KeyEvent.KEY_PRESSED, player.getKeyHandle());
        scene.addEventHandler(MouseEvent.MOUSE_MOVED, player.getMouseHandle());
        scene.setFill(Color.BLACK);

        Cursor cursor = Cursor.CROSSHAIR;
        scene.setCursor(cursor);

        stage.addEventHandler(KeyEvent.KEY_RELEASED, e -> {
            if (e.getCode() != KeyCode.F11) {
                return;
            }
            if (stage.isFullScreen()) {
                stage.setFullScreen(false);
            } else {
                stage.setFullScreen(true);
            }
        });

        stage.setAlwaysOnTop(true);
        stage.setScene(scene);
        stage.show();
    }

    private class ContentScene extends SubScene {

        public ContentScene(Group content, Group map, double width, double height) {
            super(content, width, height, true, SceneAntialiasing.BALANCED);

            PhongMaterial material = new PhongMaterial(Color.AQUA);

            for (int v = 0; v < 3_600; v += 180) {
                for (int y = 0; y < 500; y += 100) {
                    Box box = new Box(50, 50, 50);
                    box.setTranslateX(Math.sin(v / 10) * 1_000);
                    box.setTranslateY(y);
                    box.setTranslateZ(Math.cos(v / 10) * 1_000);
                    box.setMaterial(material);
                    content.getChildren().add(box);

                    Rectangle rectangle = new Rectangle(5, 5);
                    rectangle.translateXProperty().bind(box.translateXProperty().divide(10));
                    rectangle.translateYProperty().bind(box.translateZProperty().divide(10));
                    rectangle.setFill(Color.AQUA);
                    map.getChildren().add(rectangle);
                }
            }
        }
    }
}

Darth Bane
  • 83
  • 7
  • Did you bind y to z and z to y for the red box' translation on purpose? – Thomas Jan 17 '22 at 10:29
  • Yes. This is because I use the Z-axis as height and JavaFX uses the Z-axis as depth. To undo this, I twisted y and z. – Darth Bane Jan 17 '22 at 10:34
  • 1
    Ok, understood. Just note that this basically changes the coordinate system from right-hand (z pointing towards the camera) to left-hand (z-pointing away from the camera). To avoid confusion you should do that transformation only in once place if at all. Besides that, note the following portion of the JavaDoc on `PerspeciveCamera` which might be related to your problem: "We recommend setting fixedEyeAtCameraZero to true if you are going to transform (move) the camera. Transforming the camera when fixedEyeAtCameraZero is set to false may lead to results that are not intuitive" – Thomas Jan 17 '22 at 10:38
  • Another thing that strikes me odd: `camera.translateXProperty().bind(posX.subtract(centerX));` - shouldn't the camera's position be the same as the player's position and not be offset by the stage center? – Thomas Jan 17 '22 at 10:50
  • fixedEyeAtCameraZero zu setzen hat erstaunlich viel bewirkt. Vielen Dank! – Darth Bane Jan 18 '22 at 08:32
  • Great to read that :) - However, please remember that SO is an English speaking site and thus for the sake of other readers comments etc. should be kept in that language. – Thomas Jan 18 '22 at 09:04

1 Answers1

3

Thanks to Thomas, I was able to solve the problem. The code now loks like this:

enter image description here

package graphics;

import game.Player;
import javafx.application.Application;
import javafx.scene.AmbientLight;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class GameStage extends Application implements Runnable {

    @Override
    public void run() {
        launch();
    }

    @SuppressWarnings("exports")
    @Override
    public void start(Stage stage) throws Exception {

        // Parents
        BorderPane pane = new BorderPane();
        Group content = new Group(new AmbientLight()), map = new Group();
        Pane stackpane = new Pane(map);

        // Scenes
        Scene scene = new Scene(pane, 500, 500);
        SubScene contentSubscene = new SubScene(content, 500, 500, true, SceneAntialiasing.BALANCED);
        contentSubscene.widthProperty().bind(scene.widthProperty());
        contentSubscene.heightProperty().bind(scene.heightProperty());
        SubScene minimapSubscene = new SubScene(stackpane, 256, 256);
        minimapSubscene.setFill(Color.DARKGREY);

        pane.getChildren().add(contentSubscene);
        pane.setBottom(minimapSubscene);

        // Create Player
        Player player = new Player(stage);
        Rectangle currentPosition = new Rectangle(5, 5);
        currentPosition.layoutXProperty().bind(minimapSubscene.widthProperty().divide(2));
        currentPosition.layoutYProperty().bind(minimapSubscene.heightProperty().divide(2));
        currentPosition.setFill(Color.RED);
        stackpane.getChildren().add(currentPosition);

        map.layoutXProperty().bind(player.posXPorperty().divide(-10).add(minimapSubscene.widthProperty().divide(2)));
        map.layoutYProperty().bind(player.posYPorperty().divide(-10).add(minimapSubscene.heightProperty().divide(2)));

        // Create Box in
        PhongMaterial material = new PhongMaterial(Color.AQUA);
        for (int v = 0; v < 3_600; v += 180) {
            for (int y = 0; y < 500; y += 100) {
                Box box = new Box(50, 50, 50);
                box.setTranslateX(Math.sin(v / 10) * 1_000);
                box.setTranslateY(y);
                box.setTranslateZ(Math.cos(v / 10) * 1_000);
                box.setMaterial(material);
                content.getChildren().add(box);

                Rectangle boxPosition = new Rectangle(5, 5);
                boxPosition.translateXProperty().bind(box.translateXProperty().divide(10));
                boxPosition.translateYProperty().bind(box.translateZProperty().divide(10));
                boxPosition.setFill(Color.AQUA);
                map.getChildren().add(boxPosition);
            }
        }

        contentSubscene.setCamera(player.getPespectiveCamera());
        scene.addEventHandler(KeyEvent.KEY_PRESSED, player.getKeyHandle());
        scene.addEventHandler(MouseEvent.MOUSE_MOVED, player.getMouseHandle());
        scene.setCursor(Cursor.CROSSHAIR);
        scene.setFill(Color.WHITE);

        stage.addEventHandler(KeyEvent.KEY_RELEASED, e -> {
            if (e.getCode() != KeyCode.F11) {
                return;
            }
            if (stage.isFullScreen()) {
                stage.setFullScreen(false);
            } else {
                stage.setFullScreen(true);
            }
        });
        stage.setAlwaysOnTop(true);
        stage.setScene(scene);
        stage.show();

    }
}
package game;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public abstract class Character {

    protected DoubleProperty posX = new SimpleDoubleProperty();
    protected DoubleProperty posY = new SimpleDoubleProperty();
    protected DoubleProperty posZ = new SimpleDoubleProperty();

    protected DoubleProperty viewX = new SimpleDoubleProperty();
    protected DoubleProperty viewY = new SimpleDoubleProperty();

    protected DoubleProperty speed = new SimpleDoubleProperty(10);

    public void move(double x, double y, double z) {

        double fX = Math.cos(Math.toRadians(viewX.get()));
        double fY = -Math.sin(Math.toRadians(viewX.get()));
        double fZ = 1;

        posX.set(posX.get() + fX * x * speed.get());
        posY.set(posY.get() + fY * y * speed.get());
        posZ.set(posZ.get() + fZ * z);
    }

    public void view(double x, double y) {
        viewX.set(viewX.get() + x);
        viewY.set(viewY.get() + y);
    }

    @SuppressWarnings("exports")
    public DoubleProperty posXPorperty() {
        return posX;
    }

    @SuppressWarnings("exports")
    public DoubleProperty posYPorperty() {
        return posY;
    }

    @SuppressWarnings("exports")
    public DoubleProperty posZPorperty() {
        return posZ;
    }

    @SuppressWarnings("exports")
    public DoubleProperty viewXPorperty() {
        return viewX;
    }

    @SuppressWarnings("exports")
    public DoubleProperty viewYPorperty() {
        return viewY;
    }
}
package game;

import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.PerspectiveCamera;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.robot.Robot;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;

public class Player extends Character {

    private static final Robot ROBOT = new Robot();
    private DoubleProperty relativeCenterX = new SimpleDoubleProperty();
    private DoubleProperty relativeCenterY = new SimpleDoubleProperty();

    protected PerspectiveCamera camera = new PerspectiveCamera(true);
    protected Rotate xAxis = new Rotate(0, 250, 0, 0, Rotate.Y_AXIS);
    protected Rotate yAxis = new Rotate(0, 0, 250, 0, Rotate.X_AXIS);

    protected DoubleProperty centerX = new SimpleDoubleProperty();
    protected DoubleProperty centerY = new SimpleDoubleProperty();

    @SuppressWarnings("exports")
    public Player(Stage stage) {
        camera.getTransforms().addAll(xAxis, yAxis);
        camera.setFieldOfView((40 + 62) / 2);
        camera.setNearClip(0.1);
        camera.setFarClip(100000);
        camera.setVerticalFieldOfView(true);

        centerX.bind(stage.widthProperty().divide(2));
        centerY.bind(stage.heightProperty().divide(2));

        relativeCenterX.bind(stage.xProperty().add(centerX));
        relativeCenterY.bind(stage.yProperty().add(centerY));

        xAxis.angleProperty().bind(viewX.subtract(90));
        yAxis.angleProperty().bind(viewY);

        camera.translateXProperty().bind(posX);
        camera.translateZProperty().bind(posY);
        camera.translateYProperty().bind(posZ);
    }

    @SuppressWarnings("exports")
    public EventHandler<KeyEvent> getKeyHandle() {
        return e -> {
            switch (e.getCode()) {
            case A:
                view(-1, 0);
                break;
            case D:
                view(1, 0);
                break;
            case W:
                move(1, 1, 0);
                break;
            case S:
                move(-1, -1, 0);
                break;
            case SPACE:
                move(0, 0, 10);
                break;
            case F:
                move(0, 0, -10);
                break;
            default:
                break;
            }
        };
    }

    @SuppressWarnings("exports")
    public EventHandler<MouseEvent> getMouseHandle() {
        return e -> {
            view(e.getSceneX() - centerX.doubleValue(), centerY.doubleValue() - e.getSceneY());
            Platform.runLater(() -> {
                ROBOT.mouseMove(relativeCenterX.intValue(), relativeCenterY.intValue());
            });
        };
    }

    @SuppressWarnings("exports")
    public PerspectiveCamera getPespectiveCamera() {
        return camera;
    }
}
Darth Bane
  • 83
  • 7