1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene;
  27 
  28 import javafx.beans.InvalidationListener;
  29 import javafx.beans.Observable;
  30 import javafx.beans.property.DoubleProperty;
  31 import javafx.beans.property.SimpleDoubleProperty;
  32 import javafx.geometry.Point2D;
  33 import javafx.geometry.Point3D;
  34 import javafx.scene.transform.Transform;
  35 import com.sun.javafx.geom.BaseBounds;
  36 import com.sun.javafx.geom.BoxBounds;
  37 import com.sun.javafx.geom.PickRay;
  38 import com.sun.javafx.geom.Vec3d;
  39 import com.sun.javafx.geom.transform.Affine3D;
  40 import com.sun.javafx.geom.transform.BaseTransform;
  41 import com.sun.javafx.geom.transform.GeneralTransform3D;
  42 import com.sun.javafx.geom.transform.NoninvertibleTransformException;
  43 import com.sun.javafx.jmx.MXNodeAlgorithm;
  44 import com.sun.javafx.jmx.MXNodeAlgorithmContext;
  45 import com.sun.javafx.scene.CameraHelper;
  46 import com.sun.javafx.scene.DirtyBits;
  47 import com.sun.javafx.sg.prism.NGCamera;
  48 import sun.util.logging.PlatformLogger;
  49 
  50 
  51 /**
  52  * Base class for a camera used to render a scene.
  53  * The camera defines the mapping of the scene coordinate space onto the window.
  54  * Camera is an abstract class with two concrete subclasses:
  55  * {@link ParallelCamera} and {@link PerspectiveCamera}.
  56  *
  57  * <p>
  58  * The default camera is positioned in the scene such that its projection plane
  59  * in the scene coordinate space is at Z = 0, and it is looking into the screen in
  60  * the positive Z direction. The distance in Z from the camera to the projection
  61  * plane is determined by the {@code width} and {@code height} of the Scene to
  62  * which it is attached and its {@code fieldOfView}.
  63  * </p>
  64  *
  65  * <p>
  66  * The {@code nearClip} and {@code farClip} of this camera are specified in the
  67  * eye coordinate space. This space is defined such that the eye is at its
  68  * origin and the projection plane is one unit in front of the eye in the
  69  * positive Z direction.
  70  * </p>
  71  *
  72  * <p>
  73  * The following pseudo code is the math used to compute the near and far clip
  74  * distances in the scene coordinate space:
  75  *
  76  * <ul><pre>
  77  * final double tanOfHalfFOV = Math.tan(Math.toRadians(FOV) / 2.0);
  78  * final double halfHeight = HEIGHT / 2;
  79  * final double focalLenght = halfHeight / tanOfHalfFOV;
  80  * final double eyePositionZ = -1.0 * focalLenght;
  81  * final double nearClipDistance = focalLenght * NEAR + eyePositionZ;
  82  * final double farClipDistance = focalLenght * FAR + eyePositionZ;
  83  * </pre></ul>
  84  *
  85  * where {@code FOV} is {@code fieldOfView} in degrees,
  86  * {@code NEAR} is {@code nearClip} specified in eye space,
  87  * and {@code FAR} is {@code farClip} specified in eye space.
  88  * </p>
  89  *
  90  * <p>
  91  * Note: Since the ParallelCamera class has no {@code fieldOfView} property, a
  92  * 30 degrees vertical field of view is used.
  93  * </p>
  94  *
  95  * <p>
  96  * Note: For the case of a PerspectiveCamera where the fixedEyeAtCameraZero
  97  * attribute is true, the scene coordinate space is normalized in order to fit
  98  * into the view frustum (see {@link PerspectiveCamera} for more details). In
  99  * this mode, the eye coordinate space is the same as this Camera node's local
 100  * coordinate space. Hence the conversion formula mentioned above is not used.
 101  * </p>
 102  *
 103  * @since JavaFX 2.0
 104  */
 105 public abstract class Camera extends Node {
 106 
 107     private Affine3D localToSceneTx = new Affine3D();
 108 
 109     protected Camera() {
 110         InvalidationListener dirtyTransformListener = observable -> impl_markDirty(DirtyBits.NODE_CAMERA_TRANSFORM);
 111 
 112         this.localToSceneTransformProperty().addListener(dirtyTransformListener);
 113         // if camera is removed from scene it needs to stop using its transforms
 114         this.sceneProperty().addListener(dirtyTransformListener);
 115     }
 116 
 117     // NOTE: farClipInScene and nearClipInScene are valid only if there is no rotation
 118     private double farClipInScene;
 119     private double nearClipInScene;
 120 
 121     // only one of them can be non-null at a time
 122     private Scene ownerScene = null;
 123     private SubScene ownerSubScene = null;
 124 
 125     private GeneralTransform3D projViewTx = new GeneralTransform3D();
 126     private GeneralTransform3D projTx = new GeneralTransform3D();
 127     private Affine3D viewTx = new Affine3D();
 128     private double viewWidth = 1.0;
 129     private double viewHeight = 1.0;
 130     private Vec3d position = new Vec3d();
 131 
 132     private boolean clipInSceneValid = false;
 133     private boolean projViewTxValid = false;
 134     private boolean localToSceneValid = false;
 135     private boolean sceneToLocalValid = false;
 136 
 137     double getFarClipInScene() {
 138         updateClipPlane();
 139         return farClipInScene;
 140     }
 141 
 142     double getNearClipInScene() {
 143         updateClipPlane();
 144         return nearClipInScene;
 145     }
 146 
 147     private void updateClipPlane() {
 148         if (!clipInSceneValid) {
 149             final Transform localToSceneTransform = getLocalToSceneTransform();
 150             nearClipInScene = localToSceneTransform.transform(0, 0, getNearClip()).getZ();
 151             farClipInScene = localToSceneTransform.transform(0, 0, getFarClip()).getZ();
 152             clipInSceneValid = true;
 153         }
 154     }
 155 
 156     /**
 157      * An affine transform that holds the computed scene-to-local transform.
 158      * It is used to convert node to camera coordinate when rotation is involved.
 159      */
 160     private Affine3D sceneToLocalTx = new Affine3D();
 161 
 162     Affine3D getSceneToLocalTransform() {
 163         if (!sceneToLocalValid) {
 164             sceneToLocalTx.setTransform(getCameraTransform());
 165             try {
 166                 sceneToLocalTx.invert();
 167             } catch (NoninvertibleTransformException ex) {
 168                 String logname = Camera.class.getName();
 169                 PlatformLogger.getLogger(logname).severe("getSceneToLocalTransform", ex);
 170                 sceneToLocalTx.setToIdentity();
 171             }
 172             sceneToLocalValid = true;
 173         }
 174 
 175         return sceneToLocalTx;
 176     }
 177 
 178     /**
 179      * Specifies the distance from the eye of the near clipping plane of
 180      * this {@code Camera} in the eye coordinate space.
 181      * Objects closer to the eye than {@code nearClip} are not drawn.
 182      * {@code nearClip} is specified as a value greater than zero. A value less
 183      * than or equal to zero is treated as a very small positive number.
 184      *
 185      * @defaultValue 0.1
 186      * @since JavaFX 8.0
 187      */
 188     private DoubleProperty nearClip;
 189 
 190     public final void setNearClip(double value){
 191         nearClipProperty().set(value);
 192     }
 193 
 194     public final double getNearClip() {
 195         return nearClip == null ? 0.1 : nearClip.get();
 196     }
 197 
 198     public final DoubleProperty nearClipProperty() {
 199         if (nearClip == null) {
 200             nearClip = new SimpleDoubleProperty(Camera.this, "nearClip", 0.1) {
 201                 @Override
 202                 protected void invalidated() {
 203                     clipInSceneValid = false;
 204                     impl_markDirty(DirtyBits.NODE_CAMERA);
 205                 }
 206             };
 207         }
 208         return nearClip;
 209     }
 210 
 211     /**
 212      * Specifies the distance from the eye of the far clipping plane of
 213      * this {@code Camera} in the eye coordinate space.
 214      * Objects farther away from the eye than {@code farClip} are not
 215      * drawn.
 216      * {@code farClip} is specified as a value greater than {@code nearClip}.
 217      * A value less than or equal to {@code nearClip} is treated as
 218      * {@code nearClip} plus a very small positive number.
 219      *
 220      * @defaultValue 100.0
 221      * @since JavaFX 8.0
 222      */
 223     private DoubleProperty farClip;
 224 
 225     public final void setFarClip(double value){
 226         farClipProperty().set(value);
 227     }
 228 
 229     public final double getFarClip() {
 230         return farClip == null ? 100.0 : farClip.get();
 231     }
 232 
 233     public final DoubleProperty farClipProperty() {
 234         if (farClip == null) {
 235             farClip = new SimpleDoubleProperty(Camera.this, "farClip", 100.0) {
 236                 @Override
 237                 protected void invalidated() {
 238                     clipInSceneValid = false;
 239                     impl_markDirty(DirtyBits.NODE_CAMERA);
 240                 }
 241             };
 242         }
 243         return farClip;
 244     }
 245 
 246     Camera copy() {
 247         return this;
 248     }
 249 
 250     /**
 251      * @treatAsPrivate implementation detail
 252      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 253      */
 254     @Deprecated
 255     @Override
 256     public void impl_updatePeer() {
 257         super.impl_updatePeer();
 258         NGCamera peer = impl_getPeer();
 259         if (!impl_isDirtyEmpty()) {
 260             if (impl_isDirty(DirtyBits.NODE_CAMERA)) {
 261                 peer.setNearClip((float) getNearClip());
 262                 peer.setFarClip((float) getFarClip());
 263                 peer.setViewWidth(getViewWidth());
 264                 peer.setViewHeight(getViewHeight());
 265             }
 266             if (impl_isDirty(DirtyBits.NODE_CAMERA_TRANSFORM)) {
 267                 // TODO: 3D - For now, we are treating the scene as world.
 268                 // This may need to change for the fixed eye position case.
 269                 peer.setWorldTransform(getCameraTransform());
 270             }
 271 
 272             peer.setProjViewTransform(getProjViewTransform());
 273 
 274             position = computePosition(position);
 275             getCameraTransform().transform(position, position);
 276             peer.setPosition(position);
 277         }
 278     }
 279 
 280     void setViewWidth(double width) {
 281         this.viewWidth = width;
 282         impl_markDirty(DirtyBits.NODE_CAMERA);
 283     }
 284 
 285     double getViewWidth() {
 286         return viewWidth;
 287     }
 288 
 289     void setViewHeight(double height) {
 290         this.viewHeight = height;
 291         impl_markDirty(DirtyBits.NODE_CAMERA);
 292     }
 293 
 294     double getViewHeight() {
 295         return viewHeight;
 296     }
 297 
 298     void setOwnerScene(Scene s) {
 299         if (s == null) {
 300             ownerScene = null;
 301         } else if (s != ownerScene) {
 302             if (ownerScene != null || ownerSubScene != null) {
 303                 throw new IllegalArgumentException(this
 304                         + "is already set as camera in other scene or subscene");
 305             }
 306             ownerScene = s;
 307             markOwnerDirty();
 308         }
 309     }
 310 
 311     void setOwnerSubScene(SubScene s) {
 312         if (s == null) {
 313             ownerSubScene = null;
 314         } else if (s != ownerSubScene) {
 315             if (ownerScene != null || ownerSubScene != null) {
 316                 throw new IllegalArgumentException(this
 317                         + "is already set as camera in other scene or subscene");
 318             }
 319             ownerSubScene = s;
 320             markOwnerDirty();
 321         }
 322     }
 323 
 324     /**
 325      * @treatAsPrivate implementation detail
 326      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 327      */
 328     @Deprecated
 329     @Override
 330     protected void impl_markDirty(DirtyBits dirtyBit) {
 331         super.impl_markDirty(dirtyBit);
 332         if (dirtyBit == DirtyBits.NODE_CAMERA_TRANSFORM) {
 333             localToSceneValid = false;
 334             sceneToLocalValid = false;
 335             clipInSceneValid = false;
 336             projViewTxValid = false;
 337         } else if (dirtyBit == DirtyBits.NODE_CAMERA) {
 338             projViewTxValid = false;
 339         }
 340         markOwnerDirty();
 341     }
 342 
 343     private void markOwnerDirty() {
 344         // if the camera is part of the scene/subScene, we will need to notify
 345         // the owner to mark the entire scene/subScene dirty.
 346         if (ownerScene != null) {
 347             ownerScene.markCameraDirty();
 348         }
 349         if (ownerSubScene != null) {
 350             ownerSubScene.markContentDirty();
 351         }
 352     }
 353 
 354     /**
 355      * Returns the local-to-scene transform of this camera.
 356      * Package private, for use in our internal subclasses.
 357      * Returns directly the internal instance, it must not be altered.
 358      */
 359     Affine3D getCameraTransform() {
 360         if (!localToSceneValid) {
 361             localToSceneTx.setToIdentity();
 362             getLocalToSceneTransform().impl_apply(localToSceneTx);
 363             localToSceneValid = true;
 364         }
 365         return localToSceneTx;
 366     }
 367 
 368     abstract void computeProjectionTransform(GeneralTransform3D proj);
 369     abstract void computeViewTransform(Affine3D view);
 370 
 371     /**
 372      * Returns the projView transform of this camera.
 373      * Package private, for internal use.
 374      * Returns directly the internal instance, it must not be altered.
 375      */
 376     GeneralTransform3D getProjViewTransform() {
 377         if (!projViewTxValid) {
 378             computeProjectionTransform(projTx);
 379             computeViewTransform(viewTx);
 380 
 381             projViewTx.set(projTx);
 382             projViewTx.mul(viewTx);
 383             projViewTx.mul(getSceneToLocalTransform());
 384 
 385             projViewTxValid = true;
 386         }
 387 
 388         return projViewTx;
 389     }
 390 
 391     /**
 392      * Transforms the given 3D point to the flat projected coordinates.
 393      */
 394     private Point2D project(Point3D p) {
 395 
 396         final Vec3d vec = getProjViewTransform().transform(new Vec3d(
 397                 p.getX(), p.getY(), p.getZ()));
 398 
 399         final double halfViewWidth = getViewWidth() / 2.0;
 400         final double halfViewHeight = getViewHeight() / 2.0;
 401 
 402         return new Point2D(
 403                 halfViewWidth * (1 + vec.x),
 404                 halfViewHeight * (1 - vec.y));
 405     }
 406 
 407     /**
 408      * Computes intersection point of the pick ray cast by the given coordinates
 409      * and the node's local XY plane.
 410      */
 411     private Point2D pickNodeXYPlane(Node node, double x, double y) {
 412         final PickRay ray = computePickRay(x, y, null);
 413 
 414         final Affine3D localToScene = new Affine3D();
 415         node.getLocalToSceneTransform().impl_apply(localToScene);
 416 
 417         final Vec3d o = ray.getOriginNoClone();
 418         final Vec3d d = ray.getDirectionNoClone();
 419 
 420         try {
 421             localToScene.inverseTransform(o, o);
 422             localToScene.inverseDeltaTransform(d, d);
 423         } catch (NoninvertibleTransformException e) {
 424             return null;
 425         }
 426 
 427         if (almostZero(d.z)) {
 428             return null;
 429         }
 430 
 431         final double t = -o.z / d.z;
 432         return new Point2D(o.x + (d.x * t), o.y + (d.y * t));
 433     }
 434 
 435     /**
 436      * Computes intersection point of the pick ray cast by the given coordinates
 437      * and the projection plane.
 438      */
 439     Point3D pickProjectPlane(double x, double y) {
 440         final PickRay ray = computePickRay(x, y, null);
 441         final Vec3d p = new Vec3d();
 442         p.add(ray.getOriginNoClone(), ray.getDirectionNoClone());
 443 
 444         return new Point3D(p.x, p.y, p.z);
 445     }
 446 
 447 
 448     /**
 449      * Computes pick ray for the content rendered by this camera.
 450      * @param x horizontal coordinate of the pick ray in the projected
 451      *               view, usually mouse cursor position
 452      * @param y vertical coordinate of the pick ray in the projected
 453      *               view, usually mouse cursor position
 454      * @param pickRay pick ray to be reused. New instance is created in case
 455      *                of null.
 456      * @return The PickRay instance computed based on this camera and the given
 457      *         arguments.
 458      */
 459     abstract PickRay computePickRay(double x, double y, PickRay pickRay);
 460 
 461     /**
 462      * Computes local position of the camera in the scene.
 463      * @param position Position to be reused. New instance is created in case
 464      *                 of null.
 465      * @return The position of the camera in the scene in camera local coords
 466      */
 467     abstract Vec3d computePosition(Vec3d position);
 468 
 469     /**
 470      * @treatAsPrivate implementation detail
 471      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 472      */
 473     @Deprecated
 474     @Override
 475     public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 476         return new BoxBounds(0, 0, 0, 0, 0, 0);
 477     }
 478 
 479     /**
 480      * @treatAsPrivate implementation detail
 481      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 482      */
 483     @Deprecated
 484     @Override
 485     protected boolean impl_computeContains(double localX, double localY) {
 486         return false;
 487     }
 488 
 489     /**
 490      * @treatAsPrivate implementation detail
 491      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 492      */
 493     @Deprecated
 494     @Override
 495     public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) {
 496         throw new UnsupportedOperationException("Not supported yet.");
 497     }
 498 
 499 
 500     static {
 501          // This is used by classes in different packages to get access to
 502          // private and package private methods.
 503         CameraHelper.setCameraAccessor(new CameraHelper.CameraAccessor() {
 504 
 505             @Override
 506             public Point2D project(Camera camera, Point3D p) {
 507                 return camera.project(p);
 508             }
 509 
 510             @Override
 511             public Point2D pickNodeXYPlane(Camera camera, Node node, double x, double y) {
 512                 return camera.pickNodeXYPlane(node, x, y);
 513             }
 514 
 515             @Override
 516             public Point3D pickProjectPlane(Camera camera, double x, double y) {
 517                 return camera.pickProjectPlane(x, y);
 518             }
 519         });
 520     }
 521 }