First we need a universe
Before we can build our black hole, we need a universe within which to place it. To make the math easier when calculating the particle orbits, we're going to make our universe center on the black hole at
(0,0,0), which will also be the only source of gravitational pull.
In the code,
main() first calls the usual OpenGL GLUT initialization functions, registers several of our functions for event and drawing callbacks, and then calls
ourInit(). This function's job is to allocate and configure everything else needed by our program before passing control off to OpenGL's GLUT for event processing.
ourInit() starts by calling
ourBuildTextures(), which builds a single bit-plane texture known as an alpha-channel map, containing a simple dot. The texture is used to control how transparent our particles will be, and is progressively more opaque towards the center. There is a 6% randomness to the alpha channel values, which hides any obvious transitions. Remove the
+ ourRand(15), and you'll be able to see obvious concentric rings when a particle is nearby.
Of course, 3D motion is required.
ourBuildStarfield() is called next, with the number of stars desired passed as a parameter. This function builds a display list, which is a way of having OpenGL remember a sequence of calls for use later. The
glNewList() function takes an integer "name" as its first parameter; we're calling with
STAR_FIELD, which has already been defined as "1." The second parameter can either be
GL_COMPILE_AND_EXECUTE. Because we're pre-assembling this list, we'll use the first option.
ourBuildStarfield() takes advantage of the fact that OpenGL display lists record all the parameters made to the OpenGL calls. Thus, we don't have to worry about remembering where our stars are, because OpenGL will do it for us. If we didn't use a display list, we would have had to remember the star locations (and sizes), or use a repeatable pseudo-random number generator.
Another trick the function does is use OpenGL to spin the universe around a random amount while always placing the next star in the same location. This is instead of calculating the star's location ourselves, and demonstrates how rotating the MODELVIEW frame of reference can be used to quickly produce interesting results.
It should be noted that this results in slower run-time rendering speed, however, as OpenGL is having to do a matrix multiply (updating 16 values) for each rotation, which is not always implemented in hardware. A star field with pre-calculated star positions could easily be written, and would be recommended for applications where frame rate is critical.
ourInit(), calls are made to enable blending, but disabling alpha testing. We also ask for flat shading, since we'll see no benefit from smooth shading in this application, and then we ask for our blend function to be
This is a special blending mode which says that whatever is in the destination buffer should be added to by the contents of the source buffer, according to the source buffer's alpha level. The destination is what has already been drawn, with the source being whatever it is that's currently being drawn.
This blending mode must be used with care in most situations, as it's a rather unnatural form of matter. Objects rendered in this way are completely transparent (as in, they don't block any light from behind), but they also add to the scene any light reflecting off or radiating from the object. No real world objects are truly like this, although fire, laser beams in dusty rooms, and luminescent particles come close.
Since we're modeling the latter, this mode makes sense. Note that overlapping objects quickly add to the RGB values of a pixel, so individual particles should be fairly transparent. A pixel's color values will continue to climb as additional objects are drawn on top of it, with clipping occurring at white. This can be, but is not always, a desirable effect.
ourInit() next calls
ourCalcObs() which is used to figure out the X, Y, and Z coordinates based on the observer's angle around, distance from, and height above the origin. Then the
cbResizeScene callback function is used in order to set the correct screen perspective matrix.
gluNewQuadric() is next called in order to allocate a GLU object, which we'll use for the sphere of our black hole's event horizon.
ourAllocParticles() is called to allocate an array of particles of the initial size desired. This function is handy because it can be called with the number of desired particles, and the array will be reallocated as needed. To finish off, the first particle in the array (our "root particle") is introduced to the system with a call to
At this point, our universe is ready to roll.
main() prints a small help message to STDOUT, and control is passed to GLUT by calling