Smooth camera path following tips

I’ve been trying to find a simple way to move a camera along a path. It seems every example I find is either outdated itself, has dependencies that are outdated, or is too complex for my needs.

Basically what I want to achieve is to get a to camera follow a simple path in a smooth way. See this short example of movement I made using mouse and keyboard. I want to change this to an automated movement: https://twitter.com/JaspervanLoenen/status/849652697431199744)

I tried to get this to work in two ways. Both let the camera move between a series of points using some simple vector math (basically what @shiffman describes in Nature of Code over here).The path I follow in my tests has some sharp angles, but the vector movement filters this out.

Both of the ways I tried result in movement that just doesn’t feel fluent enough.

  1. Option one calculates the angle between the previous and the current camera position. This is used to project a point in front of the camera which is used as the lookat point.
    Alternatively I got the angle towards the target point on the path and used that (with some easing on the angle changes).

  2. Option two lets two points follow the camera path. The first one gets a head-start so it already moved a bit forward before the second one starts moving. The first one is then used as the lookat point and the second one is for the camera.
    This movement is kind of nice as the camera already starts moving just before it hits a corner (as the lookat already passed the corner just before).

Does anyone have some suggestions for techniques I could try to get this to work in a nicer way?

Below is the second example, where the camera looks at a point in front of it on the same path.


Update: what seems to work kind of nice is to get the center of the two points moving on the path and using that as the lookat point (not in the below code). But I’m just kind of messing around at this point.

ofApp.h

#pragma once

#include "ofMain.h"

class ofApp : public ofBaseApp{

    public:
        void setup();
        void update();
        void draw();

        void keyPressed(int key);
        void keyReleased(int key);
        void mouseMoved(int x, int y );
        void mouseDragged(int x, int y, int button);
        void mousePressed(int x, int y, int button);
        void mouseReleased(int x, int y, int button);
        void mouseEntered(int x, int y);
        void mouseExited(int x, int y);
        void windowResized(int w, int h);
        void dragEvent(ofDragInfo dragInfo);
        void gotMessage(ofMessage msg);

        vector <ofVec3f> pointsList;

        float topSpeed;
        float minDistance;
        int activeCam;

        // These vectors hold values for both the camera and its
        // lookat point. [0] is camera, [1] is lookat
        ofCamera cam[2];
        vector<ofVec3f> cam_location;
        vector<ofVec3f> cam_acceleration;
        vector<ofVec3f> cam_velocity;
        vector<int> cam_targetIndex; // the point to move towards


};

ofApp.cpp

#include "ofApp.h"

void ofApp::setup(){
    ofEnableDepthTest();
    // The route we want the camera to follow
    pointsList.push_back(ofVec3f(211, 104, 0));
    pointsList.push_back(ofVec3f(271, 164, 0));
    pointsList.push_back(ofVec3f(311, 296, 0));
    pointsList.push_back(ofVec3f(270, 398, 0));
    pointsList.push_back(ofVec3f(103, 407, 0));
    pointsList.push_back(ofVec3f(130, 308, 0));
    pointsList.push_back(ofVec3f(251, 317, 0));
    pointsList.push_back(ofVec3f(251, 360, 0));
    pointsList.push_back(ofVec3f(353, 359, 0));
    pointsList.push_back(ofVec3f(372, 130, 0));
    pointsList.push_back(ofVec3f(211, 36, 0));
    pointsList.push_back(ofVec3f(85, 33, 0));

    // what camera are we looking through
    activeCam = 0;
    // how close we can get to a point beefore we starting moving to the next
    minDistance = 3.0;
    // Topspeed of our camera
    topSpeed = 1.0;

    for(int i=0;i<2;i++){
        // start the camera at first point
        cam_location.push_back(ofVec3f(pointsList[0].x, pointsList[0].y, pointsList[0].z));
        cam_acceleration.push_back(ofVec3f(0.0, 0.0, 0.0));
        cam_velocity.push_back(ofVec3f(0.0, 0.0, 0.0));
        cam_targetIndex.push_back(1); // start moving towards second point
    }
    cam[0].setPosition(0, 0, 800);
    cam[0].lookAt({0, 0, 0}, {0.f,1.f,0.f});

    // We give our lookat point a headstart so it is in front of the camera, but still on
    // the same track
    for(int i=0; i<400; i++){
        ofVec3f dir = pointsList[cam_targetIndex[1]]-cam_location[1];
        dir.normalize();
        dir *= 0.5;
        cam_acceleration[1] = dir;

        cam_velocity[1] += cam_acceleration[1];
        cam_velocity[1].limit(topSpeed);
        cam_location[1] += cam_velocity[1];
    }
}

