Computing the Viewport of a Perspective Camera JavaFX

how would you calculate the x and y bounds of the viewport of a perspective camera in JavaFX? The method 'camera.getBoundsInParent' can be used to determine the camera's position, however, how can you use the position to calculate the size of the camera's viewport?

Upvotes: 1

Views: 1627

Answers (1)

James_D
James_D

Reputation: 209319

According to the documentation for PerspectiveCamera:

By default, this camera is located at center of the scene and looks along the positive z-axis. The coordinate system defined by this camera has its origin in the upper left corner of the panel with the Y-axis pointing down and the Z axis pointing away from the viewer (into the screen).

The fieldOfView property defines the angle subtended by the visible part of the scene to the camera, and by default this angle is measured vertically.

Finally, by default,

the Z value of the eye position is adjusted in Z such that the projection matrix generated using the specified fieldOfView will produce units at Z = 0 (the projection plane), in device-independent pixels, matches that of the ParallelCamera

In other words, the visible part of the scene at Z=0 is fixed at the size of the scene in pixel coordinates.

So, assuming these defaults, we can draw the following diagram:

enter image description here

Here, the point on the left represents the default camera position. The rectangle is the visible portion of the screen at z=0. w is the width of the scene, and h is the height of the scene (so the camera is at (w/2, h/2, -z_c) for some z_c > 0).

The line from the camera intersecting the top center of the visible part of the scene at z=0 is extended in the diagram to some point at the top center of the visible part of the scene at an arbitrary z value. It is easy to see that the top triangle is a right triangle, with height -y and length z, and is similar to the right triangle formed from the camera to the center of the scene at z=0. Because of the similar triangles, it has angle f/2, where f is the angle of the field of view. Consequently, for a point (w/2, y, z) at the top of the visible part of the scene, we must have

-y/z = tan(f/2)

or

y = -z tan(f/2) 

You can build another triangle at the bottom of the picture, and deduce that the the y coordinate satisfies

y = h + z tan(f/2)

For the width, just note the the proportions of the visible part of the scene are always in the ratio w:h, so for a point (x, h/2, z) at the center of the left edge of the visible part of the scene, we have

x = -z (w/h) tan(f/2)

and at the right of the visible part of the scene

x = w + z (w/h) tan(f/2)

So, in summary, the bounds of the scene extend from

(-z (w/h) tan(f/2), -z tan(f/2))

in the top left, to

(w + z (w/h) tan(f/2), h + z tan(f/2))

in the bottom right, where z is the z-coordinate, w the width of the scene, h the height of the scene, and f the (vertical) angle of the field of view.

Here's a demo of this. This has four spheres positioned at the top center, bottom center, left center, and right center of the visible part of the scene. The z-coordinates of all four spheres are animated (so they move away from the viewer in the z-direction, appearing to get smaller), and their x- or y-coordinates are bound so that they remain at the extreme of the visible part of the scene. (So, for example, the red sphere is animated along the hypotenuse of the right triangle in the top of the diagram above, and the other spheres are animated similarly.)

import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Sphere;
import javafx.stage.Stage;
import javafx.util.Duration;

public class PerspectiveCameraTest extends Application {

    @Override
    public void start(Stage primaryStage) {
        Pane pane = new Pane();
        Scene scene = new Scene(pane, 800, 800, true);
        PerspectiveCamera camera = new PerspectiveCamera();
        camera.boundsInParentProperty().addListener((obs, oldB, newB) -> System.out.println(newB));

        scene.setCamera(camera);
        primaryStage.setScene(scene);
        primaryStage.show();

        Sphere top = new Sphere(40);
        top.setMaterial(new PhongMaterial(Color.RED));
        top.translateXProperty().bind(pane.widthProperty().divide(2));
        top.translateYProperty().bind(Bindings.createDoubleBinding(() ->  {
            double tanFOver2 = Math.tan(Math.toRadians(camera.getFieldOfView()/2));
            return -top.getTranslateZ() * tanFOver2 ;
        }, top.translateZProperty(), pane.heightProperty(), camera.fieldOfViewProperty()));

        Sphere bottom = new Sphere(40);
        bottom.setMaterial(new PhongMaterial(Color.BLUE));
        bottom.translateXProperty().bind(pane.widthProperty().divide(2));
        bottom.translateYProperty().bind(Bindings.createDoubleBinding(() -> {
            double tanFOver2 = Math.tan(Math.toRadians(camera.getFieldOfView()/2));
            return scene.getHeight() + bottom.getTranslateZ() * tanFOver2 ;
        }, bottom.translateZProperty(), pane.heightProperty(), camera.fieldOfViewProperty()));

        bottom.translateZProperty().bind(top.translateZProperty());

        Sphere left = new Sphere(40);
        left.setMaterial(new PhongMaterial(Color.GREEN));
        left.translateYProperty().bind(pane.heightProperty().divide(2));
        left.translateXProperty().bind(Bindings.createDoubleBinding(() -> {
            double tanFOver2 = Math.tan(Math.toRadians(camera.getFieldOfView()/2));
            return -left.getTranslateZ() * tanFOver2 * pane.getWidth() / pane.getHeight();
        }, left.translateZProperty(), pane.heightProperty(), pane.widthProperty(), camera.fieldOfViewProperty()));

        left.translateZProperty().bind(top.translateZProperty());

        Sphere right = new Sphere(40);
        right.setMaterial(new PhongMaterial(Color.GOLD));
        right.translateYProperty().bind(pane.heightProperty().divide(2));
        right.translateXProperty().bind(Bindings.createDoubleBinding(() -> {
            double tanFOver2 = Math.tan(Math.toRadians(camera.getFieldOfView()/2));
            return pane.getWidth() + right.getTranslateZ() * tanFOver2 * pane.getWidth() / pane.getHeight() ;
        }, right.translateZProperty(), pane.heightProperty(), pane.widthProperty(), camera.fieldOfViewProperty()));

        right.translateZProperty().bind(top.translateZProperty());


        TranslateTransition anim = new TranslateTransition(Duration.seconds(10), top);
        anim.setByZ(5000);
        anim.play();
        pane.getChildren().addAll(top, bottom, left, right);
    }

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

enter image description here

If you add the camera to the scene and move (and possibly rotate) it, then the geometry gets considerably more complex, although the basic picture will still be the same (just not conveniently lined up with the axes). Similar computations for the more general case are beyond the scope of a forum post (and left as an exercise for the reader...).

Upvotes: 4

Related Questions