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 }