|See what's going on with flipcode!|
Modular 3D Engines
Question submitted by (19 November 1999)
|Return to The Archives|
I'm relatively new to 3D programming and trying to write a 3D engine
which can use different rendering APIs (Glide, OpenGL, DirectX).
From what I can find out, I need to create separate DLLs for each of the rendering APIs, and the DLLs have to share a common interface, is this correct? How do you handle different vertex structure layouts without writing specific transform/clipping/etc routines for each rendering API? or is that necessary?
You are correct. In my case, I call them LAPI (Local API) DLLs. They have
a common (local) API that is used by the application (game) to communicate
to any type of underlying EAPI (External API -- DirectX, OpenGL, etc.)
It's simply a layer of abstraction.
How do you handle data layouts? Well, that depends on where you want to draw the line. Do you want low-level control or high-level control? Fortunately, the higher-level interfaces tend to be better off. Here's why:
Adding more and more levels of abstraction will slow things down. You want the thinnest layer between the game and the EAPI. And you would hope that the EAPI is the thinnest layer to the 3D hardware. A thin layer is a high-level interface. I know this sounds backwareds, but hear me out.
Consider an over-simplified LAPI interface that has only three function calls:
In a design like this, the game has less to worry about. It doesn't need to worry about texture caching, for example, since the manageTextures() will take care of it. But more importantly, the manageTextures() was written for a specific EAPI so it has full freedom to optimize specifically for any given EAPI.
The advantage to a renderObject() call rather than rendering by primitives is that the data for an entire object is passed in at once, rather than many calls to pass in lots of little pieces of data. This is the same philosophy behind 'display lists' in OpenGL -- which have proven to be faster. And, again, rendering by object is simpler for the game code.
Not only is the game code simplified, but so is the LAPI code. The LAPI code is given more freedom to do it's job, rather than trying to mangle a LAPI into an EAPI. For example, if you chose a LAPI interface similar to OpenGL, you'll have a difficult time interpreting that into DirectX calls. To handle them both well, you'll need a least-common-denominator API (i.e. high-level.) This doesn't mean that you're limited to least-common-denominator functionality, though.
When you pass in your object (to renderObject()), there is a LOT of data there. This data contains the vertices (possibly shared) along with the polygons (or other primitives.) Each polygon might have a couple of texture layers (a texture & a light map for example.) Each texture layer will have the blending modes associated with it, so that the LAPI can properly render what you want.
If you're still following me, you'll notice how we've moved from a "code heavy" API to a "data heavy" API. Meaning, most of the feature set is in the data, not in functino calls. This allows the LAPI to optimize better, and it also allows it more freedom on how to handle situations where a requested feature (such as an alpha blending mode associated to a texture) isn't supported. This is how you relieve yourself from that dreaded least-common-denominator functionality.
So all the game needs to do is to fill in the data at the highest level of interpretation and pass it to the LAPI. The LAPI decides how best to filter that data down to the EAPI.
To help clarify, here's another quick example: Given such a high-level interface, you'll probably want to pass in untransformed vertices to the LAPI, so that the LAPI can either let the hardware do the transforms, or do the transforms itself. Let the LAPI choose the best way to do things. Again, the high-level interface proves itself to be the logical choice.
Of course, you'll never get away with a 3-call API. :) But start there, and only add the calls you really NEED.
Simplification is a good thing.
Response provided by Paul Nettle
This article was originally an entry in flipCode's Fountain of Knowledge, an open Question and Answer column that no longer exists.