with improved version now below!!!
Another approach, where the camera is on a circle around the player, with an easing when changing the angle of the camera on that circle.
Also, there is an easing on lookAt now also.
Chrisir
// new version with lookAtPVectorFollowCircle()
// 3D sketch: shows a ball that moves in the x/z plane (height = y = 500) and is followed by a camera.
// The goal of the sketch is to demonstrate the usage of a following camera.
// cubes
float angleCubes=45; // angle for the cubes in the scene
CameraClass cam;
PVector ballPos = new PVector (300, 500, 300);
PVector ballPosPrev = new PVector (300, 500-10, 310);
PVector ballVel = new PVector (
random(1, 2),
0,
random(-2, -1));
final int heightAbovePlayer = 63;
float angle1 = 0; // angle
float angle2 = 0;
// when the ball is not running, we have a slight up and down movement of the camera
boolean ballIsStopped=false; // flag tells whether the ball is stopped or not
float angleHeight=0; // angle for the slight up and down movement of the camera
// -----------------------------------------------------
void setup() {
size (1400, 800, P3D);
cam = new CameraClass ();
cam.camPos = ballPos.copy();
cam.camPos.x += 113;
cam.camPos.y -= heightAbovePlayer;
cam.camPos.z += 113;
} // func
void draw() {
// clear canvas
background(111);
// avoid clipping : https : // forum.processing.org/two/discussion/4128/quick-q-how-close-is-too-close-why-when-do-3d-objects-disappear
perspective(PI/3.0, (float) width/height, 1, 1000000);
// apply lights
lights();
// camera follows ball
if (! (keyPressed&&key==' ')) {
//cam.lookAtPVectorFollow(ballPos);
cam.lookAtPVectorFollowCircle(ballPos);
}
// apply the values of the class to the real camera
cam.set();
// move ball and draw it
ballPosPrev = ballPos.copy();
if (! (keyPressed&&key=='m')) {
ballPos.add(ballVel);
}
if (ballPosPrev.dist(ballPos)==0) {
if (!ballIsStopped)
angleHeight=0;
ballIsStopped=true;
} else {
ballIsStopped=false;
}
angle1 = PI + atan2(ballPos.z - ballPosPrev.z, ballPos.x - ballPosPrev.x );
fill(0, 255, 0); // green
noStroke();
mySphere(ballPos.x, ballPos.y, ballPos.z, 10);
// contain ball in a invisible box
contain(ballPos, ballVel);
// make the scene with the boxes
scene();
// text upper left corner
fill(0, 255, 0);
cam.HUD_text("The green ball is contained in an "
+"red field."
+"\nHold space bar to stop camera."
+"\nHold 'm' to stop the Movement of the ball.");
// println(angle1+", "+angle2);
//
} // func
// ---------------------------------------------------
void scene () {
// Drawing of the red field
fill(255, 2, 2);//RED
noStroke();
mySphere(500, 500, 500, 20);
mySphere(100, 500, 100, 20);
mySphere(100, 500, 500, 20);
mySphere(500, 500, 100, 20);
//
// rect
stroke(255, 2, 2);//RED
line(500, 500, 500, 500, 500, 100);//up
line(500, 500, 100, 100, 500, 100); //left
line(100, 500, 100, 100, 500, 500 ); // down
line(100, 500, 500, 500, 500, 500 ); // down
stroke(0);
float z ;
// one wall of boxes
for (int x = 10; x < 600; x+= 100)
{
fill(x/3, 2, 2);
for (int y = 10; y < 600; y+= 100)
{
z = -600;
myBox(x, y, z, 24);
// println ( "Box: " + x + ", "+y+ " "+z);
}
fill(0, 0, 254);
z=-800;
myBox(x, 10, z, 24);
}
//
// a few additional boxes
fill(0, 0, 254);
z=-400;
myBox(220, 10, z, 24);
myBox(600, 10, z, 24);
z=-400;
myBox(220, 510, z, 24);
myBox(600, 510, z, 24);
z=399;
myBox(220, 510, z, 24);
myBox(600, 510, z, 24);
z=900;
myBox(220, 510, z, 24);
myBox(600, 510, z, 24);
angleCubes++;
//
}
void myBox(float x, float y, float z,
float size1) {
// one nice wrapper for build in box-command
pushMatrix();
translate(x, y, z);
rotateY(radians(angleCubes));
rotateX(radians(45));
box(size1);
popMatrix();
}
void mySphere(float x, float y, float z,
float size1) {
// one nice wrapper for build in sphere-command
pushMatrix();
translate(x, y, z);
sphere(size1);
popMatrix();
}
void contain(PVector ballPos, PVector ballVel) {
// contain ball
if (ballPos.x>500) {
ballVel.x=abs(ballVel.x)*-1;
println("---");
}
if (ballPos.z>500) {
ballVel.z=abs(ballVel.z)*-1;
println("---");
}
if (ballPos.x<100) {
ballVel.x=abs(ballVel.x);
println("---");
}
if (ballPos.z<100) {
ballVel.z=abs(ballVel.z);
println("---");
}
}
// ===============================================================
class CameraClass {
// capsules the normal camera() command and its vectors
PVector camPos; // its vectors
PVector camLookAt;
PVector camUp;
PVector camPosInitial; // its vectors - the default (unchanged)
PVector camLookAtInitial;
PVector camUpInitial;
// for follow
PVector camWhereItShouldBe = new PVector(0, 0, 0);
PVector camAdd = new PVector(0, -60, 0);
float easing = .019; // .07; // how fast it changes
float camCurrentAngle=0;// -90; // for cam rotation around itself (around Y-axis)
float camRadius; // same situation
boolean firstTime=true;
// constructor without parameters
CameraClass() {
// constr
// set vectors
camPos = new PVector(width/2.0, height/2.0, 990);
camLookAt = new PVector(width/2.0, height/2.0, -600);
camUp = new PVector( 0, 1, 0 );
// save the initial values
camPosInitial = camPos.get();
camLookAtInitial = camLookAt.get();
camUpInitial = camUp.get();
} // constr
// ----------------------------------------
void set() {
// apply vectors to actual camera
camera (camPos.x, camPos.y, camPos.z,
camLookAt.x, camLookAt.y, camLookAt.z,
camUp.x, camUp.y, camUp.z);
}
// ---
void setLookAt (float x1, float y1, float z1) {
camLookAt = new PVector(x1, y1, z1);
}
// ---
void lookAtPVectorFollowOnlyLookAt(PVector followMe) {
// Two versions of this. Without easing.
// Follows a player (e.g.),
// change only look at.
camLookAt = followMe.get();
}
void lookAtPVectorFollowOnlyLookAtWithEasing(PVector followMe) {
// Version with easing.
// Follows a player (e.g.),
// change only look at.
float easing = 0.0398;
camLookAt.x += (followMe.x-camLookAt.x) * easing; // followMe.get();
//camLookAt.y += (followMe.y-camLookAt.y) * easing;
camLookAt.y = followMe.y; // other principle
camLookAt.z += (followMe.z-camLookAt.z) * easing;
}
// ---
void lookAtPVectorFollowCircle(PVector followMe) {
// Two versions of this.
// One version of doing it.
// The camera moves on a circle around the player.
float RadiusCam=170.0; // dist camera from the player (radius of the cam around the player)
if (firstTime) {
lookAtPVectorFollowOnlyLookAt(followMe);
firstTime=false;
} else {
lookAtPVectorFollowOnlyLookAtWithEasing(followMe);
}
float easing = 0.0198;
angle2 += (angle1-angle2) * easing;
// the camera is around the avatar/player
// in a distance RadiusCam from the avatar/player
camPos.x = followMe.x + RadiusCam*cos(angle2);
// other principles for y, no circle calculation
if (ballIsStopped)
camPos.y = (followMe.y - heightAbovePlayer) + (sin(angleHeight) * heightAbovePlayer/2); // slight movement of the cam up and down
else
camPos.y = followMe.y - heightAbovePlayer; // other principle for y, no circle calculation
camPos.z = followMe.z + RadiusCam*sin(angle2);
angleHeight+=.021;
} //
void lookAtPVectorFollow (PVector followMe) {
// second version of doing it
// camera is trying to get to player position, but with easing.
// follows a ball / player (e.g.),
// complete following (lookAt and camPos).
// manage Look At
camLookAt = followMe.get();
// manage position of the camera
float easing = 0.0198;
// x
camPos.x += (followMe.x-camPos.x) * easing;
// y
// camPos.y += (followMe.y-camPos.y) * easing -20;
camPos.y = followMe.y - heightAbovePlayer; // other principle for y !
// z
camPos.z += (followMe.z-camPos.z) * easing;
}
// ---
void printData() {
println ( "Cam at " + camPos
+ " looking at " + camLookAt
+ " (angle = "
+camCurrentAngle
+").");
}
void HUD_text (String a1) {
// HUD text upper left corner
// this must be called at the very end of draw()
// this is a 2D HUD
camera();
hint(DISABLE_DEPTH_TEST);
noLights();
// ------------------
textSize(16);
text (a1, 20, 20);
// ------------------
// reset all parameters to defaults
textAlign(LEFT, BASELINE);
rectMode(CORNER);
textSize(32);
hint(ENABLE_DEPTH_TEST); // no HUD anymore
lights();
} // method
//
} // class
// ======================================
improved version
//
// new version with lookAtPVectorFollowCircle().
// 3D sketch: shows a ball that moves in the x/z plane (height = y = GENERAL_Y_HEIGHT = 500) and is followed by a camera.
// The goal of the sketch is to demonstrate the usage of a **following** camera.
// The camera moves on a circle (in x/z plane) around the ball/Player.
// The camera Y is located HEIGHT_ABOVE_PLAYER = 63 above the ball/Player.
// How is the angle of the cam on the circle around the player calculated?
// ball.atan2FromBall() is an important function here that returns the angle between the ball and its previous position;
// this angle makes a line from current ball pos to the previous ball pos. When you prolongue the line
// you come to the x,z pos of the camera (y pos of the ball is "above" than the ball).
// The angle is used to place the cam on a circle with a fixed radius around the player/ball. The angle is changed with an
// constrained easing to make the cam movement smooth.
// So the cam rotates around the ball and follows the ball.
// The cam lookAt is towards the ball but also with a 2nd easing.
// When the ball stops, the cam slightly moves up and down (during waiting).
// Search "debug" and "println" to find vars etc. to delete when debugging is over.
// https://discourse.processing.org/t/simple-example-of-3d-movement-following-a-point/12361/21
// CONSTANTS: the floor / field (y value)
final int GENERAL_Y_HEIGHT = 500;
// Classes: The camera
CameraClass cam;
// The ball
Ball ball = new Ball();
// The scene (background, field, boxes...)
Scene scene = new Scene();
// tools
Tools tools = new Tools();
// angle stuff
float angle2 = 0; // actually used to calc cam pos
float angleToWorkWith; // that's used to calc the delta
// -----------------------------------------------------
void setup() {
size (1400, 800, P3D);
// setup camera
cam = new CameraClass ();
cam.camPos = ball.ballPos.copy();
cam.camPos.x += 113;
cam.camPos.y -= cam.HEIGHT_ABOVE_PLAYER;
cam.camPos.z += 113;
} // func
void draw() {
// set Canvas and Environment
scene.setCanvasAndEnvironment();
// camera follows ball (when Space Bar is not pressed)
cam.lookAtPVectorFollowCircle(ball.ballPos); // old Version: cam.lookAtPVectorFollow(ballPos);
cam.set(); // apply the values of the class cam to the real camera
// move ball (when m is not pressed)
ball.move(); // move ball
ball.checkStop(); // when the balls is stopped some adjustements are made
// manage angles
manageAngles();
// ball display and constrain
ball.script();
// display the scene with the boxes and HUD text
scene.displayScene();
//
} // func draw
// ---------------------------------------------------
void manageAngles() {
// manage Angles
// calc the angle from the line between the ball pos and tha balls previous position
float angle1 = PI + ball.atan2FromBall(); // gets atan2FromBall + PI
angle1 = tools.fixAngle(angle1);
angleToWorkWith=angle1;
if ( (angle1-angle2) > PI ) {
angleToWorkWith=angle1-TWO_PI;
//println("Here 13 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++###");
}//if
if ( (angle2-angle1) > PI ) {
angleToWorkWith=angle1+TWO_PI;
//println("Here 14 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++###");
}//if
// debug
//println(degrees(angle1)
// +", "
// +degrees(angle2));
//
}//func
// =================================================================
// classes
class Scene {
// cubes
float angleCubes=45; // angle for the cubes in the scene
void setCanvasAndEnvironment() {
// set Canvas and Environment
// clear canvas
background(111);
// avoid clipping : https : // forum.processing.org/two/discussion/4128/quick-q-how-close-is-too-close-why-when-do-3d-objects-disappear
perspective(PI/3.0, (float) width/height, 1, 1000000);
// apply lights
lights();
}
void displayScene() {
// Drawing of the scene / decoration
// Drawing of the red field ----
fill(255, 2, 2);//RED
noStroke();
tools.mySphere(500, GENERAL_Y_HEIGHT, 500, 20);
tools.mySphere(100, GENERAL_Y_HEIGHT, 100, 20);
tools.mySphere(100, GENERAL_Y_HEIGHT, 500, 20);
tools.mySphere(500, GENERAL_Y_HEIGHT, 100, 20);
// rect of lines
stroke(255, 2, 2);//RED
// using 3D lines:
line(500, GENERAL_Y_HEIGHT, 500, 500, GENERAL_Y_HEIGHT, 100); // up |
line(500, GENERAL_Y_HEIGHT, 100, 100, GENERAL_Y_HEIGHT, 100); // left <-
line(100, GENERAL_Y_HEIGHT, 100, 100, GENERAL_Y_HEIGHT, 500 ); // down |
line(100, GENERAL_Y_HEIGHT, 500, 500, GENERAL_Y_HEIGHT, 500 ); // right ->
// one small wall of boxes
stroke(0);
int z = -60;
for (int x = 10; x < 600; x+= 100) {
fill(x/3, 2, 2);
for (int y = 410; y < 600; y+= 100) {
tools.myBox(x, y, z, 24, angleCubes);
}
}
// a few additional blue boxes
fill(0, 0, 255);
z=-400;
tools.myBox(220, 10, z, 24, angleCubes);
tools.myBox(600, 10, z, 24, angleCubes);
z=-400;
tools.myBox(220, 510, z, 24, angleCubes);
tools.myBox(600, 510, z, 24, angleCubes);
z=399;
tools.myBox(220, 510, z, 24, angleCubes);
tools.myBox(600, 510, z, 24, angleCubes);
z=900;
tools.myBox(220, 510, z, 24, angleCubes);
tools.myBox(600, 510, z, 24, angleCubes);
angleCubes++;
//
// text upper left corner (HUD)
fill(0, 255, 0);
cam.HUD_text("The green ball is contained in a red field."
+"\nHold Space Bar to stop the camera."
+"\nHold 'm' to stop the ball.\n" ); // Hold Space Bar and m has debug purpose
}
//
} // class
// =================================================================
class CameraClass {
// capsules the normal camera() command and its vectors
PVector camPos; // its vectors
PVector camLookAt;
PVector camUp;
PVector camPosInitial; // its vectors - the default (unchanged)
PVector camLookAtInitial;
PVector camUpInitial;
// for follow
PVector camWhereItShouldBe = new PVector(0, 0, 0);
PVector camAdd = new PVector(0, -60, 0);
float easing = .019; // .07; // how fast it changes
float camCurrentAngle=0;// -90; // for cam rotation around itself (around Y-axis)
float camRadius; // same situation
// how high is the cam above the player / ball
final int HEIGHT_ABOVE_PLAYER = 63;
boolean firstTime=true;
float angleForCamerasUpAndDownMovement=0; // angle for the slight up and down movement of the camera
// constructor without parameters
CameraClass() {
// constr
// set vectors
camPos = new PVector(width/2.0, height/2.0, 990);
camLookAt = new PVector(width/2.0, height/2.0, -600);
camUp = new PVector( 0, 1, 0 );
// save the initial values
camPosInitial = camPos.copy();
camLookAtInitial = camLookAt.copy();
camUpInitial = camUp.copy();
} // constr
// ----------------------------------------
void set() {
// apply internal class vectors to actual camera
camera (camPos.x, camPos.y, camPos.z,
camLookAt.x, camLookAt.y, camLookAt.z,
camUp.x, camUp.y, camUp.z);
}
// ---
void setLookAt (float x1, float y1, float z1) {
camLookAt = new PVector(x1, y1, z1);
}
// ---
void lookAtPVectorFollowOnlyLookAt(PVector followMe) {
// Two versions of this. Without easing.
// Follows a player (e.g.),
// change only look at.
camLookAt = followMe.copy();
}
void lookAtPVectorFollowOnlyLookAtWithEasing(PVector followMe) {
// Version with easing.
// Follows a player (e.g.),
// change only look at.
float easing = 0.0398;
camLookAt.x += (followMe.x-camLookAt.x) * easing; // followMe.copy();
//camLookAt.y += (followMe.y-camLookAt.y) * easing;
camLookAt.y = followMe.y; // other principle
camLookAt.z += (followMe.z-camLookAt.z) * easing;
}
// ---
void lookAtPVectorFollowCircle(PVector followMe) {
// Two versions of this.
// One version of doing it:
// The camera moves on a circle around the player (fixed radius = RadiusCam).
// space key leaves the function (camera stands still)
if ((keyPressed&&key==' ')) { // debug purpose
return;
}//if ---------
float RadiusCam=170.0; // dist camera from the player (radius of the cam around the player)
if (firstTime) {
lookAtPVectorFollowOnlyLookAt(followMe);
firstTime=false;
} else {
lookAtPVectorFollowOnlyLookAtWithEasing(followMe);
}
// calc delta with easing (the classic easing formula is stretched over a few lines here)
float easing = 0.0198;
float delta = (angleToWorkWith-angle2) * easing;
// constrain delta:
delta=constrain(delta, -0.04, 0.04);
// add delta
angle2 += delta;
angle2 = tools.fixAngle(angle2);
// the camera is around the avatar/player
// in a distance RadiusCam from the avatar/player
camPos.x = followMe.x + RadiusCam*cos(angle2);
// other principles for y, no circle calculation
if (ball.ballIsStopped)
camPos.y = (followMe.y - HEIGHT_ABOVE_PLAYER) + (sin(angleForCamerasUpAndDownMovement) * HEIGHT_ABOVE_PLAYER/2); // slight movement of the cam up and down
else
camPos.y = followMe.y - HEIGHT_ABOVE_PLAYER; // other principle for y, no circle calculation
camPos.z = followMe.z + RadiusCam*sin(angle2);
angleForCamerasUpAndDownMovement+=.021;
} //
void lookAtPVectorFollow (PVector followMe) {
// second version of doing it
// camera is trying to get to player position, but with easing.
// follows a ball / player (e.g.),
// complete following (lookAt and camPos).
// manage Look At
camLookAt = followMe.copy();
// manage position of the camera
float easing = 0.0198;
// x
camPos.x += (followMe.x-camPos.x) * easing;
// y
// camPos.y += (followMe.y-camPos.y) * easing -20;
camPos.y = followMe.y - HEIGHT_ABOVE_PLAYER; // other principle for y !
// z
camPos.z += (followMe.z-camPos.z) * easing;
}
// ---
void printData() {
println ( "Cam at " + camPos
+ " looking at " + camLookAt
+ " (angle = "
+camCurrentAngle
+").");
}
void HUD_text (String a1) {
// HUD text upper left corner
// this must be called at the very end of draw()
// this is a 2D HUD
camera();
hint(DISABLE_DEPTH_TEST);
noLights();
// ------------------
textSize(16);
text (a1, 20, 20);
// ------------------
// reset all parameters to defaults
textAlign(LEFT, BASELINE);
rectMode(CORNER);
textSize(32);
hint(ENABLE_DEPTH_TEST); // no HUD anymore
lights();
} // method
//
} // class
// ======================================
class Ball {
// position and previous position
PVector ballPos = new PVector (300, GENERAL_Y_HEIGHT, 300);
PVector ballPosPrev = new PVector (300, GENERAL_Y_HEIGHT-10, 310);
// ball velocity - see constructor
PVector ballVel;
// when the ball is not running, we have a slight up and down movement of the camera
boolean ballIsStopped=false; // flag tells whether the ball is stopped or not
boolean keyMhasBallStopped=false; // key M
Ball() {
// constructor ----
// ball vel
ballVel = new PVector (
random(1, 2),
0,
random(1, 2));
// give vel another sign in 50 % of the cases
if (random(100) > 50)
ballVel.x*=-1;
if (random(100) > 50)
ballVel.z*=-1;
} // constructor -----
void move() {
ballPosPrev = ballPos.copy(); // store previous ball pos
// key 'm' leaves the function (ball stands still)
if (keyPressed&&key=='m') { // debug
keyMhasBallStopped=true;
return;
}//if
keyMhasBallStopped=false;
ballPos.add(ballVel); // move
}
void script() {
// display ball
fill(0, 255, 0); // green
noStroke();
tools.mySphere(ballPos.x, ballPos.y, ballPos.z, 10);
// contain ball in a field
contain(ballPos, ballVel);
}
// Tools within the class ----
boolean isMoving() {
// Returns whether the ball is moving.
// It's moving when the prev pos is different from the current pos.
return
ballPosPrev.dist(ballPos) != 0.0;
}
void checkStop() {
if (ball.keyMhasBallStopped && !ball.isMoving()) {
// ball is stopped
// the first time we detect that the ball has stopped (only the first time)
if (!ball.ballIsStopped)
cam.angleForCamerasUpAndDownMovement=0; // we reset the angle height
ball.ballIsStopped=true; // set flag
} else {
ball.ballIsStopped=false;
}
}
float atan2FromBall() {
// ball.atan2FromBall() is an important function that returns the angle between the ball and its previous position;
// this angle makes a line from current ball pos to prev ball pos. When you prolongue the line
// you come to the x,z pos of the camera (y pos of the ball is "higher" than the ball).
// The angle is used to place the cam on a circle with a fixed radius around the player/ball. The angle is changed with an
// constrained easing to make the cam movement smooth. So the cam rotates around the ball and follows the ball.
return
atan2(ballPos.z - ballPosPrev.z, ballPos.x - ballPosPrev.x );
}
void contain(PVector ballPos, PVector ballVel) {
// contain ball / bouncing on field borders
if (ballPos.x>500) {
ballVel.x=abs(ballVel.x)*-1;
println("---"); // 4x println for debug
}
if (ballPos.z>500) {
ballVel.z=abs(ballVel.z)*-1;
println("---");
}
if (ballPos.x<100) {
ballVel.x=abs(ballVel.x);
println("---");
}
if (ballPos.z<100) {
ballVel.z=abs(ballVel.z);
println("---");
}
}//method
//
}//class
// ======================================
class Tools {
// tools
float fixAngle(float angle1) {
// if > 2xPI
if (angle1>TWO_PI)
angle1-=TWO_PI;
if (angle1>TWO_PI)
angle1-=TWO_PI;
// if < 0 (negative angles like -40 should be expressed as positive angles like 320)
if (angle1<0.0) {
angle1 = TWO_PI + angle1;
// println("Here 1 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++###");
}
if (angle1<0.0)
angle1 = TWO_PI + angle1;
return angle1;
}
void myBox(float x, float y, float z,
float size1,
float angleCube) {
// one nice wrapper for build in box-command
pushMatrix();
translate(x, y, z);
rotateY(radians(angleCube));
rotateX(radians(45));
box(size1);
popMatrix();
}
void mySphere(float x, float y, float z,
float size1) {
// one nice wrapper for build in sphere-command
pushMatrix();
translate(x, y, z);
sphere(size1);
popMatrix();
}
}//class
//