Converting a 2D game to modern OpenGL; lots of architecture questions before I start.

Hi folks,

I've got a 2D RPG project that I've been working on for the past few years. When I first began this project, I thought it'd be a great way to grow my understanding of OpenGL. Problem is, the course I took in school still taught the fixed pipeline and never got into the topic of shaders. This was in 2008. I've since learned that there has been a better way for years.

I know that the core profile is something that I will have to adopt (judging by my velocity, I think immediate mode will be completely phased out before I finish my game ;). My next steps with this game (the battle system) will involve a lot of graphical work (flashy magic spells and the like) and I want to make sure I'm not accumulating technical debt by continuing on the path of the fixed pipeline. That means it's time to update my project. The question is how?

The current design

My game shows a tile-based map with an overhead look (slightly tilted to show height). The result is similar to many old RPGs, such as in this picture of the game Suikoden. This means that the camera is not directly overhead. There are also potentially multiple background and foreground layers, with tall objects obscuring objects behind them.

The engine uses a standard game loop, with a step function and a draw function. In the initialization of the game, I set up an orthographic projection that maps pixel coordinates to screen coordinates. From that point onward, I don't alter the projection matrix.

During the draw function, I do the following:

  1. Snap to my camera, which does translation and creates a scissor region to keep the player character in the center of the screen.
  2. Sort all the actors by y-location, top-to-bottom. This will be used to make sure that actors are drawn in back-to-front order.
  3. For each row:
    • Draw the background tiles for the row.
    • Draw the sprites at that row.
    • Draw the foreground tiles/objects for the row.

The source code for this function is available here.

Each sprite has a draw function, which calls its respective Spritesheet object to draw it. The Spritesheet object binds the Spritesheet texture, figures out the right u,v coordinates, and draws a quad at the specified sprite coordinates using glTexCoord and glVertex calls. This code is available here.

The tile layers have a similar construct, in which they call a Tileset object that binds a texture and draws a quad. This is code that I'd like to refactor since it's more or less the same as the Spritesheet draw code.

In summary, every object binds its texture and then draws a quad using immediate mode instructions. It works, but it's not good given the alternatives. I'd like to redesign it to use the programmable pipeline.

Redesigning my project for modern OpenGL

This is where I get a little stuck. There are lots of tutorials about how to use modern OpenGL, but they are teaching how to use OpenGL, not how to make a complex game with OpenGL. Most of these tutorials tend to focus only on the OpenGL parts and don't set up an object-oriented architecture or entity-component system to show how it'd be used "in the field". The main method creates, compiles and links some shaders, and makes some VBOs, and then there is a while loop, and if you hit ESC, we break out of the while loop, clean up and exit. This is perfect for teaching someone how to use OpenGL without confusing them or blowing up the tutorials to epic length. I'm fine with it, and I learned a lot, and I wouldn't change them. More resources on how to integrate OpenGL into a game's architecture would still be helpful.

I've come up with a bunch of questions around this, to make sure I start down the right path.

  • First off, is there a known guide for doing the kind of conversion I'm about to do? I haven't found anything after a lot of searching, but if someone's written about it then I'm sure it would answer most of my questions.

  • Since the engine just loads the maps and sprites requested by the authors of the game scripts, I can't compile a giant texture atlas ahead of time. It's possible that in the middle of gameplay, the tileset might change, or a new NPC is added with a spritesheet that hasn't yet been loaded in. I think this means that many texture switches are unavoidable. Given that, I can't just bind once and then set up a single stream of indices. How should my draw loop operate, then? How do I ensure that everything is drawn in the right order if it's not just a single VBO?

  • How many VBOs do I need anyway? Right now, everything is a square or rectangle, so if each object has to be drawn with a separate glDrawArrays call, do I even need more than one VBO?

  • Who is responsible for applying translation, scaling and rotation to the vertices in the VBO? What should the game code on the CPU do, and what calculations should I offload to the shaders? I could have each entity create its complete matrix and texture indices, then send that matrix and my single quad VBO to the vertex shader, but that doesn't seem efficient.

  • Who is responsible for drawing? It was nice to have each entity be responsible for drawing itself, but it seems like now I need to have some kind of Visitor outside of the entities collecting drawing information from them. Is that correct?

  • What OpenGL extensions should I be able to reliably assume are present nowadays? Should I even be concerned about machines that don't have VBOs available, for example?

  • Is there a standard go-to library for wrapping OpenGL resources in an RAII way? What do people think about OGLplus, for example? While I don't mind writing my own RAII Shader object, I feel like it's probably been done dozens of times already and doesn't teach me anything new.

