Around Christmas I suddenly felt compelled to try out some 3D mathematical
experiments I’ve been thinking about for a while, and I needed a simple 3D
engine to let me visualize the constructions. I looked around, and though I
was able to find quite a few systems that would do the job, e.g.,
Crystalspace, Neoengine and Nebula Device just to name three, I did not find
anything that came remotely close to meeting my requirement of “simple”.
However, I convinced myself that, by using OpenGL to do the heavy lifting of
low-level rendering and SDL to take care of device handling, I could probably
write what I needed in two weeks and a thousand lines or so, and still have
time left over to partake of some Christmas cheer.
Well, I’m into overtime by a week, and I came in under a thousand lines only
if you don’t count the headers and demos, but I feel I got close enough to
meeting my objectives to declare victory at this point, and the result has
actually proved usable for the purpose intended. I wish to contribute what
I’ve created up to now as a demonstration of some of the capabilies of OpenGL
and SDL, and of a few useful engine design techniques. Who knows, it’s even
possible that this could develop into a full blown game engine one day.[1]
Since even a demo needs a name, and I’m not feeling particularly imaginative
at the moment, I call it “world”. At least nobody can claim property rights
over that name, not unless the intellectual property regine in the U.S. gets
a lot more perverse than it already is.[2] If you need some way of
distinquishing this particular world from all other possible worlds, I
suppose its full name is “Daniel’s world”.
So what’s in it? Well, not a lot actually, and that’s how I wanted it. The
purpose of this world is to be as minimal as possible, and not get in the
way, but still provide a useful framework for the job it is supposed to do:
visualize 3D models, move around in them, and manipulate them.
Here are the features:
- 3D model viewing, navigation and event processing framework
- Resizable OpenGL surface complete with fullscreen mode, courtesy of SDL
- Simple model definition interface: empty.c is only 3 lines long
- Leaves rendering and physics to the target model
- 3D vector library in functional programming style
- Generic horizon and infinite ground reference grid
- Input event binding system unifies various input devices
- Walkaround and flythrough navigation modes
- Navigation via mouse and/or keyboard controls
- Manipulate world objects with the mouse
- Constant rate physics scheduler
- Debugger interface
Pretty bare-bones, which is, once again, the way I want it. Any fancier and
it would cease to be a demo. Still, there are a few niceties worth pointing
out. For example, the encapsulation of a world model is succinct. A typical
static OpenGL model can be recast as a world model using the line:
struct world world = { .create = create, .render = render };
where the former main routine of the model is broken up into a “render” part
the draws the scene and a “create” part that has the remainder of the former
main routine, e.g., set up lights, load models, etc. Any setup of
projections and viewpoints is ignored and can simply be deleted, since the
world engine does this in a much more useful way.
Most OpenGL demos are animations, and it’s only a little more work to convert
these to world models. The fanciest I’ve converted so far has an interface
something like:
struct world world =
{
.create = load_model,
.render = draw_model,
.motion = step_model,
.button = handle_button,
.filter = filter_SDL_event,
};
The “motion” method provides animation, beyond simple viewpoint motion. This
could simply step a time variable or it might be hooked up to a full blown
physics and collision detect system. (One of the sample world models is a
non-trivial mass-spring physics simulation.)
This method of encapsulating a world model is really just a way of breaking
up the components of a C program for separate compilation. However, it’s not
going to stay that way. I plan to implement a load/unload facility for world
models, so that they can be recompiled and reloaded without losing the state
of the engine, including viewpoint and window geometry, which is a little
luxury I grew used to in the past, and to which I wish to become accustomed
to once again.
I converted a few glut-based OpenGL demos to world models as an exercise, and
they are included in the source. They all became shorter and, to my eyes,
more elegant. I find glut to be a respectable toolkit, but it suffers from
the fatal flaw of competing with SDL on the one hand and event engine
frameworks such as mine on the other. Still, some of glut’s library
routines, e.g., stroke and bitmap text drawing, work perfectly well inside my
engine, and I have included an example of such usage in the sample worlds.
Glut’s drop-down menus don’t work at all, apparently because its windowing
system has not been initialized. That’s ok - gui widgets are planned for
implementation in my second thousand lines of code, and I would prefer a more
generic approach anyway.
One wrinkle in converting a glut model to a world engine model is the fact
that glut demos normally treat OpenGL’s default viewing direction, down the
negative Z axis as “forward”, but within my framework that is “down”. No
problem, you will simply see the model sitting below the ground grid, and a
quick tweak will bring it up into a more sensible (to my mind) southward
orientation. Unlike traditional rendering approaches, being able to walk
around in any model by default completely eliminates the “black screen
problem”, where you don’t see any graphics because it happens to be behind
the camera. In my world, you just look around until you see where the darn
thing is.
I noticed some questions recently on this list about how you would go about
treating different input devices in a similar way, so that actions can be
defined abstractly and bound perhaps to more than one input device, or moved
easily between devices. This is a capability that should not be provided by
a library like SDL, but instead be provided at a higher level in an engine
like this one. There is a nice example of a minimal but nonetheless flexible
system for doing exactly that in this engine. Please feel free to borrow the
code, after all it is GPL.[3]
The 3D vector toolkit I’ve provided deserves mention. It organized around
structure value parameters and structure value returns, which allows you to
write things like:
float v3intersect_ray_ray(vec3 p1, vec3 v1, vec3 p2, vec3 v2)
{
vec3 cross = v3cross(v1, v2);
return v3dot(v3cross(v3sub(p2, p1), v2), cross) / v3dot1(cross);
}
In other words, you can write vector code in a functional style just as you
would write scalar math expressions. So it’s very c+±like, except that you
cannot overload operators, which is probably a blessing in disguise.
Initially, I implemented the engine with a more traditional style of vector
math using pointers, which required you to provide storage for function
results and did not allow vector function cascading as in the above code,
then I rewrote it all to use the functional style. GCC generates compact,
efficient code for the former style, but it’s a great inconvenience for
development, and the resulting source code is, in a word, ugly, not to
mention hard to read. In any event, the fact that GCC generates bulky code
for structure-valued returns and parameters has more to do with the relative
newness of such features in C; there is no fundamental reason why any more
code should be required in order to save intermediate structure results on
the evaluation stack as opposed to elsewhere in the stack frame. And even
with this little luxury, the binaries are still coming in very small - around
30K for each of the demos. For code where efficiency is critical, you want
write out the vector math in full anyway, because there are always
significant optimizations you can make when you look at the details of
everything that’s going on.
About ten percent of the engine code is devoted to drawing a single polygon:
the sky polygon. It turns out that it is much more difficult to draw a sky
as a flat shaded polygon, or even a line, than it is to wrap a cubical or
spherical map texture map around the world. However, since my primary goal
is to create a design platform rather than a game engine, I did not want a
textured sky, and I did not want to see any of those distracting texture
artifacts on the all-important horizon reference, so I did it the hard way.
The ground grid is somewhat interesting in that it is infinite, and new
reference lines fade into view as you move. Whatever, I feel that a design
platform cannot do without decent ground and sky references, and this engine
has them.
As far as I’m concerned, a design platform is also pretty useless if you
cannot manipulate objects in the world. As a first cut, I provide the
ability to treat the mouse pointer as a ray in world coordinates, so that you
can intersect it with objects and manipulate them. The “waves” model
demonstrates this technique, and actually, there’s more than a little play
value there. (You’ll see.)
Now, the little glitches that came up. Starting with the time-reversing
kernel bug I found at New years, I’ve probably spent more time on these than
on actual coding, and I suppose that is what I get for being an early
adopter. The as-released demo suffers from at least two SDL window handling
glitches for which my application cannot provide workarounds, to wit, the
"store settings" window size disconnect I’ve mentioned previously, and
failure to handle deliver minimize/maximize events. Now, with my code
posted, everybody can see what I mean. Also, on my first attempt to run this
on a machine other than my laptop[4], SDL failed to create an OpenGL surface,
though both xscreensaves-gl and abuse-sdl run fine on the machine. I have
not tracked down that issue yet. Mesa, which I’m using for development, and
which explains the fact that all the demos will run comfortably on low-end
machines without 3D accelerators, proved to be a solid, accurate performer,
and not totally useless from the performance point of view. I ran into one
subtle color shifting problem with unlit flat polygons, and some very obvious
color artifacts that you will notice in the “waves” model (the Inca vase
color patterns that emerge are gratuitious, and the use of linear color
interpretation in screen space causes major color swimming problems when
pologons are clipped).
With OpenGL, attribute state leakage is a nontrivial problem, and I still do
not have it completely under control, as you will see if you try out the
keyboard controls in the shadow model demo, which managed to find yet more
ways to change the color of the my polygon. I suppose this is both a
weakness and a strength of OpenGL, as it is very convenient to be able to
have all those settable options lying around in default states until you
realize you need to change them. It’s also a fine source of subtle bugs.
Finally, these past three weeks having been my first experience with either,
I’m most pleased with the combination of SDL and OpenGL as an immersive
reality development platform, and feel comfortable in stating that I will
stay with this combination for the long term.
The code is here:
nl.linux.org/~phillips/world/world.zip
Screenshots are here:
http://people.nl.linux.org/~phillips/world/
Most of the download actually consists of a single data file for the
wireframe demo world that I inherited from the original glut demo. I suppose
I should work up a demo to illustrate why ascii-encoding your wireframe data
is a silly waste of time and space.
I provided the source as a zip file, imagining that this is friendlier for
cross-platform purposes. Please try to compile the code on your platform,
and tell me what breaks.[5]
Regards,
Daniel
[1] Stranger things have happened.
[2] Somebody has been granted a trademark for the word “Pixels” and that is
no joke.
[3] Remembering, as usual, to credit your source.
[4] Being able to develop this all on a laptop is a great credit to the
toolchain
[5] If you are using hardware acceleration, you’ll probably notice that the
frame rate is hard-coded pretty low. That is because I have not yet taken
the time to calibrate the physics for higher frame rates. Sorry, I’ll fix
that soon.