C/C++ 3D Animation Graphics

A striped ribbon, 400 points long, loops and twists through space. Its whole shape is worked out from the clock on every frame, so nothing is carried over from one frame to the next, and it's drawn as 397 flat quads in two shades of grey.

The interesting part is which way the ribbon faces. Every part of it turns its flat side towards a point that wanders around inside the loops, which is what makes it twist, and it gets narrower wherever it heads straight towards that point or away from it.

Looping ribbons striped in light and dark grey, crossing over each other against black
Looping ribbons striped in light and dark grey, crossing over each other against black

The framework

The program is in two parts. A small Windows OpenGL helper library, written by Mark Lord at Gremlin Interactive, opens the window, sets up OpenGL and runs the main loop. The ribbon itself is all in test.cpp, which the framework calls through three functions:

FunctionCalledWhat it does
Initialiseonce before the main loop, and again at the start of every GameDrawworks out the 400 points of the centre line from the clock
GameMainevery pass of the main loopclears the screen, places the camera, calls GameDraw and swaps the buffers
GameDrawfrom GameMain, and on WM_PAINTbuilds the ribbon's edges and draws the quads
The three functions test.cpp gives the framework

Since GameDraw calls Initialise itself, the call before the main loop doesn't change anything. The one on WM_PAINT draws into the back buffer without clearing it or swapping, so it doesn't show either, and the next pass of the main loop clears it straight away.

The window

The window class asks for CS_OWNDC, which gives the window its own device context that stays valid for as long as the window exists. OpenGL is attached to that device context once, when the window is created, and every frame afterwards draws through it.

The window is 800x600 in the middle of the screen. There's also a borderless version, and the two are switched with a comment trick:

//// windowed version
//*
	int width=800;
	int height=600;
	int xpos=(GetSystemMetrics(SM_CXSCREEN)-width) / 2;
	int ypos=(GetSystemMetrics(SM_CYSCREEN)-height) / 2;

	g_hWnd = CreateWindowEx (0, g_classname, g_caption,
		WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
		xpos, ypos, width, height,
		NULL, g_hMenu, g_hInstance, NULL);
/*/
//// fullscreen version
	...
	g_hWnd = CreateWindowEx (0, g_classname, g_caption,
		WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_POPUP,
		xpos, ypos, width, height,
		NULL, NULL, g_hInstance, NULL);
//*/
Choosing between the windowed and popup versions with comment markers (trimmed)

As it stands, //* is a line comment, so the windowed code is compiled, and /*/ opens a block comment that runs down to the */ in the last line. Take one slash off the first line and it becomes /*, which opens a comment that the /*/ now closes, so the popup code is compiled instead and the last line is just a line comment. One character swaps the two blocks over. (The "fullscreen" version then sets its size to 600x450 anyway, so it's a borderless window in the middle of the screen.)

With the classic Windows frame and title bar, the area inside the 800x600 window is 792x573, which is what OpenGL draws into.

Pixel format

Before OpenGL can draw into a window, the device context needs a pixel format that describes the buffers it will have. The framework asks for this:

	pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER | PFD_STEREO_DONTCARE | PFD_SWAP_EXCHANGE;
	pfd.iLayerType = PFD_MAIN_PLANE;
	pfd.iPixelType = PFD_TYPE_RGBA;
	pfd.cColorBits = 16;
	pfd.cDepthBits = 16;
	pfd.cAccumBits = 0;
	pfd.cStencilBits = 0;

	pixelformat = ChoosePixelFormat (hDC, &pfd);	
The pixel format requested: double buffered RGBA with 16-bit colour and a 16-bit depth buffer

ChoosePixelFormat finds the closest format the driver has. If it finds nothing, the framework goes through every format with DescribePixelFormat and takes the first one that's double buffered. Palettised formats are turned down with an error, since a 256-colour desktop can't show RGBA colours directly. Then wglCreateContext makes an OpenGL context for the device context and wglMakeCurrent makes it the one that OpenGL calls go to.

16 bits of depth sounds tight, but it's plenty here. With the near plane at 1, the depth buffer stores roughly 1 - 1/z, so half of its range is used up between 1 and 2 units from the camera. Around the ribbon, about 12 units away, one step of a 16-bit buffer is 144/65,536, or about 0.002 units, which is more than enough to sort out ribbons crossing over each other.

OpenGL state

The window's WM_SIZE message calls GL_Initialise with the new size, so the viewport and projection are set up when the window first appears and again whenever it's resized:

	// enable back face culling - cull counter clock wise
