Designing a flexible 2d renderer

Started by
9 comments, last by errcw 16 years, 10 months ago
I'm trying to come up with a good design for a 2d renderer, based on the current drawing code in Snowman Village. Heres a screenshot which shows a typical scene: Basically it consists of a big list of 'drawables' (which are objects implementing Drawable), each frame this list is sorted by priority (an int, which determines the layering) and then each drawable is drawn to the screen. Theres no batching, state sorting and it's all immediate mode, so theres lots of room for improvement! There is however a good range of drawables: - Sprites - Line geometry (specially textured lines) - Ring geometry (specially textured circle) - Beveled rectangle (specially textured rectangle) - Text - ...a couple of game specific ones which could probably be replaced by a general purpose 'quad' geometry This range of geometry types is what gives it it's unique look and makes it look different from your regular tiles+sprites 2d game, so it's important that not only are these supported, but it's also easy to add game-specific geometry/drawables with no or minimal code modifications elsewhere. Now for just this I'd probably rework the Drawable interface to allow access to the texture so it could sort first by priority and then by texture[1], and then make batches out of consecutive drawables with the same texture, and render each batch using vertex arrays (rebuilt from scratch every frame). However I'd also like to allow for multitextured geometry, and shader drawn geometry (the reflection effect is currently hardcoded). Shaders and 1/2/3/etc. textures makes the state sorting harder, but mostly I'm stuck trying to figure out how to make the drawable interface work when the geometry format might require one, two or more sets of texture coords, and maybe an arbitrary amount of shader vertex attributes. Absolute raw performance isn't actually the goal - rather I'm after something thats easy to customise and extend for each game as needed. Since the actual number of drawables isn't going to be too high (maybe a thousand for a typical scene) we can probably how many virtual function calls we introduce and cpu performance in general. I've a couple of ideas, but they all suck in one way or another. Anyone any suggestions or ideas? Thanks [1] sprite images are already packed into texture atlases offline. This bit probably doesn't need to change.
Advertisement
Are you familiar with D3D9s Vertex-Declarations?
You could just build a vertex declaration for the attributes in your vertex arrays, pass this declaration through your Drawable interface then parse it and interpret the attributes in the array.
Its highly flexible and simple enough to implement. D3D has direct support for vertex declarations, in OpenGL you would loop through the array yourself and set gl*Pointers.

Hope thats of some help.
I'm aiming for almost exactly the same thing; an easy-to-use 2D sprite renderer design that's also easy to tinker around with optimisations. Unfortunately I'm still in the hacking stage myself; at this instant I'm trying to port some old SDL/OpenGL initialistion code into my new library. I'm not sure myself exactly what I'm going to do. I've only briefly dabbled in what OpenGL can do, and I'd like to try using indexed vertex lists for sprite rendering.

You are further along with your design than I am with mine, but I'd just like to give you the heads up that I hope to get to the experimenting stage in a month or two and try and answer some of the things in this post myself. You might want to see what I'm doing in my GDNet+ journals around then; although I'll be more likely checking yours for ideas [smile].
I havn't heard of Vertex Declarations, I'll look into it thanks. (And if anyone's got any good links about them that'd be appreciated).
Creating an efficient and general 2D renderer is a problem I've also been tackling. As OT and TZ outlined, there really is no simple solution. I'll outline my current design and hopefully that'll spark some more discussion on this topic.

Everything to be drawn is a Renderable:
/** * An object that may be rendered by a renderer. */public interface Renderable {    /**     * Returns the shape of the renderable object.     */    Geometry getGeometry();    /**     * Returns an object describing the graphics state necessary to render.     */    RenderState getRenderState();        /**     * Returns a number representing the relative priority to render this     * object. Objects are rendered in order from low to high priority.     */    int getRenderPriority();}


