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 }