1 /*
   2  * Copyright (c) 2010, 2017, 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 com.sun.javafx.geom.PickRay;
  29 import com.sun.javafx.geom.Vec3d;
  30 import com.sun.javafx.geom.transform.Affine3D;
  31 import com.sun.javafx.geom.transform.GeneralTransform3D;
  32 import com.sun.javafx.scene.DirtyBits;
  33 import com.sun.javafx.scene.NodeHelper;
  34 import com.sun.javafx.scene.PerspectiveCameraHelper;
  35 import com.sun.javafx.sg.prism.NGNode;
  36 import com.sun.javafx.sg.prism.NGPerspectiveCamera;
  37 import javafx.application.ConditionalFeature;
  38 import javafx.application.Platform;
  39 import javafx.beans.property.BooleanProperty;
  40 import javafx.beans.property.DoubleProperty;
  41 import javafx.beans.property.SimpleBooleanProperty;
  42 import javafx.beans.property.SimpleDoubleProperty;
  43 import sun.util.logging.PlatformLogger;
  44 
  45 
  46 
  47 /**
  48  * Specifies a perspective camera for rendering a scene.
  49  *
  50  * <p> This camera defines a viewing volume for a perspective projection;
  51  * a truncated right pyramid.
  52  * The {@code fieldOfView} value can be used to change viewing volume.
  53  * By default, this camera is located at center of the scene and looks along the
  54  * positive z-axis. The coordinate system defined by this camera has its
  55  * origin in the upper left corner of the panel with the Y-axis pointing
  56  * down and the Z axis pointing away from the viewer (into the screen).
  57  * If a {@code PerspectiveCamera} node is added to the scene graph,
  58  * the transformed position and orientation of the camera will define the
  59  * position of the camera and the direction that the camera is looking.
  60  *
  61  * <p> In the default camera, where fixedEyeAtCameraZero is false, the Z value
  62  * of the eye position is adjusted in Z such that the projection matrix generated
  63  * using the specified {@code fieldOfView} will produce units at
  64  * Z = 0 (the projection plane), in device-independent pixels, matches that of
  65  * the ParallelCamera.
  66  * When the Scene is resized,
  67  * the objects in the scene at the projection plane (Z = 0) will stay the same size,
  68  * but more or less content of the scene is viewable.
  69  *
  70  * <p> If fixedEyeAtCameraZero is true, the eye position is fixed at (0, 0, 0)
  71  * in the local coordinates of the camera. The projection matrix is generated
  72  * using the specified {@code fieldOfView} and the projection volume is mapped
  73  * onto the viewport (window) such that it is stretched over more or fewer
  74  * device-independent pixels at the projection plane.
  75  * When the Scene is resized,
  76  * the objects in the scene will shrink or grow proportionally,
  77  * but the visible portion of the content is unchanged.
  78  *
  79  * <p> We recommend setting fixedEyeAtCameraZero to true if you are going to
  80  * transform (move) the camera. Transforming the camera when fixedEyeAtCameraZero
  81  * is set to false may lead to results that are not intuitive.
  82  *
  83  * <p> Note that this is a conditional feature. See
  84  * {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D}
  85  * for more information.
  86  *
  87  * @since JavaFX 2.0
  88  */
  89 public class PerspectiveCamera extends Camera {
  90 
  91     private boolean fixedEyeAtCameraZero = false;
  92 
  93     // Lookat transform for legacy case
  94     private static final Affine3D LOOK_AT_TX = new Affine3D();
  95 
  96     // Lookat transform for fixedEyeAtCameraZero case
  97     private static final Affine3D LOOK_AT_TX_FIXED_EYE = new Affine3D();
  98 
  99     static {
 100         PerspectiveCameraHelper.setPerspectiveCameraAccessor(new PerspectiveCameraHelper.PerspectiveCameraAccessor() {
 101             @Override
 102             public NGNode doCreatePeer(Node node) {
 103                 return ((PerspectiveCamera) node).doCreatePeer();
 104             }
 105 
 106             @Override
 107             public void doUpdatePeer(Node node) {
 108                 ((PerspectiveCamera) node).doUpdatePeer();
 109             }
 110         });
 111 
 112         // Compute the legacy look at matrix such that the zero point ends up at
 113         // the z=-1 plane.
 114         LOOK_AT_TX.setToTranslation(0, 0, -1);
 115         // Y-axis pointing down
 116         LOOK_AT_TX.rotate(Math.PI, 1, 0, 0);
 117 
 118         // Compute the fixed eye at (0, 0, 0) look at matrix such that the zero point
 119         // ends up at the z=0 plane and Y-axis pointing down
 120         LOOK_AT_TX_FIXED_EYE.rotate(Math.PI, 1, 0, 0);
 121     }
 122 
 123     /**
 124      * Specifies the field of view angle of the camera's projection,
 125      * measured in degrees.
 126      *
 127      * @defaultValue 30.0
 128      */
 129     private DoubleProperty fieldOfView;
 130 
 131     public final void setFieldOfView(double value){
 132         fieldOfViewProperty().set(value);
 133     }
 134 
 135     public final double getFieldOfView() {
 136         return fieldOfView == null ? 30 : fieldOfView.get();
 137     }
 138 
 139     public final DoubleProperty fieldOfViewProperty() {
 140         if (fieldOfView == null) {
 141             fieldOfView = new SimpleDoubleProperty(PerspectiveCamera.this, "fieldOfView", 30) {
 142                 @Override
 143                 protected void invalidated() {
 144                     NodeHelper.markDirty(PerspectiveCamera.this, DirtyBits.NODE_CAMERA);
 145                 }
 146             };
 147         }
 148         return fieldOfView;
 149     }
 150 
 151     /**
 152      * Defines whether the {@code fieldOfView} property will apply to the vertical
 153      * dimension of the projection. If it is false, {@code fieldOfView} will
 154      * apply to the horizontal dimension of the projection.
 155      *
 156      * @defaultValue true
 157      * @since JavaFX 8.0
 158      */
 159     private BooleanProperty verticalFieldOfView;
 160 
 161     public final void setVerticalFieldOfView(boolean value) {
 162         verticalFieldOfViewProperty().set(value);
 163     }
 164 
 165     public final boolean isVerticalFieldOfView() {
 166         return verticalFieldOfView == null ? true : verticalFieldOfView.get();
 167     }
 168 
 169     public final BooleanProperty verticalFieldOfViewProperty() {
 170         if (verticalFieldOfView == null) {
 171             verticalFieldOfView = new SimpleBooleanProperty(PerspectiveCamera.this, "verticalFieldOfView", true) {
 172                 @Override
 173                 protected void invalidated() {
 174                     NodeHelper.markDirty(PerspectiveCamera.this, DirtyBits.NODE_CAMERA);
 175                 }
 176             };
 177         }
 178         return verticalFieldOfView;
 179     }
 180 
 181     {
 182         // To initialize the class helper at the begining each constructor of this class
 183         PerspectiveCameraHelper.initHelper(this);
 184     }
 185 
 186     /**
 187      * Creates an empty instance of PerspectiveCamera.
 188      */
 189     public PerspectiveCamera() {
 190         this(false);
 191     }
 192 
 193     /**
 194      * Constructs a PerspectiveCamera with the specified fixedEyeAtCameraZero flag.
 195      *
 196      * <p> In the default camera, where fixedEyeAtCameraZero is false, the Z value of
 197      * the eye position is adjusted in Z such that the projection matrix generated
 198      * using the specified {@code fieldOfView} will produce units at
 199      * Z = 0 (the projection plane), in device-independent pixels, matches that of
 200      * the ParallelCamera.
 201      * When the Scene is resized,
 202      * the objects in the scene at the projection plane (Z = 0) will stay the same size,
 203      * but more or less content of the scene is viewable.
 204      *
 205      * <p> If fixedEyeAtCameraZero is true, the eye position is fixed at (0, 0, 0)
 206      * in the local coordinates of the camera. The projection matrix is generated
 207      * using the specified {@code fieldOfView} and the projection volume is mapped
 208      * onto the viewport (window) such that it is stretched over more or fewer
 209      * device-independent pixels at the projection plane.
 210      * When the Scene is resized,
 211      * the objects in the scene will shrink or grow proportionally,
 212      * but the visible portion of the content is unchanged.
 213      *
 214      * <p> We recommend setting fixedEyeAtCameraZero to true if you are going to
 215      * transform (move) the camera. Transforming the camera when fixedEyeAtCameraZero
 216      * is set to false may lead to results that are not intuitive.
 217      *
 218      * @param fixedEyeAtCameraZero true if the the eye position is fixed at
 219      * (0, 0, 0) in the local coordinates of the camera.
 220      * @since JavaFX 8.0
 221      */
 222     public PerspectiveCamera(boolean fixedEyeAtCameraZero) {
 223         if (!Platform.isSupported(ConditionalFeature.SCENE3D)) {
 224             String logname = PerspectiveCamera.class.getName();
 225             PlatformLogger.getLogger(logname).warning("System can't support "
 226                     + "ConditionalFeature.SCENE3D");
 227         }
 228         this.fixedEyeAtCameraZero = fixedEyeAtCameraZero;
 229     }
 230 
 231     /**
 232      * Returns a flag indicating whether this camera uses a fixed eye position
 233      * at the origin of the camera. If {@code fixedEyeAtCameraZero} is {@code true},
 234      * the the eye position is fixed at (0, 0, 0) in the local coordinates
 235      * of the camera. This attribute is immutable.
 236      *
 237      * @return a flag indicating whether this camera uses a fixed eye position
 238      * at the origin of the camera
 239      *
 240      * @since JavaFX 8.0
 241      */
 242     public final boolean isFixedEyeAtCameraZero() {
 243         return fixedEyeAtCameraZero;
 244     }
 245 
 246     @Override
 247     final PickRay computePickRay(double x, double y, PickRay pickRay) {
 248 
 249         return PickRay.computePerspectivePickRay(x, y, fixedEyeAtCameraZero,
 250                 getViewWidth(), getViewHeight(),
 251                 Math.toRadians(getFieldOfView()), isVerticalFieldOfView(),
 252                 getCameraTransform(),
 253                 getNearClip(), getFarClip(),
 254                 pickRay);
 255     }
 256 
 257     @Override Camera copy() {
 258         PerspectiveCamera c = new PerspectiveCamera(fixedEyeAtCameraZero);
 259         c.setNearClip(getNearClip());
 260         c.setFarClip(getFarClip());
 261         c.setFieldOfView(getFieldOfView());
 262         return c;
 263     }
 264 
 265     /*
 266      * Note: This method MUST only be called via its accessor method.
 267      */
 268     private NGNode doCreatePeer() {
 269         NGPerspectiveCamera peer = new NGPerspectiveCamera(fixedEyeAtCameraZero);
 270         peer.setNearClip((float) getNearClip());
 271         peer.setFarClip((float) getFarClip());
 272         peer.setFieldOfView((float) getFieldOfView());
 273         return peer;
 274     }
 275 
 276     /*
 277      * Note: This method MUST only be called via its accessor method.
 278      */
 279     private void doUpdatePeer() {
 280         NGPerspectiveCamera pgPerspectiveCamera = getPeer();
 281         if (isDirty(DirtyBits.NODE_CAMERA)) {
 282             pgPerspectiveCamera.setVerticalFieldOfView(isVerticalFieldOfView());
 283             pgPerspectiveCamera.setFieldOfView((float) getFieldOfView());
 284         }
 285     }
 286 
 287     @Override
 288     void computeProjectionTransform(GeneralTransform3D proj) {
 289         proj.perspective(isVerticalFieldOfView(), Math.toRadians(getFieldOfView()),
 290                 getViewWidth() / getViewHeight(), getNearClip(), getFarClip());
 291     }
 292 
 293     @Override
 294     void computeViewTransform(Affine3D view) {
 295 
 296         // In the case of fixedEyeAtCameraZero the camera position is (0,0,0) in
 297         // local coord. of the camera node. In non-fixed eye case, the camera
 298         // position is (w/2, h/2, h/2/tan) in local coord. of the camera.
 299         if (isFixedEyeAtCameraZero()) {
 300             view.setTransform(LOOK_AT_TX_FIXED_EYE);
 301         } else {
 302             final double viewWidth = getViewWidth();
 303             final double viewHeight = getViewHeight();
 304             final boolean verticalFOV = isVerticalFieldOfView();
 305 
 306             final double aspect = viewWidth / viewHeight;
 307             final double tanOfHalfFOV = Math.tan(Math.toRadians(getFieldOfView()) / 2.0);
 308 
 309             // Translate the zero point to the upper-left corner
 310             final double xOffset = -tanOfHalfFOV * (verticalFOV ? aspect : 1.0);
 311             final double yOffset = tanOfHalfFOV * (verticalFOV ? 1.0 : 1.0 / aspect);
 312 
 313             // Compute scale factor as 2/viewport.width or height, after adjusting for fov
 314             final double scale = 2.0 * tanOfHalfFOV /
 315                     (verticalFOV ? viewHeight : viewWidth);
 316 
 317             view.setToTranslation(xOffset, yOffset, 0.0);
 318             view.concatenate(LOOK_AT_TX);
 319             view.scale(scale, scale, scale);
 320         }
 321     }
 322 
 323     @Override
 324     Vec3d computePosition(Vec3d position) {
 325         if (position == null) {
 326             position = new Vec3d();
 327         }
 328 
 329         if (fixedEyeAtCameraZero) {
 330             position.set(0.0, 0.0, 0.0);
 331         } else {
 332             final double halfViewWidth = getViewWidth() / 2.0;
 333             final double halfViewHeight = getViewHeight() / 2.0;
 334             final double halfViewDim = isVerticalFieldOfView()
 335                     ? halfViewHeight : halfViewWidth;
 336             final double distanceZ = halfViewDim
 337                     / Math.tan(Math.toRadians(getFieldOfView() / 2.0));
 338 
 339             position.set(halfViewWidth, halfViewHeight, -distanceZ);
 340         }
 341         return position;
 342     }
 343 }