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