C/C++ 3D Animation Graphics

This simulates a reflective sphere using plain triangle-based rendering. There's no environment map and no rendering to a texture: the reflections are triangles too, drawn inside the sphere at the places where the reflected objects should appear.

The problem to solve is where those places are. A curved mirror bends what it reflects, and how it bends it depends on where the camera is. So every frame, each corner of each object is moved to the point inside the sphere where the eye would see its reflection, and the triangles between them come out stretched and squashed just as they would in a real mirror ball. This is reverse reflection mapping: the reflected objects are drawn first, then the glass and the real objects go on top.

Working out the reverse reflection means solving a quartic. A quartic can be solved exactly, but the formula is awkward, and an iterative approach is easier to understand and quickly converges on a near-accurate solution, so that's what is used here.

The floor is reflected using a different method, by raycasting reflected rays at each vertex of the sphere mesh and finding the co-ordinates of intersection with the ground plane, then using that to map the ground texture directly onto the sphere.

The scene

A glass sphere of radius 2 floats above a chequered floor, slowly rising and falling. Around it, a red block with a small blue one on top, a big green block and a red and white checked ball drift about on sine waves, and the sphere shows all of them reflected in its surface, along with the floor.

The sphere reflecting the blocks, the ball and the floor
The sphere reflecting the blocks, the ball and the floor
Looking down past the sphere
Looking down past the sphere

Everything is placed in GameDraw(), with three offsets, mx, my and mz, that swing between -4.3 and 4.3 (see Animation):

ObjectCentreSizeColour
Glass sphere(0, 4 + 1.5 sin(πt/6000), 0)radius 2white at 25%
Red block(-4, 4 + mx, mz)2 across0.8, 0, 0
Blue block(-4, 5.5 + mx, mz)1 across0.3, 0.6, 0.9
Green block(4, 4 + my, mx)3 across0, 0.6, 0
Ball(mx, 4 + mz, 5)radius 1.5red and white checks
Floory = 01000 acrossXOR texture at 0.7
The objects in the scene, with t the time in milliseconds

The blue block sits exactly on the red one: its centre is 1.5 higher, and their half sizes are 1 and 0.5. The ball stays at z = 5, between the sphere and where the camera starts, so it often passes in front of the sphere.

There's no lighting. Each block face gets a fixed shade instead: the tops and bottoms brighter, the sides facing along x in the plain colour, and the front and back darker, which is enough to make them look solid.

Setting up

The window and GL set-up come from a small Windows OpenGL helper library written by Mark Lord at Gremlin Interactive, in Winmain.cpp and qe_ogl.cpp. The demo itself is all in test.cpp. (Main.cpp and Pattern.cpp are a separate pattern generator, and nothing in common/ is used.)

It opens a 1600x1200 window and asks for a double-buffered pixel format with 16-bit colour, a 16-bit depth buffer and no stencil buffer at all, which will matter later:

	pfd.cColorBits = 16;
	pfd.cDepthBits = 16;
	pfd.cAccumBits = 0;
	pfd.cStencilBits = 0;
The pixel format: 16-bit colour and depth, and no stencil bits

The projection is gluPerspective with a 60 degree field of view, the near plane at 1 and the far plane at 30000. Depth testing is on, the clear colour is black, and Initialise() in test.cpp switches on back face culling, with clockwise triangles as the front faces. Lighting stays off, so apart from the fog everything comes out in exactly the colour it's given.

The main loop has no timer. It handles any waiting window messages and draws a frame, over and over, as fast as the machine will go. The animation follows the clock, so a faster machine only makes it smoother.

Each frame starts by clearing the screen and setting the camera, which sits 15 units back from the point (0, 4, 0) and turns about it as you drag the mouse:

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	glTranslatef(0.f, 0.f, -15.f);

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

	glTranslatef(0.f, -4.f, 0.f);

	viewPoint = getViewPoint();
Clearing the frame, setting the camera and working out where the eye is

The reflection maths happens in world space, so it needs the eye's actual position in the world, not just a matrix. getViewPoint() works it out by turning the point (0, 0, 15) through the same two angles by hand, and adding 4 to its height. With the mouse untouched, the eye is at (0, 4, 15), level with the middle of the sphere's travel.

test.cpp also carries the same four-dimensional code as Polychorons. Initialise() builds a tesseract's points and edges, and each frame works out six rotation angles for it, but nothing ever draws it.