Geometry describes the vertex, index, and texture data for a renderable:
/** * A renderable chunk of two-dimensional geometry. */public interface Geometry {    /**     * Returns the primitive type (e.g., triangles, quads) used to interpret the vertices.     */    PrimitiveType getPrimitiveType();    /**     * Writes the set of vertices (x-y format) composing the geometry.     */       void getVertices(FloatBuffer verts);        /**     * Writes the set of indices into the vertex set.     * @param base Index of vertex zero     */    void getIndices(ShortBuffer inds, short base);        /**     * Writes texture coordinates (u-v format) for each vertex in the geometry.     */    void getTextureCoordinates(FloatBuffer texCoords);        /**     * Writes vertex colours (unsigned RGBA format) for each vertex in the geometry.     */    void getColours(ByteBuffer colours);}

Geometry writes its data directly into buffers that are subsequently passed to OpenGL via vertex arrays or vertex buffer objects. Geometry is free to write as much data as it needs into the buffers, including multiple sets of texture coordinates.

I feel there are a number of problems with this interface. It is complex to implement; a BaseGeometry wrapper makes it easier but not effortless. It's a bit of a pain that indices cannot be zero-based. The data arrays may be accidentally corrupted by rogue Geometry. Geometry may feed bogus data into the arrays that will cause the renderer to choke. Furthermore, it lacks the ability to feed supplementary data to the renderer.

RenderState describes how the Geometry should be rendered:
/** * Defines a mechanism for setting and resetting the OpenGL rendering state to * enable various rendering styles and functionality. *  * States should provide an implementations of <code>equals</code> and * <code>hashCode</code> that correctly indicate when render states are * equivalent. */public interface RenderState {    /**     * Initializes the GL state prior to rendering.     */    void prerender();    /**     * Resets the GL state following rendering.     */    void postrender();}

RenderStates are free to manipulate OpenGL state however they please.

Such a generic interface, though it captures all the possible render states that may need to be set, has a propensity to encourage duplicated state setting. For example, both Text and Sprite may set/unset identical alpha state. One idea I have to prevent this is to use CompositeRenderStates that are composed of individual RenderStates such as AlphaState or TextureState. First, renderables may easily generate appropriate state without duplicating code. Second, transitions between composite states may be reduced to only a subset of the full state.

