This is an old version of WBS 3D Curves called Star Demo. It was created using Direct3D 9 and it is not under active development.




Parallel transport frame

The parallel transport frame technique takes an arbitrary initial frame, translates it along the curve, and at each iteration, rotates it to stay as parallel to the curve as possible.

This is the technique:


  • a curve C (for example a Viviani curve)
  • an existing frame F1 at t-1
  • a tangent T1 at t-1 (i.e. the 1st derivative of C at t-1)
  • a tangent T2 at t

a new frame F2 at t can be calculated as follows:

  • F2's position is the value of C at t
  • F2's orientation can be found by rotating F1 about an axis A with angle α
  • the axis A is given by A = T1 × T2
  • the angle α is given by α = arccos( (T1•T2) / (|T1||T2|) )

In the StarDemo we sample frames along the curve using the parallel transport technique. Then, in order to achieve smooth rotation between the frames, we utilize quaternion interpolation. Another technique (the Fixed Up method) is used to obtain the initial frame.

Both of the techniques (Parallel Transport Frame as well as Fixed Up method) are described in the book Game Programming Gems 2.



There are plenty of resources dealing with quaternions but in order to understand how they are used in Star Demo, we only need to know that they allow us to interpolate between two orientations in 3D space. In other words, if we have an object facing in certain, direction and we have the same object facing another direction (i.e. rotated a little bit), quaternions allow us to find all intermediate stages of this rotation (i.e. interpolate from one orientation to another). This is very helpful in creating smooth animations involving 3D rotations.

