How to: Script the Camera to Follow a Curve

This example demonstrates how to use the Curve and CurveKey classes to script the movement of the camera.

Using Curves allows a path to be defined by a small number of control points with the Curves calculating the points on the path between the control points.

To script camera movement

  1. Create an instance of the Curve class for each component being scripted. In this case, two sets of three curves will be needed, one for each of the x, y, and z components of the camera's position and of the position the camera is looking at (the "look-at" position).

    class Curve3D
    {
    
        public Curve curveX = new Curve();
        public Curve curveY = new Curve();
        public Curve curveZ = new Curve();
    
        ...
    }
    
    Curve3D cameraCurvePosition = new Curve3D();
    Curve3D cameraCurveLookat = new Curve3D();
    
  2. Set the PreLoop and PostLoop type of each Curve. The PreLoop and PostLoop types determine how the curve will interpret positions before the first key or after the last key. In this case, the values will be set to CurveLoopType.Oscillate. Values past the ends of the curve will change direction and head toward the opposite side of the curve.

    curveX.PostLoop = CurveLoopType.Oscillate;
    curveY.PostLoop = CurveLoopType.Oscillate;
    curveZ.PostLoop = CurveLoopType.Oscillate;
    
    curveX.PreLoop = CurveLoopType.Oscillate;
    curveY.PreLoop = CurveLoopType.Oscillate;
    curveZ.PreLoop = CurveLoopType.Oscillate;
    
  3. Add CurveKeys to the Curves. Specify the time each CurveKey should be reached and the camera position when the CurveKey is reached. In this case, each point in time will have three CurveKeys associated with it, one for each of the x, y, and z coordinates of the point on the Curve.

    public void AddPoint( Vector3 point, float time )
    {
        curveX.Keys.Add( new CurveKey( time, point.X ) );
        curveY.Keys.Add( new CurveKey( time, point.Y ) );
        curveZ.Keys.Add( new CurveKey( time, point.Z ) );
    }
    
    void InitCurve()
    {
        float time = 0;
        cameraCurvePosition.AddPoint( new Vector3( 7.5f, 0, -45 ), time );
        cameraCurveLookat.AddPoint( new Vector3( 9, 0, 9 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( 3f, 0, -36 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( 12f, 0, -30 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( 3f, 0, -24 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( 12f, 0, -18 ), time );
        time += 2000;
        ...
    }
    
  4. Loop through each Curve setting the TangentIn and TangentOut of each CurveKey. The tangents of the CurveKeys control the shape of the Curve. Setting the tangents of the CurveKeys to the slope between the previous and next CurveKey will give a curve that moves smoothly through each point on the curve.

    cameraCurvePosition.SetTangents();
    cameraCurveLookat.SetTangents();
    
    public void SetTangents()
    {
        CurveKey prev;
        CurveKey current;
        CurveKey next;
        int prevIndex;
        int nextIndex;
        for (int i = 0; i < curveX.Keys.Count; i++)
        {
            prevIndex = i - 1;
            if (prevIndex < 0) prevIndex = i;
    
            nextIndex = i + 1;
            if (nextIndex == curveX.Keys.Count) nextIndex = i;
    
            prev = curveX.Keys[prevIndex];
            next = curveX.Keys[nextIndex];
            current = curveX.Keys[i];
            SetCurveKeyTangent( ref prev, ref current, ref next );
            curveX.Keys[i] = current;
            ...
        }
    }
    
    static void SetCurveKeyTangent( ref CurveKey prev, ref CurveKey cur, ref CurveKey next )
    {
        float dt = next.Position - prev.Position;
        float dv = next.Value - prev.Value;
        if (Math.Abs( dv ) < float.Epsilon)
        {
            cur.TangentIn = 0;
            cur.TangentOut = 0;
        }
        else
        {
            // The in and out tangents should be equal to the slope between the adjacent keys.
            cur.TangentIn = dv * (cur.Position - prev.Position) / dt;
            cur.TangentOut = dv * (next.Position - cur.Position) / dt;
        }
    }
    
  5. Add code to evaluate the x, y, and z coordinates of the Curves at any given time by passing the elapsed time to the Evaluate method of each of the Curves.

    public Vector3 GetPointOnCurve( float time )
    {
        Vector3 point = new Vector3();
        point.X = curveX.Evaluate( time );
        point.Y = curveY.Evaluate( time );
        point.Z = curveZ.Evaluate( time );
        return point;
    }
    
  6. Create a variable to track the amount of time that has passed since the camera started moving.

    double time;
    
  7. In Game.Update, set the camera's position and look-at position based on the elapsed time since the camera started moving. Set the camera's view and projection matrices as in How to: Rotate and Move a Camera.

    // Calculate the camera's current position.
    Vector3 cameraPosition = cameraCurvePosition.GetPointOnCurve( (float)time );
    Vector3 cameraLookat = cameraCurveLookat.GetPointOnCurve( (float)time );
    
  8. In Game.Update, increment the time since the camera started moving by gameTime.ElapsedGameTime.TotalMilliseconds.

    time += gameTime.ElapsedGameTime.TotalMilliseconds;
    
//Game1.cs
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.Content;

class Game1 : Microsoft.Xna.Framework.Game
{
    ContentManager contentManager;
    GraphicsDeviceManager graphics;

    Model sphere;
    Texture2D sphereTexture;

    Matrix view;
    Matrix proj;

    // Specify the points the camera will pass through
    // and the points that the camera is looking at.
    // The number of points on the position and look-at curves
    // don't need to match, but the first and last points on the
    // curves should have the same time values if the curves oscillate.
    void InitCurve()
    {
        float time = 0;
        cameraCurvePosition.AddPoint( new Vector3( 7.5f, 0, -45 ), time );
        cameraCurveLookat.AddPoint( new Vector3( 9, 0, 9 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( 3f, 0, -36 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( 12f, 0, -30 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( 3f, 0, -24 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( 12f, 0, -18 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( 7.5f, 0, -12 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( -6, 0, -6 ), time );
        cameraCurveLookat.AddPoint( new Vector3( 0, 0, 15 ), time );
        time += 3000;
        cameraCurvePosition.AddPoint( new Vector3( -27, 0, 6f ), time );
        time += 3000;
        cameraCurvePosition.AddPoint( new Vector3( -6, 0, 21 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( 7.5f, 0, 27 ), time );
        time += 2000;
        cameraCurvePosition.AddPoint( new Vector3( 21, 0, 21 ), time );
        time += 3000;
        cameraCurvePosition.AddPoint( new Vector3( 42, 0, 6f ), time );
        time += 3000;
        cameraCurvePosition.AddPoint( new Vector3( 21, 0, -6 ), time );
        cameraCurveLookat.AddPoint( new Vector3( 15, 0, 15 ), time );

        cameraCurvePosition.SetTangents();
        cameraCurveLookat.SetTangents();

    }

    class Curve3D
    {

        public Curve curveX = new Curve();
        public Curve curveY = new Curve();
        public Curve curveZ = new Curve();


        public Curve3D()
        {
            curveX.PostLoop = CurveLoopType.Oscillate;
            curveY.PostLoop = CurveLoopType.Oscillate;
            curveZ.PostLoop = CurveLoopType.Oscillate;

            curveX.PreLoop = CurveLoopType.Oscillate;
            curveY.PreLoop = CurveLoopType.Oscillate;
            curveZ.PreLoop = CurveLoopType.Oscillate;

        }

        public void SetTangents()
        {
            CurveKey prev;
            CurveKey current;
            CurveKey next;
            int prevIndex;
            int nextIndex;
            for (int i = 0; i < curveX.Keys.Count; i++)
            {
                prevIndex = i - 1;
                if (prevIndex < 0) prevIndex = i;

                nextIndex = i + 1;
                if (nextIndex == curveX.Keys.Count) nextIndex = i;

                prev = curveX.Keys[prevIndex];
                next = curveX.Keys[nextIndex];
                current = curveX.Keys[i];
                SetCurveKeyTangent( ref prev, ref current, ref next );
                curveX.Keys[i] = current;
                prev = curveY.Keys[prevIndex];
                next = curveY.Keys[nextIndex];
                current = curveY.Keys[i];
                SetCurveKeyTangent( ref prev, ref current, ref next );
                curveY.Keys[i] = current;

                prev = curveZ.Keys[prevIndex];
                next = curveZ.Keys[nextIndex];
                current = curveZ.Keys[i];
                SetCurveKeyTangent( ref prev, ref current, ref next );
                curveZ.Keys[i] = current;
            }
        }

        static void SetCurveKeyTangent( ref CurveKey prev, ref CurveKey cur, ref CurveKey next )
        {
            float dt = next.Position - prev.Position;
            float dv = next.Value - prev.Value;
            if (Math.Abs( dv ) < float.Epsilon)
            {
                cur.TangentIn = 0;
                cur.TangentOut = 0;
            }
            else
            {
                // The in and out tangents should be equal to the slope between the adjacent keys.
                cur.TangentIn = dv * (cur.Position - prev.Position) / dt;
                cur.TangentOut = dv * (next.Position - cur.Position) / dt;
            }
        }

        public void AddPoint( Vector3 point, float time )
        {
            curveX.Keys.Add( new CurveKey( time, point.X ) );
            curveY.Keys.Add( new CurveKey( time, point.Y ) );
            curveZ.Keys.Add( new CurveKey( time, point.Z ) );
        }

        public Vector3 GetPointOnCurve( float time )
        {
            Vector3 point = new Vector3();
            point.X = curveX.Evaluate( time );
            point.Y = curveY.Evaluate( time );
            point.Z = curveZ.Evaluate( time );
            return point;
        }

    }

    Curve3D cameraCurvePosition = new Curve3D();
    Curve3D cameraCurveLookat = new Curve3D();



    double time;

    void UpdateCameraCurve( GameTime gameTime )
    {
        // Calculate the camera's current position.
        Vector3 cameraPosition = cameraCurvePosition.GetPointOnCurve( (float)time );
        Vector3 cameraLookat = cameraCurveLookat.GetPointOnCurve( (float)time );

        // Set up the view matrix and projection matrix.
        view = Matrix.CreateLookAt( cameraPosition, cameraLookat, new Vector3( 0.0f, 1.0f, 0.0f ) );

        proj = Matrix.CreatePerspectiveFieldOfView( FOV, aspectRatio, nearClip, farClip );

        time += gameTime.ElapsedGameTime.TotalMilliseconds;
    }

    // Set information about the game window.
    static int screenWidth = 800;
    static int screenHeight = 600;
    static float aspectRatio = (float)screenWidth / (float)screenHeight;

    // Set field of view of the camera in radians (pi/4 is 45 degrees).
    static float FOV = MathHelper.PiOver4;

    // Set z-values of the near and far clipping planes.
    static float nearClip = 5.0f;
    static float farClip = 1000.0f;

    protected override void Update( GameTime gameTime )
    {
        base.Update( gameTime );
        UpdateCameraCurve( gameTime );
    }

    protected override void Draw( GameTime gameTime )
    {
        base.Draw( gameTime );
        FrameRender();
    }

    void FrameRender()
    {
        GraphicsDevice device = graphics.GraphicsDevice;

        device.Clear( ClearOptions.Target | ClearOptions.DepthBuffer, new Vector4( 0, 0, 0, 255 ), 1.0f, 0 );

        for (int z = 0; z < 5; z++)
        {
            for (int x = 0; x < 5; x++)
            {
                DrawModel( sphere, Matrix.CreateTranslation( new Vector3( x * 3, 0, z * 3 ) ), sphereTexture );
            }
        }

    }

    public Game1()
    {
        graphics = new GraphicsDeviceManager( this );
        contentManager = new ContentManager( Services );

        InitCurve();
    }

    protected override void LoadGraphicsContent( bool loadAllContent )
    {
        base.LoadGraphicsContent( loadAllContent );
        if (loadAllContent)
        {
            sphere = contentManager.Load<Model>( "sphere" );
            sphereTexture = contentManager.Load<Texture2D>( "spheretexture" );
        }
    }

    protected override void UnloadGraphicsContent( bool unloadAllContent )
    {
        base.UnloadGraphicsContent( unloadAllContent );
        if (unloadAllContent)
        {
            contentManager.Unload();
        }
    }

    void DrawModel( Model model, Matrix world, Texture2D texture )
    {
        foreach (ModelMesh mesh in model.Meshes)
        {
            foreach (BasicEffect be in mesh.Effects)
            {
                be.Projection = proj;
                be.View = view;
                be.World = world;
                be.Texture = texture;
                be.TextureEnabled = true;
            }
            mesh.Draw();
        }
    }


}

See Also

How to: Rotate and Move a Camera
How to: Make a First-Person Camera
How to: Make a Third-Person Camera
How to: Render a Model