/* This file is part of the source code for 3D-XplorMath-J, Version 1.0 (January 2008). * Copyright (c) 2008 The 3D-XplorMath Consortium (http://3d-xplormath.org). * This source code is released under a BSD License, which allows redistribution * in source and binary form, with or without modification, provided copyright * and license information are included, and with no warranty or guarantee of * any kind. For details, see http://3d-xplormath.org/j/source/BSDLicense.txt */ package vmm.core3D; import java.awt.Graphics2D; import java.awt.geom.Point2D; import vmm.core.Transform; import vmm.core.VMMSave; /** * A transform that encodes a 3D view of an exhibit and the projection of that view onto a 2D view plane. * This class inherits the window-to-viewport transformation from its superclass, and adds a 3D viewing * transformation. *

The projection is defined by three unit vectors: ViewDirection, ImagePlaneYDirection, * and ImagePlaneXDirection. The ViewDirection is determined by the viewpoint that is specified for the * view; ViewDirection is a unit vector that points in the direction from the viewpoint towards (0,0,0). * The ViewDirection determines the viewing plane, which is a plane through (0,0,0) normal to the view * direction. The ImagePlaneYDirection vector is obtained by projecting a "view up" vector onto the * viewing plane, and then normalizing the projection. Initially, the view up vector is (0,0,1) * or, if (0,0,1) almost normal to the view plane, (0,1,0). When the viewpoint is modified, the new * ImagePlaneYDirection is obtained by using the previous ImagePlaneYDirection as the view up vector, if * possible. After the ViewDirection and ImagePlaneYDirection are determined, the ImagePlaneXDirection * is simply the cross product of these two vectors. *

The transform also has an associated "focalLength" which is set to the distance of the viewpoint * from the origin, and a clipDistance, which is set to 25% of the focal length. *

A Transform3D fires a ChangeEvent when any part of the data that determines a 3D view is changed, * such as the viewpoint. *

(Note that in the default view, the yz-plane projects onto the screen, and the x-axis is perpendicular * to the screen. Nevertheless, the screen is thought of as being the xy-plane.) * TODO: Possibly add support for using a point other than (0,0,0) as the view reference point; check how * clip distance and focal length should be changed when viewpoint is changed. */ public class Transform3D extends Transform { @VMMSave private Vector3D viewPoint; @VMMSave private double clipDistance; // initially set to 0.25*focalLength whenever viewPoint changes, but can be reset @VMMSave private Vector3D imagePlaneYDirection = new Vector3D(0,0,1); // changed automatically when viewPoint changes, but can be reset @VMMSave private boolean orthographicProjection = false; private Vector3D viewDirection; private double focalLength; // always set to norm(viewPoint). Is this correct? private Vector3D imagePlaneXDirection; private Vector3D tempVector = new Vector3D(); // for use in some methods (note: not thread-safe!) private Vector3D saveViewDirection, saveImagePlaneXDirection, saveImagePlaneYDirection, saveViewPoint; // These are the values as set, witout modification for left/right eye. private double objectDisplacementNormalToScreen; // Object coordinates are displaced by this amount into or out of the screeen. /** * Creates a Transform3D object with default viewpoint (20,0,0) and a default window with x and y ranges from -5 to 5. */ public Transform3D() { this(null,-5,5,-5,5); } /** * Creates a Transform3D object with a specified viewpoint and a default window with x and y ranges from -5 to 5. * @param viewPoint the viewpoint of the transformation. If null, the default, (20,0,0), is used. */ public Transform3D(Vector3D viewPoint) { this(viewPoint,-5,5,-5,5); } /** * Creates a Transform3D with a specifed viewpoint and with an xy-window determined by a given "graphic scale" * @param viewPoint the viewpoint of the transformation. If null, the default, (20,0,0), is used. * @param nominalGraphicScale number of pixels per unit along the x- and y-axes, assuming that the size of * the window is "normal". The normal size is given by a private constant NORMAL_SIZE, which * is set to 600 at the time this comment was written. */ public Transform3D(Vector3D viewPoint, double nominalGraphicScale) { super(nominalGraphicScale); setViewPoint(viewPoint); } /** * Construct a Transform3D with specified viewpoint and xy-window. The transformation is not * fully determined until a viewport in the viewing plane is also specified; this is done when * {@link Transform#setUpDrawInfo(Graphics2D, int, int, int, int, boolean, boolean)} is called. * Note that when that method is called with its preserveAspect option set to true, the * requested xmin, xmax, ymin, and ymax values might be adjusted to make the aspect ratio of * the xy-window match the aspect ratio of the viewport. * @param viewPoint The viewpoint of the transformation; if null, the default, (20,0,0), is used. * @param xmin the requested mimimum x-value for the transformation * @param xmax the requested maximum x-value for the transformation * @param ymin the requested mimimum y-value for the transformation * @param ymax the requested maximum y-value for the transformation */ public Transform3D(Vector3D viewPoint, double xmin, double xmax, double ymin, double ymax) { super(xmin,xmax,ymin,ymax); setViewPoint(viewPoint); } /** * Construct a Transform3D with the same transform data as a specified transform. If the specfied * transform is not a Transform3D, only the xy-window is copied; if it is a Transform3D, the 3D transformation * data is also copied. * @param tr the non-null transform whose data is to be copied. */ public Transform3D(Transform tr) { super(tr); if (tr instanceof Transform3D) { Transform3D tr3 = (Transform3D)tr; viewPoint = new Vector3D(tr3.viewPoint); viewDirection = new Vector3D(tr3.viewDirection); imagePlaneXDirection = new Vector3D(tr3.imagePlaneXDirection); imagePlaneYDirection = new Vector3D(tr3.imagePlaneYDirection); orthographicProjection = tr3.orthographicProjection; clipDistance = tr3.clipDistance; focalLength = tr3.focalLength; } } /** * Returns true if the projection is an orthographic projection, or false if it is a perspective projection. * @see #setOrthographicProjection(boolean) */ public boolean getOrthographicProjection() { return orthographicProjection; } /** * Sets whether the projection from 3D onto the viewplane should be a perspective projection or * an orthographic projection. * @param orthographicProjection "true" to use an orthographic projection; "false" to use a perspective projection. */ public void setOrthographicProjection(boolean orthographicProjection) { if (this.orthographicProjection == orthographicProjection) return; this.orthographicProjection = orthographicProjection; fireTransformChangeEvent(); } /** * Sets the viewpoint of the transformation. Note that when this method is called, * a new transformation is defined which has a viewDirection specified by the viewpoint. * This method then sets ImagePlaneYDirection and ImagePlaneXDirection by * calling {@link #setImagePlaneYDirection(Vector3D)} with the current ImagePlaneYDirection * as parameter. In addition, the focal length is set to the distance of the viewpoint * from (0,0,0), the clip distance is set to 0.25 times the focal length, and the * ViewDirection is set to be a unit vector that points from the viewpoint towards (0,0,0). * @param viewPoint the new viewpoint; if null, the default, (20,0,0), is used. * The viewpoint cannot be (0,0,0), or the result will be an undefined transformation. * Note that a copy of the viewPoint parameter is made (if it is non-null). * (Note: Also turns off left/right eye selection, if in use.) */ public void setViewPoint(Vector3D viewPoint) { if (viewPoint == null) this.viewPoint = viewPoint = new Vector3D(20,0,0); else this.viewPoint = new Vector3D(viewPoint); viewDirection = new Vector3D(this.viewPoint); viewDirection.normalize(); viewDirection.negate(); focalLength = viewPoint.norm(); clipDistance = 0.25*focalLength; saveViewPoint = new Vector3D(viewPoint); saveViewDirection = new Vector3D(viewDirection); setImagePlaneYDirection(imagePlaneYDirection); // calls fireTransformChangeEvent() } /** * Returns the current viewpoint of the transformation. * @return a non-null vector that is the curent viewpoint. */ public Vector3D getViewPoint() { return new Vector3D(viewPoint); } /** * Returns the clip distance. * @see #setClipDistance(double) */ public double getClipDistance() { return clipDistance; } /** * Set the clip distance. This value is also set when the viewpoint is modified by * {@link #setViewPoint(Vector3D)}. That method sets the clipDistance to 0.25 times the distance * from the viewpoint to the origin. */ public void setClipDistance(double clipDistance) { if (this.clipDistance == clipDistance) return; this.clipDistance = clipDistance; fireTransformChangeEvent(); } /** * Gets the focal length, which is just the distance from the viewpoint to the origin. */ public double getFocalLength() { return focalLength; } /** * Get the amount by which object coordinates are displaed normal to the screen. * @see #setObjectDisplacementNormalToScreen(double) */ public double getObjectDisplacementNormalToScreen() { return objectDisplacementNormalToScreen; } /** * Set the amount by which object coordinates are displaed normal to the screen. * Value is clamped so that its absolute value is less than or equal to 3/4 * of the focal length. * Before the object-to-view coordinate transformation is applied to any point, * that point is displaced in the direction of the viewDirection vector by an * amount equal to the setting of objectDisplacementNormalToScreen. The * default value is zero. (In View3D, the amount is always 0 in monocular * view modes, but can be chaned for the stereo views. BasicMouseTask3D allows the * user to adjust this value for anaglyph views by holding down the shift key * while dragging with the middle mouse button or by holding down the shift and * option/ALT keys while dragging.) A ChangeEvent is generated when the value * of this property is changed. */ public void setObjectDisplacementNormalToScreen( double objectDisplacementNormalToScreen) { if (objectDisplacementNormalToScreen < -0.75*focalLength) objectDisplacementNormalToScreen = -0.75*focalLength; else if (objectDisplacementNormalToScreen > 0.75*focalLength) objectDisplacementNormalToScreen = 0.75*focalLength; if (this.objectDisplacementNormalToScreen == objectDisplacementNormalToScreen) return; this.objectDisplacementNormalToScreen = objectDisplacementNormalToScreen; System.out.println("ObjectDisplacementNormalToScreen = " + objectDisplacementNormalToScreen); fireTransformChangeEvent(); } /** * Gets the current ImagePlaneXDirection, one of the three unit vectors that determine the projection. * @see #setImagePlaneYDirection(Vector3D) */ public Vector3D getImagePlaneXDirection() { return new Vector3D(imagePlaneXDirection); } /** * Gets the current ImagePlaneYDirection, one of the three unit vectors that determine the projection. * @see #setImagePlaneYDirection(Vector3D) */ public Vector3D getImagePlaneYDirection() { return new Vector3D(imagePlaneYDirection); } /** * Gets the current ViewDirection, one of the three unit vectors that determine the projection. * @see #setViewPoint(Vector3D) * @see #setImagePlaneYDirection(Vector3D) */ public Vector3D getViewDirection() { return new Vector3D(viewDirection); } /** * Sets both the imagePlaneYDirection and imagePlaneXDirection so that these two vectors * and the viewDirection (which points to the viewpoint) form an orthonomal system. * (Note: Also turns off left/right eye selection, if in use.) * @param viewUp the new imagePlaneYDirection, which will point upwards on the screen. */ public void setImagePlaneYDirection(Vector3D viewUp) { viewDirection = new Vector3D(saveViewDirection); // Just in case a left or right eye view is selected -- it is unselected after this method executes viewPoint = new Vector3D(saveViewPoint); double projection = viewDirection.dot(viewUp); imagePlaneYDirection = new Vector3D(viewUp.x - projection*viewDirection.x, viewUp.y - projection*viewDirection.y, viewUp.z - projection*viewDirection.z); // imagePlaneYDirection = viewUp - (viewDirection dot viewUp)*viewDirection -- projection of viewUp onto image plane if (imagePlaneYDirection.norm() < 0.00001) // try (0,0,1) as the default ( viewDirection.dot(0,0,1) is just viewDirection.z ) imagePlaneYDirection = new Vector3D(-viewDirection.z*viewDirection.x, -viewDirection.z*viewDirection.y, 1-viewDirection.z*viewDirection.z); if (imagePlaneYDirection.norm() < 0.00001) // try (0,1,0 if both viewUp and (0,0,1) fail (because they are both too close to being multiples of viewDirection) imagePlaneYDirection = new Vector3D(-viewDirection.y*viewDirection.x, 1-viewDirection.y*viewDirection.y, -viewDirection.y*viewDirection.z); imagePlaneYDirection.normalize(); imagePlaneXDirection = viewDirection.cross(imagePlaneYDirection); saveImagePlaneXDirection = new Vector3D(imagePlaneXDirection); saveImagePlaneYDirection = new Vector3D(imagePlaneYDirection); fireTransformChangeEvent(); } /** * Rotates vector e1 onto vector e2, resulting in a change of view. A ChangeEvent is generated */ public void applyTransvection(Vector3D e1, Vector3D e2) { // These must be unit vectors! Vector3D e = new Vector3D(e1.x+e2.x, e1.y+e2.y, e1.z+e2.z); e.normalize(); Vector3D temp = new Vector3D(); reflectInAxis(e,saveViewDirection,temp); reflectInAxis(e1,temp,saveViewDirection); reflectInAxis(e,saveImagePlaneXDirection,temp); reflectInAxis(e1,temp,saveImagePlaneXDirection); reflectInAxis(e,saveImagePlaneYDirection,temp); reflectInAxis(e1,temp,saveImagePlaneYDirection); double vn = saveViewPoint.norm(); saveViewPoint.x = -vn*saveViewDirection.x; saveViewPoint.y = -vn*saveViewDirection.y; saveViewPoint.z = -vn*saveViewDirection.z; selectNoEye(); fireTransformChangeEvent(); } private void doTransvection(Vector3D e1, Vector3D e2) { Vector3D e = new Vector3D(e1.x+e2.x, e1.y+e2.y, e1.z+e2.z); e.normalize(); Vector3D temp = new Vector3D(); reflectInAxis(e,viewDirection,temp); reflectInAxis(e1,temp,viewDirection); reflectInAxis(e,imagePlaneXDirection,temp); reflectInAxis(e1,temp,imagePlaneXDirection); reflectInAxis(e,imagePlaneYDirection,temp); reflectInAxis(e1,temp,imagePlaneYDirection); double vn = viewPoint.norm(); viewPoint.x = -vn*viewDirection.x; viewPoint.y = -vn*viewDirection.y; viewPoint.z = -vn*viewDirection.z; } private void reflectInAxis(Vector3D axis, Vector3D source, Vector3D destination) { double s = 2 * (axis.x * source.x + axis.y * source.y + axis.z * source.z); destination.x = s*axis.x - source.x; destination.y = s*axis.y - source.y; destination.z = s*axis.z - source.z; } /** * Changes the view data to reflect a view from the left eye position. * No change event is generated. This package-private method is intended for use in View3D to * support stereographic viewing. */ void selectLeftEye(double separationFactor) { viewDirection = new Vector3D(saveViewDirection); imagePlaneXDirection = new Vector3D(saveImagePlaneXDirection); imagePlaneYDirection = new Vector3D(saveImagePlaneYDirection); viewPoint = new Vector3D(saveViewPoint); Vector3D angularEyeSeparation = imagePlaneXDirection.times(separationFactor); Vector3D leftEyeRay = viewDirection.plus(angularEyeSeparation); leftEyeRay.normalize(); doTransvection(viewDirection,leftEyeRay); } /** * Changes the view data to reflect a view from the right eye position. * No change event is generated. This package-private method is intended for use in View3D to * support stereographic viewing. */ void selectRightEye(double separationFactor) { viewDirection = new Vector3D(saveViewDirection); imagePlaneXDirection = new Vector3D(saveImagePlaneXDirection); imagePlaneYDirection = new Vector3D(saveImagePlaneYDirection); viewPoint = new Vector3D(saveViewPoint); Vector3D angularEyeSeparation = imagePlaneXDirection.times(separationFactor); Vector3D leftEyeRay = viewDirection.minus(angularEyeSeparation); leftEyeRay.normalize(); doTransvection(viewDirection,leftEyeRay); } /** * Changes the view data to reflect non-sterographic viewing. * No change event is generated. This package-private method is intended for use in View3D to * support stereographic viewing. */ void selectNoEye() { viewDirection = new Vector3D(saveViewDirection); imagePlaneXDirection = new Vector3D(saveImagePlaneXDirection); imagePlaneYDirection = new Vector3D(saveImagePlaneYDirection); viewPoint = new Vector3D(saveViewPoint); } /** * Tests whether obj is a Transform3D with the same transform data as this transform, * that is, both {@link Transform#hasSameViewTransform(Transform)} and * {@link #hasSameProjection(Transform3D)} are true. */ public boolean equals(Object obj) { if (obj == null || !Transform3D.class.equals(obj.getClass()) ) return false; Transform3D tr = (Transform3D)obj; return hasSameProjection(tr) && hasSameViewTransform(tr); } /** * Tests whether tr has the same projection from 3D to the view plane as this transform * (but not necessarily the same window and viewport). */ public boolean hasSameProjection(Transform3D tr) { if (tr == null) return false; return ( orthographicProjection == tr.orthographicProjection && clipDistance == tr.clipDistance&& viewPoint.equals(tr.viewPoint) && imagePlaneYDirection.equals(tr.imagePlaneYDirection) ); } /** * Creates a copy of this Transform3D. */ public Object clone() { Transform3D tr = (Transform3D)super.clone(); tr.viewPoint = new Vector3D(viewPoint); tr.viewDirection = new Vector3D(viewDirection); tr.imagePlaneXDirection = new Vector3D(imagePlaneXDirection); tr.imagePlaneYDirection = new Vector3D(imagePlaneYDirection); return tr; } /** * This package-scope method exists to make it possible for View3D and View3DLit to swap * graphics contexts in and out of this transform while doing stereo viewing. It is not meant * for general use, and using it improperly would mess things up pretty thoroughly */ void useGraphics(Graphics2D g, Graphics2D untransformedGraphics) { this.g = g; this.untransformedGraphics = untransformedGraphics; } //----------------------------------------- Projection and Transformation ------------------------------------------- /** * Transform a point from object coordinates to viewing coordinates. After this method, the x- and y- coordinates * of the resulting vector are coordinates for the viewing plane, while the z-coordinate encodes the distance of * the object point from the viewing plane. * @param objectPoint The non-null point whose coordinates are to be transformed. This vector is not modified. * @param viewCoords The non-null vector that will contain the result after this method is called. The previous * components of this vector are replaced with the transformed version of objectPoint. */ public void objectToViewCoords(Vector3D objectPoint, Vector3D viewCoords) { double x1 = objectPoint.x; double y1 = objectPoint.y; double z1 = objectPoint.z; if (objectDisplacementNormalToScreen != 0) { x1 -= objectDisplacementNormalToScreen * saveViewDirection.x; y1 -= objectDisplacementNormalToScreen * saveViewDirection.y; z1 -= objectDisplacementNormalToScreen * saveViewDirection.z; } if (orthographicProjection) { // just project onto x- and y-axes of image plane viewCoords.x = x1 * imagePlaneXDirection.x + y1 * imagePlaneXDirection.y + z1 * imagePlaneXDirection.z; viewCoords.y = x1 * imagePlaneYDirection.x + y1 * imagePlaneYDirection.y + z1 * imagePlaneYDirection.z; } else { // Vector A = objectPoint - viewPoint -- copied from DrawContext in XPlorMath3D // dP = A dotproduct viewDirecction -- projects A into normal plan to viewDirection // Vector B = (focalLength / dP) times A // Vector projectedPoint3D = ViewPoint + B (I want to do it without creating Vector3D objects) double x = x1 - viewPoint.x; double y = y1 - viewPoint.y; // Computes A double z = z1 - viewPoint.z; double dP = x * viewDirection.x + y * viewDirection.y + z*viewDirection.z; x = (focalLength / dP) * x; y = (focalLength / dP) * y; // Computes B z = (focalLength / dP) * z; x += viewPoint.x; y += viewPoint.y; // Computes projectedPoint. z += viewPoint.z; // Now, compute projections onto x- and y-axes of image plane. viewCoords.x = x * imagePlaneXDirection.x + y * imagePlaneXDirection.y + z * imagePlaneXDirection.z; viewCoords.y = x * imagePlaneYDirection.x + y * imagePlaneYDirection.y + z * imagePlaneYDirection.z; } viewCoords.z = -(x1 * viewDirection.x + y1 * viewDirection.y + z1 * viewDirection.z); // z is negative because viewDirection points into the screen, and so is the negative of the unit z vvector } /** * Transform a point in world coordinates to viewing coodinates. This is done by * calling {@link #objectToViewCoords(Vector3D, Vector3D)} with a newly created vector as its * second argument, and then returning that vector. * @param objectPoint The non-null point that is to be transformed * @return The transformed version of objectPoint */ public Vector3D objectToViewCoords(Vector3D objectPoint) { Vector3D viewCoords = new Vector3D(); objectToViewCoords(objectPoint,viewCoords); return viewCoords; } /** * Project a world-coordinate point onto the view plane. This is the same as taking just the x- and y-coordinate * from the vector computed by {@link #objectToViewCoords(Vector3D, Vector3D)}. * @param objectPoint a non-null vector whose corrdinates are to be transformed * @param p a non-null point whose coordinates will be set to the projected (x,y) point. */ public void objectToXYWindowCoords(Vector3D objectPoint, Point2D p) { objectToViewCoords(objectPoint, tempVector); p.setLocation(tempVector.x, tempVector.y); } /** * Project a world-coordinate point onto the view plane. This is done by calling * {@link #objectToDrawingCoords(Vector3D, Point2D)} with a newly created Poin2D as its second * parameter, and then returning that point. */ public Point2D objectToXYWindowCoords(Vector3D objectPoint) { Point2D p = new Point2D.Double(); objectToXYWindowCoords(objectPoint, p); return p; } /** * Transform a point given in world coordinates to a 2D point that can be used for drawing on the * view plane. If the {@link Transform#appliedTransform2D} property is true, the result is * the same as the result of {@link #objectToXYWindowCoords(Vector3D, Point2D)}, since regular * xy window coordinates can be used for drawing directly. If the {@link Transform#appliedTransform2D} property is * false, then the xy-coordinates are further transformed to the viewport (pixel) coordintates that * are needed for drawing. * @param objectPoint The point whose coordinates are to be projeted and transformed. * @param drawingCoords A pre-allocated non-null point to contain the result. */ public void objectToDrawingCoords(Vector3D objectPoint, Point2D drawingCoords) { objectToXYWindowCoords(objectPoint, drawingCoords); windowToDrawingCoords(drawingCoords); } /** * Transform a point given in world coordinates to a 2D point that can be used for drawing on the * view plane. This is done by applying {@link #objectToDrawingCoords(Vector3D, Point2D)} to * a newly allocated Point2D, and then returning that point. */ public Point2D objectToDrawingCoords(Vector3D objectPoint) { Point2D p = new Point2D.Double(); objectToXYWindowCoords(objectPoint, p); windowToDrawingCoords(p); return p; } /** * Compute just the z-coordinate of a given point in view coordinates. * This is the same as the z value in the vector that would be returned by * {@link #objectToViewCoords(Vector3D)} * @param objectPoint the untransformed point in object coordinates * @return the z-coordinate of the transformed point. */ public double objectToViewZ(Vector3D objectPoint) { double x1 = objectPoint.x; double y1 = objectPoint.y; double z1 = objectPoint.z; if (objectDisplacementNormalToScreen != 0) { x1 -= objectDisplacementNormalToScreen * saveViewDirection.x; y1 -= objectDisplacementNormalToScreen * saveViewDirection.y; z1 -= objectDisplacementNormalToScreen * saveViewDirection.z; } return -(x1 * viewDirection.x + y1 * viewDirection.y + z1 * viewDirection.z); } }