To be more specific, only unit quaternions are used to represent 3D rotations. That's why we normalize quaternions. Furthermore, there are many methods of interpolating the unit quaternions. The one used in Star Demo is called spherical linear interpolation which computes an interpolated quaternion (let's call it Q) given two unit quaternions (let's call them Q1 and Q2) and a parameter t. The parameter t can take values between 0 and 1. It indicates the position of the interpolated quaternion Q between the two unit quaternions Q1 and Q2. It follows that:

  • for t=0, Q=Q1
  • for t=0.5, Q is exactly between Q1 and Q2
  • for t=1, Q=Q2

You can see the operations on quaternions in the following code snippet from the Star Demo:

// GetFrame - returns a frame (represented as a matrix) for a given index
D3DXMATRIX Trajectory::GetFrame(int shapeIndex)
    int index1 = m_Seq->Get(shapeIndex); // current index
    int index2 = m_Seq->GetNext(shapeIndex); // next index
    float angle = m_Frames[index1].Angle; // current angle

    // Scale the quaternion param to [0.0, 1.0]
    float t = m_QuatParam / m_Step;

    // Calculate quaternions.
    D3DXQUATERNION Q1, QN1; // quaternion from the current rotation matrix
    D3DXMATRIX RM1 = m_Frames[index1].RotationMatrix;
    D3DXQuaternionRotationMatrix(&Q1, &RM1);
    D3DXQuaternionNormalize(&QN1, &Q1);

    D3DXQUATERNION Q2, QN2; // quaternion from the next rotation matrix
    D3DXMATRIX RM2 = m_Frames[index2].RotationMatrix;
    D3DXQuaternionRotationMatrix(&Q2, &RM2);
    D3DXQuaternionNormalize(&QN2, &Q2);

    // Iterpolate
    D3DXQuaternionSlerp(&Q, &QN1, &QN2, t);

    // Build rotation matrix from quaternion.
    D3DXMatrixRotationQuaternion(&RM, &Q);

    // Calculate translation matrix for the current angle.
    D3DXMATRIX TM = m_Curve->GetTranslationMatrix(angle + m_QuatParam);

    // Calculate the final world matrix.

    return WM;

Below there a few useful links to MSDN documentation regarding the quaternion operations in Direct3D 9:


2D and 3D curves

The following curves are currently implemented in the Star Demo:

  • Viviani Curve
  • Spherical Nephroid
  • Ellipse

These are the formulas used to obtain the curves:

Viviani Curve:

`x = r(1 + cos(t))`
`y = r sin(t)`
`z = 2 r sin(t / 2)`

where: `-2 pi < t < 2 pi`
`dx/dt = -r sin(t)`

`dy/dt = r cos(t)`

`dz/dt = r cos(t / 2)`

Spherical Nephroid:

`x = a (3 cos(t) - cos(3 t))`
`y = a (3 sin(t) - sin(3 t))`
`z = sin(t)`

where: `0 < t < 2 pi`
`dx/dt = 3 a (sin(3 t) - sin(t))`

`dy/dt = 3 a (cos(t) - cos(3 t))`

`dz/dt = cos(t)`


Star Generator

The star generator is a procedure located in the Star class that creates vertices of a 3D star given the following input parameters:

  • the number of arms
  • star thickness
  • the short (inner) radius R1
  • the long radius R2

The stars are obtained by rotating a single arm about the center of a star. Below there is a drawing that shows the front and the back of a star arm as well as texture mapping:
Star drawing

This is the procedure that generates the stars:

// CreateVertices - populates vertex buffer with vertices
UINT Star::CreateVertices(UINT startVertex, VertexPosNormalTexture* pv)
    m_StartVertex = startVertex;

    // Prepare temporary values to calculate position of the star's verticies and normals.
    // r1 - short radius 
    // r2 - long radius
    // h - a half of the star thickness
    float r1 = m_ShortRadius, r2 = m_LongRadius, h = m_StarThickness/2.0f; 

    // An angle in the tip of the star's arm.
    float theta = 2.0f*D3DX_PI/(float)m_NumArms; 

    // A helper value containing the distance between the star's arms.
    float x = tan(theta/2.0f)*m_ShortRadius; 

    // Define geometry in the object space
    D3DXVECTOR3 A0, B0, C0, D0, E0;
    A0 = D3DXVECTOR3(0, -r2, 0);
    B0 = D3DXVECTOR3(x, -r1, 0);
    C0 = D3DXVECTOR3(-x, -r1, 0);
    D0 = D3DXVECTOR3(0, 0, -h);
    E0 = D3DXVECTOR3(0, 0, h);
    int n = -1;
    for(int i=0; i<m_NumArms; i++)
        float sint = sin((float)i * theta);
        float cost = cos((float)i * theta);

        D3DXVECTOR3 A, B, C, D, E;

        // rotate counterclockwise around the angle theta
        // x' = x*cos(a) - y*sin(a);
        // y' = x*sin(a) + y*cos(a);
    	A.x=A0.x*cost-A0.y*sint; A.y=A0.x*sint+A0.y*cost; A.z=0;
    	B.x=B0.x*cost-B0.y*sint; B.y=B0.x*sint+B0.y*cost; B.z=0;
    	C.x=C0.x*cost-C0.y*sint; C.y=C0.x*sint+C0.y*cost; C.z=0;

        // not rotated
    	D = D0;
    	E = E0;

                /|\       D(0,0)________ B(1,0)
               / | \           |        |
              /  |  \          |        |
           C /   |   \ B       |        |
             \   |   /         |________|
              \  |  /     C(0,1)         A(1,1)
               \ | /
                /|\       E(0,0)________ C(1,0)
               / | \           |        |
              /  |  \          |        |
           B /   |   \ C       |        |
             \   |   /         |________|
              \  |  /     B(0,1)         A(1,1)
               \ | /
        D3DXVECTOR3 nv1;
        DXUtils::ComputeNormal(A, C, D, &nv1);
        n++; pv[n].pos = A; pv[n].normal = nv1; pv[n].tex0 = D3DXVECTOR2(1.0f, 1.0f);
        n++; pv[n].pos = C; pv[n].normal = nv1; pv[n].tex0 = D3DXVECTOR2(0.0f, 1.0f);
        n++; pv[n].pos = D; pv[n].normal = nv1; pv[n].tex0 = D3DXVECTOR2(0.0f, 0.0f);

        D3DXVECTOR3 nv2;
        DXUtils::ComputeNormal(A, D, B, &nv2);
        n++; pv[n].pos = A; pv[n].normal = nv2; pv[n].tex0 = D3DXVECTOR2(1.0f, 1.0f);
        n++; pv[n].pos = D; pv[n].normal = nv2; pv[n].tex0 = D3DXVECTOR2(0.0f, 0.0f);
        n++; pv[n].pos = B; pv[n].normal = nv2; pv[n].tex0 = D3DXVECTOR2(1.0f, 0.0f);

        D3DXVECTOR3 nv3;
        DXUtils::ComputeNormal(A, E, C, &nv3);
        n++; pv[n].pos = A; pv[n].normal = nv3; pv[n].tex0 = D3DXVECTOR2(1.0f, 1.0f);
        n++; pv[n].pos = E; pv[n].normal = nv3; pv[n].tex0 = D3DXVECTOR2(0.0f, 0.0f);
        n++; pv[n].pos = C; pv[n].normal = nv3; pv[n].tex0 = D3DXVECTOR2(1.0f, 0.0f);

        D3DXVECTOR3 nv4;
        DXUtils::ComputeNormal(A, B, E, &nv4);
        n++; pv[n].pos = A; pv[n].normal = nv4; pv[n].tex0 = D3DXVECTOR2(1.0f, 1.0f);
        n++; pv[n].pos = B; pv[n].normal = nv4; pv[n].tex0 = D3DXVECTOR2(0.0f, 1.0f);
        n++; pv[n].pos = E; pv[n].normal = nv4; pv[n].tex0 = D3DXVECTOR2(0.0f, 0.0f);
    // Each arm has 12 vertices (we do not use an index buffer so vertices are duplicated).
    // The last index in the vertex buffer (n) must be equal the total number of vertices 
    // less one. 
    assert(n == m_NumArms*12-1);
    return ++n;



Download Star Demo - Windows Installer

Download Star Demo - C++ Source Code 

The Star Demo Installer is provided as an .MSI file. Simply download the file, unzip it, and double-click to install the application.

The source code is provided as a Visual Studio 2010 solution file (.SLN). It requires DirectX SDK (June 2010) and contains two projects:


  • StarDemo
  • AppBase - a simple framework that serves as a base class for StarDemo


Useful Books

Two books were particularly useful when writing this application:

Introduction to 3D Game Programming with DirectX 9.0c
by Frank Luna
Introduction to 3D Game Programming with DirectX 9.0c

Programming 2D Games
by Charles Kelly
Programming 2D Games by Charles Kelly