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