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