Implementation of 2D Tiles using OpenGL

Member
Posts: 233
Joined: 2003.05
Post: #1
I'm developing a 2D tile engine in OpenGL using SDL. (The SDL part doesn't pertain to this question)

I'm currently loading a targa image into a large texture (256x256 max) with all my tiles in it and then drawing rects using fractional parts of that texture.

For the longest time I had a problem with the the inexact pixel thing and I thought it was because I was taking just a portion of a large texture to make my tiles. Once I used my little hack (the offset at 200 and 300 pixels over) , that problem was fixed though. Rolleyes

In other words, everything LOOKS good now. Cool

Anyway, my question is would it be more advantageous for me to store EACH tile in a seperate texture at the start by breaking up my original targa and creating a new texture for each tile?

Or is there too much overhead to store each tile in a seperate area of VRAM using OpenGL's textures?

My performance is decent, but I don't want to go too much further if I'm doing something fundamentally wrong.

Because OpenGL can't draw a portion of a texture by PIXELS I am doing a divide to get the correct portions of the larger texture. My next step is to create a lookup table for each table of tiles when I load them. That may give a substantial improvement.

Are you guys using any different methods? Smile

Aaron

P.S. I was unsure where to post this as it IS an OpenGL question but NOT for 3D. So, I put a link to this question in the OpenGL board. Hope that's okay. :?:

"Pay no attention to that man behind the curtain." - Wizard of Oz
Quote this message in a reply
Feanor
Unregistered
 
Post: #2
2D is a subset of 3D and since it is API specific, it is most appropriate here, but seriously, not to worry.

Personally, I store texture data in the biggest texture object possible. I believe the most common reason is that it is better for you to hand-optimize the texture layout. Firstly, it keeps them together (all on the gfx card, hopefully), and in a memory-efficient form. Moreover, it lets you put non-power-of-2 textures into memory more easily (esp. in cases where the hardware does not support it). It might be more efficient not to have to switch from texture object to texture object for rendering different textures.

So, keep them all bunched up together. Oh, if you have non-rectangular textures, you definitely want to put them into the same image, otherwise you waste all the space where the texel data isn't being used.
Quote this message in a reply
Moderator
Posts: 365
Joined: 2002.04
Post: #3
And I'm doing it the other way, just to be contrary! Wink

I got a bit sick of texture tiles bleeding into one another so I switched from selecting tiles using UV coordinates to breaking the image into separate bitmaps and making every tile a separate texture object. I didn't notice any change in performance, even if I do that to several sprites (16 frames each) and the font (128 characters).

I kept my tiles in single texture files and split them into separate bitmaps as I load them. It's more convenient for editing in my opinion.

Neil Carter
Nether - Mac games and comic art
Quote this message in a reply
Member
Posts: 233
Joined: 2003.05
Post: #4
I think I used to have the same bleading problem that you were having. It was driving me mad.

It has to do with a bug that should be fixed soon.

In the meantime I have a "fix."

After that, no bleeds. Cool

Is there any current or future way for OpenGL to choose UV coordinates on a per pixel basis? That would make so much more sense for 2D AND 3D.

Aaron

"Pay no attention to that man behind the curtain." - Wizard of Oz
Quote this message in a reply
Sage
Posts: 1,232
Joined: 2002.10
Post: #5
Quote:Originally posted by aaronsullivan
I think I used to have the same bleading problem that you were having. It was driving me mad.


No, I'm pretty sure the problem NCarter is referring to is different. Consider:

You have one large tileset texture.
You want to copy several different subportions of it onto the screen.
The obvious way is to texture it onto small quads, using fractional texture coordinates. E.g. if the tileset texture is 4x4 tiles, use 0..1/4, 1/4..1/2, etc for 2D textures.

This works fine, if filtering is off. But if filtering is on, perhaps because you want to scale or rotate some tiles, then you will have problems at all the tile edges, because texels from the adjacent tile in the tileset texture will be blended in.

There's no way around this texturing with fractional texture coordinates, because there is no way in OpenGL to respecify the texture border behavior for subportions of the texture.

One workaround is to break the tileset up into a bunch of individual texture objects. That works with filtering because now you can specify the texture border behavior for each tile texture.

For basic 2D tile engines, plus a little bit of fancy effects (scaling, rotation) this is good enough.



Quote:Are you guys using any different methods? Smile

Ok, what if you want to do more than the basic tile engine? (giving away secrets here... maybe I ought to write a tutorial on this...)