The floor and fog

The floor texture is made at start-up rather than loaded. It's 256x256 grey levels, where each texel is the XOR of its x and y:

for( j=0; j<256; j++ ) for( i=0; i<256; i++ )
{
  c = i^j;
  data[ (i+256*j)*3+0 ] = c;
  data[ (i+256*j)*3+1 ] = c;
  data[ (i+256*j)*3+2 ] = c;
}
Filling the floor texture with x XOR y

XOR gives squares within squares. The top bit of i^j is set where exactly one of i and j is 128 or more, so the texture splits into a 2x2 check of dark and light quarters. Each lower bit does the same thing at half the size, all the way down to single texels, so every big square has smaller checks inside it. It also tiles without a seam.

The floor is a single quad 1000 units across at y = 0, with texture coordinates running from -50 to 50:

  glBegin(GL_QUADS);
	  // Front Face
	  glTexCoord2f( -50.0f, -50.0f ); glVertex3f( -500.0f, 0.0f, -500.0f );
	  glTexCoord2f(  50.0f, -50.0f ); glVertex3f(  500.0f, 0.0f, -500.0f );
	  glTexCoord2f(  50.0f,  50.0f ); glVertex3f(  500.0f, 0.0f,  500.0f );
	  glTexCoord2f( -50.0f,  50.0f ); glVertex3f( -500.0f, 0.0f,  500.0f );
  glEnd();
The floor quad: 1000 units across, with the texture repeating every 10

So the texture repeats every 10 units, and one unit of floor is 0.1 of the texture. That 0.1 comes back later, in the reflection. The floor is drawn in 0.7 grey, which the texture is multiplied by, and the texture uses linear filtering with no mipmaps.

A floor 1000 units across would end in a hard edge, so fog fades it out. It's linear fog to black, starting 25 units from the eye and fully black by 50:

GLfloat fogColor[4] = {0,0,0,1.0};
glFogi(GL_FOG_MODE, GL_LINEAR);		// Fog Mode
glFogfv(GL_FOG_COLOR, fogColor);			// Set Fog Color
glFogf(GL_FOG_DENSITY, 0.35f);				// How Dense Will The Fog Be
glHint(GL_FOG_HINT, GL_DONT_CARE);			// Fog Hint Value
glFogf(GL_FOG_START, 25.0f);				// Fog Start Depth
glFogf(GL_FOG_END, 50.0f);				// Fog End Depth
glEnable(GL_FOG);					// Enables GL_FOG
Linear fog to black between 25 and 50 units (the density is only used by the other fog modes)

Fog depends on how far in front of the eye each point is, and that has a nice side effect here. The reflections are drawn as real geometry at a distance equal to the length of their light path, as we'll see, so a reflection of far-off floor fades into the black just as the floor itself does.

Finding the reflection

Here's the question at the heart of it. There's an eye at o, a sphere with centre c and radius r, and a point p somewhere in the world. Where on the sphere do you see p reflected?

Call that point q. The law of reflection says the ray from the eye meets the surface at q and leaves towards p at the same angle to the surface normal, and on a sphere the normal points straight out from the centre. The incoming ray, the normal and the outgoing ray all lie in one plane, so q has to be in the plane through o, c and p. That turns it into a 2D problem: find the right point on a circle.

Even in 2D it isn't simple. Writing out the equal angles for a circle leads to a quartic equation, with up to four roots to pick from. (It's a very old problem, known as Alhazen's problem.) A quartic can be solved exactly, but the formula is awkward, so the program searches for q instead.

Which points have a reflection

First of all, not every point can be seen in the sphere. A point inside it can't, and nor can a point hidden behind it. The test treats a point as hidden when the line from the eye to it passes within r of the centre and it's further from the eye than the centre is:

double getPointNearestDistance( vec3 a, vec3 b, vec3 p )
{
	return ( (b-a) * (a-p) ).length() / (b-a).length();
}

...

bool reflectedPointExists( vec3 o, vec3 c, double r, vec3 p )
{
	return (p-c).length2() >= r*r && ( getPointNearestDistance( o, p, c ) >= r || (p-o).length2() < (c-o).length2() );
}
Whether a point can be seen in the sphere: outside it, and not behind it (trimmed)

In vec3, * between two vectors is the cross product. |(b - a) × (a - p)| is the area of the parallelogram on those two vectors, and dividing by the length of its base, |b - a|, leaves its height, which is the distance from the point to the line.

