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 }