//	glEnable (GL_CULL_FACE);
	glCullFace (GL_BACK);
	glFrontFace (GL_CW);

	// set up the viewport dimensions
	glViewport (0, 0, width, height);

	// load a default projection matrix at 60 degree F.O.V.
	glMatrixMode (GL_PROJECTION);
	glLoadIdentity ();
	gluPerspective (60.0, (GLfloat)width/(GLfloat)height, 1.0, 30000.0);
From GL_Initialise: culling set up but switched off, and a 60 degree perspective

The culling settings are there, but the glEnable is commented out, so both sides of every triangle are drawn. That matters for a ribbon that twists, because you see its front and its back. Depth testing is on and blending is off, the clear colour is black, and lighting is never switched on, so every triangle comes out in exactly the colour it's given.

The main loop

Once the window is up, WinMain goes round a loop that handles any waiting messages and then draws a frame:

	g_exitflag = FALSE;
	while (! g_exitflag)
	{
		ProcessMessages ();
		GameMain ();
	}
	return 0;
The main loop: handle messages, draw a frame, repeat

ProcessMessages uses PeekMessage, which returns straight away when there's nothing to do, so the loop never waits and draws frames as fast as the graphics card can take them. That's fine because the animation runs on the clock rather than counting frames, so it moves at the same speed however fast the machine is. (A DrawMe flag is set when the window loses focus, but nothing reads it, so it carries on drawing in the background too.)

Escape posts a close message to the window. Destroying the window releases the OpenGL context and posts WM_QUIT, and when ProcessMessages sees that it sets g_exitflag and the loop ends.

The camera

Every frame starts in GameMain, which clears the screen and sets up where we're looking from:

void GameMain()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	glTranslatef(0.f, 0.f, -12.f);

	glRotatef(-r2*0.2, 1.0, 0.0, 0.0);
	glRotatef(-r1*0.2, 0.0, 1.0, 0.0);

	GameDraw();
	SwapBuffers (gl_dc);
}
GameMain: clear, place the camera, draw the ribbon and show it

OpenGL applies these transforms to each point in the opposite order to how they're written. So the ribbon is first turned about its own vertical axis by r1, then tipped about the horizontal axis by r2, and then pushed 12 units into the screen. Both angles start at 0 and are changed by dragging with the mouse (see dragging to turn it).

The ribbon stays inside a box 12 units across, from -6 to 6 on each axis. With a 60 degree field of view, the window shows 12 × tan 30°, or 6.93 units, above and below the centre at a distance of 12, so the box about fills the height of the window, with more room at the sides for the 1.38 aspect ratio. The corners of the box are 10.4 units from its centre, so however it's turned, nothing comes closer to the camera than 1.6 units, and the near plane at 1 never cuts into it.

SwapBuffers shows the finished frame, and since the frame was drawn into the back buffer you never see it half drawn.

The centre line

GameDraw starts by calling Initialise, which works out the centre line of the ribbon from scratch. Each of its 400 points has three coordinates, and each coordinate is a sine wave along the line:

  int time = timeGetTime();

  for(i=0; i<LENGTH; i++) {

    ax[i] = 6.0*sin((i+12)*3.14159/(23+4*sin(time*0.000248))+time*0.000054);
    ay[i] = 6.0*sin((i+45)*3.14159/(37+6*sin(time*0.000597))+time*0.000017);
    az[i] = 6.0*cos((i+23)*3.14159/(40+9*sin(time*0.000725))+time*0.000093);

  }
Initialise: three sine waves of the point number and the clock

timeGetTime is the number of milliseconds since Windows started. Let's take the x coordinate on its own, with the clock at 0. Then the bottom of the fraction is 23, and x = 6 sin((i + 12)π/23), so it swings between -6 and 6 and goes through a whole wave every 46 points. The y and z coordinates do the same with longer waves, and because the three wavelengths are different, the pattern doesn't repeat along the line and it wanders all round the box in loops. Three sine waves of different frequencies like this make a Lissajous curve, here in three dimensions.

Neighbouring points are about three quarters of a unit apart on average, which is close to the ribbon's width, so the stripes come out roughly square.

Now let the clock run. Two things change with time:

  • The time*0.000054 added on the end slides the wave along the line, very slowly.
  • The bottom of the fraction swings up and down, so each wave gets longer and shorter. For x it goes between 19 and 27, so a whole wave is anywhere from 38 to 54 points.
