javafx.scene.PerspectiveCamera Maven / Gradle / Ivy
/*
* Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene;
import com.sun.javafx.geom.PickRay;
import com.sun.javafx.geom.Vec3d;
import com.sun.javafx.geom.transform.Affine3D;
import com.sun.javafx.geom.transform.GeneralTransform3D;
import com.sun.javafx.scene.DirtyBits;
import com.sun.javafx.scene.NodeHelper;
import com.sun.javafx.scene.PerspectiveCameraHelper;
import com.sun.javafx.sg.prism.NGNode;
import com.sun.javafx.sg.prism.NGPerspectiveCamera;
import javafx.application.ConditionalFeature;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import com.sun.javafx.logging.PlatformLogger;
/**
* Specifies a perspective camera for rendering a scene.
*
* This camera defines a viewing volume for a perspective projection;
* a truncated right pyramid.
* The {@code fieldOfView} value can be used to change viewing volume.
* 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).
* If a {@code PerspectiveCamera} node is added to the scene graph,
* the transformed position and orientation of the camera will define the
* position of the camera and the direction that the camera is looking.
*
*
In the default camera, where fixedEyeAtCameraZero is false, the Z value
* of the eye position is adjusted in Z such that the projection matrix generated
* using the specified {@code fieldOfView} will produce units at
* Z = 0 (the projection plane), in device-independent pixels, matches that of
* the ParallelCamera.
* When the Scene is resized,
* the objects in the scene at the projection plane (Z = 0) will stay the same size,
* but more or less content of the scene is viewable.
*
*
If fixedEyeAtCameraZero is true, the eye position is fixed at (0, 0, 0)
* in the local coordinates of the camera. The projection matrix is generated
* using the specified {@code fieldOfView} and the projection volume is mapped
* onto the viewport (window) such that it is stretched over more or fewer
* device-independent pixels at the projection plane.
* When the Scene is resized,
* the objects in the scene will shrink or grow proportionally,
* but the visible portion of the content is unchanged.
*
*
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.
*
*
Note that this is a conditional feature. See
* {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D}
* for more information.
*
* @since JavaFX 2.0
*/
public class PerspectiveCamera extends Camera {
private boolean fixedEyeAtCameraZero = false;
// Lookat transform for legacy case
private static final Affine3D LOOK_AT_TX = new Affine3D();
// Lookat transform for fixedEyeAtCameraZero case
private static final Affine3D LOOK_AT_TX_FIXED_EYE = new Affine3D();
static {
PerspectiveCameraHelper.setPerspectiveCameraAccessor(new PerspectiveCameraHelper.PerspectiveCameraAccessor() {
@Override
public NGNode doCreatePeer(Node node) {
return ((PerspectiveCamera) node).doCreatePeer();
}
@Override
public void doUpdatePeer(Node node) {
((PerspectiveCamera) node).doUpdatePeer();
}
});
// Compute the legacy look at matrix such that the zero point ends up at
// the z=-1 plane.
LOOK_AT_TX.setToTranslation(0, 0, -1);
// Y-axis pointing down
LOOK_AT_TX.rotate(Math.PI, 1, 0, 0);
// Compute the fixed eye at (0, 0, 0) look at matrix such that the zero point
// ends up at the z=0 plane and Y-axis pointing down
LOOK_AT_TX_FIXED_EYE.rotate(Math.PI, 1, 0, 0);
}
/**
* Specifies the field of view angle of the camera's projection,
* measured in degrees.
*
* @defaultValue 30.0
*/
private DoubleProperty fieldOfView;
public final void setFieldOfView(double value){
fieldOfViewProperty().set(value);
}
public final double getFieldOfView() {
return fieldOfView == null ? 30 : fieldOfView.get();
}
public final DoubleProperty fieldOfViewProperty() {
if (fieldOfView == null) {
fieldOfView = new SimpleDoubleProperty(PerspectiveCamera.this, "fieldOfView", 30) {
@Override
protected void invalidated() {
NodeHelper.markDirty(PerspectiveCamera.this, DirtyBits.NODE_CAMERA);
}
};
}
return fieldOfView;
}
/**
* Defines whether the {@code fieldOfView} property will apply to the vertical
* dimension of the projection. If it is false, {@code fieldOfView} will
* apply to the horizontal dimension of the projection.
*
* @defaultValue true
* @since JavaFX 8.0
*/
private BooleanProperty verticalFieldOfView;
public final void setVerticalFieldOfView(boolean value) {
verticalFieldOfViewProperty().set(value);
}
public final boolean isVerticalFieldOfView() {
return verticalFieldOfView == null ? true : verticalFieldOfView.get();
}
public final BooleanProperty verticalFieldOfViewProperty() {
if (verticalFieldOfView == null) {
verticalFieldOfView = new SimpleBooleanProperty(PerspectiveCamera.this, "verticalFieldOfView", true) {
@Override
protected void invalidated() {
NodeHelper.markDirty(PerspectiveCamera.this, DirtyBits.NODE_CAMERA);
}
};
}
return verticalFieldOfView;
}
{
// To initialize the class helper at the begining each constructor of this class
PerspectiveCameraHelper.initHelper(this);
}
/**
* Creates an empty instance of PerspectiveCamera.
*/
public PerspectiveCamera() {
this(false);
}
/**
* Constructs a PerspectiveCamera with the specified fixedEyeAtCameraZero flag.
*
*
In the default camera, where fixedEyeAtCameraZero is false, the Z value of
* the eye position is adjusted in Z such that the projection matrix generated
* using the specified {@code fieldOfView} will produce units at
* Z = 0 (the projection plane), in device-independent pixels, matches that of
* the ParallelCamera.
* When the Scene is resized,
* the objects in the scene at the projection plane (Z = 0) will stay the same size,
* but more or less content of the scene is viewable.
*
*
If fixedEyeAtCameraZero is true, the eye position is fixed at (0, 0, 0)
* in the local coordinates of the camera. The projection matrix is generated
* using the specified {@code fieldOfView} and the projection volume is mapped
* onto the viewport (window) such that it is stretched over more or fewer
* device-independent pixels at the projection plane.
* When the Scene is resized,
* the objects in the scene will shrink or grow proportionally,
* but the visible portion of the content is unchanged.
*
*
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.
*
* @param fixedEyeAtCameraZero true if the the eye position is fixed at
* (0, 0, 0) in the local coordinates of the camera.
* @since JavaFX 8.0
*/
public PerspectiveCamera(boolean fixedEyeAtCameraZero) {
if (!Platform.isSupported(ConditionalFeature.SCENE3D)) {
String logname = PerspectiveCamera.class.getName();
PlatformLogger.getLogger(logname).warning("System can't support "
+ "ConditionalFeature.SCENE3D");
}
this.fixedEyeAtCameraZero = fixedEyeAtCameraZero;
}
/**
* Returns a flag indicating whether this camera uses a fixed eye position
* at the origin of the camera. If {@code fixedEyeAtCameraZero} is {@code true},
* the the eye position is fixed at (0, 0, 0) in the local coordinates
* of the camera. This attribute is immutable.
*
* @return a flag indicating whether this camera uses a fixed eye position
* at the origin of the camera
*
* @since JavaFX 8.0
*/
public final boolean isFixedEyeAtCameraZero() {
return fixedEyeAtCameraZero;
}
@Override
final PickRay computePickRay(double x, double y, PickRay pickRay) {
return PickRay.computePerspectivePickRay(x, y, fixedEyeAtCameraZero,
getViewWidth(), getViewHeight(),
Math.toRadians(getFieldOfView()), isVerticalFieldOfView(),
getCameraTransform(),
getNearClip(), getFarClip(),
pickRay);
}
@Override Camera copy() {
PerspectiveCamera c = new PerspectiveCamera(fixedEyeAtCameraZero);
c.setNearClip(getNearClip());
c.setFarClip(getFarClip());
c.setFieldOfView(getFieldOfView());
return c;
}
/*
* Note: This method MUST only be called via its accessor method.
*/
private NGNode doCreatePeer() {
NGPerspectiveCamera peer = new NGPerspectiveCamera(fixedEyeAtCameraZero);
peer.setNearClip((float) getNearClip());
peer.setFarClip((float) getFarClip());
peer.setFieldOfView((float) getFieldOfView());
return peer;
}
/*
* Note: This method MUST only be called via its accessor method.
*/
private void doUpdatePeer() {
NGPerspectiveCamera pgPerspectiveCamera = getPeer();
if (isDirty(DirtyBits.NODE_CAMERA)) {
pgPerspectiveCamera.setVerticalFieldOfView(isVerticalFieldOfView());
pgPerspectiveCamera.setFieldOfView((float) getFieldOfView());
}
}
@Override
void computeProjectionTransform(GeneralTransform3D proj) {
proj.perspective(isVerticalFieldOfView(), Math.toRadians(getFieldOfView()),
getViewWidth() / getViewHeight(), getNearClip(), getFarClip());
}
@Override
void computeViewTransform(Affine3D view) {
// In the case of fixedEyeAtCameraZero the camera position is (0,0,0) in
// local coord. of the camera node. In non-fixed eye case, the camera
// position is (w/2, h/2, h/2/tan) in local coord. of the camera.
if (isFixedEyeAtCameraZero()) {
view.setTransform(LOOK_AT_TX_FIXED_EYE);
} else {
final double viewWidth = getViewWidth();
final double viewHeight = getViewHeight();
final boolean verticalFOV = isVerticalFieldOfView();
final double aspect = viewWidth / viewHeight;
final double tanOfHalfFOV = Math.tan(Math.toRadians(getFieldOfView()) / 2.0);
// Translate the zero point to the upper-left corner
final double xOffset = -tanOfHalfFOV * (verticalFOV ? aspect : 1.0);
final double yOffset = tanOfHalfFOV * (verticalFOV ? 1.0 : 1.0 / aspect);
// Compute scale factor as 2/viewport.width or height, after adjusting for fov
final double scale = 2.0 * tanOfHalfFOV /
(verticalFOV ? viewHeight : viewWidth);
view.setToTranslation(xOffset, yOffset, 0.0);
view.concatenate(LOOK_AT_TX);
view.scale(scale, scale, scale);
}
}
@Override
Vec3d computePosition(Vec3d position) {
if (position == null) {
position = new Vec3d();
}
if (fixedEyeAtCameraZero) {
position.set(0.0, 0.0, 0.0);
} else {
final double halfViewWidth = getViewWidth() / 2.0;
final double halfViewHeight = getViewHeight() / 2.0;
final double halfViewDim = isVerticalFieldOfView()
? halfViewHeight : halfViewWidth;
final double distanceZ = halfViewDim
/ Math.tan(Math.toRadians(getFieldOfView() / 2.0));
position.set(halfViewWidth, halfViewHeight, -distanceZ);
}
return position;
}
}