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.


Everything is placed in GameDraw(), with three offsets, mx, my and mz, that swing between -4.3 and 4.3 (see Animation):
| Object | Centre | Size | Colour |
|---|---|---|---|
| Glass sphere | (0, 4 + 1.5 sin(πt/6000), 0) | radius 2 | white at 25% |
| Red block | (-4, 4 + mx, mz) | 2 across | 0.8, 0, 0 |
| Blue block | (-4, 5.5 + mx, mz) | 1 across | 0.3, 0.6, 0.9 |
| Green block | (4, 4 + my, mx) | 3 across | 0, 0.6, 0 |
| Ball | (mx, 4 + mz, 5) | radius 1.5 | red and white checks |
| Floor | y = 0 | 1000 across | XOR texture at 0.7 |
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 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();
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;
}
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();
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
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() );
}
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 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:
| Step | q (x, z) | a·rv | a·pv | Next |
|---|---|---|---|---|
| 1 | 0.455, 1.948 | -11.28 | 6.68 | move out |
| 2 | 1.057, 1.698 | -3.92 | 7.56 | move out |
| 3 | 1.531, 1.287 | 5.31 | 7.70 | move out |
| 4 | 1.764, 0.943 | 10.54 | 7.09 | move in |
| 5 | 1.652, 1.127 | 8.01 | 7.49 | move in |
| 6 | 1.592, 1.210 | 6.67 | 7.62 | move out |
| 7 | 1.623, 1.169 | 7.34 | 7.56 | move out |
| 8 | 1.637, 1.148 | 7.68 | 7.53 | move in |
| 9 | 1.630, 1.159 | 7.51 | 7.54 | move out |
| 10 | 1.634, 1.154 | 7.59 | 7.54 | return q |
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.
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();
}
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.
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 )
);
}
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 );
}
}
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();
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;
}
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.
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 );
}
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 );
...
| Pass | What | Depth test and write | Blending | Texture |
|---|---|---|---|---|
| 1 | The blocks and the ball, reflected | on | off | none |
| 2 | The floor, mapped onto the sphere | on | off | XOR, in white |
| 3 | The glass sphere, white at 25% | on | source alpha, one minus source alpha | none |
| 4 | The blocks and the ball | on | off | none |
| 5 | The floor | on | off | XOR, in 0.7 grey |
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.
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 );
| Wave | Swing | Period | Moves |
|---|---|---|---|
| mx | ±4.3 | 15.0 seconds | the red and blue blocks up and down, the green block in z, the ball in x |
| my | ±4.3 | 8.7 seconds | the green block up and down |
| mz | ±4.3 | 13.8 seconds | the red and blue blocks in z, the ball up and down |
| sphere | ±1.5 | 12 seconds | the glass sphere up and down |
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
| Input | What it does |
|---|---|
| Drag with the left button | Turn the view about the point (0, 4, 0), 0.2 degrees per pixel |
| Esc | Close the window |
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;
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);
}
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
- common/001/vector.cpp C++ · 103 lines · vector length, normalising, cross product and turns about the axes
- common/001/vector.h C header · 37 lines · the vector functions
- common/001/stack.cpp C++ · 76 lines · stacks for setting a global and putting it back afterwards
- common/001/stack.h C header · 37 lines · the stack functions
- common/001/interpolate.cpp C++ · 32 lines · linear and springy interpolation
- common/001/interpolate.h C header · 38 lines · the interpolation functions
- common/001/cylinder.cpp C++ · 170 lines · a cylinder along any path, with a radius map
- common/001/cylinder.h C header · 89 lines · the cylinder settings and path functions
- common/001/radialobject.cpp C++ · 425 lines · animated shapes made by pushing a divided cube out to a surface
- common/001/radialobject.h C header · 88 lines · the radial shapes and their drawing function
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