xyz
Wave6 sin6 sin6 cos
Points per wave38 to 5462 to 8662 to 98
Waves along the ribbon7.4 to 10.54.7 to 6.54.1 to 6.5
Wavelength swings every25.3s10.5s8.7s
Slide goes right round every116s370s68s
The three waves that make the centre line

The slide is gentle: a whole turn takes a minute or more. The changing wavelength is what makes it lively, and it doesn't move every point equally. The phase of point i is (i + 12)π divided by the wavelength term, so when that term changes, the phase changes by an amount proportional to i. For x, point 0's phase moves at most 0.07 radians a second, while point 399's moves up to 2.4 radians a second. In z, with the bigger swing, the far end moves up to 5.4 radians a second.

So the waves squeeze together and stretch apart like a concertina held still at the start of the ribbon. The start barely moves, and the far end whips round the box.

Giving it width

A line has no width, so for every point we need a sideways direction to put the two edges along. It should be at right angles to the line, but that still leaves a whole circle of directions to choose from. The ribbon picks one using a second point that wanders around inside it:

  mx = 5.0*sin(time*3.14159/2516);
  my = 5.0*sin(time*3.14159/2351);
  mz = 5.0*sin(time*3.14159/2892);
The wandering point, which goes round inside the ribbon every five seconds or so

Each coordinate swings between -5 and 5, with periods of 5.0, 4.7 and 5.8 seconds, so the point stays inside the ribbon's loops and takes a slightly different path every time round.

Then for each point along the line, apart from the first and last, it works out three directions:

  for(i=1; i<LENGTH-1; i++) {

    //  Direction along pipe

    dx = ax[i+1]-ax[i-1];
    dy = ay[i+1]-ay[i-1];
    dz = az[i+1]-az[i-1];
    vector_unit(&dx, &dy, &dz);

    //  Direction to red cube (view point)

    dmx = (mx-ax[i]);
    dmy = (my-ay[i]);
    dmz = (mz-az[i]);
    vector_unit(&dmx, &dmy, &dmz);

    //  Perpendicular direction

    ...
    vector_cross(&px, &py, &pz, dmx, dmy, dmz, dx, dy, dz);

    ...
    //  Make points

    ax1[i] = ax[i]-px*0.5;
    ay1[i] = ay[i]-py*0.5;
    az1[i] = az[i]-pz*0.5;
    ax2[i] = ax[i]+px*0.5;
    ay2[i] = ay[i]+py*0.5;
    az2[i] = az[i]+pz*0.5;

  }
Working out the two edge points either side of each centre point (trimmed)

Step by step:

  1. d, the direction along the line, is the next point minus the previous one, scaled to length 1. Using the points either side rather than the next point alone gives the direction right at point i, not halfway to the next one. This is why the first and last points are skipped: they don't have a neighbour on both sides.
  2. m, the direction to the wandering point, is the wandering point minus this point, also scaled to length 1.
  3. p is the cross product m × d. The cross product of two vectors is at right angles to both of them, so p is at right angles to the line, and also at right angles to the direction to the wandering point.
  4. The two edges go half of p either side of the centre point.
The sideways direction as the cross product of m and d
The sideways direction as the cross product of m and d

The cross product is the usual one:

void vector_cross(double *x, double *y, double *z, double ax, double ay, double az, double bx, double by, double bz)
{
  *x = ay*bz - az*by;
  *y = az*bx - ax*bz;
  *z = ax*by - ay*bx;
}
vector_cross: the cross product of a and b

Let's try it on a point at the origin, where the line is heading along x, so d = (1, 0, 0), and the wandering point is at (0, 0, 5), so m = (0, 0, 1). Then:

p = m × d =(0*0 - 1*0, 1*1 - 0*0, 0*0 - 0*1) = (0, 1, 0)

edges at (0, -0.5, 0) and (0, 0.5, 0)
Working out the edges for a line along x with the wandering point straight ahead in z

The ribbon stands upright, one unit wide, with its flat side facing straight down the z axis at the wandering point.

That's true in general. The ribbon's surface is made of the direction along the line and the sideways direction, and since p is at right angles to m, the flat side turns to face the wandering point as squarely as it can while still running along the line. As the point moves around, every part of the ribbon turns to follow it, and that's what makes it twist.

How wide it is

d and m are both scaled to length 1, but p isn't. The length of a cross product is the lengths of the two vectors multiplied together, times the sine of the angle between them, so the ribbon's width is sin θ, where θ is the angle between the line and the direction to the wandering point.

Move the wandering point in the example to (5, 0, 5), 45° off to the side. Now m = (0.707, 0, 0.707), p = (0, 0.707, 0), and the ribbon is 0.71 wide, which is sin 45°. Where the line passes the point side on, it's the full width of 1. Where the line heads straight towards the point or away from it, it narrows towards nothing:

