    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}