TL;DR: Best way to handle level-specific textures without overkilling memory usage and killing performance and keeping things clean in OpenGL for mobile (also lower-end devices) and desktop using power-of-two-textures. Any thoughts? (Check my idea in the update).
I'm developing a game for desktop and mobile using OpenGL and I've come to the point where I would like to add textures that are specific to one single level. Adding these textures to the atlas that is used in all levels feels wrong and is eventually a bad thing, since the amount of levels could become arbitrary large and so could be the amount of level-specific images, and thus the atlas would become too big.
So, I'm looking for a mechanism or approach to load/prepare/use level-specific textures. I've thought of a couple of solutions, but I don't like any of them:
Use two textures: one global atlas and one level specific atlas. I don't like this because this would make it impossible to use textures of both atlases in one texture render batch. Using a cool shader that uses two samplers is something I don't like either, since samplers accessing by dynamic index is not allowed in a shader. The solution to this is to introduce an if-statement in the fragment shader. I would rather avoid that since I'm targeting a wide range of devices, so I would like to avoid branches in the shader.
Use one big texture that contains the global atlas and a level-specific atlas. I don't like this because I don't know in advance how much space will be required for the level-specific textures. So I would have to greatly oversize this big atlas while not using it 95% of the time. The oversizing might be huge, since I'm using power-of-two-texture dimensions. Given the fact that my current atlas at lowest texture quality is already 1024x256 pixels large, and texture quality for iPad is already 2048x1024 pixels large. This approach would allow me to update a sub-area of the texture when loading the level and would require constant memory usage.
Of the two proposed techniques, I think I like the second one the most, however, the expensive memory usage might become a problem on some devices. There is one last technique I came up with while writing this question:
- Completely rewrite my rendering system so that I could render arbitrarily from each of both textures (global vs. level-specific) and the system would split the batches when a new texture should be bound. This would increase my number of OpenGL draw calls from let's say 10 to 16. This is not really an obstacle I think, however, I guess I would have to rewrite big parts of my rendering system and would take me probably a couple of days (since I'm also drawing plain (non-textured) geometry, using a different shader. This is done by 5 draw calls in by depth sorted layers: textures, geometry, textures, geometry, textures).
Has anyone experience with this? A good suggestion? A technique I didn't think of? Any input that might help me take a decision is welcome.
Thanks for reading and for help in advance,
Update (possibly a good solution): When thinking about the last idea, I think that this might be the best solution, since that would solve some of my depth-sorting problems as well, since I'm bound to those 5 layers, I pretty much run every month of active development into the situation where the number of render layers I have available is too low for proper depth sorting. So this generic system to which you could submit arbitrary jobs would solve that. The system I'm thinking about sort of works like this:
- Request a draw batch buffer of a certain type (global textures, level-specific textures or geometry).
- The system checks if this type is the same as the last type, if so, just return the current buffer that is being built (and if the request size fits in the remaining buffer capacity). If not, submit the previous batch to the GPU (bind textures, configure shaders, etc) and return a new buffer of the requested type.
- The requesting code gets the requested buffer of its type and fills it up.
- Finally flush the last buffer to the GPU.
To keeps things fast, you should keep the amount of draw calls low, which ultimately comes down to intelligent depth sorting of the objects in the level editor. This seems no obstacle.
I think I like this approach pretty much. However, if people still want to share their own experience on this topic, that would be great.