A point between the eye and the sphere passes the test even when the line to it goes through the sphere, because you can see its back reflected. That's what lets the ball show up in the sphere while it passes in front of it.

The search

getReflectedPointOnSphere() searches along the straight line from the eye towards p. The range starts at the eye itself, s0, and ends at s1, the point on that line nearest the centre of the sphere. Each test point s in the range is pushed out from the centre onto the surface, giving a candidate q. Since s is on the line from o to p, every candidate is in the plane through o, c and p, which is where the answer has to be.

As s moves from the eye to s1, q moves from the spot that faces the eye, where the reflected ray points straight back at the eye, round towards the edge, where the ray hardly bends at all. Somewhere in between, the reflected ray points at p. So it's a bisection: test the middle of the range, see which side of the answer it's on, and keep the half the answer is in:

vec3 getReflectedPointOnSphere( vec3 o, vec3 c, double r, vec3 p )
{
	//  s is the interpolated point

	vec3 s0 = o;
	vec3 s1 = getNearestPointOnLine( o, p, c );

	int count = DEFAULT_REFLECTION_ITERATIONS;

	while( true )
	{
		//  Test point

		vec3 s = (s0+s1) * 0.5;

		//  Direction from centre of sphere

		vec3 n = (s-c).normalise();

		//  Map to surface of sphere

		vec3 q = c + n * r;

		//  Get reflected p

		vec3 a = q-o;
		vec3 rv = (a - n*a.dot(n)*2.0 ).normalise();  //  reflection vector from q
		vec3 pv = (p-q).normalise();                  //  vector from q to p

		double arv = a.dot( rv );
		double apv = a.dot( pv );

		count --;

		if( arv == apv || count <= 0 )
		{
			return q;
		}
		else if( arv > apv )
		{
			s1 = s;
		}
		else
		{
			s0 = s;
		}
	}
}
The bisection, with DEFAULT_REFLECTION_ITERATIONS at 10

The reflected ray is a - 2(a·n)n: the part of a along the normal is flipped and the rest is kept. Then the test is the two dot products. rv and pv both have length 1, so a·rv and a·pv are |a| times the cosine of the angle each one makes with the incoming ray. If a·rv is bigger, the reflected ray is bent less than it needs to be to reach p, which means q is too far out towards the edge, so the far end of the range comes in. Otherwise the near end moves out. Everything is in the one plane, so when the two angles match, rv points at p.

There's no tolerance, just a count. The two dot products will practically never be exactly equal, so it's always ten steps, which narrows the range to 1/1024 of where it started.

Let's try it with the mouse untouched and the sphere halfway up its travel. The eye is at (0, 4, 15), the centre at (0, 4, 0), and p is (4, 4, 0), level with the centre and 4 units to the right. Everything has y = 4, so only x and z change. The nearest point to the centre on the line from the eye to p is (3.73, 4, 1.00), and here are the ten steps:

Stepq (x, z)a·rva·pvNext
10.455, 1.948-11.286.68move out
21.057, 1.698-3.927.56move out
31.531, 1.2875.317.70move out
41.764, 0.94310.547.09move in
51.652, 1.1278.017.49move in
61.592, 1.2106.677.62move out
71.623, 1.1697.347.56move out
81.637, 1.1487.687.53move in
91.630, 1.1597.517.54move out
101.634, 1.1547.597.54return q
Searching for the reflection of (4, 4, 0) seen from (0, 4, 15)

At step 1, a·rv is -11.28, not far off the lowest it can be, -|a| or -13.1: q is nearly facing the eye, and its reflected ray heads most of the way back. By step 4 the ray is bending too little, and from there the two numbers close in on each other.

Searching along the line from the eye for the reflection point
Searching along the line from the eye for the reflection point

After ten steps, q = (1.634, 4, 1.154). The ray from the eye meets the surface there at 61.50 degrees to the normal and leaves towards p at 61.22, where the exact answer has 61.38 for both, and q is 0.004 units from the exact point. At this distance that's about a third of a pixel in the original window.

Where to draw it

Knowing q isn't quite enough. If every reflected corner were put on the sphere's surface, the depth buffer couldn't tell a near reflection from a far one. So getReflectedPoint() carries on past the surface, along the line from the eye through q, by the distance from q to p:

