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