package tk.amberide.engine.gl.camera;
/*
* Copyright (c) 2013, Oskar Veerhoek
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. 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.
*
* 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.
*
* The views and conclusions contained in the software and documentation are those
* of the authors and should not be interpreted as representing official policies,
* either expressed or implied, of the FreeBSD Project.
*/
import static tk.amberide.engine.data.map.Direction.*;
import tk.amberide.engine.input.AbstractKeyboard;
import tk.amberide.engine.input.AbstractMouse;
import org.lwjgl.opengl.GLContext;
import org.lwjgl.util.glu.GLU;
import static java.lang.Math.*;
import org.lwjgl.input.Keyboard;
import static org.lwjgl.opengl.ARBDepthClamp.GL_DEPTH_CLAMP;
import static org.lwjgl.opengl.GL11.*;
import tk.amberide.engine.data.map.Direction;
/**
* A camera set in 3D perspective. The camera uses Euler angles internally, so
* beware of a gimbal lock.
*
* @author Oskar Veerhoek
*/
public final class EulerCamera {
private float x = 0;
private float y = 0;
private float z = 0;
private float pitch = 0;
private float yaw = 0;
private float roll = 0;
private float fov = 90;
private float aspectRatio = 1;
private final float zNear;
private final float zFar;
private EulerCamera(Builder builder) {
this.x = builder.x;
this.y = builder.y;
this.z = builder.z;
this.pitch = builder.pitch;
this.yaw = builder.yaw;
this.roll = builder.roll;
this.aspectRatio = builder.aspectRatio;
this.zNear = builder.zNear;
this.zFar = builder.zFar;
this.fov = builder.fov;
}
/**
* Creates a new camera with the given aspect ratio. It's located at [0 0 0]
* with the orientation [0 0 0]. It has a zNear of 0.3, a zFar of 100.0, and
* an fov of 90.
*/
public EulerCamera() {
this.zNear = 0.3f;
this.zFar = 100;
}
/**
* Creates a new camera with the given aspect ratio. It's located at [0 0 0]
* with the orientation [0 0 0]. It has a zNear of 0.3, a zFar of 100.0, and
* an fov of 90.
*
* @param aspectRatio the aspect ratio (width/height) of the camera
* @throws IllegalArgumentException if aspectRatio is 0 or smaller than 0
*/
public EulerCamera(float aspectRatio) {
if (aspectRatio <= 0) {
throw new IllegalArgumentException("aspectRatio " + aspectRatio + " was 0 or was smaller than 0");
}
this.aspectRatio = aspectRatio;
this.zNear = 0.3f;
this.zFar = 100;
}
/**
* Creates a new camera with the given aspect ratio and location.
*
* @param aspectRatio the aspect ratio (width/height) of the camera
* @param x the first location coordinate
* @param y the second location coordinate
* @param z the third location coordinate
* @throws IllegalArgumentException if aspectRatio is 0 or smaller than 0
*/
public EulerCamera(float aspectRatio, float x, float y, float z) {
this(aspectRatio);
this.x = x;
this.y = y;
this.z = z;
}
/**
* Creates a new camera with the given aspect ratio, location, and
* orientation.
*
* @param aspectRatio the aspect ratio (width/height) of the camera
* @param x the first location coordinate
* @param y the second location coordinate
* @param z the third location coordinate
* @param pitch the pitch (rotation on the x-axis)
* @param yaw the yaw (rotation on the y-axis)
* @param roll the roll (rotation on the z-axis)
* @throws IllegalArgumentException if aspectRatio is 0 or smaller than 0
*/
public EulerCamera(float aspectRatio, float x, float y, float z, float pitch, float yaw, float roll) {
this(aspectRatio, x, y, z);
this.pitch = pitch;
this.yaw = yaw;
this.roll = roll;
}
/**
* Creates a new camera with the given aspect ratio, location, zNear, zFar
* and orientation.
*
* @param aspectRatio the aspect ratio (width/height) of the camera
* @param x the first location coordinate
* @param y the second location coordinate
* @param z the third location coordinate
* @param pitch the pitch (rotation on the x-axis)
* @param yaw the yaw (rotation on the y-axis)
* @param roll the roll (rotation on the z-axis)
* @throws IllegalArgumentException if aspectRatio is 0 or smaller than 0 or
* if zNear is 0 or smaller than 0 or if zFar is the same or smaller than
* zNear
*/
public EulerCamera(float aspectRatio, float x, float y, float z, float pitch, float yaw, float roll, float zNear,
float zFar) {
if (aspectRatio <= 0) {
throw new IllegalArgumentException("aspectRatio " + aspectRatio + " was 0 or was smaller than 0");
}
if (zNear <= 0) {
throw new IllegalArgumentException("zNear " + zNear + " was 0 or was smaller than 0");
}
if (zFar <= zNear) {
throw new IllegalArgumentException("zFar " + zFar + " was smaller or the same as zNear " + zNear);
}
this.aspectRatio = aspectRatio;
this.x = x;
this.y = y;
this.z = z;
this.pitch = pitch;
this.yaw = yaw;
this.roll = roll;
this.zNear = zNear;
this.zFar = zFar;
}
public Direction getFacingDirection() {
// From CommandBook
double rot = (yaw - 90) % 360;
if (rot < 0) {
rot += 360.0;
}
if (0 <= rot && rot < 22.5) {
return NORTH;
} else if (22.5 <= rot && rot < 67.5) {
return NORTH_EAST;
} else if (67.5 <= rot && rot < 112.5) {
return EAST;
} else if (112.5 <= rot && rot < 157.5) {
return SOUTH_EAST;
} else if (157.5 <= rot && rot < 202.5) {
return SOUTH;
} else if (202.5 <= rot && rot < 247.5) {
return SOUTH_WEST;
} else if (247.5 <= rot && rot < 292.5) {
return WEST;
} else if (292.5 <= rot && rot < 337.5) {
return NORTH_WEST;
} else if (337.5 <= rot && rot < 360.0) {
return NORTH;
} else {
throw new IllegalStateException("illegal yaw: " + rot);
}
}
/**
* Processes mouse input and converts it in to camera movement.
*/
public void processMouse() {
final float MAX_LOOK_UP = 90;
final float MAX_LOOK_DOWN = -90;
float mouseDX = AbstractMouse.getDX() * 0.16f;
float mouseDY = AbstractMouse.getDY() * 0.16f;
if (yaw + mouseDX >= 360) {
yaw = yaw + mouseDX - 360;
} else if (yaw + mouseDX < 0) {
yaw = 360 - yaw + mouseDX;
} else {
yaw += mouseDX;
}
if (pitch - mouseDY >= MAX_LOOK_DOWN && pitch - mouseDY <= MAX_LOOK_UP) {
pitch += -mouseDY;
} else if (pitch - mouseDY < MAX_LOOK_DOWN) {
pitch = MAX_LOOK_DOWN;
} else if (pitch - mouseDY > MAX_LOOK_UP) {
pitch = MAX_LOOK_UP;
}
}
/**
* Processes mouse input and converts it in to camera movement.
*
* @param mouseSpeed the speed (sensitivity) of the mouse, 1.0 should
* suffice
*/
public void processMouse(float mouseSpeed) {
final float MAX_LOOK_UP = 90;
final float MAX_LOOK_DOWN = -90;
float mouseDX = AbstractMouse.getDX() * mouseSpeed * 0.16f;
float mouseDY = AbstractMouse.getDY() * mouseSpeed * 0.16f;
if (yaw + mouseDX >= 360) {
yaw = yaw + mouseDX - 360;
} else if (yaw + mouseDX < 0) {
yaw = 360 - yaw + mouseDX;
} else {
yaw += mouseDX;
}
if (pitch - mouseDY >= MAX_LOOK_DOWN && pitch - mouseDY <= MAX_LOOK_UP) {
pitch += -mouseDY;
} else if (pitch - mouseDY < MAX_LOOK_DOWN) {
pitch = MAX_LOOK_DOWN;
} else if (pitch - mouseDY > MAX_LOOK_UP) {
pitch = MAX_LOOK_UP;
}
}
/**
* Processes mouse input and converts it into camera movement.
*
* @param mouseSpeed the speed (sensitivity) of the mouse, 1.0 should
* suffice
* @param maxLookUp the maximum angle in degrees at which you can look up
* @param maxLookDown the maximum angle in degrees at which you can look
* down
*/
public void processMouse(float mouseSpeed, float maxLookUp, float maxLookDown) {
float mouseDX = AbstractMouse.getDX() * mouseSpeed * 0.16f;
float mouseDY = AbstractMouse.getDY() * mouseSpeed * 0.16f;
if (yaw + mouseDX >= 360) {
yaw = yaw + mouseDX - 360;
} else if (yaw + mouseDX < 0) {
yaw = 360 - yaw + mouseDX;
} else {
yaw += mouseDX;
}
if (pitch - mouseDY >= maxLookDown && pitch - mouseDY <= maxLookUp) {
pitch += -mouseDY;
} else if (pitch - mouseDY < maxLookDown) {
pitch = maxLookDown;
} else if (pitch - mouseDY > maxLookUp) {
pitch = maxLookUp;
}
}
/**
* Processes keyboard input and converts into camera movement.
*
* @param delta the elapsed time since the last frame update in milliseconds
* @param speed the speed of the movement (normal = 1.0)
* @throws IllegalArgumentException if delta is 0 or delta is smaller than 0
*/
public void processKeyboard(float delta, float speed) {
processKeyboard(delta, speed, speed, speed);
}
/**
* Processes keyboard input and converts into camera movement.
*
* @param delta the elapsed time since the last frame update in milliseconds
* @param speedX the speed of the movement on the x-axis (normal = 1.0)
* @param speedY the speed of the movement on the y-axis (normal = 1.0)
* @param speedZ the speed of the movement on the z-axis (normal = 1.0)
* @throws IllegalArgumentException if delta is 0 or delta is smaller than 0
*/
public void processKeyboard(float delta, float speedX, float speedY, float speedZ) {
if (delta <= 0) {
throw new IllegalArgumentException("delta " + delta + " is 0 or is smaller than 0");
}
boolean keyUp = AbstractKeyboard.isKeyDown(Keyboard.KEY_UP) || AbstractKeyboard.isKeyDown(Keyboard.KEY_W);
boolean keyDown = AbstractKeyboard.isKeyDown(Keyboard.KEY_DOWN) || AbstractKeyboard.isKeyDown(Keyboard.KEY_S);
boolean keyLeft = AbstractKeyboard.isKeyDown(Keyboard.KEY_LEFT) || AbstractKeyboard.isKeyDown(Keyboard.KEY_A);
boolean keyRight = AbstractKeyboard.isKeyDown(Keyboard.KEY_RIGHT) || AbstractKeyboard.isKeyDown(Keyboard.KEY_D);
boolean flyUp = AbstractKeyboard.isKeyDown(Keyboard.KEY_SPACE);
boolean flyDown = AbstractKeyboard.isKeyDown(Keyboard.KEY_LSHIFT);
if (keyUp && keyRight && !keyLeft && !keyDown) {
moveFromLook(speedX * delta * 0.003f, 0, -speedZ * delta * 0.003f);
}
if (keyUp && keyLeft && !keyRight && !keyDown) {
moveFromLook(-speedX * delta * 0.003f, 0, -speedZ * delta * 0.003f);
}
if (keyUp && !keyLeft && !keyRight && !keyDown) {
moveFromLook(0, 0, -speedZ * delta * 0.003f);
}
if (keyDown && keyLeft && !keyRight && !keyUp) {
moveFromLook(-speedX * delta * 0.003f, 0, speedZ * delta * 0.003f);
}
if (keyDown && keyRight && !keyLeft && !keyUp) {
moveFromLook(speedX * delta * 0.003f, 0, speedZ * delta * 0.003f);
}
if (keyDown && !keyUp && !keyLeft && !keyRight) {
moveFromLook(0, 0, speedZ * delta * 0.003f);
}
if (keyLeft && !keyRight && !keyUp && !keyDown) {
moveFromLook(-speedX * delta * 0.003f, 0, 0);
}
if (keyRight && !keyLeft && !keyUp && !keyDown) {
moveFromLook(speedX * delta * 0.003f, 0, 0);
}
if (flyUp && !flyDown) {
y += speedY * delta * 0.003f;
}
if (flyDown && !flyUp) {
y -= speedY * delta * 0.003f;
}
}
/**
* Move in the direction you're looking. That is, this method assumes a new
* coordinate system where the axis you're looking down is the z-axis, the
* axis to your left is the x-axis, and the upward axis is the y-axis.
*
* @param dx the movement along the x-axis
* @param dy the movement along the y-axis
* @param dz the movement along the z-axis
*/
public void moveFromLook(float dx, float dy, float dz) {
this.z += dx * (float) cos(toRadians(yaw - 90)) + dz * cos(toRadians(yaw));
this.x -= dx * (float) sin(toRadians(yaw - 90)) + dz * sin(toRadians(yaw));
//this.y += dy * (float) sin(toRadians(pitch - 90)) + dz * sin(toRadians(pitch));
}
/**
* Sets the position of the camera.
*
* @param x the x-coordinate of the camera
* @param y the y-coordinate of the camera
* @param z the z-coordinate of the camera
*/
public void setPosition(float x, float y, float z) {
this.x = x;
this.y = y;
this.z = z;
}
/**
* Sets GL_PROJECTION to an orthographic projection matrix. The matrix mode
* will be returned it its previous value after execution.
*/
public void applyOrthographicMatrix() {
glPushAttrib(GL_TRANSFORM_BIT);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(-aspectRatio, aspectRatio, -1, 1, 0, zFar);
glPopAttrib();
}
/**
* Enables or disables OpenGL states that will enhance the camera
* appearance. Enable GL_DEPTH_CLAMP if ARB_depth_clamp is supported
*/
public void applyOptimalStates() {
if (GLContext.getCapabilities().GL_ARB_depth_clamp) {
glEnable(GL_DEPTH_CLAMP);
}
}
/**
* Sets GL_PROJECTION to an perspective projection matrix. The matrix mode
* will be returned it its previous value after execution.
*/
public void applyPerspectiveMatrix() {
glPushAttrib(GL_TRANSFORM_BIT);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
GLU.gluPerspective(fov, aspectRatio, zNear, zFar);
glPopAttrib();
}
/**
* Applies the camera translations and rotations to GL_MODELVIEW.
*/
public void applyTranslations() {
glPushAttrib(GL_TRANSFORM_BIT);
glMatrixMode(GL_MODELVIEW);
glRotatef(pitch, 1, 0, 0);
glRotatef(yaw, 0, 1, 0);
glRotatef(roll, 0, 0, 1);
glTranslatef(-x, -y, -z);
glPopAttrib();
}
/**
* Sets the rotation of the camera.
*
* @param pitch the rotation around the x-axis in degrees
* @param yaw the rotation around the y-axis in degrees
* @param roll the rotation around the z-axis in degrees
*/
public void setRotation(float pitch, float yaw, float roll) {
this.pitch = pitch;
this.yaw = yaw;
this.roll = roll;
}
/**
* @return the x-coordinate of the camera
*/
public float x() {
return x;
}
/**
* @return y the y-coordinate of the camera
*/
public float y() {
return y;
}
/**
* @return the z-coordinate of the camera
*/
public float z() {
return z;
}
/**
* @return the pitch of the camera in degrees
*/
public float pitch() {
return pitch;
}
/**
* @return the yaw of the camera in degrees
*/
public float yaw() {
return yaw;
}
/**
* @return the roll of the camera in degrees
*/
public float roll() {
return roll;
}
/**
* @return the fov of the camera in degrees in the y direction
*/
public float fieldOfView() {
return fov;
}
/**
* Sets the field of view angle in degrees in the y direction. Note that
* this.applyPerspectiveMatrix() must be applied in order to see any
* difference.
*
* @param fov the field of view angle in degrees in the y direction
*/
public void setFieldOfView(float fov) {
this.fov = fov;
}
public void setAspectRatio(float aspectRatio) {
if (aspectRatio <= 0) {
throw new IllegalArgumentException("aspectRatio " + aspectRatio + " is 0 or less");
}
this.aspectRatio = aspectRatio;
}
/**
* @return the aspect ratio of the camera
*/
public float aspectRatio() {
return aspectRatio;
}
/**
* @return the distance from the camera to the near clipping pane
*/
public float nearClippingPane() {
return zNear;
}
/**
* @return the distance from the camera to the far clipping pane
*/
public float farClippingPane() {
return zFar;
}
@Override
public String toString() {
return "EulerCamera [x=" + x + ", y=" + y + ", z=" + z + ", pitch=" + pitch + ", yaw=" + yaw + ", "
+ "roll=" + roll + ", fov=" + fov + ", aspectRatio=" + aspectRatio + ", zNear=" + zNear + ", "
+ "zFar=" + zFar + "]";
}
/**
* A builder helper class for the EulerCamera class.
*/
public static class Builder {
private float aspectRatio = 1;
private float x = 0, y = 0, z = 0, pitch = 0, yaw = 0, roll = 0;
private float zNear = 0.3f;
private float zFar = 100;
private float fov = 90;
public Builder() {
}
/**
* Sets the aspect ratio of the camera.
*
* @param aspectRatio the aspect ratio of the camera (window width /
* window height)
* @return this
*/
public Builder setAspectRatio(float aspectRatio) {
if (aspectRatio <= 0) {
throw new IllegalArgumentException("aspectRatio " + aspectRatio + " was 0 or was smaller than 0");
}
this.aspectRatio = aspectRatio;
return this;
}
/**
* Sets the distance from the camera to the near clipping pane.
*
* @param nearClippingPane the distance from the camera to the near
* clipping pane
* @return this
* @throws IllegalArgumentException if nearClippingPane is 0 or less
*/
public Builder setNearClippingPane(float nearClippingPane) {
if (nearClippingPane <= 0) {
throw new IllegalArgumentException("nearClippingPane " + nearClippingPane + " is 0 or less");
}
this.zNear = nearClippingPane;
return this;
}
/**
* Sets the distance from the camera to the far clipping pane.
*
* @param farClippingPane the distance from the camera to the far
* clipping pane
* @return this
* @throws IllegalArgumentException if farClippingPane is 0 or less
*/
public Builder setFarClippingPane(float farClippingPane) {
if (farClippingPane <= 0) {
throw new IllegalArgumentException("farClippingPane " + farClippingPane + " is 0 or less");
}
this.zFar = farClippingPane;
return this;
}
/**
* Sets the field of view angle in degrees in the y direction.
*
* @param fov the field of view angle in degrees in the y direction
* @return this
*/
public Builder setFieldOfView(float fov) {
this.fov = fov;
return this;
}
/**
* Sets the position of the camera.
*
* @param x the x-coordinate of the camera
* @param y the y-coordinate of the camera
* @param z the z-coordinate of the camera
* @return this
*/
public Builder setPosition(float x, float y, float z) {
this.x = x;
this.y = y;
this.z = z;
return this;
}
/**
* Sets the rotation of the camera.
*
* @param pitch the rotation around the x-axis in degrees
* @param yaw the rotation around the y-axis in degrees
* @param roll the rotation around the z-axis in degrees
*/
public Builder setRotation(float pitch, float yaw, float roll) {
this.pitch = pitch;
this.yaw = yaw;
this.roll = roll;
return this;
}
/**
* Constructs an instance of EulerCamera from this builder helper class.
*
* @return an instance of EulerCamera
* @throws IllegalArgumentException if farClippingPane is the same or
* less than nearClippingPane
*/
public EulerCamera build() {
if (zFar <= zNear) {
throw new IllegalArgumentException("farClippingPane " + zFar + " is the same or less than "
+ "nearClippingPane " + zNear);
}
return new EulerCamera(this);
}
}
}