How do you deal with scaling and zooming?

BlitzMax Forums/BlitzMax Programming/How do you deal with scaling and zooming?

Imphenzia(Posted 2009) [#1]
When it comes to scaling and zooming (especially in tiled environments) I've found quite a few posts regarding it in the forum. Unfortunately I see major disadvantages and advantages with the different solutions suggested and I'm interested if we can summarize the various methods in one thread.

How do you deal with offering a varity of screen resolutions if you take into account tiled environments and split screen functions etc?

1. Lock game to a specific screen resolution leaving user to run in window or full screen
Pros: Simple (one coordinate system only), no glitches or gaps
Cons: Player does not get a great visual result (as you pick a relsolution most can use, e.g. 1024x768)

2. Use OpenGL screen zooming
Pros: Fast, no gliches or gaps
Cons: Does not support split screen as entire screen area is zoomed(?), Multiple coordinatessystems

3. Scale your objects depending on selected screen resolution
Pros: Accomodates for more choices of screenresolutions
Cons: Multiple coordinates system, could be subject to glitches and gaps between tiles

4. Other solution?


Arowx(Posted 2009) [#2]
4. Go 3d with a fixed perspective on a 2d world with miniB3D?


Jur(Posted 2009) [#3]
In my project I use 3 fixed game area sizes to cover major monitor ratios - 4:3, 5:4 and 16:10. Then I use projection matrix to scale these game sizes to particular resolution. That means I need 3 variants of full screen graphics (title screen and such). Tiled graphics just cover the game area with some more or less tiles.


Imphenzia(Posted 2009) [#4]
Arowx - do you then lose all the features that blitzmax offers for 2D-graphics?

Jur - sounds interesting - thanks for the tip. Is it possible to use projection matrix scaling for multiple viewports on a sceen, so you can have 2 different zoom levels in e.g. a split screen game?


Arowx(Posted 2009) [#5]
Personally I just go with 1024x786 as it's the modal graphics resolution and I'm still working on 2D games.

But I believe you can still switch to rendering in 2d.


Jur(Posted 2009) [#6]
No, projection matrix works on entire screen. It is great for handling different resolutions but not suited for ingame content (unless you have some scaling effects which work on entire screen).

Jur


ImaginaryHuman(Posted 2009) [#7]
You can do multiple `viewports` each with its own projection matrix to handle resolutions and do splitscreen as much as you want, in OpenGL. It doesn't apply to the whole screen, it applies to the viewport. Or if not, then it's poorly coded.


Imphenzia(Posted 2009) [#8]
@Imaginar - I've got support for unlimited viewports to my "tiling engine" and it works perfectly in a any set resolution. I've also tried the approach with projection matrix for each viewport, but I must be missing something as I am still experiencing some troubles with offsetting withing the viewport if matrixed, I'll keep trying.

You cant stack projection matrixes ontop of eachother right? I.e. do a projection matrix for lets say two viewports and then another projection matrix for the entire screen to scale it to the desired screen resolution.


ImaginaryHuman(Posted 2009) [#9]
You can multiply matrices.. haven't tried it. glMultMatrix?

Or you can do the scaling calculations yourself.


Fry Crayola(Posted 2009) [#10]
You can get rid of the problems of multiple coords systems simply by applying a multiplier to the coords you're using.

For example, if you designed things for an 800*600 resolution, and the user opts for 1024x768, every coordinate can be multiplied by 1.28. Applying this is trivial.

The problems of tile gaps and so forth are harder to overcome, of course.


MGE(Posted 2009) [#11]
Never could get a tile engine working with a proj matrix without seeing little artifacting lines around the tiles. (Regardless of spaces between tiles. Tiles had alpha values, perhaps it wouldn't be as bad for tiles without alpha shadows, etc.) For tile games looks like you're going to have to work in a resolution where there is no scaling on your tiles and simply render a border or leave it black, or increase/decrease your game area.


ImaginaryHuman(Posted 2009) [#12]
You probably need some form of antialiasing around the edges of tiles to get them to appear to be seamless.


dmaz(Posted 2009) [#13]
Never could get a tile engine working with a proj matrix without seeing little artifacting lines around the tiles

create a mesh tile engine an you shouldn't have any issues at all like that...


Grey Alien(Posted 2009) [#14]
Yes please, someone make a mesh tile engine and I'll buy it!


ImaginaryHuman(Posted 2009) [#15]
I'm still not exactly sure if this `mesh tile engine` is just a fictional wish or something that actually technically can work. ie what does it entail? is it a matter of copying lots of tiles onto a large texture then drawing the large texture? Or specifying triangle strips with varying texture coordinates? Does the triangle strip's edges join together properly when defined that way? Does one have to use like glDrawArray() or glDrawElements() in order to make it seamless? or is it really a matter of using proper antialiasing?


_Skully(Posted 2009) [#16]
Sure it could work!

You'd have a mesh (or several that can hide when out of view) with enough surfaces to account for each texture, then create quads for each tile and assign the appropriate texture and UV coords to display the desired tile (part of the texture) at the desired location. Some tweaking and voila... not that unlike the terrain system I did in Blitz3D really...


Czar Flavius(Posted 2009) [#17]
4. Just have the tiles the same size, so at a higher resolution you get to see more of the map.


_Skully(Posted 2009) [#18]
oh... the biggest issue of this method is that once you enter the 3D realm, it actually becomes easier to just make the game in 3D. Why? Almost all the really hard stuff is already done for you with just about any 3D engine now.

If your going to render the tiles with 3D then you are going to ask yourself, how am I going to do collisions, then you realize that if the floor was a 3D surface, you could just have a 3D model move on top of it using the built-in system... continues from there.


_Skully(Posted 2009) [#19]
I'm going to play with this with minib3d I think


ImaginaryHuman(Posted 2009) [#20]
Just drawing separate quads is not going to make it render with perfect edges. Max2D is already doing that.

Also Max2D is using a projection matrix, it's just that its an orthographic projection with a mapping of 1 unit to 1 pixel.

Something that comes to mind is that when you draw to floating point coordinates (ie subpixel), there are only so many bits to represent the float accuracy, and this could translate to positioning a graphic `not quite` at exactly a given coordinate, causing it to slightly overlap another pixel when it shouldn't?

Either way, the way I see it I think you would have to do polygon antialiasing so that the edges of each quad are properly antialiased against the background, and then when you draw a second quad right next to it it should blend the two edges together near-enough exactly. Wouldn't that solve it?

I'm just not sure what you're going to do in terms of this `mesh` idea ... like I said, is it just a matter of drawing a triangle strip so that the gpu has all the coordinates of all adjacent tiles at once and can thus calculate the colors properly? I want to know what is technically the issue and what exact piece of technology is required to overcome it.


_Skully(Posted 2009) [#21]
The way I'm looking at it is that TileMax is currently drawing the tiles (images) with quads (Max2D), like you say... so it would only make sense that if each tile can be seamless now, it should also work for permanent quads that are assigned appropriate texture coords. Of that I'm pretty sure, but what makes me wonder is what happens when we move the camera back or forward a bit (zoom zoom)


Grey Alien(Posted 2009) [#22]
@ImaginaryHuman: Yes I was seeing it as drawing the tiles as triangles next to each other so that the GPU knows they join and can anti-alias properly because the problem we all get at the moment is caused by the edge anti-aliasing to the background (which may be black for example if you just CLSed it) instead of to each other.


_Skully(Posted 2009) [#23]
Ya, one mesh, multiple surfaces


ImaginaryHuman(Posted 2009) [#24]
One thing to bear in mind is that if your texture is not big enough to cover the whole screen or mesh, and you need to do a texture swap, you have to also stop the mesh data. Between texture swaps you have to `end` the previous mesh and start a new one because you're not allowed to change textures while you're defining vertex data. And given that some hardware doesn't support very big textures that could be a problem.

I'd like to know where anyone here has found information that says your triangles will be properly antialiased when defined as a triangle strip/mesh? I've still to find out if this is just wishful thinking or something that actually can happen.

Grey - the way you describe it, it's not what I was expecting. If I understand you right, you're saying that you draw one textured quad (image of a square or whatever) on a black background at a sub-pixel position, it somehow blends the edge pixels of the quad with the black pixels? I thought it would only do that if polygon smoothing is in use, not just bilinear filtering? because the filtering only applies to pixels `within` the quad?

Like, test this:
SetGraphicsDriver GLMax2DDriver()
Graphics 640,480,0

Local a:TImage=CreateImage(64,64,1,FILTEREDIMAGE|DYNAMICIMAGE)
For Local xx:Int=0 Until 64
For Local yy:Int=0 Until 64
SetColor Rand(0,255),Rand(0,255),Rand(0,255)
Plot xx,yy
Next
Next
GrabImage a,0,0,0
SetScale 4,4
Cls

Local x:Double=0
Repeat
	Cls
	DrawImage a,x,100
	DrawImage a,x+64*4,100
	x:+0.01:Double
	Flip 1
Until KeyHit(KEY_ESCAPE)

For me, the pixels within the 2 quads are smoothly filtered and very gradually and smoothly scroll from left to right at sub-pixel precision. However, the pixels at the edge of the quads are locked to integer boundaries. To my understanding this is because normal quads without antialiasing options in place do not blend their edges with the background pixels. When the scroll position gets to the next whole pixel, the edge jumps one pixel. Also for me, as the texture scrolls smoothly to the right, the pixels on the left edge are streaked/duplicated by up to 1 pixel.

Is this what you are talking about? There is no blending here with the black in the background. Or is there some other issue I'm not getting?


Grey Alien(Posted 2009) [#25]
@ImaginaryHuman: Good, point. Yes, I should have said that it blends the edge of the quad with NOTHING, so not the background, and certainly not the adjacent tile that it has no knowledge of. UNLESS of course you add the one pixel blank border that I'm so fond of adding to sprites and which I discovered makes tile movement and scaling look smoother (you can't see the integer pixel jumps as clearly because the anti-aliasing blends with the background, which may be black, or a previously drawn tile of course), but it still looks a bit horrid. That probably sounds a bit confusing so checkout the code and tests below.

Check out this code, it does a bunch of things but mainly demonstrates the difference between having an image with and without a 1 pixel alpha border.

Testing tips:

TEST A (no 1-pixel border):

1) Make two 32x32 32-bit png images called testimage1 and testimage2. Make them totally different colours.
2) Run the code as is. Watch it move slowly with the edges and middle joins jerking every so often.
3) Try uncommending the ZoomSpeed line if you want to see the projection matrix zoom it in (same jerky effect).

TEST B (1-pixel border):
1) Get the two images from TEST A and rename them testimage1b and testimage2b. Add a 1 pixel blank alpha border to them using canvas size in any decent paint package.
2) Change the pixel_border from 0 to 1
3) Change the LoadImage lines to testimage1b and testimage2b
4) Run the code as is. Watch it move slowly with the edges and middle joins looking a lot smoother. Check out the anti-aliasing on the edges as they are blended with the black background. Possible also test with scale=1 instead of 4 to see what difference that makes.
5) Try uncommenting the ZoomSpeed line if you want to see the projection matrix zoom it in (again the joins and edges are smoother).

However the joins are a bit unsightly and lose color intensity (they appear as darker lines) so it's not a viable solution either. What I was hoping the mesh would do is allow all joined quads to be anti-aliased to each other as if they are 1 giant texture instead of being anti-aliased to the background. Someone led me to believe that was possible ages ago (3D games do it without bad joins right?) but I've never seen it in BMax yet and would LOVE to get this worked out for super smooth tile based games.




I'm at work which is why I have not posted the images or downloadable because I don't have access to my Grey Alien FTP.


ImaginaryHuman(Posted 2009) [#26]
Thanks for the test code, it provides useful insight into what the problem is. Btw you need to put the SetGraphicsDriver and Graphics commands before the creation of the projection matrix.

I can only talk on behalf of OpenGL as I have no interest in DirectX.

When drawing a quad it can only affect whole pixels. It seems that the pixels which the quad covers are anchored at their top left corner. I think that any pixel in the quad which covers more than 0% of a pixel, draws to that pixel. What that suggests to me is that a) for a 32x32 image there are 33x33 pixels affected whenever the quad is not at an exact whole integer coordinate, and b) when a pixel on the left side is no longer covered at all, that whole pixel stops being affected at the same time as a whole new pixel being affected on the opposite side. I guess we could test that more closely. It might also be true that occasionally the area affected wavers between 32x32 and 33x33.

Regardless of what pixels are drawn, the rendering is always drawing whole pixels. With no antialiasing technique in use, it seems to streak/copy the pixels from 1 pixel inside of the shape to the edge pixels, so it can look like there are up to (but not =) almost 2 pixels of the same color at the edge. The bilinear texture filtering does seem to work exactly right, with sub-pixel accuracy, regardless of what's happening to the edges of the rendered shape. So it's kind of like the hardware is generating a window 33x33 pixels, into which to draw the 32x32 shape. I haven't tested if this is exactly true but it looks like it.

In the second test, with the added empty border around the shape (ie 34x34), I did see an `improvement` in the result of the bilinear filtering at the new internalized edges. However, like you said, it's not great, as if the edge pixels of the whole `area being affected` still comply to whole pixel rendering. That means that two adjacent tiles still fight for control of the adjoining pixels, and pixel colors still `pop` as the coordinates reveal another whole-pixel column or row. Also like you say, it seems that the quad is antialiasing against the black background and not the adjacent tile. I think that is because it's drawing to whole pixels in a 35x35 block and doesn't consider the 35th pixels to be a part of the adjacent shape, ie overwriting them.

I found a partial solution, however.

In OpenGL there is a thing called polygon smoothing, whereby (in conjunction with some kind of alphablend drawing mode, or lightblend etc), it attempts to blend the edge pixels of the drawn area with those of the existing background, based on pixel coverage instead of based on whole pixels. If this is used with 32x32 no-border tiles, however, it doesn't solve the problem. I expected that it would, but in your test at least it doesn't help. It's as if there is still a 33x33 area of influence, and the 33rd pixels are being given a fractional coverage. I don't know if its your test or just normal, but without a 1-pixel border on the image it still has the whole-pixel jumping effect.

That said, therefore, if you combine polygon smoothing with a 1-pixel empty border around the image, it DOES correctly smooth the edge of the tile. You simply add the following lines somewhere just prior to your rendering, after the graphics command:

SetBlend ALPHABLEND
glHint (GL_POLYGON_SMOOTH,GL_NICEST)
glEnable (GL_POLYGON_SMOOTH)


This entirely solves the occasional whole-pixel edge jumping. It also entirely solves the ugly `shifting` which previously was seen inbetween the tiles, where they join. Although there is still a black strip separating the tiles, it is now properly antialiased. Before, it seemed to sort of shift as it tried to move between subpixel coordinates, but with the polygon antialiasing switched on it is steady. It also works with the `zoom` in your test, which really is just a way of triggering the whole-pixel edge problem under different conditions (ie that isn't really a separate problem).

A few issues remain, however. First of all, in order to draw with polygon smoothing (or to draw antialiased lines on top of the edges of tiles), you must be drawing with blending enabled, so that means something like alphablend, lightblend, saturate etc. This means the pixels in the tile are going to be combined with the background as well. It works fine if the background beneath the tile is all black. It also is okay if you have an alphablended tile, but you can't lightblend with it on anything but a black background. If you want tile layers with tiles on top of tiles it's going to be a problem with anything other than alphablend.

Another issue is that the individual quads are still for the most part antialiasing against the background color. So you draw the first quad, it antialiases against black, producing partly-black and partly-color pixels at the edges, depending on coverage. Then you draw tile B next to it and it now blends into the semi-black-semi-colored pixel left by the previous tile. This creates a muddy gray/blackish result. The reason is twofold.. a) storing the result of antialiasing tile A in the background pixel prior to calculating the other tile's contribution or the `overall shared coverage` creates a temporary irreversible error which is compounded by adding more tiles, and b) the second tile has no way to tell how much of the pixel was covered by the first tile because there is no sub-pixel resolution. All of that information has been lost by the writing of the pixel by tile A. Tile B has no option but to blend into the result of Tile A's blending, which is wrong. Tile B doesn't know about tile A's pixels, like you said Grey.

I seem to recall in the OpenGL red book is said somewhere about needing to use the border pixels in a texture, to fill them with pixels copied from the adjacent tile, so that when it comes to antialiasing/filtering it will calculate the correct colors, ie the current tile knows about the pixels at its adjacent edges. Textures in GL actually have a `border` which is normally not used, which is 1 pixel wide, and can be filled with pixel data, if the border settings are set right. Usually it's set to `clamp` which I think is what causes the streaking of the edge pixels - ie it tries to get pixel color from pixels which are outside the normal texture area, looks at the border, sees that it is switched off and clamps the texture coordinate to the maximum of the interior of the texture, thus repeating the edge pixel. You could switch the border on and copy pixels from the adjacent tiles into that tile's border, and then when you draw it with polygon smoothing it should blend perfectly. This may account for why there needs to be a 1-pixel interior border (your solution, Grey) to force the blend, because we're not using the proper texture border feature. It also may explain why there is still an additional whole pixel affected by the antialiased tile when there is no canvas-added border.

With this in mind you may actually be able to ignore the OpenGL texture borders and just implement your own 1-pixel canvas border, but instead of making that border empty, make it contain pixels from the adjacent tiles. With THAT in mind, it occurs to me that *probably*, when you define a `mesh` of triangle strips/indexed vertices, with a single texture covering the mesh (if possible), it *would* have the correct adjacent pixels for neighboring triangles and should filter correctly at the interior edges. I think.

Tile borders has a drawback though... it means you have to copy tile pixels from one texture to another which is not straightforward. It also means therefore that each tile can't be just a generic tile which you can re-use in lots of combinations - it can only be reused in combination with those tiles that it has been set up to be adjacent to, which may result in lots of additional required memory consumption for all the different tile combinations you need (almost like not using tiles at all).

This leaves us with the question of what effect a triangle strip will have on rendering, using a single large texture across it, and what effect if any an indexed vertex array would have on the result. That's something we need to test beyond your basic test. As mentioned earlier/in another thread, though, this too may have drawbacks - you are limited by texture size supported by the hardware, and you cannot change textures while still defining a mesh, so you still end up with mesh edges, which need to be antialiased somehow.

Ideally we'd have 256 buffers in a 16x16 arrangement, draw everything 16 times larger, then combine the results of all those buffers using floating point math into a single pixel value. That would effectively antialiase, but requires way huge amounts of memory and gpu time. So it's a no-go.

I'll play with this more tomorrow.


Chroma(Posted 2009) [#27]
Nice work IH. So this means we can now get letterbox working and the graphics don't look horribly blurred? Can we get this put into the archives?


ImaginaryHuman(Posted 2009) [#28]
If you use letterboxing, I presume what you mean is taking your 4:3 content and compressing it horizontally so that it looks right on a 16:9/10 display? Or do you mean using a 16:9/10 display to show a 4:3 play area with black bars at the side, which is what letterboxing is?

My thoughts are that if you do ANY stretching or shrinking of graphics it is going to add some degree of blurriness. That's especially true if you stretch your graphics, since you are probably then exposing the lack of resolution in the textures (unless you use high-res textures much larger than they need to be, with mipmapping or anisotropic filtering).

Ideally you don't want to have to scale the graphics with the projection matrix at all. It's fine for 3D where you know that you'll be closer to and further away from textures and can't really do anything about the need for `infinite texture resolution`, ie just go high-res-ish and hope it's good enough close-up. But in 2D you don't really need to worry about that, you just need to get a 1:1 pixel mapping. It would be much better if you could render your graphics to the exact resolution and aspect ratio of the display on which it is being viewed, requiring no use of the projection matrix for anything other than an orthographic projection 1:1. Achieving that, though, pretty much means either real-time/in-game computer-generated graphics, or lots of pre-rendered graphics sets per resolution/aspect. I suppose one way to do it is to make 1 graphics set for 4:3 screens and one for 16:9/10? Do it in a high resolution and scale it down realtime?

In terms of the whole texture border thing, in order to get two tiles to properly antialiase against each other, you have to either a) have both tiles exist in a larger texture next to each other so that the bilinear filter knows what pixel values to interpolate, or b) if the two tiles are on different textures/geometry then you have to copy a strip of pixels from the adjacent edge(s) into each of the tiles so that again the biliear filter knows what to antialiase against. In both cases you're going to need somewhere a border of 1 unused pixel around the outside of the tiles (but not where they join, if they do), OR switch on texture borders in GL and put the duplicated pixels into the actual border.

So.... to review, to make this whole thing work for a tilemap engine, and without getting into vertex arrays or meshes too much, I am thinking you'd want to have a) as large textures as possible onto which you draw tiles in their `finished positions` next to each other, like a snapshot of an area of the drawn map, b) these map sections could be composed from other textures on which you store a `tileset` randomly positioned, so that the larger textures used as a snapshot are kind of like a cache, refreshed from individual tile resources, c) you draw large quads containing the snapshot textures (comprising multiple adjacent tiles) to the backbuffer and if you need more than one to implement scrolling (might need 4, or a clever scroll technique) then you have to deal with texture edges, d) if you do have texture edges then you must copy strips of pixels from the adjoining edges of these textures to each other so that they'll filter properly, e) you'll have polygon smoothing switched on as you draw these snapshots, f) you'll have a 1 pixel border (or use gl borders) around each snapshot texture, g) you draw the snapshot textures with alphablending - you can still have alpha values in your tiles (which must be copied into the snapshot textures) and it should still work alongside the antialiasing, h) you won't need to draw a `mesh` as such because to draw each snapshot you only need one large quad onto which you texture map an image of multiple pre-positioned tiles, i) the `mesh` concept moves into the idea of sub-textures or rectangles within textures, representing assembled tiles, j) if your hardware doesn't support very large textures then you must make sure that either your individual tiles are smaller than the minimum supported texture size so that the snapshots work easily, or you have to think about drawing tiles across snapshot-texture boundaries and you might end up having to draw lots of snapshot textures to fill the screen, and j) when you draw the snapshot textures to the screen you can easily switch textures and start new geometry/quad for each texture, and k) if you want animated tiles then you'll have to re-copy their next animation frame into the appropriate snapshot texture(s) before the final screen update, and k) you could optimize the handling of snapshot textures by using frame buffer objects (a GL extension) enabling direct draw to a texture. Alternatively if you want to just draw lots of individual tiles directly to the backbuffer then every tile has to have the adjacent tile's pixels copied into its border prior to drawing - which again would require some access to drawing to/copying to the texture (unless you do it to their pixmaps before turning them into images?)

I think that if you did all that you'd get a working tilemap engine with perfect seamlessness, perfect edge blending, alphablending, multiple layer support, animation support, and no ugly shaking/popping/streaking/juddering/dark edges etc.

The only downside really is the time taken to have to draw to the snapshot cache textures, although their eventual drawing to the backbuffer is going to be faster than drawing lots of individual small tiles, AND once you have assembled a bunch of tiles on a cache texture you won't have to re-draw those same tiles again in the next frame - you just render the cache texture again which may actually be faster than drawing individual tiles. ie once you've set up your cache textures the first time, there isn't very much overhead in subsequent frames and screen updates become faster. You then just draw to the cache when you want to animate, or at the edges of a scroll region, or when doing a sudden teleport to a whole new map section.

I don't see that you'd really need to create a mesh - a mesh is a piece of geometry containing multiple joined triangles/quads, with a texture wrapped on top of it spanning multiple triangles. To use that the texture is going to have to be like the cache textures anyway, and so long as your tilemap is flat and not warping all over the place you really only need one quad per cache texture. The extra vertices of the mesh aren't going to help or change anything. I don't see that there would be any benefit to splitting the geometry into lots of separate little one-quad-per-tile sections, unless you wanted to visually warp the tiles by changing the vertex coordinates at the corners of each tile in realtime - a possibility, but not a necessity. A mesh has to share texture coordinates at each vertex so it wouldn't let you pull from a bunch of random tile images and put them next to each other properly, anyway - a) there would be interpolation between the position of the adjacent texture and the new one, and b) you're not allowed more than one texture per mesh anyway. So you should be able to do all this without a mesh.

You just need a way to draw individual tiles into large cache textures (including alpha channels), handle the outside border copying (or just draw the adjacent tiles to the texture's edges and let it clip automatically), and then draw the cache textures to the screen with alphablending and polygon smoothing.


ImaginaryHuman(Posted 2009) [#29]
Some possibly useful/relevant information from the GL red book, this talks about texture borders and their use:

This talks about clamping of texture coordinates (which produces streaking of edges when there is no antialiasing active):

A border is defined when the texture is created with glTexImage2D(), with a value of 0 or 1 for the border width. But I think if you do a manual self-made 1-pixel border around the texture (with 0 alpha) it would work the same.


Grey Alien(Posted 2009) [#30]
@ImaginaryHuman: Great research, glad my code made the issues clear. So I was pretty impressed with those few lines you added, it really did make everything smoother, but of course retained the "grey" line along the joins, which is not desirable. The idea of filling in the edge pixels with the adjacent textures (x8 due to the 4x1 pixel corners, gah!) certainly sounds as if it would work perfectly, but it could be slow, plus I don't currently know how to do that. The other idea of pre-rendering all the tiles onto a big texture and then drawing that at sub-pixel coords again sounds viable, but possibly slow. Or your proposed method of using cached areas of joined tiles (uses lots of VRAM), if I understood it correctly.

This actually reminds me of a scrolling technique I used on the Amiga as follows:

- I defined an area of RAM that was twice as tall as the screen and I set the Copper to just view a portion of that RAM.
- Then when the screen scrolled down I drew the new data just below the bottom of the visible area AND repeated it just above the visible area.
- Then I shifted the view port down so the new data was visible.
- When the view port reached the bottom of the RAM area I just reset it to the top and because I was duplicating the new data above the view port, the top area of RAM had an exact duplicate of the bottom area of RAM, so the player didn't notice the changeover!

Hope that makes sense. Then I made a 4x screen size area of RAM and did it with multi-way scrolling. It was lush, and revolutionary to me at the time.

So I'm wondering if the same thing could be done with a large texture and then using the UV coords to just output the desired viewport. Of course this does run into the problem of having textures > 1024 which won't work on older graphics cards. It would save tons of time though because you wouldn't be having to prerender the entire screen, just a thin sliver as wide as the scrolling.


xlsior(Posted 2009) [#31]
Grey: Yup, did the same thing back in the Amiga days - the screen scrolling thing was really pretty nice... On the downside, re-drawing the entire screen itself each frame like you're used to doing these days was next to impossible. :-?

Another nice thing of the copper stuff is that you could run multiple resolutions at the same time, since the video chip could change frequencies mid-sweep... So you could start a screen at 320x256x32 (low-res, for your game graphics), then 2/3 of the way down change it to 640x480x16 (highres, for a line of status text or something), and after that switch it back to 320x256x32 for the remainder of the screen. The only penalty was that you got a 1 vertical pixel seperator line between the different resolutions, but that's a small price to pay to be able to add readable high-res text to an otherwise low-res game.


Grey Alien(Posted 2009) [#32]
@xlsior: Yeah because redrawing the whole screen was basically way too slow on the Amiga, that's why I came up with that scrolling technique. Did it in assembly too so it was pretty quick. The copper was pretty cool at manipulating the display for sure.


TartanTangerine (was Indiepath)(Posted 2009) [#33]
You guys need to read some of the stuff on the blitz3d code-archives - this stuff has been done to death many times. You really do need to think of the tiled environment as a single mesh (single surface if you will) with shared vertices. Drawing tiles as quads that don't share vertices with neighbours is never going to give whan you need.

http://blitzbasic.com/codearcs/codearcs.php?code=1377


Grey Alien(Posted 2009) [#34]
Thanks Indiepath, so it is possible with a mesh with shared vertices then, cool. I figured that other programmers must have dealt with this before in other 3D tiled games. The question is how to achieve a similar thing in BMax...


ImaginaryHuman(Posted 2009) [#35]
Grey - yes, I am familiar with that technique. There is also an even more clever technique which requires a cache only as big as 1 screen plus a strip of 2 tiles along two sides. You don't actually need the 4 full screens, nor do you need to thus draw every tile twice. You can even implement it on a single texture if big enough, or join 4 textures together.

What I am doing at the moment is holding a large bitmap in main memory and spooling little parts of it to video ram, by copying them to the cache texture, and then redrawing the cache texture each frame as the old-school tile-engine system just described. You can get HUGE memory savings. e.g. a 4096x4096 play area would consume over 64 megs of video ram, but only needs a few megabytes with the cache & spool system, and still only needs a few megabytes with even bigger worlds - but does need a lot more main memory. I think you might need to add an extra strip of tiles along two sides in order to produce properly filtering as in the previously mentioned tile-border techniques.

When it comes to rendering the screen, it's pretty efficient since you are drawing much less geometry (ie larger quad(s) rather than lots of small items with overhead). The only extra bandwidth is needed to spool small tile areas from main memory to the cache texture as it scrolls - which can also be implemented in a very efficient manner.

I will have a read of that other thread about meshes. [edit] Hmm, well it's a chunk of code in another language so not much use here. It shows cryptically some effort to build some geometry but without being able to run it we can't really see its results. I thought maybe you were directing us as a discussion of the topic, Indiepath? Or some BlitzMax code. I still say meshes are not needed and if they are we still need to test them properly in blitz code.


Grey Alien(Posted 2009) [#36]
@ImaginaryHuman: That other scrolling technique sounds intriguing.

So you have got this texture spooling system working then? Sounds cool. Does it yield beautiful seamless scrolling tiles now then? Or is everything still "in theory"? :-)


ImaginaryHuman(Posted 2009) [#37]
Yes, almost done and it's mostly working :-)

In the first Blitzmax framework competition my submission was an example of a very large scrolling destructible procedurally-generated landscape - I think it was like 8192x8192 pixels, with a second destructible landscape in the background as a parallax layer at like 2048x2048, also a 2x1 screen parallax layer and a 1 screen wraparound sky. It didn't use more than about 12mb of video ram - but would've required about 300 megabytes if it was all stored in video ram at once.

I implemented the `just larger than 1 screen` scroll system, including the cache texture system. Actually what I implemented was something I sort of invented that I call a SuperTexture, which is a way to say `you can have whatever size texture you like`. It virtualizes a grid of textures and lets you upload pieces of textures as a kind of dirty-rects system, spooled from main memory.

In main memory I have a SuperPixmap which is a similar idea - have whatever size bitmap you want. It could really be one large pixmap but lots of smaller ones is better for fragmented memory conditions. I spool `sub pixmaps` from main memory to `sub textures` in video ram. Then I draw a group of textures in a grid, enough to cover the size of the screen. The other benefit of the SuperTexture is it doesn't matter what the maximum supported texture size is. You could have one large texture if it's supported, or lots of small ones.

Within the SuperTexture I implemented a `just larger than 1 screen` `tilemap` system. There are a couple of ways to upload tiles. You could upload a whole row and a whole column each frame (sloppy but it works) or you can spread the uploads out smoothly across frames, dynamically dependent on the scroll speed. ie the faster you scroll the more tiles must be uploaded per frame. When scrolling slowly or not at all there is almost no upload needed. It pretty much works but I didn't have the foresight to see a flaw in the design that causes tiles to be occasionally skips, leaving holes that don't get updated. So the competition demo has that flaw and is broken but I did a quick-hack workaround that's inefficient. I'm working on a `version 2` now using an improved algorithm which totally resolves this problem.

The only part I haven't done yet is to copy/doubly-render adjacent tiles that are along the edges of individual textures, into each other's borders. I also haven't implemented the 1-pixel gap around the edge. Both of these are fairly straightforward to do and are only needed to fix the tile gap/edge ugliness problem. As it stands though the system is very efficient. With my system, updating the destructible landscape entails eventually writing image data to the SuperPixmap in main memory - which means using the CPU, which is pretty simple also - slightly more complicated when images need to be split across pixmap edges. Dynamic objects are easily handled like a regular realtime particle system with the images stored in video ram and drawn on top of the backbuffer after the cache-textures are drawn. Moving objects don't need to be considered a part of the destructible landscape - except for land objects which are kind of a combination of both dynamic and cpu-updated.

So it's working, I'm just missing the border edge fix and have to clean up my more efficient tile spooler. It's very efficient and even in its broken state it easily runs 1024x768 at 60hz with 4 fullscreen layers on a low-medium gpu.


Grey Alien(Posted 2009) [#38]
That sounds awesome! (link?) I missed the whole compo thing as I've had my head buried in my current project. I hope you get it fully working with the border pixel tweaks.

I also thought of the idea to spread out the scrolling drawing over several frames depending on the scrolling speed back in the Amiga days, but it turned out my assembly scrolling system was so fast it didn't need it (you remember how you could display how long certain tasks look (primitive profiling) by getting the copper to simply change the background colour when a task was done, and thus you could see how much CPU time it was taking each frame). The reason it was fast was that I was not rendering whole tiles, just the portion that the screen had scrolled which could be a row 1 pixel high or as much as 16 pixels high for very fast scrolling. Made a game using the system which had green arrows to speed you up and red arrows to slow you down and scenery you had to navigate (this was years before Wipeout on PS2). I tried remaking it with my framework and got as far as my SpeedRun demo (http://www.greyaliengames.com/framework.php link is about 1/4 way down) but noticed the tile issue (tested with both integer and float scrolling and border/no border tiles) and stopped before I got to the scenery code. Think it would be fun to continue it one day if I can resolve the tiling issue, I'll see how you get on first though ;-)


ImaginaryHuman(Posted 2009) [#39]
Ok, so I'm sure you're curious to know how a 1-screen tilemap technique could work. ;-)

First of all, I'm going to have to make reference to the Amiga, here, because it was with the Amiga that these techniques became both necessary and possible. It was probably done on other platforms in a similar or modified way.

Second of all, let's say that in this modern era of temporary backbuffers and powerful GPU's, it is commonplace to just draw as many tiles as you need to fill the screen, each frame. You basically draw a grid of tiles based on the virtual scroll position. You pull the tiles from a tile-pool and draw the appropriate tile at the appropriate coordinate in the backbuffer. You flip the screen and repeat. Not exactly efficient, but very simple and easy. The up-side of this approach is that varying the tile image is almost for free - so you can animate a screen full of tiles with almost no overhead.

The Amiga game `First Samurai` did this, to some extent, by relatively heavy use of animated backgroud tiles. I recall a lengthy discussion with the developers in Amiga Format magazine about how the tile engine works - a very inspirational read at the time. It sounded like they were basically refreshing the whole screen every frame - which was considered ahead of its time (in the early 90's?), and provided lots of animation potential, but was slower than typical high-speed tile engines. In fact it ran at 16fps or less on an Amiga 500 instead of 30 or 60. But when it came to fast, smooth scrolling with higher refresh rates, which was considered the mark of the most advanced action-platformers (like Zool/SuperFrog), a different system had to be used.

In order to achieve smoother scrolling, platform games had to preserve their buffer contents. Back in the day that was not an issue. The Amiga had something called `hardware scrolling`, which when used in combination with the `Copper` graphics co-processor could change characteristics about the display as it was being refreshed (ie on-the-fly while being read by the display DMA and prior to becoming a video signal). To hardware scroll all you had to do was adjust the pointer to the screen's pixels and it would start displaying pixels from that address.

You could increment/offset the address to easily scroll the whole screen without having to copy/paste any graphics whatsoever. The existing buffer was simply redisplayed at a different offset. It required practically zero cpu time to implement a scroll - it's just that memory addresses wrap around - when scrolling to the right new pixels would come in from higher memory addresses, which were previously pixels on the left of the screen on the row below. But that was a problem, and it was not possible at the time to just redraw the whole screen every frame, so you had to come up with a clever way to update the graphics which were coming into view, without the user noticing.

You basically needed to draw new graphics at the right of the displayed image as soon as possible after the vertical blank so that by the time the video beam refreshed the page the user would be seeing new graphics. I implemented this in BlitzBasic on the Amiga, actually, by using the CPU to blit a thin strip of upcoming image data from a wide main-memory bitmap, over the leading edge of the scroll region, so that when the display updated you would see `new` image data to the right. It worked, actually.

What it would require is to have a bitmap which is the width of the screen and then 1 extra row for every screen that you want to scroll to the right - since memory is continuous you need some space to scroll into. Worked fine for a horizontal scroll (only). But then scrolling vertically is a problem, you can't just keep scrolling down the bitmap indefinitely with limited memory available, and on the Amiga graphics ram was quite limited.

So then you need a more complicated tile system. That's where the duplicating 2x2-screen system came into play, as described by GreyAlien. All you needed was a bitmap a little larger than 2x2 full screens and to then update hidden tiles at the leading edge of a scrolling window. When the window got to the bottom or right of the bitmap its coordinates would wrap to the left/top, showing a previously updated exact copy of the map section. The user never noticed a change. Lots of scrolling tilemap games used this technique. It's reasonably efficient, it lets you keep the backbuffer content and only update the edge tiles, and you could even optimize it to only update as many tiles as are needed per frame based on the scroll speed.

But there is yet an even more efficient method. (ok ok, enough of the history lesson)... the 1-screen method. This method was initially made possible by the Amiga's hardware scrolling, and the ability to start displaying any region of memory at any time during the vertical refresh. Basically, you need a bitmap which is 1 screen wide and tall, plus the width and height of two tiles. The space of 1 extra tile is needed because at any time in the scrolling, the scroll position might be straddling 2 tiles, ie any scroll position which is a non-multiple of the tile width or height means you are looking at 1-screen + 1 tile. The space for the second tile is needed because, by virtue of the previous tile space being viewable, you need somewhere to draw new tiles which isn't currently being displayed. ie it has to be hidden otherwise the player will notice tiles being updated, especially when you update fewer tiles based on the scroll speed.

The way the system works is similar to the 2x2-screen method, initially. You start scrolling to the right, let's say, so you start drawing new tiles in the far right column - in the hidden zone. Before the scroll offset gets to = the width of one tile, it's easy to update the display. All you need to do is draw the full bitmap at a slight offset. New tiles will be busy being drawn in the hidden column and predrawn tiles will be coming into view in the 2nd-to-last column. It's all one solid piece of memory so requires only one `quad` to render the whole screen. The complicated part comes when you try to scroll further than the width of 1 tile.

What used to be the column of tiles on the left edge of the bitmap has now scrolled to the left, out of view, and the column which was on the right edge (containing newly refreshed tiles) is ready to start being viewed. And you want to scroll further to the right. You are now starting to draw new tiles to ... what used to be the left column, which is now `virtually` the far right column. This is okay, and the player won't see it. But the problem is in how to now update the screen. You have to *SPLIT* the update operation into two parts. Let's say you've scrolled 3 or 4 tiles to the right, and updated the tile columns accordingly (the column to be updated simply wraps around). You first draw from the visible left edge of the screen, as far right as you can go before you reach the real edge of the bitmap. That part is easy. Then you need to do a second render using a portion starting at the right edge of the `update column`, as far right as you can go before you get to the `left edge of the scroll position`.

So by splitting the scroll into two sections, you can let it wrap around and still update a full screen - the player doesn't realize they're looking at part of a bitmap from the right hand side set alongside a part from the left. You just draw then next to each other on-screen and they look seamless. The same thing happens vertically, too. So you now split your update into up to *4* sections. Depending on the scroll position, basically when you would've wanted to update tiles which are outside of the bitmap, you do a separate update starting over at the opposite edge.

I don't know if this is clearly enough explained, but that's basically how it works. You only need 1 screen plus a colum and row of 2 tiles width. On the Amiga this was accomplished without ever having to actually draw the whole screen each frame. You could create a `copperlist` which, at the horizontal split position, would reset the memory address to start drawing from the top of the bitmap. So you would be seeing the bottom of the bitmap at the top of the screen and vice versa. Horizontal scroll was managed the same as the sideways-only hardware scroll I mentioned above - you just give yourself some extra bitmap rows to accomodate scrolling to the right in memory and update the hardware address to start displaying in the middle of a row. Memory would wrap around by itself. The bitmap would have to be wider than the display in order to not see newly rendered tiles, and several rows taller based on the width of the environment.

Nowadays we don't have hardware scrolling as such, so we just draw portions of one or more textures to the backbuffer, and use the texture as our `preserved backbuffer`. ie a cache. It keeps the visible portion of the tilemap plus space for upcoming tiles. New tiles can be drawn straight to the cache texture(s) as needed, either by the cpu in main memory and spooled, or using a normal set of tiles in video ram (e.g. render to texture, or render to backbuffer and copy-to-texture - ie only copy the updated tile areas). You don't really need a main memory bitmap unless you want to preserve changes made to the overall environment.


MGE(Posted 2009) [#40]
@ImaginaryHuman - Interesting! Does your proposed system support:

Animation on a tile by tile basis? (flames, water, etc.)
Fx like alpha, scaling, rotation on a tile by tile basis?

What about the map itself, scaling, rotation, etc?


Grey Alien(Posted 2009) [#41]
@ImaginaryHuman: HA, neat 1 screen + a bit solution. I should have thought of that because I knew you could split the display with the copper. So you are only rendered 1 set of new tiles instead of 2 like in my solution although there will be more overhead on the Copper due to it having to change memory location once per line. However because the Copper was really a co-processor, it won't affect the main CPU! Thanks for sharing. Now to go back in time and use it ;-)


ImaginaryHuman(Posted 2009) [#42]
Actually you only had to change the address twice for the whole display, once to define the pointers to the top of the display, and once at the split. So that didn't take much time at all. I never actually implemented it with a copperlist, tho. Note that the copper did not affect the cpu except that the copperlist had to be fetched from `chip memory` by the graphics DMA which could temporarily shut out the cpu/blitter/disk because they shared the bus. So there was some speed hit, but not much.

Point is, the same basic technique can be used to make more efficient tilemap engines in the present day - ie like the `cache textures` concept mentioned previously. That's why I implemented the 1-screen system to support my multi-screen large landscape thing.

MGE - Yes. There is really no difference when it comes to what you can do. Instead of drawing tiles/animations directly to the backbuffer you draw them to the cache textures, and later draw the cache textures to the backbuffer. So you can have animated tiles, it just means that if an animated tile is at the edge of a cache texture you have to draw part of it again to the adjacent texture's border. The larger the textures are the better because it makes that a necessity much less often. You could technically just use a single large cache texture 2x2 times the size of the screen e.g. 2048x2048 and then you wouldn't have to worry about texture borders at all.

Of course you can animate the tiles any way you like, be it that they rotate or zoom or fade or whatever. At the moment I think the system will only work properly with alphablending. Lightblend might be a little odd looking or less accurate, we'll see. No reason why you can't do multiple layers and as much animation as you like. However, if you plan to do a tonne of animation you might be better off just drawing every tile every frame the easy way.

As to the map itself, I personally have opted to modify the modelview matrix to move the world rather than to move the projection matrix. There are reasons why in OpenGL they recommend you do that, because when it comes to things like lighting, they lights may not calculate properly due to not taking into account the projection matrix's `position`. So I'm moving the model in front of the camera, you can rotate in 2d/3d, zoom/scale etc which applies to the whole screen, or per layer, or whatever. My aspect-ratio adjustment zoom/rotate system is separate from the tile system. You'd just have to make sure you are showing a large enough window onto the world so that if you rotate it it doesn't leave gaps at the edges.


Grey Alien(Posted 2009) [#43]
I figured for vertical scrolling you'd only need to change the copper pointer once to account for the split, but for horizontal it would need to be on every line right? (same for 4 way) but maybe I'm not thinking things out properly...


ImaginaryHuman(Posted 2009) [#44]
I dunno, doesn't matter now that we're not on the Amiga. .. but I am thinking you'd change it once and then it would simply continue where it left off on the next line.