/*
 * Copyright (c) 2009-2021 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.renderer;

import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingVolume;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.export.Savable;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix4f;
import com.jme3.math.Plane;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.math.Vector4f;
import com.jme3.util.TempVars;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A standalone, purely mathematical class for doing
 * camera-related computations.
 *
 * <p>Given input data such as location, orientation (direction, left, up),
 * and viewport settings, it can compute data necessary to render objects
 * with the graphics library. Two matrices are generated, the view matrix
 * transforms objects from world space into eye space, while the projection
 * matrix transforms objects from eye space into clip space.</p>
 *
 * <p>Another purpose of the camera class is to do frustum culling operations,
 * defined by six planes which define a 3D frustum shape, it is possible to
 * test if an object bounded by a mathematically defined volume is inside
 * the camera frustum, and thus to avoid rendering objects that are outside
 * the frustum
 * </p>
 *
 * @author Mark Powell
 * @author Joshua Slack
 */
public class Camera implements Savable, Cloneable {

    private static final Logger logger = Logger.getLogger(Camera.class.getName());

    /**
     * The result of a culling check operation.
     * see {@link #contains(com.jme3.bounding.BoundingVolume) }
     */
    public enum FrustumIntersect {

        /**
         * Defines a constant assigned to spatials that are completely outside
         * of this camera's view frustum.
         */
        Outside,
        /**
         * Defines a constant assigned to spatials that are completely inside
         * the camera's view frustum.
         */
        Inside,
        /**
         * Defines a constant assigned to spatials that are intersecting one of
         * the six planes that define the view frustum.
         */
        Intersects;
    }
    /**
     * LEFT_PLANE represents the left plane of the camera frustum.
     */
    private static final int LEFT_PLANE = 0;
    /**
     * RIGHT_PLANE represents the right plane of the camera frustum.
     */
    private static final int RIGHT_PLANE = 1;
    /**
     * BOTTOM_PLANE represents the bottom plane of the camera frustum.
     */
    private static final int BOTTOM_PLANE = 2;
    /**
     * TOP_PLANE represents the top plane of the camera frustum.
     */
    private static final int TOP_PLANE = 3;
    /**
     * FAR_PLANE represents the far plane of the camera frustum.
     */
    private static final int FAR_PLANE = 4;
    /**
     * NEAR_PLANE represents the near plane of the camera frustum.
     */
    private static final int NEAR_PLANE = 5;
    /**
     * FRUSTUM_PLANES represents the number of planes of the camera frustum.
     */
    private static final int FRUSTUM_PLANES = 6;
    /**
     * MAX_WORLD_PLANES holds the maximum planes allowed by the system.
     */
    private static final int MAX_WORLD_PLANES = 6;
    /**
     * Camera's location.
     */
    protected Vector3f location;
    /**
     * The orientation of the camera.
     */
    protected Quaternion rotation;
    /**
     * Distance from camera to near frustum plane.
     */
    protected float frustumNear;
    /**
     * Distance from camera to far frustum plane.
     */
    protected float frustumFar;
    /**
     * Distance from camera to left frustum plane.
     */
    protected float frustumLeft;
    /**
     * Distance from camera to right frustum plane.
     */
    protected float frustumRight;
    /**
     * Distance from camera to top frustum plane.
     */
    protected float frustumTop;
    /**
     * Distance from camera to bottom frustum plane.
     */
    protected float frustumBottom;
    /**
     * Temporary values computed in onFrustumChange that are needed if a call is made to onFrameChange.
     */
    protected float[] coeffLeft;
    /**
     * Temporary values computed in onFrustumChange that are needed if a call is made to onFrameChange.
     */
    protected float[] coeffRight;
    /**
     * Temporary values computed in onFrustumChange that are needed if a call is made to onFrameChange.
     */
    protected float[] coeffBottom;
    /**
     * Temporary values computed in onFrustumChange that are needed if a call is made to onFrameChange.
     */
    protected float[] coeffTop;
    //view port coordinates
    /**
     * Percent value on display where horizontal viewing starts for this camera.
     * Default is 0.
     */
    protected float viewPortLeft;
    /**
     * Percent value on display where horizontal viewing ends for this camera.
     * Default is 1.
     */
    protected float viewPortRight;
    /**
     * Percent value on display where vertical viewing ends for this camera.
     * Default is 1.
     */
    protected float viewPortTop;
    /**
     * Percent value on display where vertical viewing begins for this camera.
     * Default is 0.
     */
    protected float viewPortBottom;
    /**
     * Array holding the planes that this camera will check for culling.
     */
    protected Plane[] worldPlane;
    /**
     * A mask value set during contains() that allows fast culling of a Node's
     * children.
     */
    private int planeState;
    /**
     * The width of the viewport in pixels.
     */
    protected int width;
    /**
     * The height of the viewport in pixels.
     */
    protected int height;
    /**
     * True if the renderer needs to update its viewport boundaries.
     */
    protected boolean viewportChanged = true;
    /**
     * store the value for field parallelProjection
     */
    private boolean parallelProjection = true;
    /**
     * Temporarily overrides the projection matrix.
     */
    protected Matrix4f projectionMatrixOverride = new Matrix4f();
    private boolean overrideProjection;
    /**
     * Transforms world space into eye space.
     */
    protected Matrix4f viewMatrix = new Matrix4f();
    /**
     * Transforms eye space into clip space, unless overridden by projectionMatrixOverride.
     */
    protected Matrix4f projectionMatrix = new Matrix4f();
    /**
     * Transforms world space into clip space.
     */
    protected Matrix4f viewProjectionMatrix = new Matrix4f();
    private BoundingBox guiBounding = new BoundingBox();
    /** The camera's name. */
    protected String name;

    /**
     * Serialization only. Do not use.
     */
    protected Camera() {
        worldPlane = new Plane[MAX_WORLD_PLANES];
        for (int i = 0; i < MAX_WORLD_PLANES; i++) {
            worldPlane[i] = new Plane();
        }
    }