For instance, your generic tile engine uses a tilemap, where each tile has an X,Y index into the tileset to specify which tile should be used. There are a lot of variations on this, such as allowing multiple layers of tiles, or flipping tiles horizontally/vertically, etc. But generally, the tiles are aligned to a grid.

This is pretty boring.

What if you instead specify the X,Y index as _texture coordinates_? Now you could use any arbitrary rectangle from the tileset as a tile. As an example, if your tileset has a Christmas tree made of several tiles in it:

Code:
0  
123
45678
  9

instead of being limited to using only tiles 0..9 aligned to a grid, you could use a rectangle halfway overlapping 5 and 6 as a tile. Or any arbitrary place. This gives you a lot more freedom in map creation, and better reusability of your tileset. (Though, it makes the map editing slightly more complex.) If you think about it a little bit, you can come up with many additional things to specify for each tile besides the texture coordinates, that you wouldn't be able to do in a generic tile engine. (hint: what state does GL maintain for textures?)

Well, now you have a real problem with filtering, because you have to use one big tile texture and fractional texture coordinates.

The solution is to use glTexSubImage2D() to selectively update portions of your playfield texture with new tile texels. Because this call is essentially a memcpy(), it is immune from filtering problems.

Yes, I said playfield _texture_. In an advanced tile engine you won't be drawing tiles directly to the viewport. You want to use an offscreen texture context for each layer of your playfield. Hmm... this really needs a tutorial to explain.
Quote this message in a reply
Member
Posts: 233
Joined: 2003.05
Post: #6
I understand what you are talking about in general.

Isn't there a big limitation though? Texture Size can get shaky compatibility when it is over 256x256, right?

Or would you break up the parts of your image into 4 or 8 textures each at 256x256... or are texture "contexts" not limited in this way... hmm... interesting.

Anyway, I never did understand the glTexSubImage2D() thingy... Smile I've got to look into that.

In essence, does that mean I take a portion of my texture like I'm doing now (by fractions) and then copy that into the playfield texture... but then, how do I get it to rotate before hitting the playfield texture? Huh unless you have several layers of playfield textures like you say...

I think I'm getting it. Does it sound like I'm on the right track?

I was impressed by your TileScroller, I'd be interested in a short, simple tutorial if you thought it was worth the effort.

It would help with some of the details and I know others would benefit. Cool

I'm learning a bunch already, thanks! Any other implentations out there? I'm getting really curious now all you people with your secrets! Ninja

Grin

"Pay no attention to that man behind the curtain." - Wizard of Oz
Quote this message in a reply
Luminary
Posts: 5,143
Joined: 2002.04
Post: #7
I'm going to have to disagree with just about everything said so far...

Switching textures is a performance hit, particularly once you overflow VRAM. That means you should keep texture state changes as few as possible, by storing multiple tiles in a texture and drawing all the tiles in one texture before moving to the next texture.

You can avoid the filtering issues by duplicating tile edge pixels. To illustrate, suppose we have 2x2 red, green, blue and yellow tiles in the texture:

[font=monaco,courier]rrgg
rrgg
bbyy
bbyy[/font]

This will look bad at tile edges since you'll get the other tiles filtered in. If you change the texture so it looks like this:

[font=monaco,courier]rrrrgggg
rrrrgggg
rrrrgggg
rrrrgggg
bbbbyyyy
bbbbyyyy
bbbbyyyy
bbbbyyyy[/font]

But still only cut out the 2x2 section from the center of each tile, you'll avoid the filtering issues. Basically, expand each tile by 1x1 pixel duplicating the border texels.

You will always want to render to the screen. There's no reason to use texSubImage2D (slow!!) or render-to-texture (slow on < 10.2, not portable between plaforms)

The real solution, though, is to embrace OpenGL and not try to produce pixel-perfect graphics. If you antialias everything and deliberately use sub-pixel offsets and rotations and things, you just won't notice that things aren't pixel-perfect.
Quote this message in a reply
Luminary
Posts: 5,143
Joined: 2002.04
Post: #8
The Rage 128 (worst video card OpenGL-accelerated by Mac OS X) supports 1024x1024 textures, though some models only have 8MiB of VRAM, so using a 1024x1024x32bpp texture may not always work.

Radeons and GeForces support 2048x2048, GeForce3s (and Radeon 8500+s?) support 4096x4096.

The issue you have is that when a new texture gets swapped into VRAM, the whole texture gets swapped in regardless of how much of it you're actually going to use. That means you need to balance the size of the texture (and therefore efficiency of tiling) against the hit of swapping to a new texture. Not a problem if all your tiles fit into VRAM at once, but that seems a bit unlikely...
Quote this message in a reply
Sage
Posts: 1,232
Joined: 2002.10
Post: #9
Quote:Originally posted by OneSadCookie
You can avoid the filtering issues by duplicating tile edge pixels.