vec3 getReflectedPoint( vec3 o, vec3 c, double r, vec3 p )
{
	vec3 q = getReflectedPointOnSphere( o, c, r, p );
	vec3 n = (q-c).normalise();

	vec3 a = (q-o).normalise();

	return q + a * (q-p).length();
}
The reflected position: on through q, as far again as p is from q (n isn't used)

That point is at the same place on the screen as q, so the reflection lands on the sphere, but its distance from the eye is |o - q| + |q - p|, the whole length of the light path from the eye to p. A flat mirror does the same thing: the image of a point is as far behind the mirror as the point is in front of it. In the example, p is drawn at (1.94, 4, -1.46), 16.57 units from the eye: 13.94 to the surface and 2.63 more on to p.

Where a reflected point is drawn
Where a reflected point is drawn

This gets the depth buffer to sort the reflections out properly. Two points that are seen at the same spot on the sphere lie on the same reflected ray, and whichever is nearer along that ray ends up nearer in depth, so it hides the other one, just as it would in a real mirror.

Reflecting the objects

With a function that reflects a point, reflecting an object is a matter of reflecting its corners. Each triangle is drawn with its three corners passed through getReflectedPoint(), and if any one of them has no reflection the whole triangle is skipped:

void ReflectedTriangle( vec3 a, vec3 b, vec3 c )
{
	if( ! reflectedPointExists( viewPoint, sphereOrigin, sphereRadius, a ) ) return;
	if( ! reflectedPointExists( viewPoint, sphereOrigin, sphereRadius, b ) ) return;
	if( ! reflectedPointExists( viewPoint, sphereOrigin, sphereRadius, c ) ) return;

	Triangle
	(
		getReflectedPoint( viewPoint, sphereOrigin, sphereRadius, a ),
		getReflectedPoint( viewPoint, sphereOrigin, sphereRadius, b ),
		getReflectedPoint( viewPoint, sphereOrigin, sphereRadius, c )
	);
}
A reflected triangle is drawn only if all three corners can be seen in the sphere

So when an object goes behind the sphere, its reflection disappears one small triangle at a time rather than being cut off along a clean line. ReflectedQuad() draws a quad as the two triangles a, b, c and c, d, a.

A reflection in a curved mirror is curved, but a triangle drawn between three reflected corners is still flat with straight edges, so a cube face reflected as two triangles would look like a flat card. Each face is split into four, and each of those into four again, before it's reflected. That's 16 small quads, or 32 triangles, per face, which is enough for the edges to follow the curve:

void SubdividedReflectedQuad_Recurse( vec3 a, vec3 b, vec3 c, vec3 d, int level = DEFAULT_SUBDIVIDE_LEVEL )
{
	if( level > 0 )
	{
		vec3 ab = (a+b) * 0.5;
		vec3 bc = (b+c) * 0.5;
		vec3 cd = (c+d) * 0.5;
		vec3 da = (d+a) * 0.5;
		vec3 m  = (a+c) * 0.5;

		SubdividedReflectedQuad_Recurse( a, ab, m, da, level-1 );
		SubdividedReflectedQuad_Recurse( ab, b, bc, m, level-1 );
		SubdividedReflectedQuad_Recurse( m, bc, c, cd, level-1 );
		SubdividedReflectedQuad_Recurse( da, m, cd, d, level-1 );
	}
	else
	{
		ReflectedQuad( a, b, c, d );
	}
}
Splitting a face into quarters twice before reflecting it, with DEFAULT_SUBDIVIDE_LEVEL at 2

The splitting happens on the real face, before the reflection, so the small quads are all the same size on the block. In the sphere they bunch up and stretch with the curve.

There's one more thing to get right. A mirror image is back to front, so reflecting a triangle turns clockwise into anticlockwise, and with back face culling on, a reflected block would show only its far faces. ReflectedCube() deals with this by taking its corners the other way round from Cube():

void ReflectedCube( vec3 a, vec3 b, Colour col )
{
	double x1 = b.x();
	double y1 = b.y();
	double z1 = b.z();

	double x2 = a.x();
	double y2 = a.y();
	double z2 = a.z();
ReflectedCube() takes x1, y1 and z1 from the high corner, where Cube() uses the low one (trimmed)

The six faces then use the same vertex pattern as Cube(), so with the corners swapped every face is wound inside out, and the reflection turns it back again. The ball does the same thing by listing each quad's corners in a different order from Sphere().

The reflected faces are shaded with the Colour class: lighter(), which mixes in 20% white, for the tops and bottoms, and darker(), 80% of the colour, for the front and back. The ball isn't split any further, since its 16x16 mesh is already made of small quads.

That adds up to a lot of searches. Up to 1,728 reflected corners for the blocks and 1,536 for the ball every frame, each one ten steps of the bisection, because a corner shared by several triangles is worked out again for each of them.

Reflecting the floor

The floor could be reflected the same way, and there's a ReflectedFloor() that tries: it reflects nine 50-unit squares of floor near the middle, each one as a single quad. Its call is commented out with the note "Not working yet...", and a quad that big, reflected through just its four corners, can't bend round the sphere.

So the floor is done the other way round. Instead of asking where each point of the floor appears on the sphere, it asks what each point of the sphere sees. That's an ordinary forward reflection: take the ray from the eye to a point on the sphere, reflect it about the normal there, and see where it meets the floor:

bool ReflectFloor( vec3 c, double r, vec3 n, vec3 &q, vec3 &rq )
{
	vec3 p = c + n * r;

	vec3 a = (p-viewPoint).normalise();

	vec3 rv = (a - n*a.dot(n)*2.0 );

	if( rv.y() >= 0 ) return false;

	double t = -p.y() / rv.y();

	q = p + rv * t;

	rq = p + a * (q-p).length();

	return true;
}
Reflecting the view ray at a point on the sphere and finding where it meets the floor

If the reflected ray points level or up, it never reaches the floor. Otherwise the floor is at y = 0, so the ray p + t rv gets there when t = -p.y / rv.y, and that gives the floor point q.

q decides what to draw. Its x and z times 0.1 are exactly the texture coordinates the real floor has at that spot, so the reflection matches the floor texel for texel. rq decides where to draw it, with the same trick as the objects: on along the view ray, behind the surface, by the length of the reflected ray. So the reflected floor and the reflected objects sort against each other by the lengths of their light paths, and a block seen in the sphere hides the floor behind it.

Raycasting from the sphere mesh down to the floor
Raycasting from the sphere mesh down to the floor

This runs over the sphere's own mesh of 16x16 quads, with each corner given by its normal. A quad is split into four, twice, like the block faces, and at the finest level each small quad is drawn only if all four of its corners reach the floor:

	bool ok1 = ReflectFloor( c, r, n1, q1, r1 );
	bool ok2 = ReflectFloor( c, r, n2, q2, r2 );
	bool ok3 = ReflectFloor( c, r, n3, q3, r3 );
	bool ok4 = ReflectFloor( c, r, n4, q4, r4 );

	if( ok1 && ok2 && ok3 && ok4 && level == 0 )
	{
		glBegin(GL_QUADS);
			glTexCoord2f( q1.x()*0.1, q1.z()*0.1 ); glVertex( r1 );
			glTexCoord2f( q2.x()*0.1, q2.z()*0.1 ); glVertex( r2 );
			glTexCoord2f( q3.x()*0.1, q3.z()*0.1 ); glVertex( r3 );
			glTexCoord2f( q4.x()*0.1, q4.z()*0.1 ); glVertex( r4 );
		glEnd();
	}
	else if( (ok1 || ok2 || ok3 || ok4) && level > 0 )
	{
		vec3 n12 = ((n1+n2)*0.5).normalise();
		vec3 n23 = ((n2+n3)*0.5).normalise();
		vec3 n34 = ((n3+n4)*0.5).normalise();
		vec3 n41 = ((n4+n1)*0.5).normalise();
		vec3 nm  = ((n1+n3)*0.5).normalise();

		SphereQuadSubdivide( c, r, n1, n12, nm, n41, level-1 );
		SphereQuadSubdivide( c, r, n12, n2, n23, nm, level-1 );
		SphereQuadSubdivide( c, r, nm, n23, n3, n34, level-1 );
		SphereQuadSubdivide( c, r, n41, nm, n34, n4, level-1 );
	}
From SphereQuadSubdivide(): drawing a quad of the floor reflection, or splitting it (trimmed)

A quad with no corner on the floor is dropped straight away. One with some corners on the floor is split down to sixteenths, so the line where the floor reflection stops, the horizon in the sphere, is followed in steps of a quarter of a mesh quad. The new corners are the averages of the old normals, normalised again so they stay on the sphere.

The corners' normals come from SphereNormal(), which hands back the sphere table negated. Together with the order the corners are taken in, that leaves these quads wound the same way as the glass sphere's own, so the half of the sphere facing away is culled just like the glass's back half.

Drawing a frame

The order things are drawn in is what makes it all work. GameDraw() draws five things, one after the other:

  ReflectedCube( vec3( -4, 4+mx, mz ), 1, Colour( 0.8, 0.0, 0.0 ) );
  ReflectedCube( vec3( -4, 5.5+mx, mz ), 0.5, Colour( 0.3, 0.6, 0.9 ) );
  ReflectedCube( vec3(  4, 4+my, mx ), 1.5, Colour( 0.0, 0.6, 0.0 ) );

  ReflectedSphere( mx, 4+mz, 5, 1.5 );

  //  Not working yet...
  //ReflectedFloor();

  SphereFloorReflection( sphereOrigin, sphereRadius );

  glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
	glColor4f( 1, 1, 1, 0.25 );
	Sphere( sphereOrigin.x(), sphereOrigin.y(), sphereOrigin.z(), sphereRadius, false );
  glDisable(GL_BLEND);

  Cube( vec3( -4, 4+mx, mz ), 1, Colour( 0.8, 0.0, 0.0 ) );
  Cube( vec3( -4, 5.5+mx, mz ), 0.5, Colour( 0.3, 0.6, 0.9 ) );
  Cube( vec3(  4, 4+my, mx ), 1.5, Colour( 0.0, 0.6, 0.0 ) );

  Sphere( mx, 4+mz, 5, 1.5 );

  ...
The passes in GameDraw(), with the floor last (trimmed)
PassWhatDepth test and writeBlendingTexture
1The blocks and the ball, reflectedonoffnone
2The floor, mapped onto the sphereonoffXOR, in white
3The glass sphere, white at 25%onsource alpha, one minus source alphanone
4The blocks and the ballonoffnone
5The flooronoffXOR, in 0.7 grey
The five passes and the GL state each one is drawn with

Back face culling and fog are on for all of them. Nothing changes the depth function or masks off depth writes, and there's no stencil buffer, so it's all done by the order.

A flat mirror in OpenGL is usually drawn with the stencil buffer: draw the mirror into the stencil first, then draw the reflected scene only where the stencil is set, so it doesn't spill out past the mirror's edges. This program doesn't need that. Every reflected corner is on the line from the eye through its point q on the sphere, so it lands on the screen inside the sphere's outline anyway. The reflection is clipped to the mirror by the way it's built.

What it does need is for the reflections not to be covered up by whatever is really behind the sphere. The reflections are drawn at the length of their light paths, behind the sphere's surface, and that can be further away than real things behind the sphere. Here's one view ray, with the eye raised 30 degrees: it meets the glass 13.1 units away, the real floor behind the sphere is at 21.2, and the floor seen reflected at that spot is drawn at 37.7.

Depths along one view ray through the sphere
Depths along one view ray through the sphere

If the real floor went in first, it would win the depth test against the reflection, and the sphere would look like clear glass. So the reflections are drawn first, while the depth buffer is still empty, and then the glass sphere goes over them. It's blended, so it lightens the reflections a little and gives the sphere a grey body against the black sky, but it also writes its own depth, 13.1 here, and that seals the reflections in. When the real objects and the floor come along in passes 4 and 5, anything behind the sphere fails the depth test against the glass, and anything in front of it, like the ball, is drawn over it as normal.

Passes 1 and 2 don't need any particular order between them, because both are placed by the length of their light paths and the depth test sorts them out.

Animation

Everything moves on sine waves of the time from timeGetTime(), in milliseconds:

  int time = timeGetTime();
  double mx, my, mz;

  mx = 4.3*sin(time*3.14159/7516);
  my = 4.3*sin(time*3.14159/4351);
  mz = 4.3*sin(time*3.14159/6892);

  sphereOrigin = vec3( 0, 4 + 1.5*sin( 3.14159*time/6000 ), 0 );
The three bobbing offsets and the height of the glass sphere
WaveSwingPeriodMoves
mx±4.315.0 secondsthe red and blue blocks up and down, the green block in z, the ball in x
my±4.38.7 secondsthe green block up and down
mz±4.313.8 secondsthe red and blue blocks in z, the ball up and down
sphere±1.512 secondsthe glass sphere up and down
The four waves and what each one moves

The periods don't fit into each other, so the arrangement doesn't repeat for a very long time. A swing of 4.3 takes the blocks' centres from -0.3 to 8.3 high, so at the bottom of their travel they sink part way into the floor, and so does the ball.

Since everything moves, the sphere included, all the reflections are worked out again from scratch every frame.

Controls

InputWhat it does
Drag with the left buttonTurn the view about the point (0, 4, 0), 0.2 degrees per pixel
EscClose the window
The controls

Dragging captures the mouse and adds how far it moved, in screen pixels, to the two angles:

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

case WM_LBUTTONUP:
  lbutton = FALSE;
  SetCapture(NULL);
  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;
Turning the view by dragging, in the window procedure

The scene turns with the mouse, as if you'd grabbed it: dragging down lifts the eye to look down on it, and dragging sideways takes the eye round it. There are no limits, so you can drag right over the top or down under the floor.

The WebGL version

A live WebGL version of the code is at the top of the page. It does the same sums in the same order: the same ten-step search for every corner, the same two levels of splitting, the same raycast for the floor and the same five passes, all worked out on the CPU in JavaScript every frame. The camera, the 60 degree projection and the shape of the original's 1592x1173 drawing area are the same, and drags are counted in the original window's pixels, so a drag across the picture turns it as far as it did on Windows.

WebGL has no immediate mode, so each glBegin() shape is split into triangles, keeping the vertex order so back face culling picks the same faces, and the whole frame goes into one vertex buffer. That's drawn in runs, one for each change of texture or blending, in the original's order. There's no fixed-function pipeline either, so the texture modulation and the linear fog are a few lines of fragment shader:

void main() {
  vec4 c = vColour;
  if (uTextured) c *= texture2D(uTexture, vTexCoord);
  float f = clamp((50.0 - abs(vFogZ)) / (50.0 - 25.0), 0.0, 1.0);
  gl_FragColor = vec4(c.rgb * f, c.a);
}
The port's fragment shader: texture times colour, then fading to black between 25 and 50

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 where everything was when the program opened depended on how long the machine had been on. The port starts its clock at 558,460 milliseconds, so it always opens with the blocks raised and the ball off to the right, as in the screenshots.
  • 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 program is test.cpp, built with Winmain.cpp and qe_ogl.cpp from the Windows OpenGL helper library Mark Lord wrote at Gremlin Interactive. physics1.vcproj is the Visual Studio .NET 2003 project, which opens directly and builds just those three files. Main.cpp and Pattern.cpp are a separate pattern generator, and the common library under common/001 isn't used by this program: nothing calls it, and the project doesn't build it.

Program

  • test.cpp C++ · 1434 lines · the scene, the reflection search, the reflected objects and floor, and each frame

Framework

  • Winmain.cpp C++ · 284 lines · the window, the message loop and the mouse
  • WINMAIN.H C header · 10 lines · the window handles and CloseDownApplication
  • qe_ogl.cpp C++ · 243 lines · the pixel format, the GL context and the projection
  • qe_ogl.h C header · 54 lines · the GL globals and set-up functions
  • Vars.h C header · 16 lines · the standard C headers the framework includes

Pattern generator

  • Main.cpp C++ · 199 lines · the pattern generator's window and main loop
  • Pattern.cpp C++ · 263 lines · the plasma, checkers and lookup patterns
  • Pattern.h C header · 32 lines · the pattern buffers and functions

Common library

Project

  • physics1.vcproj Visual C++ project · 225 lines · the Visual Studio .NET 2003 project, which opens directly and builds Winmain.cpp, qe_ogl.cpp and test.cpp

See also

  • ZSystem - A universe generated from random seeds, tried out in OpenGL test programs on the same framework
  • Cloth simulation - A cloth falling and slipping over bobbing spheres
  • OpenGL patterns with cubes - Moving shapes and patterns from a 3D grid of cubes
  • OpenGL ribbon - A looping, twisting striped ribbon worked out from sine waves of the clock
  • Procedural city block - A whole city laid out from one random seed, the third ZSystem test program
  • Polychorons - Animated 4-dimensional objects mapped down to 3D, on the same OpenGL framework
  • Lewpen's pattern generator - The animated patterns that Main.cpp and Pattern.cpp come from