Renderer handles, well, rendering:
/** * Renders geometry. */public class Renderer {    /**     * Initializes this renderer to begin a new frame.     */    public void prerender();        /**     * Queues the given renderable to be rendered.     */    public void render(Renderable renderable);        /**     * Renders all the queued renderables.     */    public void postrender() {        preprocess();        doRender();    }        /**     * Sort and batch the renderables for final rendering.     */    private void preprocess() {        sortGeometry();        createBatches();    }        /**     * Sorts the geometry first by render state, then primitive type, and finally by priority.     */    private void sortGeometry();        /**     * Constructs the render batches from the sorted geometry data.     */    private void createBatches();        /**     * Draw all of the renderables for this frame.     */    private void doRender() {        setup();        for (RenderBatch batch : _batches) {            batch.render();        }    }    private void setup() {        // stuff        _texCoordBuffer.bufferData(_texCoords, DataBuffer.Usage.STREAM);        _texCoordBuffer.textureCoordPointer(2, 0, 0);    }}

Very simply, the Renderer aggregates Renderables over a frame, sorts them, then renders them in batches. Renderables are sorted by render state, primitive type, and priority. Batches of identical state and primitive are built from the sorted list. The Renderer uses a single vertex, texture coordinate, colour, and index buffer (passed to Geometry).

Currently the renderer is exceedingly primitive. Though the geometry interface cleanly supports multiple texture coordinates (and, potentially other data via getGeomParams(Buffer)) the render has no means of dealing with them. And, in fact, I'm really not sure how to handle them. How should the renderer know which geometry needs multiple coordinates? How do I set the texture coordinate pointer appropriately for only that Geometry?

Maybe the concept of render state could be expanded to include setting up multitexturing? But then how to get the vertex arrays set up correctly? How does shader data play into this? Ahhh! I'm really at a loss.

One parting thought: is there any value in emulating the DX10 model and using vertex/fragment programs for everything? We could essentially eliminate the concept of render state and replace it with that of program state. Knowing so little about this subject it's hard for me to say what would be the impact on Geometry, Renderer, or anything really... but I'd love to hear other people's thoughts.

So, there's my take on the subject. This is a really interesting topic so I hope this post will spark some more interest.
Thats pretty close to how I've been thinking about. At the moment I've got some basic stuff done, but lots of things are still hardcoded (like vertex formats and only supporting single texturing).

One thing I've added but am still debating about is support for "direct drawables". These are drawables/renderables which are added and priority sorted like others, but instead of using the usual batching/geometry/state interfaces it just has a direct callback for user-specified drawing.

On the one hand it's a very useful feature, as legacy drawing code (like my menu system) can be interleaved with the other sprites with minimal effort, and also to set state/effects which would need explicit support otherwise. However it does mean that you've got to be careful to restore any changed state to how it was before, otherwise the batched drawing gets messed up.

Partly I'd like to remove it, as one of the main reasons behind the system was to remove the need to touch gl directly, and make things more robust (specifically, that one messed up object should only mess up it's own drawing, not everything else as well). But it's just so damn handy that I think it'll have to remain.
Pursuing a brief tangent, how do you feel about pre-transformed vertices versus using glTranslate/Rotate/Scale on each object individually? My intuition says that batching is ultimately superior and the CPU can afford to do a few matrix multiplications but, of course, I lack all the pieces to verify this assumption.

Right, so my idea is a Transform object (really nothing more than a fancy name for a 3x3 matrix) very much like the SDK's AffineTransform. Geometry vertices will be transformed prior to being added to the Renderer's vertex arrays. (Hmm. Not sure how this will work in code, though, if Geometry feeds data directly into a buffer. Ideas?) I have a trivial design question: is this transform a property of the Renderable or the Geometry? I suppose it depends on whether I consider Geometry to be a generic polygon in local space or something tied to a specific object on screen. I wonder what the "better" (more flexible, easier to understand, more maintainable... choose any metric) choice is?
I've found that doing transformation on the CPU is much better for performance. Usin glTranslate/Rotate/etc. chops your batches up really small and sucks up performance. I still use glTranslate/Rotate for camera positioning though (where it's setup once or twice over the course of a frame).

I havn't really thought about representing transforms and geometry separately, at the moment my geometry classes encapsulate the basic shape (sprite, ring, etc.) as well as the position/scale/rotation in world space. I suppose this is less flexible, as each geometry type needs to manually include code if they want to rotate, scale, and so on.

My gut reaction would be that the separation isn't needed (for me at least). When I've used 3d engines which separate out the two it just makes it much more difficult and tedious to position anything correctly. Also, doing it this way means that each geometry type can have more specific, useful methods of positioning and transforming - eg. lines can be set with a start and an end point, rather than trying to concoct a matrix to scale and rotate a unit length line into the right place.
I was initially prompted to separate the transform when I realized that my sprites and text, both simply textured quads, were using essentially duplicate geometry code. Thinking about it now, it ultimately might make more sense to factor out a Quad class that both Renderables share. Moreover, I think you're right in saying that most custom geometry (i.e., more complex than a polygon) will have a unique or more useful interpretation of a transform than could be easily represented generically.

My concern then becomes, given the classes Triangle, Quad, and Polygon, how do you factor out what is bound to be redundant transformation code? I suppose I can answer my own question and say that we only really need Polygon and if I'm desperate for specialization I can construct something like Polygon.Quad.
At the moment I don't have much/any duplicate code in this area mainly because I've just converted the features my games previously used - which means the only thing that actually has transformations applied is sprites. The other geometry types either don't require rotation/scaling or are better served through other methods.

However I can see that if you're aiming for something lower level, like Triangle / Quad / Poly stuff, then unifying the transformation code is probably a good idea (and it would be nice to get rotatable text For Free too). I think a with some suitably flexible utility/helper classes and functions then you could make the actual geometry code within each renderable class almost trivial.

This topic is closed to new replies.

Advertisement