Ribbon width along a straight line passing the wandering point
Ribbon width along a straight line passing the wandering point

At those narrow places the sideways direction also swings round quickly, because a small change in where the point is makes a big change in which way p points when m and d are nearly lined up. So the ribbon pinches into a thin neck and twists sharply through it.

Facing the camera

The comment in the loop calls the wandering point the "red cube (view point)", and two lines just after it's worked out are commented out:

//  Cube(mx-0.5, my-0.5, mz-0.5, mx+0.5, my+0.5, mz+0.5, 1, 0, 0);

//  getviewpoint(&mx, &my, &mz, r1, r2);
Two commented-out lines: one draws the wandering point, one moves it to the camera

The first draws a red cube one unit across at the wandering point, so you can see what the ribbon is turning towards. The second replaces the point with one 15 units out in the direction of the camera, using the same drag angles as GameMain.

With that line in, every part of the ribbon faces you, and it becomes a flat band that looks the same width all along, apart from perspective. That's a handy way to draw thick lines in 3D. There's also a commented-out block headed "Distance compensation", which scales the sideways direction by the depth of each point. With the point wandering instead, the ribbon twists and pinches as it goes.

Drawing the stripes

With both edges worked out, the ribbon is drawn as one quad between each pair of neighbouring points:

  r = 0.25;

  for(i=1; i<LENGTH-2; i++) {

    r = 1-r;

    glBegin(GL_TRIANGLE_FAN);
      glColor3f(r, r, r); glVertex3f( ax1[i]  , ay1[i]  , az1[i]   );
      glColor3f(r, r, r); glVertex3f( ax2[i]  , ay2[i]  , az2[i]   );
      glColor3f(r, r, r); glVertex3f( ax2[i+1], ay2[i+1], az2[i+1] );
      glColor3f(r, r, r); glVertex3f( ax1[i+1], ay1[i+1], az1[i+1] );
    glEnd();

  }
Drawing each quad as a triangle fan, flipping between two greys

The stripes come from one line: r = 1-r. Starting from 0.25, it gives 0.75, 0.25, 0.75, and so on, so the quads alternate between light and dark grey. Edges exist for points 1 to 398, so the quads run from 1 to 397: 397 quads, 199 light and 198 dark.

The quad strip with alternating greys and one quad split into its fan triangles
The quad strip with alternating greys and one quad split into its fan triangles

Each quad goes round its four corners: the first edge at i, the second edge at i, the second edge at i+1, and back along the first edge at i+1. A triangle fan makes triangles from the first corner to each following pair, so four corners give two triangles, (v0, v1, v2) and (v0, v2, v3), split along the diagonal from v0 to v2. That matters because the quads aren't flat. The ribbon is twisting, so the edges at i+1 are turned a little from the edges at i, and two triangles can follow that where a single flat quad couldn't.

All four corners get the same grey, so even with smooth shading switched on each quad is one flat colour. With no lighting, the stripes are the only thing that shows the shape: you can follow the ribbon round its loops by the stripes, and the depth buffer sorts out which part is in front where it crosses itself.

Each stripe is one step from a centre point to the next, so the stripes are longer where the points are spread out and shorter where they bunch up, at the turning points of the sine waves.

This is OpenGL's immediate mode, so every frame sends 397 glBegin and glEnd pairs and 1,588 vertices to the driver one call at a time. For a ribbon this size that's no trouble.

Dragging to turn it

The only controls are the mouse and Escape:

ControlWhat it does
Left button and dragturns the ribbon around, 0.2° for each pixel moved
Escapecloses the window
The controls

The window procedure keeps track of the left button and adds up how far the mouse has moved while it's held:

    case WM_LBUTTONDOWN:
      lbutton = TRUE;
      SetCapture(hWnd);
      return 0;

    ...

    case WM_MOUSEMOVE:
      POINT pt;
      GetCursorPos(&pt);

      if(lbutton) {
        r1 += (mx-pt.x);
        r2 += (my-pt.y);
      }

      mx = pt.x;
      my = pt.y;

      return 0;
Adding up the mouse movement while the left button is held (trimmed)

r1 and r2 are in pixels, and GameMain turns them into angles at 0.2° per pixel, so a drag of 450 pixels turns the ribbon a quarter of the way round. The signs work out so that it follows the mouse: drag right and the front of the ribbon turns to the right, drag down and the front tips down.