TL;DR - I have a project that I'd like to "upgrade" to modern OpenGL. Various tutorials are available for the modern OpenGL API, but I don't know how to best re-architect my engine to use it.

Posted Jul 6, 2014, 10:52 PM UTC

4613
mysticredditProfessionalGameDevTurnedIndie•

Coulple of quick comments:

  • GLEW - OpenGL Extension Wrangler

The standard render function does

  • Apply 3D Camera Matrix for player- set modelview & perspective matrices
  • Draw World - for each object, push object matrix, draw entity, pop object matrix
  • Apply UI camera - set modelview (Identity) & perspective (orthographic) matrices
  • Draw HUD - iterate and draw all the UI elements

There are 2 ways os setting the orthographic camera:

  • -1.0 .. +1 = left .. right
  • -y .. +y = using aspect ratio

or

  • 0 ... ScreenW-1 = left .. right
  • 0 ... ScreenH-1 = top .. bottom

It doesn't really matter which coordinates you pick, just as long as you are consistent.

You could use multiple texture atlases, or dynamic textures. You could use the z coordinate of the texture coordinates to specify which texture to use. This article has an example of how to use OpenGL Texture Arrays. Note that Texture Arrays require OpenGL 3.x+.

You could use one VBO just dumping all your objects in there but that is an optimization.

The Book: OpenGL Insights shows the modern shader approach to starting OpenGL. As long as you target OpenGL 2.0 (or higher) you'll be good.

Older laptops won't support OpenGL 2.1+ -- there was a big cry last year when Minecraft dropped support for pre OpenGL 2.1. Depending on the size of your community you may get a) no, or b) some backlash. It sounds like for you this isn't / won't be an issue -- just something to keep in mind when you get bug reports when people complain they can't run your game.

Hope this helps.

11
Error_ValueOP•

Thanks for responding!

GLEW[1] - OpenGL Extension Wrangler

I've seen GLEW used in plenty of tutorials, and having tried to do it myself, I think could do far better by using the tried-and-tested method everyone else uses instead. :)

You could use multiple texture atlases, or dynamic textures. You could use the z coordinate of the texture coordinates to specify which texture to use. This article[2] has an example of how to use OpenGL Texture Arrays[3] . Note that Texture Arrays require OpenGL 3.x+.

You could use one VBO just dumping all your objects in there but that is an optimization.

That's good to know. I didn't think about putting each texture into a texture array instead; that's a good way to take on the single VBO stream optimization if I need it later. :)

Older laptops won't support OpenGL 2.1+ -- there was a big cry last year when Minecraft dropped support[5] for pre OpenGL 2.1. Depending on the size of your community you may get a) no, or b) some backlash. It sounds like for you this isn't / won't be an issue -- just something to keep in mind when you get bug reports when people complain they can't run your game.

This is something that I'm wrestling with now. If I continue with what I have, I can focus on gameplay and I can still create all the visual effects that I want to have (albeit in a less efficient way). Pretty much any hardware that supports the compatibility profile will be able to run my game if I continue with it.

On the other hand, I'm concerned about devices that don't support the compatibility profile. I can't think of any that exist now, but my game stops working once these deprecated functions are simply dropped in the future. Do you think that this is a valid concern any time soon?
As another minor con to sticking with what I have, I lose a learning opportunity if I continue this way -- I want to ship this game but I've also become a better programmer through making this game and it's a good chance to apply what I learned in modern OpenGL tutorials.