void ofApp::update(){
    for(int i=0;i<2;i++){
        // Move closer towards the new point
        ofVec3f dir = pointsList[cam_targetIndex[i]]-cam_location[i];
        dir.normalize();
        dir *= 0.5;
        cam_acceleration[i] = dir;
        cam_velocity[i] += cam_acceleration[i];
        cam_velocity[i].limit(topSpeed);
        cam_location[i] += cam_velocity[i];

        // Get distance to target point
        float curDist = cam_location[i].distance(pointsList[cam_targetIndex[i]]);
        // are we close, then start moving towards the next
        if(curDist < minDistance){
            cam_targetIndex[i]++;
            if(cam_targetIndex[i] > pointsList.size()-1){
                cam_targetIndex[i] = 0;
            }
        }
    }
    cam[1].setPosition(cam_location[0].x, cam_location[0].y, 10);
    cam[1].lookAt({cam_location[1].x, cam_location[1].y, 10}, {0.f,0.f,1.f});
}

void ofApp::draw(){
    if(activeCam == 1){
        cam[activeCam].begin();

        // draw a ground plane and some boxes so it is easier to see movement in 3d
        ofFill();
        ofSetColor(100);
        ofDrawPlane(0, 0, -1, 1000, 1000);

        ofFill();
        ofSetColor(120);
        ofDrawBox(10,    10,        15,    20,        20,        30);
        ofDrawBox(200,    200,    15,    20,        20,        30);
        ofDrawBox(150,    200,    15,    20,        20,        30);
        ofDrawBox(300,    200,    15,    20,        20,        30);
        ofDrawBox(300,    50,        15,    20,        20,        30);
        ofDrawBox(300,    500,    15,    20,        20,        30);
        ofDrawBox(300,    20,        15,    20,        20,        30);
    }

    // draw the path
    ofFill();
    ofSetColor(0);
    for(int i=0;i<pointsList.size();i++){
        if(i < pointsList.size()-1){
            ofDrawLine(pointsList[i], pointsList[i+1]);
        }
        ofDrawCircle(pointsList[i], 4);
    }

    // draw the camera location
    ofSetColor(255, 0, 0);
    ofDrawSphere(cam_location[0], 4);
    // draw the lookat location
    ofSetColor(0, 255, 0);
    ofDrawSphere(cam_location[1], 4);

    if(activeCam == 1){
        cam[activeCam].end();
    }
}

void ofApp::keyPressed(int key){}
void ofApp::keyReleased(int key){
    switch(key){
        case 'c': // switch cams
        activeCam == 0 ? activeCam = 1 : activeCam = 0;
        break;
    }
}
void ofApp::mouseMoved(int x, int y ){}
void ofApp::mouseDragged(int x, int y, int button){}
void ofApp::mousePressed(int x, int y, int button){}
void ofApp::mouseReleased(int x, int y, int button){}
void ofApp::mouseEntered(int x, int y){}
void ofApp::mouseExited(int x, int y){}
void ofApp::windowResized(int w, int h){}
void ofApp::gotMessage(ofMessage msg){}
void ofApp::dragEvent(ofDragInfo dragInfo){}

Hi,
take a look at the travelingCam class in here


it does what you are describing.
if it is not smooth enough increase the value of “resolucionCurvas”.