Yes, this is another workaround, often used in font textures. But it's not applicable in the case where you want to specify tiles as arbitrary parts of a texture.

Quote:You will always want to render to the screen. There's no reason to use texSubImage2D (slow!!) or render-to-texture (slow on < 10.2, not portable between plaforms)

Well, those are valid concerns. TexSubImage2D is only accelerated if you use rectangle textures. So that limits you to Radeon/GF2 or newer. And with Radeons, it is only fully accelerated on 10.2.5 or newer. But if you don't care about cross-platform or old Mac hardware, it works very well.

I'm going to disagree with you about always rendering to the screen. Off-screen contexts are perfect for a whole slew of effects, and are fast.

Quote:The real solution, though, is to embrace OpenGL and not try to produce pixel-perfect graphics. If you antialias everything and deliberately use sub-pixel offsets and rotations and things, you just won't notice that things aren't pixel-perfect.

I bet you'd notice if all your windows, scrollbars, etc in Finder had fuzzy edges. Smile
Quote this message in a reply
Luminary
Posts: 5,143
Joined: 2002.04
Post: #10
TexSubImage2D is always slow, OS version and/or rectangular textures notwithstanding. It's a main memory -> VRAM copy, which is slow.

There's absolutely no benefit to rendering to an offscreen unless you want to draw that image multiple times. Otherwise, you might as well draw to the screen and save yourself a copy.

You're right, duplicating tile edge texels doesn't work if a "tile" can be an arbitrary portion of your image, but I fail to see how that is at all useful...

And yes, I would complain if a traditional GUI was all fuzzy, but for a game, it's just a different look. It might not be appropriate for all games, but It'll be fine for most.
Quote this message in a reply
Sage
Posts: 1,232
Joined: 2002.10
Post: #11
Quote:Originally posted by aaronsullivan
Isn't there a big limitation though? Texture Size can get shaky compatibility when it is over 256x256, right?
... or are texture "contexts" not limited in this way...

As OSC says, texture size is not really an issue on any modern card (although, running out of VRAM with multiple textures/contexts is always an issue.)

When you create a context, you're asking for a front/back buffer of a certain size. E.g., an NSOpenGLView in a window, or a fullscreen context. You need to use a pixel format supported by your hardware, but it can be any arbitrary size (like 640x480), as long as it's within GL limits (2048x2048) and you have the VRAM.

However, when you turn that context into a texture ([ctx createTexture: fromView: internalFormat:] is the method I use) you have to worry about the texture size. You need rectangle texture support if you want arbitrary sizes. Again, this will limit you to Radeon/GF2 or newer.

See these Apple code samples for info on creating off screen contexts and updating textures quickly:

AGLSurfaceTexture
TextureRange

Quote:but then, how do I get it to rotate before hitting the playfield texture? Huh unless you have several layers of playfield textures like you say...

If you only want to transform (rotate, etc) entire chunks of playfield (a layer, for instance) then you just do the normal texturing transforms on that layer when you draw it to the viewport.

If you want to draw each tile into a layer with transforms, it is more complicated. I was midway through implementing this in TileScroller when I got distracted by another project, but the idea is to TexSubImage2D the tile into a temporary texture, to get a tile with no edge filtering artifacts. Then draw that texture with whatever transforms you want, onto the layer texture. Obviously there is some penalty for the extra copy here, so you'll want to build a little cache of temporary textures.

Quote:I was impressed by your TileScroller, I'd be interested in a short, simple tutorial if you thought it was worth the effort.

There certainly are a lot of threads about 2D tile engines recently. I'm thinking about writing an article... it doesn't help that the Writers Kit is unavailable.

I should also point out that the current incarnation of TileScroller is only using a tiny subset of the possibilities offscreen rendering allows. It only maintains one H/V scrolling layer per room (since this is all Metroid needs...) and optionally blends it with a feedback texture, for that motion blur effect. But, you can easily imagine how this can be expanded to multiple layers, or more complex effects. For instance, using a separate layer which only gets "hot" objects such as lava drawn into it. Then the lava can have the heatwave feedback on it, while the rest of the room does not.
Quote this message in a reply
Member
Posts: 233
Joined: 2003.05
Post: #12
Quote:Originally posted by OneSadCookie
The Rage 128 (worst video card OpenGL-accelerated by Mac OS X) supports 1024x1024 textures, though some models only have 8MiB of VRAM, so using a 1024x1024x32bpp texture may not always work.