The last position is stored on every move, not just while dragging, so pressing the button doesn't make the ribbon jump. SetCapture keeps the mouse messages coming while you drag outside the window, and GetCursorPos gives screen coordinates, which is fine because only the difference between two positions is used.

Because of the order of the rotations in GameMain, dragging sideways always spins the ribbon about its own vertical axis, and dragging up and down tips it towards you or away, like a turntable that you can tilt.

The WebGL version

The demo at the top is a port of the program to WebGL. It does the same sums in the same order: 400 centre points from the same three sine waves (with the same 3.14159 for π), the same wandering point, the same cross product and the same two greys, all in double precision like the C++. The camera is the same 12 units back with the same 60° projection, at the aspect ratio of the original's 792x573 drawing area, and drags are scaled to the original's pixels so a drag across the picture turns it as far as it did on Windows.

WebGL has no fixed-function pipeline, so the transform and colour that OpenGL did by itself are a small pair of shaders. With lighting off, all the vertex shader has to do is transform the position and pass the colour through:

attribute vec3 aPosition;
attribute vec3 aColour;
uniform mat4 uProjection;
uniform mat4 uModelview;
varying vec3 vColour;
void main() {
  vColour = aColour;
  gl_Position = uProjection * uModelview * vec4(aPosition, 1.0);
}
The port's vertex shader, standing in for the fixed-function transform

There's no immediate mode either, so instead of 397 triangle fans, each frame writes two triangles for every quad, 2,382 vertices in all, into one buffer and draws them with a single call. The matrices from glTranslatef, glRotatef and gluPerspective are built in JavaScript and passed in as the two uniforms.

A few things can't be the same:

  • Escape can't close a browser tab, so the port has no keys at all.
  • The original asked for 16-bit colour and a 16-bit depth buffer. The browser chooses its own, usually 8 bits per colour channel and a 24-bit depth buffer, which gives finer steps of both.
  • timeGetTime counts from when Windows started, so the ribbon's shape when the program opened depended on how long the machine had been on. The port starts its clock at 1,520,000 milliseconds, about 25 minutes, so it always opens the same way, and adding ?t= with another number of milliseconds to its address starts it somewhere else, for example ?t=0.
  • The original draws as fast as it can, and the browser draws once per screen refresh instead. Because the animation follows the clock, it moves at the same speed either way.

Source code

The ribbon is all in test.cpp. The framework, a small Windows OpenGL helper library written by Mark Lord at Gremlin Interactive, opens the window, sets up OpenGL and runs the main loop. Main.cpp and Pattern.cpp, with Pattern.h, are a separate program, a pattern generator that draws 256x256 plasma patterns into a window. The ribbon doesn't use them, and the project leaves them out. physics1.dsp is the Visual C++ 6 project that builds it, which Visual C++ 6 opens directly.

Program

  • test.cpp C++ · 249 lines · the centre line, the wandering point, the edges and the stripes

Framework

  • Winmain.cpp C++ · 284 lines · the window, the main loop, the mouse drag and Escape
  • WINMAIN.H C header · 10 lines · declares the window handles and CloseDownApplication()
  • qe_ogl.cpp C++ · 243 lines · the pixel format, the OpenGL context, the projection and screen mode changes
  • qe_ogl.h C header · 54 lines · declares the OpenGL globals and set-up functions
  • Vars.h C header · 16 lines · the standard headers for Winmain.cpp and qe_ogl.cpp

Pattern generator, not used by the ribbon

  • Main.cpp C++ · 199 lines · the pattern generator's window, Startup() and Redraw()
  • Pattern.cpp C++ · 263 lines · the sine table, the plasmas, the lookups and the display
  • Pattern.h C header · 32 lines · the 256x256 pattern types and functions

Project

  • physics1.dsp Visual C++ project · 126 lines · the six ribbon source files and the Release and Debug build settings

See also

  • Lewpen's pattern generator - The plasma pattern program that Main.cpp and Pattern.cpp come from
  • ZSystem - A universe generated from random seeds, tried out in OpenGL test programs on the same framework
  • Cloth simulation - Flat-coloured quads on the same framework, a cloth of springs falling over moving spheres
  • OpenGL patterns with cubes - Shapes and patterns from a 3D grid of cubes, on the same framework
  • Procedural city block - A whole city laid out from one random seed, the third ZSystem test program
  • Polychorons - A tesseract turning in four dimensions, drawn as a Schlegel diagram made of cubes
  • Reflective sphere with polygon rendering - A mirrored sphere drawn with ordinary triangles, each object bent to where its reflection appears