001/* =========================================================== 002 * Orson Charts : a 3D chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C)opyright 2013-2022, by David Gilbert. All rights reserved. 006 * 007 * https://github.com/jfree/orson-charts 008 * 009 * This program is free software: you can redistribute it and/or modify 010 * it under the terms of the GNU General Public License as published by 011 * the Free Software Foundation, either version 3 of the License, or 012 * (at your option) any later version. 013 * 014 * This program is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 017 * GNU General Public License for more details. 018 * 019 * You should have received a copy of the GNU General Public License 020 * along with this program. If not, see <http://www.gnu.org/licenses/>. 021 * 022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 023 * Other names may be trademarks of their respective owners.] 024 * 025 * If you do not wish to be bound by the terms of the GPL, an alternative 026 * commercial license can be purchased. For details, please see visit the 027 * Orson Charts home page: 028 * 029 * http://www.object-refinery.com/orsoncharts/index.html 030 * 031 */ 032 033package org.jfree.chart3d.graphics3d; 034 035import java.awt.Color; 036import java.awt.Dimension; 037import java.awt.geom.Dimension2D; 038import java.awt.geom.Point2D; 039import java.io.Serializable; 040import org.jfree.chart3d.graphics3d.internal.Utils2D; 041import org.jfree.chart3d.graphics3d.internal.Utils3D; 042 043/** 044 * Specifies the location and orientation of the view point in 3D space. 045 * Assumes the eye looks towards the origin in world coordinates. 046 * <br><br> 047 * There are four basic operations to move the view point: 048 * <ul> 049 * <li>{@link #panLeftRight(double)} - rotates around the scene horizontally 050 * from the perspective of the viewer;</li> 051 * <li>{@link #moveUpDown(double)} - rotates around the scene vertically from 052 * the perspective of the viewer;</li> 053 * <li>{@link #roll(double)} - maintains the same viewing location but rolls 054 * by the specified angle (like tilting a camera);</li> 055 * <li>{@link #setRho(double)} - sets the distance of the view location from 056 * the center of the 3D scene (zoom in and out).</li> 057 * </ul> 058 * <br><br> 059 * NOTE: This class is serializable, but the serialization format is subject 060 * to change in future releases and should not be relied upon for persisting 061 * instances of this class. 062 */ 063@SuppressWarnings("serial") 064public class ViewPoint3D implements Serializable { 065 066 /** 067 * Creates and returns a view point for looking at a chart from the 068 * front and above. 069 * 070 * @param rho the distance. 071 * 072 * @return A view point. 073 */ 074 public static ViewPoint3D createAboveViewPoint(double rho) { 075 return new ViewPoint3D(-Math.PI / 2, 9 * Math.PI / 8, rho, 0); 076 } 077 078 /** 079 * Creates and returns a view point for looking at a chart from the 080 * front and above and to the left. 081 * 082 * @param rho the distance. 083 * 084 * @return A view point. 085 */ 086 public static ViewPoint3D createAboveLeftViewPoint(double rho) { 087 ViewPoint3D vp = createAboveViewPoint(rho); 088 vp.panLeftRight(-Math.PI / 6); 089 return vp; 090 } 091 092 /** 093 * Creates and returns a view point for looking at a chart from the 094 * front and above and to the right. 095 * 096 * @param rho the distance. 097 * 098 * @return A view point. 099 */ 100 public static ViewPoint3D createAboveRightViewPoint(double rho) { 101 ViewPoint3D vp = createAboveViewPoint(rho); 102 vp.panLeftRight(Math.PI / 6); 103 return vp; 104 } 105 106 /** The rotation of the viewing point from the x-axis around the z-axis. */ 107 private double theta; 108 109 /** The rotation (up and down) of the viewing point. */ 110 private double phi; 111 112 /** The distance of the viewing point from the origin. */ 113 private double rho; 114 115 /** Transformation matrix elements. */ 116 private double v11, v12, v13, v21, v22, v23, v32, v33, v43; 117 118 /** 119 * A point 1/4 turn "upwards" on the sphere, to define the camera 120 * orientation. 121 */ 122 private Point3D up; 123 124 /** Applies the rotation for the orientation of the view. */ 125 private Rotate3D rotation; 126 127 /** A workspace for calling the Rotate3D class. */ 128 private double[] workspace; 129 130 /** 131 * Creates a new viewing point. 132 * 133 * @param theta the rotation of the viewing point from the x-axis around 134 * the z-axis (in radians) 135 * @param phi the rotation of the viewing point up and down (from the 136 * XZ plane, in radians) 137 * @param rho the distance of the viewing point from the origin. 138 * @param orientation the angle of rotation. 139 */ 140 public ViewPoint3D(double theta, double phi, double rho, 141 double orientation) { 142 this.theta = theta; 143 this.phi = phi; 144 this.rho = rho; 145 updateMatrixElements(); 146 this.rotation = new Rotate3D(Point3D.ORIGIN, Point3D.UNIT_Z, 147 orientation); 148 this.up = this.rotation.applyRotation(Point3D.createPoint3D(this.theta, 149 this.phi - Math.PI / 2, this.rho)); 150 this.workspace = new double[3]; 151 } 152 153 /** 154 * Creates a new instance using the specified point and orientation. 155 * 156 * @param p the viewing point. 157 * @param orientation the orientation. 158 */ 159 public ViewPoint3D(Point3D p, double orientation) { 160 this.rho = (float) Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z); 161 if (Math.sqrt(p.x * p.x + p.y * p.y) > 0.000001) { 162 this.theta = (float) Math.atan2(p.y, p.x); 163 } 164 this.phi = (float) Math.acos(p.z / this.rho); 165 updateMatrixElements(); 166 this.rotation = new Rotate3D( Point3D.ORIGIN, Point3D.UNIT_Z, 167 orientation); 168 this.up = this.rotation.applyRotation(Point3D.createPoint3D(this.theta, 169 this.phi - Math.PI / 2, this.rho)); 170 this.workspace = new double[3]; 171 } 172 173 /** 174 * Creates a new instance that is an exact copy of the supplied viewpoint. 175 * 176 * @param vp the view point ({@code null} not permitted). 177 * 178 * @since 1.6.1 179 */ 180 public ViewPoint3D(ViewPoint3D vp) { 181 this.theta = vp.theta; 182 this.phi = vp.phi; 183 this.rho = vp.rho; 184 updateMatrixElements(); 185 this.rotation = new Rotate3D(Point3D.ORIGIN, Point3D.UNIT_Z, 186 vp.rotation.angle); 187 this.up = vp.up; 188 this.workspace = new double[3]; 189 } 190 191 /** 192 * Returns the angle of rotation from the x-axis about the z-axis, 193 * in radians. This attribute is set via the constructor and updated 194 * via the {@link #panLeftRight(double)} and {@link #moveUpDown(double)} 195 * methods - there is no setter method, you cannot update it directly. 196 * 197 * @return The angle (in radians). 198 */ 199 public final double getTheta() { 200 return this.theta; 201 } 202 203 /** 204 * Returns the angle of the viewing point down from the z-axis. This 205 * attribute is set via the constructor and updated via the 206 * {@link #panLeftRight(double)} and {@link #moveUpDown(double)} methods 207 * - there is no setter method, you cannot update it directly. 208 * 209 * @return The angle of the viewing point down from the z-axis. 210 * (in radians). 211 */ 212 public final double getPhi() { 213 return this.phi; 214 } 215 216 /** 217 * Returns the distance of the viewing point from the origin. 218 * 219 * @return The distance of the viewing point from the origin. 220 * 221 * @see #setRho(double) 222 */ 223 public final double getRho() { 224 return this.rho; 225 } 226 227 /** 228 * Sets the distance of the viewing point from the origin. 229 * 230 * @param rho the new distance. 231 */ 232 public void setRho(double rho) { 233 this.rho = rho; 234 this.up = Point3D.createPoint3D(this.up.getTheta(), this.up.getPhi(), 235 rho); 236 updateMatrixElements(); 237 } 238 239 /** 240 * Returns the x-coordinate of the viewing point. This value is 241 * calculated from the spherical coordinates. 242 * 243 * @return The x-coordinate of the viewing point. 244 */ 245 public final double getX() { 246 return this.rho * Math.sin(this.phi) * Math.cos(this.theta); 247 } 248 249 /** 250 * Returns the y-coordinate of the viewing point. This value is 251 * calculated from the spherical coordinates. 252 * 253 * @return The y-coordinate of the viewing point. 254 */ 255 public final double getY() { 256 return this.rho * Math.sin(this.phi) * Math.sin(this.theta); 257 } 258 259 /** 260 * Returns the z-coordinate of the viewing point. This value is 261 * calculated from the spherical coordinates. 262 * 263 * @return The z-coordinate of the viewing point. 264 */ 265 public final double getZ() { 266 return this.rho * Math.cos(this.phi); 267 } 268 269 /** 270 * Returns the location of the view point. Note that a new instance of 271 * {@code Point3D} is created each time this method is called. 272 * 273 * @return The viewing point (never {@code null}). 274 */ 275 public final Point3D getPoint() { 276 return new Point3D(getX(), getY(), getZ()); 277 } 278 279 /** 280 * Returns the roll angle (orientation) for the view point. This is 281 * calculated by reference to second point on the sphere that is a 282 * quarter turn from the view point location (this second point defines 283 * the "up" direction for the view). 284 * 285 * @return The roll angle (in radians). 286 */ 287 public double calcRollAngle() { 288 Point3D vp = getPoint(); 289 Point3D n1 = Utils3D.normal(vp, this.up, Point3D.ORIGIN); 290 Point3D screenup = Point3D.createPoint3D(this.theta, 291 this.phi - (Math.PI / 2), this.rho); 292 Point3D n2 = Utils3D.normal(vp, screenup, Point3D.ORIGIN); 293 double angle = Utils3D.angle(n1, n2); 294 if (Utils3D.scalarprod(n1, screenup) >= 0.0) { 295 return angle; 296 } else { 297 return -angle; 298 } 299 } 300 301 /** 302 * Moves the viewing point left or right around the 3D scene. 303 * 304 * @param delta the angle (in radians). 305 */ 306 public void panLeftRight(double delta) { 307 Point3D v = getVerticalRotationAxis(); 308 Rotate3D r = new Rotate3D(Point3D.ORIGIN, v, delta); 309 Point3D p = r.applyRotation(getX(), getY(), getZ()); 310 this.theta = p.getTheta(); 311 this.phi = p.getPhi(); 312 updateMatrixElements(); 313 this.rotation.setAngle(calcRollAngle()); 314 } 315 316 /** 317 * Moves the viewing point up or down on the viewing sphere. 318 * 319 * @param delta the angle delta (in radians). 320 */ 321 public void moveUpDown(double delta) { 322 Point3D v = getHorizontalRotationAxis(); 323 Rotate3D r = new Rotate3D(Point3D.ORIGIN, v, delta); 324 Point3D p = r.applyRotation(getX(), getY(), getZ()); 325 this.up = r.applyRotation(this.up); 326 this.theta = p.getTheta(); 327 this.phi = p.getPhi(); 328 updateMatrixElements(); 329 this.rotation.setAngle(calcRollAngle()); 330 } 331 332 /** 333 * Rolls the view while leaving the location of the view point unchanged. 334 * 335 * @param delta the angle (in radians). 336 */ 337 public void roll(double delta) { 338 // we rotate the "up" point around the sphere by delta radians 339 Rotate3D r = new Rotate3D(getPoint(), Point3D.ORIGIN, delta); 340 this.up = r.applyRotation(this.up); 341 this.rotation.setAngle(calcRollAngle()); 342 } 343 344 /** 345 * Converts a point in world coordinates to a point in eye coordinates. 346 * 347 * @param p the point ({@code null} not permitted). 348 * 349 * @return The point in eye coordinates. 350 */ 351 public Point3D worldToEye(Point3D p) { 352 double x = this.v11 * p.x + this.v21 * p.y; 353 double y = this.v12 * p.x + this.v22 * p.y + this.v32 * p.z; 354 double z = this.v13 * p.x + this.v23 * p.y + this.v33 * p.z + this.v43; 355 double[] rotated = this.rotation.applyRotation(x, y, z, this.workspace); 356 return new Point3D(rotated[0], rotated[1], rotated[2]); 357 } 358 359 /** 360 * Calculates and returns the screen coordinates for the specified point 361 * in (world) 3D space. 362 * 363 * @param p the point. 364 * @param d the projection distance. 365 * 366 * @return The screen coordinate. 367 */ 368 public Point2D worldToScreen(Point3D p, double d) { 369 double x = this.v11 * p.x + this.v21 * p.y; 370 double y = this.v12 * p.x + this.v22 * p.y + this.v32 * p.z; 371 double z = this.v13 * p.x + this.v23 * p.y + this.v33 * p.z + this.v43; 372 double[] rotated = this.rotation.applyRotation(x, y, z, this.workspace); 373 return new Point2D.Double(-d * rotated[0] / rotated[2], 374 -d * rotated[1] / rotated[2]); 375 } 376 377 /** 378 * Calculate the distance that would render a box of the given dimensions 379 * within a screen area of the specified size. 380 * 381 * @param target the target dimension ({@code null} not permitted). 382 * @param dim3D the dimensions of the 3D content ({@code null} not 383 * permitted). 384 * @param projDist the projection distance. 385 * 386 * @return The optimal viewing distance. 387 */ 388 public float optimalDistance(Dimension2D target, Dimension3D dim3D, 389 double projDist) { 390 391 ViewPoint3D vp = new ViewPoint3D(this.theta, this.phi, this.rho, 392 calcRollAngle()); 393 float near = (float) dim3D.getDiagonalLength(); 394 float far = near * 40; 395 396 World w = new World(); 397 double ww = dim3D.getWidth(); 398 double hh = dim3D.getHeight(); 399 double dd = dim3D.getDepth(); 400 w.add(Object3D.createBox(0, ww, 0, hh, 0, dd, Color.RED)); 401 402 while (true) { 403 vp.setRho(near); 404 Point2D[] nearpts = w.calculateProjectedPoints(vp, projDist); 405 Dimension neardim = Utils2D.findDimension(nearpts); 406 double nearcover = coverage(neardim, target); 407 vp.setRho(far); 408 Point2D[] farpts = w.calculateProjectedPoints(vp, projDist); 409 Dimension fardim = Utils2D.findDimension(farpts); 410 double farcover = coverage(fardim, target); 411 if (nearcover <= 1.0) { 412 return near; 413 } 414 if (farcover >= 1.0) { 415 return far; 416 } 417 // bisect near and far until we get close enough to the specified 418 // dimension 419 float mid = (near + far) / 2.0f; 420 vp.setRho(mid); 421 Point2D[] midpts = w.calculateProjectedPoints(vp, projDist); 422 Dimension middim = Utils2D.findDimension(midpts); 423 double midcover = coverage(middim, target); 424 if (midcover >= 1.0) { 425 near = mid; 426 } else { 427 far = mid; 428 } 429 } 430 } 431 432 private double coverage(Dimension2D d, Dimension2D target) { 433 double wpercent = d.getWidth() / target.getWidth(); 434 double hpercent = d.getHeight() / target.getHeight(); 435 if (wpercent <= 1.0 && hpercent <= 1.0) { 436 return Math.max(wpercent, hpercent); 437 } else { 438 if (wpercent >= 1.0) { 439 if (hpercent >= 1.0) { 440 return Math.max(wpercent, hpercent); 441 } else { 442 return wpercent; 443 } 444 } else { 445 return hpercent; // don't think it will matter 446 } 447 } 448 } 449 450 /** 451 * Updates the matrix elements. 452 */ 453 private void updateMatrixElements() { 454 float cosTheta = (float) Math.cos(this.theta); 455 float sinTheta = (float) Math.sin(this.theta); 456 float cosPhi = (float) Math.cos(this.phi); 457 float sinPhi = (float) Math.sin(this.phi); 458 this.v11 = -sinTheta; 459 this.v12 = -cosPhi * cosTheta; 460 this.v13 = sinPhi * cosTheta; 461 this.v21 = cosTheta; 462 this.v22 = -cosPhi * sinTheta; 463 this.v23 = sinPhi * sinTheta; 464 this.v32 = sinPhi; 465 this.v33 = cosPhi; 466 this.v43 = -this.rho; 467 } 468 469 470 /** 471 * Returns the vector that points "up" in relation to the orientation of 472 * the view point. This vector can be used to rotate the viewing point 473 * around the 3D scene (pan left / right). 474 * 475 * @return The vector (never {@code null}). 476 */ 477 public Point3D getVerticalRotationAxis() { 478 return this.up; 479 } 480 481 /** 482 * Returns a vector at right angles to the viewing direction and the "up" 483 * vector (this axis can be used to rotate forward and backwards). 484 * 485 * @return A vector (never {@code null}). 486 */ 487 public Point3D getHorizontalRotationAxis() { 488 return Utils3D.normal(getPoint(), this.up, Point3D.ORIGIN); 489 } 490 491 /** 492 * Returns a string representation of this instance, primarily for 493 * debugging purposes. 494 * 495 * @return A string. 496 */ 497 @Override 498 public String toString() { 499 return "[theta=" + this.theta + ", phi=" + this.phi + ", rho=" 500 + this.rho + "]"; 501 } 502 503 /** 504 * Tests this view point for equality with an arbitrary object. 505 * 506 * @param obj the object ({@code null} permitted). 507 * 508 * @return A boolean. 509 */ 510 @Override 511 public boolean equals(Object obj) { 512 if (obj == this) { 513 return true; 514 } 515 if (!(obj instanceof ViewPoint3D)) { 516 return false; 517 } 518 ViewPoint3D that = (ViewPoint3D) obj; 519 if (this.theta != that.theta) { 520 return false; 521 } 522 if (this.phi != that.phi) { 523 return false; 524 } 525 if (this.rho != that.rho) { 526 return false; 527 } 528 if (!this.up.equals(that.up)) { 529 return false; 530 } 531 return true; 532 } 533 534}