Radeons and GeForces support 2048x2048, GeForce3s (and Radeon 8500+s?) support 4096x4096.


From my understanding this might be what's "written on the box", but in actual practice you run into blank textures when going up to 512x512 (or 1024x1024?). At least, that's what I thought was being discussed on the Apple OpenGL lists... I'll have to look it up.

[update] I haven't been able to find anything about this so just ignore me for now. Blush [/update]

Aaron

"Pay no attention to that man behind the curtain." - Wizard of Oz
Quote this message in a reply
Sage
Posts: 1,232
Joined: 2002.10
Post: #13
Quote:Originally posted by OneSadCookie
TexSubImage2D is always slow, OS version and/or rectangular textures notwithstanding. It's a main memory -> VRAM copy, which is slow.

Well, if you set it up right, it's an AGP DMA copy, so there is no CPU involvement. My little Metroid Tilescroller eats 0-5% of CPU (on an 800Mhz TiBook), updating textures like crazy at 60fps.

Yes, this *is* slower than a VRAM-VRAM copy (such as a normal texture draw) but there is still around 500 MB/s of bandwidth over AGP2x (a GB/s over AGP4x), which is enough for some very complex effects in a 2D game.

Quote:There's absolutely no benefit to rendering to an offscreen unless you want to draw that image multiple times.

Well, the whole point is to enable a more complex engine than the simple tile engines of 10 years ago. So you might as well do something extra with the offscreen texture.

But, I can think of three examples of benefits even if you only draw it once:

1) It's a texture. So you can draw your content using whatever system you want into the offscreen context, and then draw the texture to the viewport with separate transforms. If you want to perspective warp your playfield, it's a lot easier transforming one texture than it would be to figure out the transforms for each tile/sprite etc.

2) It's a texture. So you can draw your content assuming the fixed texture size, and then draw the texture to a variable window size without worrying about the coordinate system changing. Untima does exactly this for it's resizable window.

3) It's a texture. So it can be drawn independently of other content in the viewport. For instance, my current fractal painting program is rendering each layer into a separate offscreen context, and then compositing them all to the viewport. If you want to drag a layer around, the texture can be quickly redrawn without needing to recompute the content of the texture.

Quote:You're right, duplicating tile edge texels doesn't work if a "tile" can be an arbitrary portion of your image, but I fail to see how that is at all useful...

As I said, getting more flexible usage out of your tileset.
Quote this message in a reply
Luminary
Posts: 5,143
Joined: 2002.04
Post: #14
Quote:Originally posted by aaronsullivan
From my understanding this might be what's "written on the box", but in actual practice you run into blank textures when going up to 512x512. At least, that's what I thought was being discussed on the Apple OpenGL lists... I'll have to look it up.

AFAIK, there are no problems with 512x512 textures on Rage 128s (My SmileyTag game from uDevGame2002 used 512x512 textures and no Rage128ers had problems).

In general, using MAX_TEXTURE_SIZE x MAX_TEXTURE_SIZE textures under 10.2.x is horribly broken, but I think 1024x1024 on Rage 128s might be OK...

Quote:Originally posted by arekkusu
When you create a context, [...] it can be any arbitrary size [...], as long as it's within GL limits (2048x2048) and you have the VRAM.

My understanding is that the limits are in the graphics hardware, not in OpenGL, and that different cards support different maximum surface sizes, though it's generally about the same as the maximum texture size.
Quote this message in a reply
Sage
Posts: 1,232
Joined: 2002.10
Post: #15
Quote:Originally posted by OneSadCookie
My understanding is that the limits are in the graphics hardware, not in OpenGL

Doh! You're right:
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, max_v)

Time to update my code...
Quote this message in a reply
Post Reply 

Possibly Related Threads...
Thread: Author Replies: Views: Last Post
  Music Composition, Sound Design and Implementation for your games MyMiniGemini 0 87 Oct 21, 2014 03:13 PM
Last Post: MyMiniGemini
  Suggestions for Sound Implementation soulstorm 15 7,510 May 15, 2009 07:54 PM
Last Post: Gillissie
  Tiles/Sprites question Bonked 0 2,114 Dec 16, 2006 06:06 AM
Last Post: Bonked
  gluUnproject implementation TomorrowPlusX 3 10,459 Sep 19, 2006 08:27 AM
Last Post: TomorrowPlusX
  Shader implementation questions akb825 12 5,294 Jun 5, 2006 04:39 PM
Last Post: akb825