1
Omnicrola@Alomax - Unnamed Voxel RTS, Alien Gardener•

Most of these tutorials tend to focus only on the OpenGL parts and don't set up an object-oriented architecture or entity-component

This is what I banged my head on in my spare time for about two months or so. I'm using Java/LWJGL making a voxel-based game. Since VAOs help minimize the amount of calls to the JNI, I knew they where the way to go. This is probably not the best architecture, or the most efficient, but it's working pretty well for me so far. I've been getting >150fps with a decent number of triangles/Entities on my laptops Intel HD4000 GPU. However I don't have a lot of textures or a high polygon count at the moment. My desktop's nVidia 650 runs it at about 500fps.

What I ended up doing for my architecture around the VAO is I have a few classes:

  • TriangleMeshVao (I used a different concrete of the same interface for particles)
  • TriangleMeshData
  • VertexData
  • VoxelShaderProgram
  • Texture (this is from LWJGL) and GLTexture (my custom implementation)

These work together to form the basic rendering architecture. Here's a short explanation of what each class' responsibilities are:

TriangleMeshVao

  • Contains the actual VAO id as well as associated VBO ids, which are created on construction but contain no data.
  • render() - Binds the VAO/VBO and (optional) texture, calls glDrawElements with GL_TRIANGLES, and unbinds itself
  • destroy() - frees the VAO and associated VBOs (does not free the texture memory)
  • bufferData() - takes a TriangleMeshData object and buffers its data to the appropriate VBOs

TriangleMeshData

  • A rather simple model object containing a list of VertexData objects, and a list of indexes, as well as scale/translation/rotation values that will be applied to the entire buffer when called by the VAO class
  • addTriangle() - Takes 3 VertexData objects and stores them internally
  • getIndiciesAsShortBuffer() - converts the list of indexes into a ShortBuffer backed by a ByteBuffer that can be loaded into GPU memory
  • getVertexesAsFloatBuffer() - converts the list of vertexes into a FloatBuffer backed by a ByteBuffer to load into GPU memory (also applies scale/rotate/translate)
  • can additionally create a bounding box based on the max/min of the vertexes on each axis, and can clone() itself

VertexData

  • A class that holds onto a set of integer arrays for position(x,y,z,w), color(r,g,b,a), textureUv(s,t), and normal(x,y,z)
  • Also has some simple methods to translate and rotate

VoxelShaderProgram

  • Built using 2 strings specifying the vertex and fragment shaders to load and attach
  • Can bind() and unbind()
  • Each Entity being rendered uses this class to set it's own Model matrix4f uniform describing translation/rotation/scaling
  • The class modeling my camera uses this class to set uniforms for View and Projection matrices every cycle

Texture / GLTexture

  • On construction, executes glGenTextures and loads a ByteBuffer containing the image data to the GPU
  • can bind() and unbind() itself
  • destroy() - frees the texture from GPU memory. To be economical with memory, I made sure the texture is never destroyed when a VAO is disposed, since most textures get reused by other VAOs later.

I've left out a lot of the other objects that collaborate with these to make this happen, but hopefully this will give a rough idea of how it's pieced together. The general theory I've used for VAOs is that any set of triangles that need to move together go into the same VAO. If they move separately then they get separate VAOs so that instead of updating individual vertex data to make things move, I can update only the Model matrix uniform on the shader each cycle. The shader program itself is responsible for applying the Projection/View/Model matrices to each vertex and normal, as well as calculating lights.

9
Error_ValueOP•

Thanks, I really appreciate you getting into the implementation details a bit here. I envisioned a similar setup -- I probably won't have a voxel shader, but I can see having objects to manage the lifetimes of VAOs/VBOs, and I already have a Texture object. :)

2
Omnicrola@Alomax - Unnamed Voxel RTS, Alien Gardener•

Naturally you should have a shader specific for yours :)

