Monday, June 18, 2012

New world rendering scheme

So I said I'd talk about how I reworked my Scene class (which manages the world and all objects in it) to allow a lot of flexibility in drawing. Previously, drawing the Scene was as simple as calling Scene::DrawScene(). This would draw the world, then call Draw() on all GameObjects. This was really simple and intuitive, and someone using my engine could get stuff on the screen quickly. Little is more frustrating than not being able to even get started with something because you can't figure out the basics. Unfortunately, this puts a lot of control out of the user's hands; once they call the function they're limited to what Scene is hardcoded to do.

New features in my engine often come out of want or need while I'm developing ProjectFPS, and this time was no different. I was looking to implement toon shading, and wanted to try the post-process version. In this version, you render normal and depth information to separate buffers, then render the scene normally. Finally, you use the normal and depth info to detect edges and add the black outlines. However, my Scene class didn't support this at all. While it was possible to put a post-process effect on the final color image, there was no way I was getting normal or depth info.

There are a couple solutions to this problem. The simplest method is to modify Scene code, maybe through adding a bool called m_bToonShading or something that specifies whether you want your scene toon-shaded. But now I'm bringing game-specific stuff into a general purpose engine, and that's no good. Plus, you're still stuck if you wanted a different post-process effect, or even a different toon-shading algorithm. And if you try to add support for multiple special effects this way, the Scene code could get messy fast.

Another is to allow Scene::DrawScene() to be overloaded (i.e. make it a virtual function). This solution certainly gives you all the power over rendering you'd need. Doing so, however, would be very complicated. You're asking the user, who did not design nor program the engine, to delve into all the messy details of how Scene does what it does. As you can imagine, it is not a small class. Rendering a scene efficiently takes a lot of steps, and if the user forgets any when writing their own function, they get a black screen with little indication of what went wrong. And if they plan to support multiple special effects, they either have to overload the function multiple times (which is frustrating and time-consuming), or be faced with the messy code branching problems Solution 1 had.

Solution 3 is to tell Scene what kind of extra rendering information you want when rendering the Scene. The actual post-process part, however, is not done by Scene. So if you wanted color, normal, and depth info rendered, you might say:

m_Scene.DrawScene(SCENEDRAW_COLOR | SCENEDRAW_NORMALS | SCENEDRAW_DEPTH);

This is a fairly good solution. It gives you info you need without assuming what you intend to do with it, giving the developer flexibility without making things needlessly complicated. This is not perfect, however. Maybe you wanted to exclude transparent objects from showing up in the normal buffer. Maybe you wanted linearly-distributed depth, as opposed to standard non-linear. Maybe you wanted your own special type of data that Scene doesn't natively support. Still, this is a great step in the right direction.

The solution I settled on was to give high-level rendering control to the Camera class. You call Scene::DrawScene() as normal. Scene then iterates over all currently enabled cameras and calls Camera::DrawScene() on them, a virtual function that can be overloaded in derived camera classes. Scene then provides a couple of high-level functions (such as DrawLevelGeometry() or DrawOpaqueObjects()) that Camera can call at will. In between, Camera can switch render targets or techniques or set parameters or whatever it needs to do. This lets the developer only render what they want, how they want, while sparing them from all the gritty details.

To give the user further control, Camera can set tags that have special meaning to the application. For example, you could define a tag that, when set, excludes all objects that are not players. You set this tag to the camera and call Scene::DrawOpaqueObjects(). Scene will then call GameObject::DrawOpaque() on all GameObjects. The GameObjects will check for the tag, and react appropriately.

If you need to switch the special effect you're using during gameplay, you could configure the camera, or you could just derive a different class and swap cameras instead. Doing the latter would be much less daunting a task than overloading Scene::DrawScene() multiple times would be. Plus, having Scene just pass control to all enabled cameras makes doing effects like picture-in-picture and "arena cam" simple to implement.

The base Camera class provides a default implementation, so for those who just want to get up and running, they can just call Scene::DrawScene() like before. In this way, the simplicity and intuitiveness of the original design is maintained.

This solution is still not perfect. Currently, my main issue is a lack of precise control over how exactly level geometry is rendered. I can swap techniques and set parameters, but that requires knowledge of all shaders used by level geometry. And if I set a parameter on a shader, it will apply to all level geometry that uses that shader. But maybe that's not what I want. Maybe I want wall A to have a different parameter than wall B. Level geometry can't check tags, either.

I intend to solve these problems, but I'm holding off until the level format I use is more finalized and robust. Currently, I'm using a level editor called GILES that comes with courses at a site called the Game Institute. This editor has a number of limitations, however, that prevent me from fully committing to it. They're working on a new level editor that's going to be released alongside their Carbon game engine. I expect huge data format changes, so when that comes out, I'm going to take a look at it and revisit these issues.