I was looking for resources on how to make a first-person camera myself a while ago and made a sketch with a working one. The way I did it was tracking the direction using pitch and yaw values, then executing translate(camX, camY, camZ)
, rotateY(yaw)
and rotateX(pitch)
in that order to position and orient the camera. I also make sure to always keep the pitch value clamped between -pi/2 and pi/2 so there is not confusing upside-down camera. In addition, I use some funky imports to hide the mouse and set its position to the center of the screen, then use the change in mouseX and mouseY every frame to change the yaw and pitch of the camera.
The Camera Class is actually a 3rd person camera, but with a small distance, it essentially becomes first-person.
public class Camera {
private PVector focus, prevFocus; // world position of where the camera should look
private float glide; //internal value for gliding from one focus to another, used for cutscenes and stuff
float yaw, pitch, distance, glideRate;
float fov, aspect, zNear, zFar; // camera settings
public Camera() {
prevFocus = new PVector(0, 0, 0);
focus = new PVector(0, 0, 0);
glide = 1;
glideRate = 0.005;
yaw = 0;
pitch = HALF_PI;
distance = 1;
fov = radians(80);
zNear = 1;
zFar = 65536;
}
public PVector getPosition() {
PVector tempFocus = PVector.lerp(prevFocus, focus, glideValue()); //glideValue applies a logistic curve to the normally linearly changing glide value to make it smoother.
return new PVector(distance * sin(yaw) * cos(pitch) + tempFocus.x, distance * -sin(pitch) + tempFocus.y, distance * cos(yaw) * cos(pitch) + tempFocus.z);
}
public PVector getOrientation() {
return PVector.sub(focus, getPosition()).normalize();
}
// for instantaneous movement or for movement that occurs every frame
public void setFocus(PVector newFocus) {
prevFocus = focus.copy();
focus = newFocus.copy();
glide = 1;
}
// for smooth movement. It is best not to interrupt this by checking when the glide value != 1 with isGliding().
public void glideToFocus(PVector newFocus) {
prevFocus = focus.copy();
focus = newFocus.copy();
glide = 0;
}
private float glideValue() {
float e = exp(20 * (glide - 0.5));
return (e / (1 + e));
}
public boolean isGliding() {
return glide != 1;
}
public void render() {
if (glide < 0) glide = 0;
if (glide < 1) {
glide += glideRate;
}
if (glide > 1) glide = 1;
clampValues();
PVector pos = getPosition();
PVector tempfocus = PVector.lerp(prevFocus, focus, glideValue());
perspective(fov, float(width)/float(height), zNear, zFar);
camera(pos.x, pos.y, pos.z, tempfocus.x, tempfocus.y, tempfocus.z, 0, 1, 0);
}
public void clampValues() {
if (distance <= 0) {
distance = 1;
}
if (pitch <= -HALF_PI)
pitch = -HALF_PI + 0.0001;
if (pitch >= HALF_PI)
pitch = HALF_PI - 0.0001;
while (yaw < 0 || yaw >= TAU) {
if (yaw < 0)
yaw += TAU;
if (yaw >= TAU)
yaw -= TAU;
}
}
public String toString() {
return "Pitch: " + pitch + "\nYaw: " + yaw + "\nZoom: " + distance + "\nfov: " + degrees(fov) + "\nnearClip: " + zNear + "\nfarClip: " + zFar;
}
}
The driver class has all the code necessary to drive the camera, and includes features like deltaTime which allows you to have framerate-independent code, and full-axis movement so you can explore the scene.
import com.jogamp.newt.opengl.GLWindow; //used for funky mouse position and hiding stuff
PVector camfocus; //3D coordinates of the target
Camera maincam;
//deltaTime is in seconds
float deltaTime;
long ptime;
//mouse control
boolean Lock;
GLWindow r;
float Sensitivity;
int offsetX=0;
int offsetY=0;
PVector oldMouse;
//player control
boolean w, a, s, d, space, shift;
// more efficient environment
PShape env;
void setup() {
size(1600, 900, P3D); // 1600 x 900 pog
frameRate(75);
r=(GLWindow)surface.getNative(); //for the funky
Lock = false;
Sensitivity = 0.5;
oldMouse = new PVector(mouseX, mouseY);
camfocus = new PVector(0, 50, 0);
maincam = new Camera();
maincam.yaw = 5 * PI/4.0;
maincam.distance = 2; //Camera is actually a 3rd person camera, but at distances as short as 2, it's basically first person. Distance cannot be negative.
ptime = 0;
env = createEnvironment();
}
void draw() {
//time update
long curTime = System.nanoTime();
deltaTime = (curTime - ptime) * 0.000000001; //converts nanoseconds to seconds as a float
ptime = curTime;
// If you multiply time sensitive actions by deltaTime, the time it takes for that action to complete becomes independent of framerate!
// mouse shenanigans are not by me originally, but I don't remember which forum user showed me this
if (Lock) {
r.setPointerVisible(false); //When locked and trying to move, the pointer jerks all over the place, so best to hide it.
r.warpPointer(width/2, height/2); //Move it to the exact center of the sketch window.
r.confinePointer(true); //Locks pointer inside of the sketch's window so it doesn't escape.
//The pointer will still hit the window's edges, limiting moves fast enough.
//But as soon as the pointer is outside of the window it's visible until it's
//teleported back inside the window, which is less preferable.
} else {
r.confinePointer(false);
r.setPointerVisible(true);
} //Else undo that stuff.
if (Lock) {
maincam.yaw -= (mouseX-offsetX-width/2)*Sensitivity*deltaTime;
maincam.pitch += (mouseY-offsetY-height/2)*Sensitivity*deltaTime;
} //If lock, then adjust position...
offsetX=offsetY=0; // used to store mouse position when mouse is not locked. This prevents the camera from looking to a new location when the mouse becomes locked again
float moveSpeed = 420.69; // heh
if(w){
camfocus = camfocus.add(PVector.mult(maincam.getOrientation(), moveSpeed * deltaTime));
}
if(a){
camfocus = camfocus.add(moveSpeed * deltaTime * -cos(maincam.yaw), 0, moveSpeed * deltaTime * sin(maincam.yaw));
}
if(s){
camfocus = camfocus.add(PVector.mult(maincam.getOrientation(), -moveSpeed * deltaTime));
}
if(d){
camfocus = camfocus.add(moveSpeed * deltaTime * cos(maincam.yaw), 0, moveSpeed * deltaTime * -sin(maincam.yaw));
}
if(space){
camfocus = camfocus.add(0, -moveSpeed * deltaTime, 0);
}
if(shift){
camfocus = camfocus.add(0, moveSpeed * deltaTime, 0);
}
maincam.setFocus(camfocus); //sets new camera position
maincam.render();
background(0);
//renderEnvironment();
shape(env); //using a pre-computed PShape significantly improves real-time performance
}
// Creats a ground plane out of boxes.
void renderEnvironment() {
// number of boxes that are placed along each axis (X and Z only) to form the ground plain.
int boxNumberPerAxis = 25;
fill(198, 101, 35);
// Forming the celling plain.
for (int z = 0; z < boxNumberPerAxis; z++ ) {
for (int x = 0; x < boxNumberPerAxis; x++) {
block(x*50, 0, z*50, 50);
}
}
// Forming the ground plain.
fill(105, 88, 114);
for (int z = 0; z < boxNumberPerAxis; z++ ) {
for (int x = 0; x < boxNumberPerAxis; x++) {
block(x*50, height * (2.0/3.0), z*50, 50);
}
}
}
void block(float x, float y, float z, float s){
pushMatrix();
translate(x, y, z);
box(s);
popMatrix();
}
PShape createEnvironment() {
PShape out = createShape(GROUP);
// number of boxes that are placed along each axis (X and Z only) to form the ground plane.
int boxNumberPerAxis = 25;
fill(198, 101, 35);
// Forming the ceiling plane.
for (int z = 0; z < boxNumberPerAxis; z++ ) {
for (int x = 0; x < boxNumberPerAxis; x++) {
PShape child = createShape(BOX, 50);
child.translate(x*50,0,z*50);
child.setFill(color(198, 101, 35));
out.addChild(child);
}
}
// Forming the ground plain.
fill(105, 88, 114);
for (int z = 0; z < boxNumberPerAxis; z++ ) {
for (int x = 0; x < boxNumberPerAxis; x++) {
PShape child = createShape(BOX, 50);
child.translate(x*50,height * (2.0/3.0),z*50);
child.setFill(color(105, 88, 114));
out.addChild(child);
}
}
return out;
}
void keyPressed() {
if(key == 'w' || key == 'W'){
w = true;
}
if(key == 'a' || key == 'A'){
a = true;
}
if(key == 's' || key == 'S'){
s = true;
}
if(key== 'd' || key == 'D'){
d = true;
}
if(keyCode == SHIFT){
shift = true;
}
if(key == ' '){
space = true;
}
if (key == 'e') {
if (!Lock) {
oldMouse = new PVector(mouseX, mouseY);
offsetX = mouseX - width/2;
offsetY = mouseY - height/2;
}
if (Lock) {
r.warpPointer((int) oldMouse.x, (int) oldMouse.y);
}
Lock = !Lock;
}
if (key == 'v') {
maincam.fov = radians(20);
Sensitivity *= 0.125;
}
}
void keyReleased() {
if(key == 'w' || key == 'W'){
w = false;
}
if(key == 'a' || key == 'A'){
a = false;
}
if(key == 's' || key == 'S'){
s = false;
}
if(key== 'd' || key == 'D'){
d = false;
}
if(keyCode == SHIFT){
shift = false;
}
if(key == ' '){
space = false;
}
if (key == 'v') {
maincam.fov = radians(80);
Sensitivity *= 8;
}
}
void mousePressed() {
if (!Lock) {
oldMouse = new PVector(mouseX, mouseY);
offsetX = mouseX - width/2;
offsetY = mouseY - height/2;
Lock = true;
}
}
void mouseWheel(MouseEvent event) {
float e = event.getCount();
maincam.fov += (PI/180) * e;
}