The VoxelShaderProgram class I have is actually mostly contained in an abstract class that does the majority of the work. It does very general shader things such as updating uniforms and initializing the program. The Voxel class just extends it, adds a few extra specific uniforms, and calls the abstract with the path to the specific vertex/fragment .glsl files it wants to load.

I also have a ParticleShaderProgram class, which works pretty much the same way, it just loads an alternate set of vertex/fragment files which makes the shader itself behave differently.

1
fullouterjoin•

If you stick to OpenGL ES 2 you will have an easier time porting to mobile and The Web (tm).

5
ginsweater•

Out of curiosity, what sort of issues are you having that make you want to switch to modern GL? I haven't heard anything about drivers dropping support for good ol' 1.2-style immediate mode - there are too many existing games that would stop working.

Anyway, I might be able to answer some of your questions:

  • Binding a new texture for every quad is probably not going to be disastrous if you have less than several hundred. I used to get away with it in a 2D game I wrote back in 2005 - yeah, it's wasteful, but if you run at 60 who cares?

  • If you do have the need, look into texture arrays - they're in core these days. As long as all your tiles are the same size, you can pack them all into an array and your shader can refer to different textures by index. Presumably you'd pack the texture indices into your attributes somewhere.

  • If you're using core profile then you're using at least OpenGL 3.2; 3.0 is where core was introduced and 3.2 drivers are available for basically all 3.0-capable chips. If you're willing to ditch people who are on Macs but haven't updated to Mavericks, you can use 3.3. VBOs are core as of 3.0, so everyone has them. Generally the OpenGL.org wiki will tell you when a feature was added to core.

  • In core profile there are no implicit transforms or functions applied. The matrix stack is gone, for example. No MODELVIEW or any of that stuff. Your vertex shader gets exactly what you pass in and it's responsible for outputting vertices in clip space; that's it. It's up to you to divide responsibilities between GPU and CPU.

  • Where to divide responsibilities? Well, anything in the shader will be run on every vertex. It's the inside of a big multicore FOR loop. So the usual best practice is to hoist anything that's shared across the whole drawcall to the CPU. Textbook example: if you're rendering a 20,000-vertex model you would multiply the model matrix by the camera matrix on CPU, then the vertshader would multiply the combined matrix by each vertex.

  • Me, if I was writing a 2D tile renderer to support rotation and scale, my vertex attributes would probably start out as center-x, center-y, sin-theta, cos-theta, and size. (You can convert to quads in the shader using glVertexID. But I'd expect to change at least one of those.

  • Having "each entity be responsible for drawing itself" is nice and I wouldn't move away from it. There are a few patterns people use; one is to have the DrawTile() function actually just push a little struct with position and texture info onto a list - later in the frame a Manager comes along and scrobbles over the list to actually set up the drawcalls. Another pattern makes the entity responsible for creating and updating the little struct with position and texture info. The two have the obvious tradeoffs.

  • Oh, and draws are ordered by drawcall. The GPU is required to behave "as if" it's singlethreaded and executing your commands in the order you sent them, so you shouldn't have ordering problems between different VBOs.

Any of that help? Hope I wasn't too braindumpy.

4
burito•

VBOs are core as of 3.0

I think you mean 1.5

edit: So whichever genius though it was a good idea to downvote this comment, perhaps you should maybe get a clue.

Wikipedia VBO's

The Vertex Buffer Object specification has been standardized by the OpenGL Architecture Review Board as of OpenGL Version 1.5 (in 2003).

2†
ginsweater•

Aaah, that always gets me. The OpenGL wiki says "Core since 3.0" on a bunch of things - that's cause core has only existed since 3.0. So technically nothing can have been in core before 3.0. ;)

Yeah, VBOs were promoted from extensions in 1.5. I actually didn't do any OpenGL programming for a few years at one point so I'm fuzzy on the order of things happening between 1.2 and 3.0.

In my defense, I'm right on OpenGL ES - VBOs were an extension until ES 3.0. ;)

2
Craftfield•

You might want to try to render front to back too. If you're not using blending it might be faster. I have a current case where I render front to back with discard which is insanely faster than anything back to front. Discard causes early z testing to "fail", so it depends on your case. Discarding also isn't recommended for iphone afaik.

3
burito•

Consider carefully why you want to switch to modern OpenGL.

You're making an 2D RPG. You have no need whatsoever to switch.

Chances are your RPG will run on old hardware. Think Intel 915 chips, the kind found in Eee PC's, and all of those Atom based netbooks.

If you go modern, all of that hardware is out the window. There's no modern OpenGL support for them, and there never will be. Well, there is modern OpenGL support for them on Linux, but that's because of Mesa. Those upgrades will never make their way back to Windows.

Think about it this way. You have a map. You have characters. You have "effects" (spell flashes, projectiles, etc). 3 rendering tasks. Make sure there is a clear line down the middle between the rendering code and your game.

Why not support both? You've already got a working renderer, no need to throw it away. There's an old adage in software development.

0, 1, Infinity. They are the 3 object count limits you are allowed to have.

If you can make your program support 1, making it support N should be trivial.

In direct answer to your bullet points... (these answers are equally valid to modern and ancient OpenGL)

  • Head over to r/OpenGL, they have a side bar.
  • Why? Have a blank texture that requested resources are inserted into (a "Texture Atlas"), OpenGL has had features to do this since the 1980's. Sidestep the "shop floor packing problem" by making them all the same size, who cares if you waste a few megabytes of VRAM.
  • 1, no.
  • You. I think you're over thinking it. Shaders can do whatever you want them to do. IMHO, I'd have each object occupying a "slot" in the "texture library". You tell the "rendering code" that "Object 14, is to be drawn at (x,y), with rotation (r)". A shader can very easily figure out where in the texture atlas "object 14" is. The rendering code should have no idea at all about map coordinates, it should only know about screen space. You're doing 2D after all.
  • You, again. Ideally, you should have a nice list of all objects visible, so that your rendering code can simply look at this list, process it, and finish. This has many advantages, most importantly you can have two lists, one for what the screen looks like now, one for what the screen looks like last frame. This allows for all sorts of cool effects (eg motion blur). It also allows for you to decouple your physics simulation from your rendering. Your physics can operate at only 10 fps, and your renderer can say "Well, I'm 20% of the way between these two physics frames, so I'll just call blend(last_frame_pos, next_frame_pos, 0.2);. I'm sure "visitor" models sounded like a great idea in class, but in the real world, computers operate on lists. If you can turn your rendering into a list processing task, this gets soooo much easier. That way your renderer doesn't have to "visit" anything. It just sits there, does its job, and nothing more. An added bonus, because all data needed to render a frame is in one list, you don't thrash the cache searching through your entire game state to see if something is visible.
  • None. VBO's have been in OpenGL since dinosaurs roamed the Earth.
  • RAII in rendering is a mistake. A big one. You want to do all of your initialisation at the start, in one go. This can mean when you transition areas. Build a list of things you need. Process that list into your Texture Atlas. Have this processing step return the Atlas ID's to the things that requested the resources. When these "things" want to draw something, they say "Draw resource N at pos (x,y), rotation R" (well, they say that, but really it gets put into a list of things to draw this frame). Your rendering code comes along entire milliseconds later, and says, "Right, I've got List_Count items to draw, lets draw them".

Your rendering code should have no idea at all what it is rendering.

3
knight666•

One thing you could look at is Gameplay3D, which is an open source engine built on OpenGL. It doesn't have the best architecture for making a game in, but it abstracts the OpenGL stuff really nicely.

I'm in the on-going process of refactoring everything but the render code, which is fine the way it is.

2
amnesiasoft•

If everything is a square or rectangle then you only need a single VBO, probably containing information to draw a unit square (though you could go for some arbitrary, weird shape if you wanted), and then transform it as needed.

1
[deleted]•

[deleted]

1