cheers.

I think that’s one of the examples I found that seemed too complex :wink: but I guess if I’m going to spend any more time on this I might just as well spend it on deconstructing a script I know should work and give it another try. Thanks

(guess this was one of those; ‘understanding an existing script takes a lot of time, I’m sure I can make something myself, but in the end going for the existing one actually works better’ moments)

It is actually quite simple and very much in the logic of what you’ve tried. For the path that the camera follows use an ofPolyline. use this to get the position on the polyline and remember to set a higher curve resolution using this

to avoid having sharp vertices use ofPolyline::curveTo which will make a curve between points instead of straight lines. If you want to have a sharp angle use ofPolyline::addVertex()

cheers

1 Like

Hey, I am working on similar project now. And I just kind of achieved what I am looking for : )
by using exactly what @roymacdonald suggested above: ofPolyine, curveTo, set a high curve resolution… etc.

Here is my code if it helps:
(hit space bar to add a vertex to the polyline, “1” --> put camera on the polyline, “2” take camera off the polyline)

ofApp.h

#pragma once

#include "ofMain.h"
#include "ofxGui.h"
#include "demoParticle.h"

class ofApp : public ofBaseApp{

	public:
		void setup();
		void update();
		void draw();

		void keyPressed(int key);
		

    ofEasyCam cam;
    ofPolyline polyline;
    ofPolyline polylineControl;

    
    float lookAtPointLength = 0.0;
    float camPosLength = 0.0;
    glm::vec3 lookAtPoint;
    glm::vec3 camPosition;

    bool cameraOnLine = false;

};

ofApp.cpp

#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
    for (int i = 0; i < 3; ++i)
    {
        glm::vec3 randomPoint;
        randomPoint.x = ofRandom(-500, 500);
        randomPoint.y = ofRandom(-500, 500);
        randomPoint.z = ofRandom(-500, 500);
        polyline.curveTo(randomPoint,500);
        polylineControl.addVertex(randomPoint);        
    }
}

//--------------------------------------------------------------
void ofApp::update(){
    
    float totalLength = polyline.getLengthAtIndex(polyline.size() - 1);
    
    if (lookAtPointLength < totalLength)
    {
        lookAtPointLength += ofMap(totalLength - lookAtPointLength, 500, 0, 5.0f, 0.0f);
    }

    
    float camPosLength = lookAtPointLength - 10;
    
    if (camPosLength < 0) {
        camPosLength = lookAtPointLength;
    }
    
    lookAtPoint = polyline.getPointAtLength(lookAtPointLength);
    camPosition = polyline.getPointAtLength(camPosLength);
    
}

//--------------------------------------------------------------
void ofApp::draw(){

    ofBackground(0);
    
    cam.begin();
    
    cam.setScale(10);
    
    ofSetColor(10);
    polyline.draw();
    
    for (auto& point : polylineControl)
    {
        ofSetColor(255,50);
        ofDrawSphere(point, 10);
    }
       
    
    cam.lookAt(lookAtPoint);
    ofSetColor(255,255,0,10); 
    ofDrawSphere(camPosition, 50);
    
    
    if(cameraOnLine){
        cam.setPosition(camPosition);
    }    
    cam.end();
    
}



//--------------------------------------------------------------
void ofApp::keyPressed(int key){
    
    switch(key) {
            
        case '1':
            cameraOnLine = true;
            break;
            
        case '2':
            cameraOnLine = false;
            break;
            
        case ' ':
            glm::vec3 randomPoint;
            randomPoint.x = ofRandom(-500, 500);
            randomPoint.y = ofRandom(-500, 500);
            randomPoint.z = ofRandom(-500, 500);
            polyline.curveTo(randomPoint, 500);
            polylineControl.addVertex(randomPoint);
            break;
        
    }

}



and I set the curve resolution to 500 right here in the curveTo() function, to make the line super smooth:

polyline.curveTo(randomPoint, 500);
1 Like