    /**
     * Instantiates a new <code>Camera</code> object. All
     * values of the camera are set to default.
     *
     * @param width the desired width (in pixels)
     * @param height the desired height (in pixels)
     */
    public Camera(int width, int height) {
        this();
        location = new Vector3f();
        rotation = new Quaternion();

        frustumNear = 1.0f;
        frustumFar = 2.0f;
        frustumLeft = -0.5f;
        frustumRight = 0.5f;
        frustumTop = 0.5f;
        frustumBottom = -0.5f;

        coeffLeft = new float[2];
        coeffRight = new float[2];
        coeffBottom = new float[2];
        coeffTop = new float[2];

        viewPortLeft = 0.0f;
        viewPortRight = 1.0f;
        viewPortTop = 1.0f;
        viewPortBottom = 0.0f;

        this.width = width;
        this.height = height;

        onFrustumChange();
        onViewPortChange();
        onFrameChange();

        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "Camera created (W: {0}, H: {1})", new Object[]{width, height});
        }
    }

    @Override
    public Camera clone() {
        try {
            Camera cam = (Camera) super.clone();
            cam.viewportChanged = true;
            cam.planeState = 0;

            cam.worldPlane = new Plane[MAX_WORLD_PLANES];
            for (int i = 0; i < worldPlane.length; i++) {
                cam.worldPlane[i] = worldPlane[i].clone();
            }

            cam.coeffLeft = new float[2];
            cam.coeffRight = new float[2];
            cam.coeffBottom = new float[2];
            cam.coeffTop = new float[2];

            cam.location = location.clone();
            cam.rotation = rotation.clone();

            if (projectionMatrixOverride != null) {
                cam.projectionMatrixOverride = projectionMatrixOverride.clone();
            }

            cam.viewMatrix = viewMatrix.clone();
            cam.projectionMatrix = projectionMatrix.clone();
            cam.viewProjectionMatrix = viewProjectionMatrix.clone();
            cam.guiBounding = (BoundingBox) guiBounding.clone();

            cam.update();

            return cam;
        } catch (CloneNotSupportedException ex) {
            throw new AssertionError();
        }
    }

    /**
     * Copies the settings of the given camera.
     *
     * @param cam
     *            the camera we copy the settings from
     */
    public void copyFrom(Camera cam) {
        location.set(cam.location);
        rotation.set(cam.rotation);

        frustumNear = cam.frustumNear;
        frustumFar = cam.frustumFar;
        frustumLeft = cam.frustumLeft;
        frustumRight = cam.frustumRight;
        frustumTop = cam.frustumTop;
        frustumBottom = cam.frustumBottom;

        coeffLeft[0] = cam.coeffLeft[0];
        coeffLeft[1] = cam.coeffLeft[1];
        coeffRight[0] = cam.coeffRight[0];
        coeffRight[1] = cam.coeffRight[1];
        coeffBottom[0] = cam.coeffBottom[0];
        coeffBottom[1] = cam.coeffBottom[1];
        coeffTop[0] = cam.coeffTop[0];
        coeffTop[1] = cam.coeffTop[1];

        viewPortLeft = cam.viewPortLeft;
        viewPortRight = cam.viewPortRight;
        viewPortTop = cam.viewPortTop;
        viewPortBottom = cam.viewPortBottom;

        this.width = cam.width;
        this.height = cam.height;

        this.planeState = 0;
        this.viewportChanged = true;
        for (int i = 0; i < MAX_WORLD_PLANES; ++i) {
            worldPlane[i].setNormal(cam.worldPlane[i].getNormal());
            worldPlane[i].setConstant(cam.worldPlane[i].getConstant());
        }

        this.parallelProjection = cam.parallelProjection;
        this.overrideProjection = cam.overrideProjection;
        this.projectionMatrixOverride.set(cam.projectionMatrixOverride);

        this.viewMatrix.set(cam.viewMatrix);
        this.projectionMatrix.set(cam.projectionMatrix);
        this.viewProjectionMatrix.set(cam.viewProjectionMatrix);

        this.guiBounding.setXExtent(cam.guiBounding.getXExtent());
        this.guiBounding.setYExtent(cam.guiBounding.getYExtent());
        this.guiBounding.setZExtent(cam.guiBounding.getZExtent());
        this.guiBounding.setCenter(cam.guiBounding.getCenter());
        this.guiBounding.setCheckPlane(cam.guiBounding.getCheckPlane());

        this.name = cam.name;
    }

    /**
     * Sets the camera's name.
     *
     * @param name the camera's name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Returns the camera's name.
     *
     * @return the camera's name
     */
    public String getName() {
        return name;
    }

    /**
     * Sets a clipPlane for this camera.
     * The clipPlane is used to recompute the
     * projectionMatrix using the plane as the near plane
     * This technique is known as the oblique near-plane clipping method introduced by Eric Lengyel
     * more info here
     * <ul>
     * <li><a href="http://www.terathon.com/code/oblique.html">http://www.terathon.com/code/oblique.html</a>
     * <li><a href="http://aras-p.info/texts/obliqueortho.html">http://aras-p.info/texts/obliqueortho.html</a>
     * <li><a href="http://hacksoflife.blogspot.com/2008/12/every-now-and-then-i-come-across.html">
     * http://hacksoflife.blogspot.com/2008/12/every-now-and-then-i-come-across.html</a>
     * </ul>
     *
     * <p>Note that this will work properly only if it's called on each update,
     * and be aware that it won't work properly with the sky bucket.
     * if you want to handle the sky bucket, look at how it's done in SimpleWaterProcessor.java
     *
     * @param clipPlane the plane
     * @param side the side the camera stands from the plane
     */
    public void setClipPlane(Plane clipPlane, Plane.Side side) {
        float sideFactor = 1;
        if (side == Plane.Side.Negative) {
            sideFactor = -1;
        }
        //we are on the other side of the plane no need to clip anymore.
        if (clipPlane.whichSide(location) == side) {
            return;
        }

        TempVars vars = TempVars.get();
        try {
            Matrix4f p = projectionMatrixOverride.set(projectionMatrix);

            Matrix4f ivm = viewMatrix;

            Vector3f point = clipPlane.getNormal().mult(clipPlane.getConstant(), vars.vect1);
            Vector3f pp = ivm.mult(point, vars.vect2);
            Vector3f pn = ivm.multNormal(clipPlane.getNormal(), vars.vect3);
            Vector4f clipPlaneV = vars.vect4f1.set(pn.x * sideFactor, pn.y * sideFactor, pn.z * sideFactor,
                    -(pp.dot(pn)) * sideFactor);

            Vector4f v = vars.vect4f2.set(0, 0, 0, 0);

            v.x = (Math.signum(clipPlaneV.x) + p.m02) / p.m00;
            v.y = (Math.signum(clipPlaneV.y) + p.m12) / p.m11;
            v.z = -1.0f;
            v.w = (1.0f + p.m22) / p.m23;

            float dot = clipPlaneV.dot(v);
            //clipPlaneV.x * v.x + clipPlaneV.y * v.y + clipPlaneV.z * v.z + clipPlaneV.w * v.w;
            Vector4f c = clipPlaneV.multLocal(2.0f / dot);

            p.m20 = c.x - p.m30;
            p.m21 = c.y - p.m31;
            p.m22 = c.z - p.m32;
            p.m23 = c.w - p.m33;
            setProjectionMatrix(p);
        } finally {
            vars.release();
        }
    }

    /**
     * Sets a clipPlane for this camera.
     * The cliPlane is used to recompute the projectionMatrix using the plane as the near plane
     * This technique is known as the oblique near-plane clipping method introduced by Eric Lengyel
     * more info here
     * <ul>
     * <li><a href="http://www.terathon.com/code/oblique.html">
     * http://www.terathon.com/code/oblique.html</a></li>
     * <li><a href="http://aras-p.info/texts/obliqueortho.html">
     * http://aras-p.info/texts/obliqueortho.html</a></li>
     * <li><a href="http://hacksoflife.blogspot.com/2008/12/every-now-and-then-i-come-across.html">
     * http://hacksoflife.blogspot.com/2008/12/every-now-and-then-i-come-across.html</a></li>
     * </ul>
     *
     * <p>Note that this will work properly only if it's called on each update,
     * and be aware that it won't work properly with the sky bucket.
     * if you want to handle the sky bucket, look at how it's done in SimpleWaterProcessor.java
     *
     * @param clipPlane the plane
     */
    public void setClipPlane(Plane clipPlane) {
        setClipPlane(clipPlane, clipPlane.whichSide(location));
    }

    /**
     * Resizes this camera's view for the specified display size. Invoked by an
     * associated {@link RenderManager} to notify the camera of changes to the
     * display dimensions.
     *
     * @param width the new width of the display, in pixels
     * @param height the new height of the display, in pixels
     * @param fixAspect if true, recompute the camera's frustum to preserve its
     *     prior aspect ratio
     */
    public void resize(int width, int height, boolean fixAspect) {
        this.width = width;
        this.height = height;
        onViewPortChange();

        if (fixAspect) {
            float h = height * (viewPortTop - viewPortBottom);
            float w = width * (viewPortRight - viewPortLeft);
            float aspectRatio = w / h;
            frustumRight = frustumTop * aspectRatio;
            frustumLeft = -frustumRight;
            onFrustumChange();
        }
    }

    /**
     * Returns the value of the bottom frustum
     * plane.
     *
     * @return the value of the bottom frustum plane.
     */
    public float getFrustumBottom() {
        return frustumBottom;
    }

    /**
     * Sets the value of the bottom frustum
     * plane.
     *
     * @param frustumBottom the value of the bottom frustum plane.
     */
    public void setFrustumBottom(float frustumBottom) {
        this.frustumBottom = frustumBottom;
        onFrustumChange();
    }

    /**
     * Gets the value of the far frustum plane.
     *
     * @return the value of the far frustum plane.
     */
    public float getFrustumFar() {
        return frustumFar;
    }

    /**
     * Sets the value of the far frustum plane.
     *
     * @param frustumFar the value of the far frustum plane.
     */
    public void setFrustumFar(float frustumFar) {
        this.frustumFar = frustumFar;
        onFrustumChange();
    }

    /**
     * Gets the value of the left frustum plane.
     *
     * @return the value of the left frustum plane.
     */
    public float getFrustumLeft() {
        return frustumLeft;
    }

    /**
     * Sets the value of the left frustum plane.
     *
     * @param frustumLeft the value of the left frustum plane.
     */
    public void setFrustumLeft(float frustumLeft) {
        this.frustumLeft = frustumLeft;
        onFrustumChange();
    }

    /**
     * Gets the value of the near frustum plane.
     *
     * @return the value of the near frustum plane.
     */
    public float getFrustumNear() {
        return frustumNear;
    }

    /**
     * Sets the value of the near frustum plane.
     *
     * @param frustumNear the value of the near frustum plane.
     */
    public void setFrustumNear(float frustumNear) {
        this.frustumNear = frustumNear;
        onFrustumChange();
    }

    /**
     * Gets the value of the right frustum plane.
     *
     * @return frustumRight the value of the right frustum plane.
     */
    public float getFrustumRight() {
        return frustumRight;
    }

    /**
     * Sets the value of the right frustum plane.
     *
     * @param frustumRight the value of the right frustum plane.
     */
    public void setFrustumRight(float frustumRight) {
        this.frustumRight = frustumRight;
        onFrustumChange();
    }

    /**
     * Gets the value of the top frustum plane.
     *
     * @return the value of the top frustum plane.
     */
    public float getFrustumTop() {
        return frustumTop;
    }

    /**
     * Sets the value of the top frustum plane.
     *
     * @param frustumTop the value of the top frustum plane.
     */
    public void setFrustumTop(float frustumTop) {
        this.frustumTop = frustumTop;
        onFrustumChange();
    }

    /**
     * Obtains field of view when the camera is in perspective mode.
     *
     * @return Frame of view angle along the Y in degrees, or 0 if the camera is in orthogonal mode.
     */
    public float getFov() {
        if (!this.parallelProjection) {
            float fovY = frustumTop / frustumNear;
            fovY = FastMath.atan(fovY);
            fovY /= 0.5F * FastMath.DEG_TO_RAD;
            return fovY;
        }
        return 0;
    }

    /**
     * Sets the field of view when the camera is in perspective mode. Note that this method has no
     * effect when the camera is in orthogonal mode.
     *
     * @param fovY   Frame of view angle along the Y in degrees. This must be greater than 0.
     */
    public void setFov(float fovY) {
        if (fovY <= 0) {
            throw new IllegalArgumentException("Field of view must be greater than 0");
        }
        if (this.parallelProjection) {
            throw new IllegalArgumentException("Cannot set field of view on orthogonal camera");
        }
        float h = FastMath.tan(fovY * FastMath.DEG_TO_RAD * .5f) * frustumNear;
        float w = h * getAspect();
        frustumLeft = -w;
        frustumRight = w;
        frustumBottom = -h;
        frustumTop = h;
        onFrustumChange();
    }

    /**
     * Obtains the aspect ratio.
     *
     * @return Width:Height ratio.
     */
    public float getAspect() {
        float h = height * (viewPortTop - viewPortBottom);
        float w = width * (viewPortRight - viewPortLeft);
        return w / h;
    }

    /**
     * Retrieves the location vector of the camera.
     *
     * @return the position of the camera.
     * @see Camera#getLocation()
     */
    public Vector3f getLocation() {
        return location;
    }

    /**
     * Retrieves the rotation quaternion of the camera.
     *
     * @return the rotation of the camera.
     */
    public Quaternion getRotation() {
        return rotation;
    }

    /**
     * Retrieves the direction vector the camera is
     * facing.
     *
     * @return the direction the camera is facing.
     * @see Camera#getDirection()
     */
    public Vector3f getDirection() {
        return rotation.getRotationColumn(2);
    }

    /**
     * Retrieves the left axis of the camera.
     *
     * @return the left axis of the camera.
     * @see Camera#getLeft()
     */
    public Vector3f getLeft() {
        return rotation.getRotationColumn(0);
    }

    /**
     * Retrieves the up axis of the camera.
     *
     * @return the up axis of the camera.
     * @see Camera#getUp()
     */
    public Vector3f getUp() {
        return rotation.getRotationColumn(1);
    }

    /**
     * Retrieves the direction vector the camera is
     * facing.
     *
     * @param store storage for the result (modified if not null)
     * @return the direction the camera is facing.
     * @see Camera#getDirection()
     */
    public Vector3f getDirection(Vector3f store) {
        return rotation.getRotationColumn(2, store);
    }

    /**
     * Retrieves the left axis of the camera.
     *
     * @param store storage for the result (modified if not null)
     * @return the left axis of the camera.
     * @see Camera#getLeft()
     */
    public Vector3f getLeft(Vector3f store) {
        return rotation.getRotationColumn(0, store);
    }

    /**
     * Retrieves the up axis of the camera.
     *
     * @param store storage for the result (modified if not null)
     * @return the up axis of the camera.
     * @see Camera#getUp()
     */
    public Vector3f getUp(Vector3f store) {
        return rotation.getRotationColumn(1, store);
    }

    /**
     * Sets the position of the camera.
     *
     * @param location the position of the camera.
     */
    public void setLocation(Vector3f location) {
        this.location.set(location);
        onFrameChange();
    }

    /**
     * Sets the orientation of this camera. This will
     * be equivalent to setting each of the axes:
     * <code><br>
     * cam.setLeft(rotation.getRotationColumn(0));<br>
     * cam.setUp(rotation.getRotationColumn(1));<br>
     * cam.setDirection(rotation.getRotationColumn(2));<br>
     * </code>
     *
     * @param rotation the rotation of this camera
     */
    public void setRotation(Quaternion rotation) {
        this.rotation.set(rotation);
        onFrameChange();
    }

    /**
     * Sets the direction the camera is facing
     * given a direction and an up vector.
     *
     * @param direction the direction this camera is facing.
     * @param up the desired "up" direction for the camera (not null,
     *     unaffected, typically (0,1,0))
     */
    public void lookAtDirection(Vector3f direction, Vector3f up) {
        this.rotation.lookAt(direction, up);
        onFrameChange();
    }

    /**
     * Sets the axes (left, up and direction) for this
     * camera.
     *
     * @param left      the left axis of the camera.
     * @param up        the up axis of the camera.
     * @param direction the direction the camera is facing.
     *
     * @see Camera#setAxes(com.jme3.math.Quaternion)
     */
    public void setAxes(Vector3f left, Vector3f up, Vector3f direction) {
        this.rotation.fromAxes(left, up, direction);
        onFrameChange();
    }

    /**
     * Uses a rotational matrix to set the axes of the
     * camera.
     *
     * @param axes the matrix that defines the orientation of the camera.
     */
    public void setAxes(Quaternion axes) {
        this.rotation.set(axes);
        onFrameChange();
    }

    /**
     * Normalizes the camera vectors.
     */
    public void normalize() {
        this.rotation.normalizeLocal();
        onFrameChange();
    }

    /**
     * Sets the frustum of this camera object.
     *
     * @param near   the near plane.
     * @param far    the far plane.
     * @param left   the left plane.
     * @param right  the right plane.
     * @param top    the top plane.
     * @param bottom the bottom plane.
     * @see Camera#setFrustum(float, float, float, float,
     *      float, float)
     */
    public void setFrustum(float near, float far, float left, float right,
            float top, float bottom) {

        frustumNear = near;
        frustumFar = far;
        frustumLeft = left;
        frustumRight = right;
        frustumTop = top;
        frustumBottom = bottom;
        onFrustumChange();
    }

    /**
     * Defines the frustum for the camera.  This
     * frustum is defined by a viewing angle, aspect ratio, and near/far planes
     *
     * @param fovY   Frame of view angle along the Y in degrees.
     * @param aspect Width:Height ratio
     * @param near   Near view plane distance
     * @param far    Far view plane distance
     */
    public void setFrustumPerspective(float fovY, float aspect, float near,
            float far) {
        if (Float.isNaN(aspect) || Float.isInfinite(aspect)) {
            // ignore.
            logger.log(Level.WARNING, "Invalid aspect given to setFrustumPerspective: {0}", aspect);
            return;
        }

        float h = FastMath.tan(fovY * FastMath.DEG_TO_RAD * .5f) * near;
        float w = h * aspect;
        frustumLeft = -w;
        frustumRight = w;
        frustumBottom = -h;
        frustumTop = h;
        frustumNear = near;
        frustumFar = far;

        // Camera is no longer parallel projection even if it was before
        parallelProjection = false;

        onFrustumChange();
    }

    /**
     * Sets the orientation and location of the camera.
     *
     * @param location  the point position of the camera.
     * @param left      the left axis of the camera.
     * @param up        the up axis of the camera.
     * @param direction the facing of the camera.
     * @see Camera#setFrame(com.jme3.math.Vector3f,
     *      com.jme3.math.Vector3f, com.jme3.math.Vector3f, com.jme3.math.Vector3f)
     */
    public void setFrame(Vector3f location, Vector3f left, Vector3f up,
            Vector3f direction) {

        this.location = location;
        this.rotation.fromAxes(left, up, direction);
        onFrameChange();
    }

    /**
     * A convenience method for auto-setting the frame
     * based on a world position the user desires the camera to look at. It
     * repoints the camera towards the given position using the difference
     * between the position and the current camera location as a direction
     * vector and the worldUpVector to compute up and left camera vectors.
     *
     * @param pos           where to look at in terms of world coordinates
     * @param worldUpVector a normalized vector indicating the up direction of the world.
     *                      (typically {0, 1, 0} in jME.)
     */
    public void lookAt(Vector3f pos, Vector3f worldUpVector) {
        TempVars vars = TempVars.get();
        Vector3f newDirection = vars.vect1;
        Vector3f newUp = vars.vect2;
        Vector3f newLeft = vars.vect3;

        newDirection.set(pos).subtractLocal(location).normalizeLocal();

        newUp.set(worldUpVector).normalizeLocal();
        if (newUp.equals(Vector3f.ZERO)) {
            newUp.set(Vector3f.UNIT_Y);
        }

        newLeft.set(newUp).crossLocal(newDirection).normalizeLocal();
        if (newLeft.equals(Vector3f.ZERO)) {
            if (newDirection.x != 0) {
                newLeft.set(newDirection.y, -newDirection.x, 0f);
            } else {
                newLeft.set(0f, newDirection.z, -newDirection.y);
            }
        }

        newUp.set(newDirection).crossLocal(newLeft).normalizeLocal();

        this.rotation.fromAxes(newLeft, newUp, newDirection);
        this.rotation.normalizeLocal();
        vars.release();

        onFrameChange();
    }

    /**
     * Sets the orientation and location of the camera.
     *
     * @param location
     *            the point position of the camera.
     * @param axes
     *            the orientation of the camera.
     */
    public void setFrame(Vector3f location, Quaternion axes) {
        this.location = location;
        this.rotation.set(axes);
        onFrameChange();
    }

    /**
     * Updates the camera parameters by calling
     * <code>onFrustumChange</code>,<code>onViewPortChange</code> and
     * <code>onFrameChange</code>.
     *
     * @see Camera#update()
     */
    public void update() {
        onFrustumChange();
        onViewPortChange();
        //...this is always called by onFrustumChange()
        //onFrameChange();
    }

    /**
     * Returns the state of the frustum planes. So
     * checks can be made as to which frustum plane has been examined for
     * culling thus far.
     *
     * @return the current plane state int.
     */
    public int getPlaneState() {
        return planeState;
    }

    /**
     * Sets the state to keep track of tested
     * planes for culling.
     *
     * @param planeState the updated state.
     */
    public void setPlaneState(int planeState) {
        this.planeState = planeState;
    }

    /**
     * Gets the left boundary of the viewport.
     *
     * @return the left boundary of the viewport
     */
    public float getViewPortLeft() {
        return viewPortLeft;
    }

    /**
     * Sets the left boundary of the viewport.
     *
     * @param left the left boundary of the viewport
     */
    public void setViewPortLeft(float left) {
        viewPortLeft = left;
        onViewPortChange();
    }

    /**
     * Gets the right boundary of the viewport.
     *
     * @return the right boundary of the viewport
     */
    public float getViewPortRight() {
        return viewPortRight;
    }

    /**
     * Sets the right boundary of the viewport.
     *
     * @param right the right boundary of the viewport
     */
    public void setViewPortRight(float right) {
        viewPortRight = right;
        onViewPortChange();
    }

    /**
     * Gets the top boundary of the viewport.
     *
     * @return the top boundary of the viewport
     */
    public float getViewPortTop() {
        return viewPortTop;
    }

    /**
     * Sets the top boundary of the viewport.
     *
     * @param top the top boundary of the viewport
     */
    public void setViewPortTop(float top) {
        viewPortTop = top;
        onViewPortChange();
    }

    /**
     * Gets the bottom boundary of the viewport.
     *
     * @return the bottom boundary of the viewport
     */
    public float getViewPortBottom() {
        return viewPortBottom;
    }

    /**
     * Sets the bottom boundary of the viewport.
     *
     * @param bottom the bottom boundary of the viewport
     */
    public void setViewPortBottom(float bottom) {
        viewPortBottom = bottom;
        onViewPortChange();
    }

    /**
     * Sets the boundaries of the viewport.
     *
     * @param left   the left boundary of the viewport (default: 0)
     * @param right  the right boundary of the viewport (default: 1)
     * @param bottom the bottom boundary of the viewport (default: 0)
     * @param top    the top boundary of the viewport (default: 1)
     */
    public void setViewPort(float left, float right, float bottom, float top) {
        this.viewPortLeft = left;
        this.viewPortRight = right;
        this.viewPortBottom = bottom;
        this.viewPortTop = top;
        onViewPortChange();
    }

    /**
     * Returns the pseudo distance from the given position to the near
     * plane of the camera. This is used for render queue sorting.
     * @param pos The position to compute a distance to.
     * @return Distance from the near plane to the point.
     */
    public float distanceToNearPlane(Vector3f pos) {
        return worldPlane[NEAR_PLANE].pseudoDistance(pos);
    }

    /**
     * Tests a bounding volume against the planes of the
     * camera's frustum. The frustum's planes are set such that the normals all
     * face in towards the viewable scene. Therefore, if the bounding volume is
     * on the negative side of the plane is can be culled out.
     *
     * NOTE: This method is used internally for culling, for public usage,
     * the plane state of the camera must be saved and restored, e.g:
     * <code>BoundingVolume bv;<br>
     * Camera c;<br>
     * int planeState = c.getPlaneState();<br>
     * c.setPlaneState(0);<br>
     * c.contains(bv);<br>
     * c.setPlaneState(plateState);<br>
     * </code>
     *
     * @param bound the bound to check for culling
     * @return See enums in <code>FrustumIntersect</code>
     */
    public FrustumIntersect contains(BoundingVolume bound) {
        if (bound == null) {
            return FrustumIntersect.Inside;
        }

        int mask;
        FrustumIntersect rVal = FrustumIntersect.Inside;

        for (int planeCounter = FRUSTUM_PLANES; planeCounter >= 0; planeCounter--) {
            if (planeCounter == bound.getCheckPlane()) {
                continue; // we have already checked this plane at first iteration
            }
            int planeId = (planeCounter == FRUSTUM_PLANES) ? bound.getCheckPlane() : planeCounter;
//            int planeId = planeCounter;

            mask = 1 << (planeId);
            if ((planeState & mask) == 0) {
                Plane.Side side = bound.whichSide(worldPlane[planeId]);

                if (side == Plane.Side.Negative) {
                    //object is outside of frustum
                    bound.setCheckPlane(planeId);
                    return FrustumIntersect.Outside;
                } else if (side == Plane.Side.Positive) {
                    //object is visible on *this* plane, so mark this plane
                    //so that we don't check it for sub nodes.
                    planeState |= mask;
                } else {
                    rVal = FrustumIntersect.Intersects;
                }
            }
        }

        return rVal;
    }

    /**
     * Provides access to one of the planes used for culling.
     *
     * @param planeId the index of the Plane to access (0&rarr;left, 1&rarr;right, 2&rarr;bottom, 3&rarr;top,
     *     4&rarr;far, 5&rarr;near)
     * @return the pre-existing instance
     */
    public Plane getWorldPlane(int planeId) {
        return worldPlane[planeId];
    }

    /**
     * Tests a bounding volume against the ortho
     * bounding box of the camera. A bounding box spanning from
     * 0, 0 to Width, Height. Constrained by the viewport settings on the
     * camera.
     *
     * @param bound the bound to check for culling
     * @return True if the camera contains the gui element bounding volume.
     */
    public boolean containsGui(BoundingVolume bound) {
        if (bound == null) {
            return true;
        }
        return guiBounding.intersects(bound);
    }

    /**
     * Provides access to the view matrix.
     *
     * @return the view matrix of the camera.
     *
     * <p>The view matrix transforms world space into eye space.
     * This matrix is usually defined by the position and
     * orientation of the camera.
     */
    public Matrix4f getViewMatrix() {
        return viewMatrix;
    }

    /**
     * Overrides the projection matrix used by the camera. Will
     * use the matrix for computing the view projection matrix as well.
     * Use null argument to return to normal functionality.
     *
     * @param projMatrix the desired projection matrix (unaffected) or null
     *     to cease the override
     */
    public void setProjectionMatrix(Matrix4f projMatrix) {
        if (projMatrix == null) {
            overrideProjection = false;
            projectionMatrixOverride.loadIdentity();
        } else {
            overrideProjection = true;
            projectionMatrixOverride.set(projMatrix);
        }
        updateViewProjection();
    }

    /**
     * Provides access to the projection matrix.
     * @return the projection matrix of the camera.
     *
     * <p>The view projection matrix  transforms eye space into clip space.
     * This matrix is usually defined by the viewport and perspective settings
     * of the camera.
     */
    public Matrix4f getProjectionMatrix() {
        if (overrideProjection) {
            return projectionMatrixOverride;
        }

        return projectionMatrix;
    }

    /**
     * Updates the view projection matrix.
     */
    public void updateViewProjection() {
        if (overrideProjection) {
            viewProjectionMatrix.set(projectionMatrixOverride).multLocal(viewMatrix);
        } else {
            //viewProjectionMatrix.set(viewMatrix).multLocal(projectionMatrix);
            viewProjectionMatrix.set(projectionMatrix).multLocal(viewMatrix);
        }
    }

    /**
     * Provides access to the view projection matrix.
     *
     *  @return The result of multiplying the projection matrix by the view
     *     matrix. This matrix is required for rendering an object. It is
     *     precomputed to avoid computing it every time an object is rendered.
     */
    public Matrix4f getViewProjectionMatrix() {
        return viewProjectionMatrix;
    }

    /**
     * Tests whether the viewport (width, height, left, right, bottom, up)
     * has been changed. This is needed in the renderer so that the proper
     * viewport can be set-up.
     *
     * @return true if changed, otherwise false
     */
    public boolean isViewportChanged() {
        return viewportChanged;
    }

    /**
     * Clears the viewport changed flag once it has been updated inside
     * the renderer.
     */
    public void clearViewportChanged() {
        viewportChanged = false;
    }

    /**
     * Called when the viewport has been changed.
     */
    public void onViewPortChange() {
        viewportChanged = true;
        setGuiBounding();
    }

    private void setGuiBounding() {
        float sx = width * viewPortLeft;
        float ex = width * viewPortRight;
        float sy = height * viewPortBottom;
        float ey = height * viewPortTop;
        float xExtent = Math.max(0f, (ex - sx) / 2f);
        float yExtent = Math.max(0f, (ey - sy) / 2f);
        guiBounding.setCenter(sx + xExtent, sy + yExtent, 0);
        guiBounding.setXExtent(xExtent);
        guiBounding.setYExtent(yExtent);
        guiBounding.setZExtent(Float.MAX_VALUE);
    }

    /**
     * Updates the frustum to reflect any changes
     * made to the planes. The new frustum values are kept in a temporary
     * location for use when calculating the new frame. The projection
     * matrix is updated to reflect the current values of the frustum.
     */
    public void onFrustumChange() {
        if (!isParallelProjection()) {
            float nearSquared = frustumNear * frustumNear;
            float leftSquared = frustumLeft * frustumLeft;
            float rightSquared = frustumRight * frustumRight;
            float bottomSquared = frustumBottom * frustumBottom;
            float topSquared = frustumTop * frustumTop;

            float inverseLength = FastMath.invSqrt(nearSquared + leftSquared);
            coeffLeft[0] = -frustumNear * inverseLength;
            coeffLeft[1] = -frustumLeft * inverseLength;

            inverseLength = FastMath.invSqrt(nearSquared + rightSquared);
            coeffRight[0] = frustumNear * inverseLength;
            coeffRight[1] = frustumRight * inverseLength;

            inverseLength = FastMath.invSqrt(nearSquared + bottomSquared);
            coeffBottom[0] = frustumNear * inverseLength;
            coeffBottom[1] = -frustumBottom * inverseLength;

            inverseLength = FastMath.invSqrt(nearSquared + topSquared);
            coeffTop[0] = -frustumNear * inverseLength;
            coeffTop[1] = frustumTop * inverseLength;
        } else {
            coeffLeft[0] = 1;
            coeffLeft[1] = 0;

            coeffRight[0] = -1;
            coeffRight[1] = 0;

            coeffBottom[0] = 1;
            coeffBottom[1] = 0;

            coeffTop[0] = -1;
            coeffTop[1] = 0;
        }

        projectionMatrix.fromFrustum(frustumNear, frustumFar, frustumLeft, frustumRight,
                frustumTop, frustumBottom, parallelProjection);
//        projectionMatrix.transposeLocal();

        // The frame is affected by the frustum values
        // update it as well
        onFrameChange();
    }

    /**
     * Updates the view frame of the camera.
     */
    public void onFrameChange() {
        TempVars vars = TempVars.get();

        Vector3f left = getLeft(vars.vect1);
        Vector3f direction = getDirection(vars.vect2);
        Vector3f up = getUp(vars.vect3);

        float dirDotLocation = direction.dot(location);

        // left plane
        Vector3f leftPlaneNormal = worldPlane[LEFT_PLANE].getNormal();
        leftPlaneNormal.x = left.x * coeffLeft[0];
        leftPlaneNormal.y = left.y * coeffLeft[0];
        leftPlaneNormal.z = left.z * coeffLeft[0];
        leftPlaneNormal.addLocal(direction.x * coeffLeft[1], direction.y
                * coeffLeft[1], direction.z * coeffLeft[1]);
        worldPlane[LEFT_PLANE].setConstant(location.dot(leftPlaneNormal));

        // right plane
        Vector3f rightPlaneNormal = worldPlane[RIGHT_PLANE].getNormal();
        rightPlaneNormal.x = left.x * coeffRight[0];
        rightPlaneNormal.y = left.y * coeffRight[0];
        rightPlaneNormal.z = left.z * coeffRight[0];
        rightPlaneNormal.addLocal(direction.x * coeffRight[1], direction.y
                * coeffRight[1], direction.z * coeffRight[1]);
        worldPlane[RIGHT_PLANE].setConstant(location.dot(rightPlaneNormal));

        // bottom plane
        Vector3f bottomPlaneNormal = worldPlane[BOTTOM_PLANE].getNormal();
        bottomPlaneNormal.x = up.x * coeffBottom[0];
        bottomPlaneNormal.y = up.y * coeffBottom[0];
        bottomPlaneNormal.z = up.z * coeffBottom[0];
        bottomPlaneNormal.addLocal(direction.x * coeffBottom[1], direction.y
                * coeffBottom[1], direction.z * coeffBottom[1]);
        worldPlane[BOTTOM_PLANE].setConstant(location.dot(bottomPlaneNormal));

        // top plane
        Vector3f topPlaneNormal = worldPlane[TOP_PLANE].getNormal();
        topPlaneNormal.x = up.x * coeffTop[0];
        topPlaneNormal.y = up.y * coeffTop[0];
        topPlaneNormal.z = up.z * coeffTop[0];
        topPlaneNormal.addLocal(direction.x * coeffTop[1], direction.y
                * coeffTop[1], direction.z * coeffTop[1]);
        worldPlane[TOP_PLANE].setConstant(location.dot(topPlaneNormal));

        if (isParallelProjection()) {
            worldPlane[LEFT_PLANE].setConstant(worldPlane[LEFT_PLANE].getConstant() + frustumLeft);
            worldPlane[RIGHT_PLANE].setConstant(worldPlane[RIGHT_PLANE].getConstant() - frustumRight);
            worldPlane[TOP_PLANE].setConstant(worldPlane[TOP_PLANE].getConstant() - frustumTop);
            worldPlane[BOTTOM_PLANE].setConstant(worldPlane[BOTTOM_PLANE].getConstant() + frustumBottom);
        }

        // far plane
        worldPlane[FAR_PLANE].setNormal(left);
        worldPlane[FAR_PLANE].setNormal(-direction.x, -direction.y, -direction.z);
        worldPlane[FAR_PLANE].setConstant(-(dirDotLocation + frustumFar));

        // near plane
        worldPlane[NEAR_PLANE].setNormal(direction.x, direction.y, direction.z);
        worldPlane[NEAR_PLANE].setConstant(dirDotLocation + frustumNear);

        viewMatrix.fromFrame(location, direction, up, left);

        vars.release();

//        viewMatrix.transposeLocal();
        updateViewProjection();
    }

    /**
     * Determines whether the projection is parallel or perspective.
     *
     * @return true if parallel projection is enabled, false if in normal perspective mode
     * @see #setParallelProjection(boolean)
     */
    public boolean isParallelProjection() {
        return this.parallelProjection;
    }

    /**
     * Enables/disables parallel projection.
     *
     * @param value true to set up this camera for parallel projection,
     *     false to enter perspective mode
     */
    public void setParallelProjection(final boolean value) {
        this.parallelProjection = value;
        onFrustumChange();
    }

    /**
     * Computes the z value in projection space from the z value in view space
     * Note that the returned value goes non-linearly from 0 to 1.
     * For more explanation of non-linear z buffer see
     * http://www.sjbaker.org/steve/omniv/love_your_z_buffer.html
     *
     * @param viewZPos the z value in view space.
     * @return the z value in projection space.
     */
    public float getViewToProjectionZ(float viewZPos) {
        float far = getFrustumFar();
        float near = getFrustumNear();
        float a = far / (far - near);
        float b = far * near / (near - far);
        return a + b / viewZPos;
    }

    /**
     * Computes a position in World space given a screen position in screen space (0,0 to width, height)
     * and a z position in projection space ( 0 to 1 non-linear).
     * This former value is also known as the Z buffer value or non-linear depth buffer.
     * For more explanation of non-linear Z buffer see
     * http://www.sjbaker.org/steve/omniv/love_your_z_buffer.html
     *
     * <p>To compute the projection space z from the view space z (distance from cam to object)
     * @see Camera#getViewToProjectionZ
     *
     * @param screenPos 2d coordinate in screen space
     * @param projectionZPos non linear z value in projection space
     * @return the position in world space.
     */
    public Vector3f getWorldCoordinates(Vector2f screenPos, float projectionZPos) {
        return getWorldCoordinates(screenPos, projectionZPos, null);
    }

    /**
     * Converts the given position from screen space to world space.
     *
     * @param screenPosition a (2-D) location in screen space (not null)
     * @param projectionZPos a (non-linear) Z value in projection space
     * @param store storage for the result (modified if not null)
     * @return a location vector (in world coordinates, either
     *     <code>store</code> or a new vector)
     * @see Camera#getWorldCoordinates
     */
    public Vector3f getWorldCoordinates(Vector2f screenPosition,
            float projectionZPos, Vector3f store) {
        if (store == null) {
            store = new Vector3f();
        }

        Matrix4f inverseMat = new Matrix4f(viewProjectionMatrix);
        inverseMat.invertLocal();

        store.set(
                (screenPosition.x / getWidth() - viewPortLeft) / (viewPortRight - viewPortLeft) * 2 - 1,
                (screenPosition.y / getHeight() - viewPortBottom) / (viewPortTop - viewPortBottom) * 2 - 1,
                projectionZPos * 2 - 1);

        float w = inverseMat.multProj(store, store);
        store.multLocal(1f / w);

        return store;
    }

    /**
     * Converts the given position from world space to screen space.
     *
     * @param worldPos a location in world coordinates (not null, unaffected)
     * @return a new (3-D) location vector (in screen coordinates)
     * @see Camera#getScreenCoordinates
     */
    public Vector3f getScreenCoordinates(Vector3f worldPos) {
        return getScreenCoordinates(worldPos, null);
    }

    /**
     * Converts the given position from world space to screen space.
     *
     * @param worldPosition a location in world coordinates (not null, unaffected)
     * @param store storage for the result (modified if not null)
     * @return a (3-D) location vector (in screen coordinates, either
     *     <code>store</code> or a new vector)
     * @see Camera#getScreenCoordinates(Vector3f, Vector3f)
     */
    public Vector3f getScreenCoordinates(Vector3f worldPosition, Vector3f store) {
        if (store == null) {
            store = new Vector3f();
        }

//        TempVars vars = vars.lock();
//        Quaternion tmp_quat = vars.quat1;
//        tmp_quat.set( worldPosition.x, worldPosition.y, worldPosition.z, 1 );
//        viewProjectionMatrix.mult(tmp_quat, tmp_quat);
//        tmp_quat.multLocal( 1.0f / tmp_quat.getW() );
//        store.x = ( ( tmp_quat.getX() + 1 ) * ( viewPortRight - viewPortLeft ) / 2 + viewPortLeft )
//                  * getWidth();
//        store.y = ( ( tmp_quat.getY() + 1 ) * ( viewPortTop - viewPortBottom ) / 2 + viewPortBottom )
//                  * getHeight();
//        store.z = ( tmp_quat.getZ() + 1 ) / 2;
//        vars.release();

        float w = viewProjectionMatrix.multProj(worldPosition, store);
        store.divideLocal(w);

        store.x = ((store.x + 1f) * (viewPortRight - viewPortLeft) / 2f + viewPortLeft) * getWidth();
        store.y = ((store.y + 1f) * (viewPortTop - viewPortBottom) / 2f + viewPortBottom) * getHeight();
        store.z = (store.z + 1f) / 2f;

        return store;
    }

    /**
     * Returns the display width.
     *
     * @return the width/resolution of the display.
     */
    public int getWidth() {
        return width;
    }

    /**
     * Returns the display height.
     *
     * @return the height/resolution of the display.
     */
    public int getHeight() {
        return height;
    }

    @Override
    public String toString() {
        return "Camera[location=" + location + "\n, direction=" + getDirection() + "\n"
                + "res=" + width + "x" + height + ", parallel=" + parallelProjection + "\n"
                + "near=" + frustumNear + ", far=" + frustumFar + "]";
    }

    @Override
    public void write(JmeExporter e) throws IOException {
        OutputCapsule capsule = e.getCapsule(this);
        capsule.write(location, "location", Vector3f.ZERO);
        capsule.write(rotation, "rotation", Quaternion.DIRECTION_Z);
        capsule.write(frustumNear, "frustumNear", 1);
        capsule.write(frustumFar, "frustumFar", 2);
        capsule.write(frustumLeft, "frustumLeft", -0.5f);
        capsule.write(frustumRight, "frustumRight", 0.5f);
        capsule.write(frustumTop, "frustumTop", 0.5f);
        capsule.write(frustumBottom, "frustumBottom", -0.5f);
        capsule.write(coeffLeft, "coeffLeft", new float[2]);
        capsule.write(coeffRight, "coeffRight", new float[2]);
        capsule.write(coeffBottom, "coeffBottom", new float[2]);
        capsule.write(coeffTop, "coeffTop", new float[2]);
        capsule.write(viewPortLeft, "viewPortLeft", 0);
        capsule.write(viewPortRight, "viewPortRight", 1);
        capsule.write(viewPortTop, "viewPortTop", 1);
        capsule.write(viewPortBottom, "viewPortBottom", 0);
        capsule.write(width, "width", 0);
        capsule.write(height, "height", 0);
        capsule.write(name, "name", null);
    }

    @Override
    public void read(JmeImporter importer) throws IOException {
        InputCapsule capsule = importer.getCapsule(this);
        location = (Vector3f) capsule.readSavable("location", Vector3f.ZERO.clone());
        rotation = (Quaternion) capsule.readSavable("rotation", Quaternion.DIRECTION_Z.clone());
        frustumNear = capsule.readFloat("frustumNear", 1);
        frustumFar = capsule.readFloat("frustumFar", 2);
        frustumLeft = capsule.readFloat("frustumLeft", -0.5f);
        frustumRight = capsule.readFloat("frustumRight", 0.5f);
        frustumTop = capsule.readFloat("frustumTop", 0.5f);
        frustumBottom = capsule.readFloat("frustumBottom", -0.5f);
        coeffLeft = capsule.readFloatArray("coeffLeft", new float[2]);
        coeffRight = capsule.readFloatArray("coeffRight", new float[2]);
        coeffBottom = capsule.readFloatArray("coeffBottom", new float[2]);
        coeffTop = capsule.readFloatArray("coeffTop", new float[2]);
        viewPortLeft = capsule.readFloat("viewPortLeft", 0);
        viewPortRight = capsule.readFloat("viewPortRight", 1);
        viewPortTop = capsule.readFloat("viewPortTop", 1);
        viewPortBottom = capsule.readFloat("viewPortBottom", 0);
        width = capsule.readInt("width", 1);
        height = capsule.readInt("height", 1);
        name = capsule.readString("name", null);
        onFrustumChange();
        onViewPortChange();
        onFrameChange();
    }
}
