/*
* 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 javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import javafx.scene.transform.Transform;
import com.sun.javafx.geom.BaseBounds;
import com.sun.javafx.geom.BoxBounds;
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.BaseTransform;
import com.sun.javafx.geom.transform.GeneralTransform3D;
import com.sun.javafx.geom.transform.NoninvertibleTransformException;
import com.sun.javafx.scene.CameraHelper;
import com.sun.javafx.scene.DirtyBits;
import com.sun.javafx.scene.NodeHelper;
import com.sun.javafx.scene.transform.TransformHelper;
import com.sun.javafx.sg.prism.NGCamera;
import com.sun.javafx.logging.PlatformLogger;
Base class for a camera used to render a scene. The camera defines the mapping of the scene coordinate space onto the window. Camera is an abstract class with two concrete subclasses: ParallelCamera
and PerspectiveCamera
. The default camera is positioned in the scene such that its projection plane in the scene coordinate space is at Z = 0, and it is looking into the screen in the positive Z direction. The distance in Z from the camera to the projection plane is determined by the width
and height
of the Scene to which it is attached and its fieldOfView
.
The nearClip
and farClip
of this camera are specified in the eye coordinate space. This space is defined such that the eye is at its origin and the projection plane is one unit in front of the eye in the positive Z direction.
The following pseudo code is the math used to compute the near and far clip
distances in the scene coordinate space:
final double tanOfHalfFOV = Math.tan(Math.toRadians(FOV) / 2.0);
final double halfHeight = HEIGHT / 2;
final double focalLenght = halfHeight / tanOfHalfFOV;
final double eyePositionZ = -1.0 * focalLenght;
final double nearClipDistance = focalLenght * NEAR + eyePositionZ;
final double farClipDistance = focalLenght * FAR + eyePositionZ;
where FOV
is fieldOfView
in degrees, NEAR
is nearClip
specified in eye space, and FAR
is farClip
specified in eye space.
Note: Since the ParallelCamera class has no fieldOfView
property, a 30 degrees vertical field of view is used.
Note: For the case of a PerspectiveCamera where the fixedEyeAtCameraZero attribute is true, the scene coordinate space is normalized in order to fit into the view frustum (see PerspectiveCamera
for more details). In this mode, the eye coordinate space is the same as this Camera node's local coordinate space. Hence the conversion formula mentioned above is not used.
An application should not extend the Camera class directly. Doing so may lead to
an UnsupportedOperationException being thrown.
Since: JavaFX 2.0
/**
* Base class for a camera used to render a scene.
* The camera defines the mapping of the scene coordinate space onto the window.
* Camera is an abstract class with two concrete subclasses:
* {@link ParallelCamera} and {@link PerspectiveCamera}.
*
* <p>
* The default camera is positioned in the scene such that its projection plane
* in the scene coordinate space is at Z = 0, and it is looking into the screen in
* the positive Z direction. The distance in Z from the camera to the projection
* plane is determined by the {@code width} and {@code height} of the Scene to
* which it is attached and its {@code fieldOfView}.
* </p>
*
* <p>
* The {@code nearClip} and {@code farClip} of this camera are specified in the
* eye coordinate space. This space is defined such that the eye is at its
* origin and the projection plane is one unit in front of the eye in the
* positive Z direction.
* </p>
*
* <p>
* The following pseudo code is the math used to compute the near and far clip
* distances in the scene coordinate space:
* </p>
*
* <pre>
* final double tanOfHalfFOV = Math.tan(Math.toRadians(FOV) / 2.0);
* final double halfHeight = HEIGHT / 2;
* final double focalLenght = halfHeight / tanOfHalfFOV;
* final double eyePositionZ = -1.0 * focalLenght;
* final double nearClipDistance = focalLenght * NEAR + eyePositionZ;
* final double farClipDistance = focalLenght * FAR + eyePositionZ;
* </pre>
*
* <p>
* where {@code FOV} is {@code fieldOfView} in degrees,
* {@code NEAR} is {@code nearClip} specified in eye space,
* and {@code FAR} is {@code farClip} specified in eye space.
* </p>
*
* <p>
* Note: Since the ParallelCamera class has no {@code fieldOfView} property, a
* 30 degrees vertical field of view is used.
* </p>
*
* <p>
* Note: For the case of a PerspectiveCamera where the fixedEyeAtCameraZero
* attribute is true, the scene coordinate space is normalized in order to fit
* into the view frustum (see {@link PerspectiveCamera} for more details). In
* this mode, the eye coordinate space is the same as this Camera node's local
* coordinate space. Hence the conversion formula mentioned above is not used.
* </p>
*
* <p>
* An application should not extend the Camera class directly. Doing so may lead to
* an UnsupportedOperationException being thrown.
* </p>
*
* @since JavaFX 2.0
*/
public abstract class Camera extends Node {
static {
// This is used by classes in different packages to get access to
// private and package private methods.
CameraHelper.setCameraAccessor(new CameraHelper.CameraAccessor() {
@Override
public void doMarkDirty(Node node, DirtyBits dirtyBit) {
((Camera) node).doMarkDirty(dirtyBit);
}
@Override
public void doUpdatePeer(Node node) {
((Camera) node).doUpdatePeer();
}
@Override
public BaseBounds doComputeGeomBounds(Node node,
BaseBounds bounds, BaseTransform tx) {
return ((Camera) node).doComputeGeomBounds(bounds, tx);
}
@Override
public boolean doComputeContains(Node node, double localX, double localY) {
return ((Camera) node).doComputeContains(localX, localY);
}
@Override
public Point2D project(Camera camera, Point3D p) {
return camera.project(p);
}
@Override
public Point2D pickNodeXYPlane(Camera camera, Node node, double x, double y) {
return camera.pickNodeXYPlane(node, x, y);
}
@Override
public Point3D pickProjectPlane(Camera camera, double x, double y) {
return camera.pickProjectPlane(x, y);
}
});
}
private Affine3D localToSceneTx = new Affine3D();
{
// To initialize the class helper at the begining each constructor of this class
CameraHelper.initHelper(this);
}
protected Camera() {
InvalidationListener dirtyTransformListener = observable
-> NodeHelper.markDirty(this, DirtyBits.NODE_CAMERA_TRANSFORM);
this.localToSceneTransformProperty().addListener(dirtyTransformListener);
// if camera is removed from scene it needs to stop using its transforms
this.sceneProperty().addListener(dirtyTransformListener);
}
// NOTE: farClipInScene and nearClipInScene are valid only if there is no rotation
private double farClipInScene;
private double nearClipInScene;
// only one of them can be non-null at a time
private Scene ownerScene = null;
private SubScene ownerSubScene = null;
private GeneralTransform3D projViewTx = new GeneralTransform3D();
private GeneralTransform3D projTx = new GeneralTransform3D();
private Affine3D viewTx = new Affine3D();
private double viewWidth = 1.0;
private double viewHeight = 1.0;
private Vec3d position = new Vec3d();
private boolean clipInSceneValid = false;
private boolean projViewTxValid = false;
private boolean localToSceneValid = false;
private boolean sceneToLocalValid = false;
double getFarClipInScene() {
updateClipPlane();
return farClipInScene;
}
double getNearClipInScene() {
updateClipPlane();
return nearClipInScene;
}
private void updateClipPlane() {
if (!clipInSceneValid) {
final Transform localToSceneTransform = getLocalToSceneTransform();
nearClipInScene = localToSceneTransform.transform(0, 0, getNearClip()).getZ();
farClipInScene = localToSceneTransform.transform(0, 0, getFarClip()).getZ();
clipInSceneValid = true;
}
}
An affine transform that holds the computed scene-to-local transform.
It is used to convert node to camera coordinate when rotation is involved.
/**
* An affine transform that holds the computed scene-to-local transform.
* It is used to convert node to camera coordinate when rotation is involved.
*/
private Affine3D sceneToLocalTx = new Affine3D();
Affine3D getSceneToLocalTransform() {
if (!sceneToLocalValid) {
sceneToLocalTx.setTransform(getCameraTransform());
try {
sceneToLocalTx.invert();
} catch (NoninvertibleTransformException ex) {
String logname = Camera.class.getName();
PlatformLogger.getLogger(logname).severe("getSceneToLocalTransform", ex);
sceneToLocalTx.setToIdentity();
}
sceneToLocalValid = true;
}
return sceneToLocalTx;
}
Specifies the distance from the eye of the near clipping plane of this Camera
in the eye coordinate space. Objects closer to the eye than nearClip
are not drawn. nearClip
is specified as a value greater than zero. A value less than or equal to zero is treated as a very small positive number. @defaultValue 0.1 Since: JavaFX 8.0
/**
* Specifies the distance from the eye of the near clipping plane of
* this {@code Camera} in the eye coordinate space.
* Objects closer to the eye than {@code nearClip} are not drawn.
* {@code nearClip} is specified as a value greater than zero. A value less
* than or equal to zero is treated as a very small positive number.
*
* @defaultValue 0.1
* @since JavaFX 8.0
*/
private DoubleProperty nearClip;
public final void setNearClip(double value){
nearClipProperty().set(value);
}
public final double getNearClip() {
return nearClip == null ? 0.1 : nearClip.get();
}
public final DoubleProperty nearClipProperty() {
if (nearClip == null) {
nearClip = new SimpleDoubleProperty(Camera.this, "nearClip", 0.1) {
@Override
protected void invalidated() {
clipInSceneValid = false;
NodeHelper.markDirty(Camera.this, DirtyBits.NODE_CAMERA);
}
};
}
return nearClip;
}
Specifies the distance from the eye of the far clipping plane of this Camera
in the eye coordinate space. Objects farther away from the eye than farClip
are not drawn. farClip
is specified as a value greater than nearClip
. A value less than or equal to nearClip
is treated as nearClip
plus a very small positive number. @defaultValue 100.0 Since: JavaFX 8.0
/**
* Specifies the distance from the eye of the far clipping plane of
* this {@code Camera} in the eye coordinate space.
* Objects farther away from the eye than {@code farClip} are not
* drawn.
* {@code farClip} is specified as a value greater than {@code nearClip}.
* A value less than or equal to {@code nearClip} is treated as
* {@code nearClip} plus a very small positive number.
*
* @defaultValue 100.0
* @since JavaFX 8.0
*/
private DoubleProperty farClip;
public final void setFarClip(double value){
farClipProperty().set(value);
}
public final double getFarClip() {
return farClip == null ? 100.0 : farClip.get();
}
public final DoubleProperty farClipProperty() {
if (farClip == null) {
farClip = new SimpleDoubleProperty(Camera.this, "farClip", 100.0) {
@Override
protected void invalidated() {
clipInSceneValid = false;
NodeHelper.markDirty(Camera.this, DirtyBits.NODE_CAMERA);
}
};
}
return farClip;
}
Camera copy() {
return this;
}
/*
* Note: This method MUST only be called via its accessor method.
*/
private void doUpdatePeer() {
NGCamera peer = getPeer();
if (!NodeHelper.isDirtyEmpty(this)) {
if (isDirty(DirtyBits.NODE_CAMERA)) {
peer.setNearClip((float) getNearClip());
peer.setFarClip((float) getFarClip());
peer.setViewWidth(getViewWidth());
peer.setViewHeight(getViewHeight());
}
if (isDirty(DirtyBits.NODE_CAMERA_TRANSFORM)) {
// TODO: 3D - For now, we are treating the scene as world.
// This may need to change for the fixed eye position case.
peer.setWorldTransform(getCameraTransform());
}
peer.setProjViewTransform(getProjViewTransform());
position = computePosition(position);
getCameraTransform().transform(position, position);
peer.setPosition(position);
}
}
void setViewWidth(double width) {
this.viewWidth = width;
NodeHelper.markDirty(this, DirtyBits.NODE_CAMERA);
}
double getViewWidth() {
return viewWidth;
}
void setViewHeight(double height) {
this.viewHeight = height;
NodeHelper.markDirty(this, DirtyBits.NODE_CAMERA);
}
double getViewHeight() {
return viewHeight;
}
void setOwnerScene(Scene s) {
if (s == null) {
ownerScene = null;
} else if (s != ownerScene) {
if (ownerScene != null || ownerSubScene != null) {
throw new IllegalArgumentException(this
+ "is already set as camera in other scene or subscene");
}
ownerScene = s;
markOwnerDirty();
}
}
void setOwnerSubScene(SubScene s) {
if (s == null) {
ownerSubScene = null;
} else if (s != ownerSubScene) {
if (ownerScene != null || ownerSubScene != null) {
throw new IllegalArgumentException(this
+ "is already set as camera in other scene or subscene");
}
ownerSubScene = s;
markOwnerDirty();
}
}
/*
* Note: This method MUST only be called via its accessor method.
*/
private void doMarkDirty(DirtyBits dirtyBit) {
if (dirtyBit == DirtyBits.NODE_CAMERA_TRANSFORM) {
localToSceneValid = false;
sceneToLocalValid = false;
clipInSceneValid = false;
projViewTxValid = false;
} else if (dirtyBit == DirtyBits.NODE_CAMERA) {
projViewTxValid = false;
}
markOwnerDirty();
}
private void markOwnerDirty() {
// if the camera is part of the scene/subScene, we will need to notify
// the owner to mark the entire scene/subScene dirty.
if (ownerScene != null) {
ownerScene.markCameraDirty();
}
if (ownerSubScene != null) {
ownerSubScene.markContentDirty();
}
}
Returns the local-to-scene transform of this camera.
Package private, for use in our internal subclasses.
Returns directly the internal instance, it must not be altered.
/**
* Returns the local-to-scene transform of this camera.
* Package private, for use in our internal subclasses.
* Returns directly the internal instance, it must not be altered.
*/
Affine3D getCameraTransform() {
if (!localToSceneValid) {
localToSceneTx.setToIdentity();
TransformHelper.apply(getLocalToSceneTransform(), localToSceneTx);
localToSceneValid = true;
}
return localToSceneTx;
}
abstract void computeProjectionTransform(GeneralTransform3D proj);
abstract void computeViewTransform(Affine3D view);
Returns the projView transform of this camera.
Package private, for internal use.
Returns directly the internal instance, it must not be altered.
/**
* Returns the projView transform of this camera.
* Package private, for internal use.
* Returns directly the internal instance, it must not be altered.
*/
GeneralTransform3D getProjViewTransform() {
if (!projViewTxValid) {
computeProjectionTransform(projTx);
computeViewTransform(viewTx);
projViewTx.set(projTx);
projViewTx.mul(viewTx);
projViewTx.mul(getSceneToLocalTransform());
projViewTxValid = true;
}
return projViewTx;
}
Transforms the given 3D point to the flat projected coordinates.
/**
* Transforms the given 3D point to the flat projected coordinates.
*/
private Point2D project(Point3D p) {
final Vec3d vec = getProjViewTransform().transform(new Vec3d(
p.getX(), p.getY(), p.getZ()));
final double halfViewWidth = getViewWidth() / 2.0;
final double halfViewHeight = getViewHeight() / 2.0;
return new Point2D(
halfViewWidth * (1 + vec.x),
halfViewHeight * (1 - vec.y));
}
Computes intersection point of the pick ray cast by the given coordinates
and the node's local XY plane.
/**
* Computes intersection point of the pick ray cast by the given coordinates
* and the node's local XY plane.
*/
private Point2D pickNodeXYPlane(Node node, double x, double y) {
final PickRay ray = computePickRay(x, y, null);
final Affine3D localToScene = new Affine3D();
TransformHelper.apply(node.getLocalToSceneTransform(), localToScene);
final Vec3d o = ray.getOriginNoClone();
final Vec3d d = ray.getDirectionNoClone();
try {
localToScene.inverseTransform(o, o);
localToScene.inverseDeltaTransform(d, d);
} catch (NoninvertibleTransformException e) {
return null;
}
if (almostZero(d.z)) {
return null;
}
final double t = -o.z / d.z;
return new Point2D(o.x + (d.x * t), o.y + (d.y * t));
}
Computes intersection point of the pick ray cast by the given coordinates
and the projection plane.
/**
* Computes intersection point of the pick ray cast by the given coordinates
* and the projection plane.
*/
Point3D pickProjectPlane(double x, double y) {
final PickRay ray = computePickRay(x, y, null);
final Vec3d p = new Vec3d();
p.add(ray.getOriginNoClone(), ray.getDirectionNoClone());
return new Point3D(p.x, p.y, p.z);
}
Computes pick ray for the content rendered by this camera.
Params: - x – horizontal coordinate of the pick ray in the projected
view, usually mouse cursor position
- y – vertical coordinate of the pick ray in the projected
view, usually mouse cursor position
- pickRay – pick ray to be reused. New instance is created in case
of null.
Returns: The PickRay instance computed based on this camera and the given
arguments.
/**
* Computes pick ray for the content rendered by this camera.
* @param x horizontal coordinate of the pick ray in the projected
* view, usually mouse cursor position
* @param y vertical coordinate of the pick ray in the projected
* view, usually mouse cursor position
* @param pickRay pick ray to be reused. New instance is created in case
* of null.
* @return The PickRay instance computed based on this camera and the given
* arguments.
*/
abstract PickRay computePickRay(double x, double y, PickRay pickRay);
Computes local position of the camera in the scene.
Params: - position – Position to be reused. New instance is created in case
of null.
Returns: The position of the camera in the scene in camera local coords
/**
* Computes local position of the camera in the scene.
* @param position Position to be reused. New instance is created in case
* of null.
* @return The position of the camera in the scene in camera local coords
*/
abstract Vec3d computePosition(Vec3d position);
/*
* Note: This method MUST only be called via its accessor method.
*/
private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) {
return new BoxBounds(0, 0, 0, 0, 0, 0);
}
/*
* Note: This method MUST only be called via its accessor method.
*/
private boolean doComputeContains(double localX, double localY) {
return false;
}
}