kurt miller's homepage
( My development journal and homepage has moved to a new system here: kurtm.flipcode.com )

Generic Rendering Interfaces 27 October 2004, 6:55PM

I've been messing with some ideas for a new game engine that I intend to work on in my spare time, specifically the rendering aspect of it at the moment. I'd simply like to have a high level interface through which I can access any type of underlying renderer (DirectX/OpenGL) while keeping it flexible and extendable. In my past attempts at doing this, I always found myself sacrificing a fair bit of functionality and optimization opportunities for the sake of interface consistency. Its a difficult problem to find exactly the right balance, but I guess that's what makes it fun.

The problem with designing a flexible rendering interface isn't so much the rendering itself, but the items related to the renderer, such as vertex buffers, index buffers, shaders, and textures. Because each of these are handled differently by DirectX and OpenGL, you have to design a platform somewhere in the middle. A good place to start is by considering vertex formats.

Flexible vertex formats, as seen in Direct3D for instance, allow you to create different types of vertices (depending on your application's needs) that the API's native functions can handle. For example you could do something like this:


struct my_vertex
{
float position[3];
DWORD diffuse;
float texture_uv[2];
float cubemap_uvw[3];
};


The flexible vertex format (FVF) code would look like this for Direct3D9:


DWORD fvf = D3DFVF_XYZ|D3DFVF_DIFFUSE|D3DFVF_TEX2|D3DFVF_TEXCOORDSIZE2(0)|D3DFVF_TEXCOORDSIZE3(1);


This simply states what elements are included in the vertex, and the last 2 macros indicate the size of each set of texture coordinates by index (index 0 has 2D coordinates, index 1 has 3D coordinates.)

This approach to specifying data structures is indeed flexible, allowing you to only include data you need. If you only need a position vector and a normal, there's no reason to store all that other data in some arbitary fixed format for thousands and thousands of vertices. Likewise you can always add more data (within reason), such as additional texture coordinates where you can store data that's used elsewhere (such as in shaders.)

Knowing how much I like this approach, my goal became to start out with a generic version of a similar system that my engine can use. The first thing that bothered me though are the bitfields to specify format codes. While its indeed simple and easy to pass around, its not very extendable (who knows, in a few years we may be doing rendering with dozens of texture sets/coordinates), and there's also no error checking. For instance if two flags are supposed to be mutually exclusive (like D3DFVF_XYZ and D3DFVF_XYZRHW), its expected the user will just be sensible and do it properly. Fat chance. On the other hand I very much like the compact look and feel of the above fvf, so I didn't want to end up with a format object that has methods like vformat.addField(RVF_XYZ) or anything like that. That would get klunky very quickly.

After fooling with a number of ideas, the approach I ended up implementing looks like this:


vf = RVFORMAT( RVF_XYZ, RVF_DIFFUSE, RVF_TEX2, RVF_TEX2_2D, RVF_TEX3_3D, 0 );


The RVFORMAT derives from a simple, general purpose SettingField class that is basically just a way to store a 'true' or 'false' value for any unique integer. The constructor takes a variable number of integer arguments, and a 0 at the end signifies its completion. Doing it this way maintains the simple 'look' of the original bitfield approach, is infinitely extendable without needing to shift bits around (any number of texture sets can be supported), allows for renderer-specific error checking on flags in the RVFORMAT class, and still allows the vertex specification to be passed around easily, like as a function parameter.

On the downside I suppose its a bit more overhead, but truthfully this kind of formatting is pretty much always in areas of code where fighting for every millisecond isn't the biggest concern. Not that its particularly slow or anything anyway.

With a vertex format in hand, we move on to vertex buffers themselves. The way I did it was to have the rendering interface accept requests to create vertex buffers. It looks like the following:


vb = renderer->createVertexBuffer(60000,
RVFORMAT( RVF_XYZ, RVF_DIFFUSE, RVF_TEX2, RVF_TEX2_2D, RVF_TEX3_3D, 0 ),
BUFFER_STATIC);


I should explain quickly that both the renderer and the vertex buffer interfaces are written as abstract base classes. The actual renderer object is created through a plugin DLL, allowing me to easily swap out between DirectX and OpenGL. So of course code like the above must be rendering-API independent since it knows nothing of those specific rendering APIs.

To create the vertex buffer itself within the Direct3D DLL (via the above creation method), the code checks through the RVFORMAT to see what structures are included in the vertex format. With that knowledge it builds a Direct3D-specific flexible vertex format as shown in the original example above. That's enough information to create the buffer itself, accept vertex data, and pass it along to Direct3D. You just needed to know what kind of data you have in each vertex and its size. The same basic idea applies to ARB_vertex_buffer under OpenGL as well. The question then becomes how exactly do you do the passing along of data to the underlying API? What kind of interface is generic enough?

My implementation currently has two approaches. For really simple stuff, I do something like the following:


if(!vb->lock(0, 60000))
{
// ...
}

for(int j=0; j<60000; j++)
{
vb->pushXYZ((rand()%300) - 150, (rand()%300) - 150, (rand()%300) + 250);
vb->pushDiffuse(RGBCOLOR(rand()%255, rand()%255, rand()%255));
vb->push2DTexCoord(0, 0.0f, 0.0f);
vb->push3DTexCoord(1, 0.0f, 0.0f, 0.0f);

vb->sealVertex();
}

vb->unlock();


Alternatively, the interface supports the following (after the vb lock shown above), which is especially useful if say the vertex structures were all loaded to memory directly from disk:


struct my_vertex
{
float position[3];
DWORD diffuse;
float texture_uv[2];
float cubemap_uvw[3];
};

for(int i=0; i<60000; i++)
{
my_vertex t;

t.position[0] = (rand()%300) - 150;
t.position[1] = (rand()%300) - 150;
t.position[2] = (rand()%300) + 250;
t.diffuse = RGBCOLOR(rand()%255, rand()%255, rand()%255);

//... set other data

vb->pushVertex(&t);
}


The renderer itself then takes a vertex buffer and a primitive type (points, lines, triangles, whatever) and does its magic (calls DrawPrimitive or what-not) behind the scenes. And here's what all those random triangles look like:



My main concern about the above approach is that it could be slow to copy, say, millions of vertices to different buffers using a virtual function like pushVertex (which does a memcpy in its implementation.) I'm looking at the code now to see if its possible (ie, guaranteed completely safe) to add an interface where the vertex memory can be copied over directly per buffer since the size of each vertex entry is known beforehand (at buffer creation time.) I think it should be possible, or in the worst case, special-cased within an API-specific DLL.

So that's an example of how the new renderer is currently designed. The details may change, but that's the kind of thing I'm shooting for throughout the engine; the same principles of simplicity, flexibility and extendability.

In my older renderer implementations, I tried to hide the vertex buffer concept altogether from the end-user. Basically I'd have the renderer itself act as a vertex buffer manager and let the user just pass in data (like triangles) that was automatically handled. The renderer would try to be smart and classify/buffer the incoming data in such a way that it would render as quickly as possible. While this approach was very simple from the end-user's point of view, it limits the functionality. What if the user knows what he's doing and his specific application would benefit from storing his data a particular way with a vertex buffer? Why shouldn't he have access to such a structure? The thing is, he should. However, the engine will also have a separate vertex buffer manager to offer the simpler interface described above in case he couldn't care less about vertex buffers. In other words, you get the best of both worlds at no significant cost. The end user has the option of easily writing his own manager if he wants.

Of course by 'end user' I just mean me right now, but its still important to offer this flexibility. Hardware or new algorithms might come out tommorow that perform better with a certain technique, and allowing for its use _without_ altering the engine itself is a huge time saver. And in any case, who knows what I'll end up doing with my engine down the road. I may not always be the only end user, so I want to keep it as open as possible.

*Deep breath*

That's all for now. I'll probably end up writing more about handling some of the other things I mentioned (like shaders, index buffers and textures) at some point.






Complete List Of Journal Updates:
  • Auto-Terrain Texturing (15 November 2004, 12:27PM)
  • On Orientation Interfaces (14 November 2004, 7:30PM)
  • Basic Terrain Generation (10 November 2004, 4:39AM)
  • Engine Tool Interface (07 November 2004, 2:37AM)
  • Render Buffer Ranges (02 November 2004, 5:15AM)
  • wxWidgets GUI Toolkit (29 October 2004, 2:31AM)
  • Generic Rendering Interfaces (27 October 2004, 6:55PM)
  • Source Documentation with Doxygen (25 October 2004, 6:34PM)
  • Signs of Life? (25 October 2004, 5:14PM)
  • Key Value Scripts (08 November 2003, 4:32PM)
  • Octrees for Potential Colliders (01 November 2003, 4:09PM)
  • Flexporter and Game Levels (31 October 2003, 12:14PM)
  • New Project and More Updates (31 October 2003, 6:14AM)
  • HSL Color Space (07 May 2003, 6:14AM)
  • A Couple Graphics Books (02 December 2002, 5:12PM)
  • Texture Detail Using Colored Triangles (02 November 2002, 3:26AM)
  • Voxel Mesh Creation and Rendering (27 October 2002, 11:30AM)
  • Development Journal (25 October 2002, 10:57AM)



  • Site Contents Copyright (c) 2002-2004 Kurt Miller. Please do not reprint this jibberish anywhere.