March 7, 2009

Zooming and performance

One problem I was continually having with SnowballZ was zooming. The further you zoomed out the slower it went (as, obviously, it had to draw everything). To the point where zoomed all the way in would give me 120fps and all the way out 30fps. Unacceptable.

I started working on making it fade into a paper-looking map when zooming out; So I could strip out the trees and terrain. But on the fly cartography that looks even half way decent is a lot of work. That's when Matthew suggested rendering the entire map to a texture. Bingo.

I created a function that set the viewport to cover the entire map and rendered just the terrain, trees and other decor to an off-screen buffer; Which I then saved to a texture. Now when the player zooms out to a certain point it switches to drawing that one texture which, compared to a 80,000+ vertex terrain and a couple hundred trees, is much faster.

The one drawback of this method is that it isn't perfectly seamless. But you wouldn't notice the difference unless you were looking for it. I rendered to a 1024x1024 texture so it is a little blurry when you use it too close up. This could be solved by rendering it to a larger texture but my concern comes when you have a map that is bigger than my 150x150 tiles (odd number, I know).

If there is any interest in seeing how I did it here it is:
(slightly modified for clarity out of context, my thanks to Richard Jones for showing how to do this on the pyglet mailing list)
# create our frame buffer
fbo
= GLuint()
glGenFramebuffersEXT
(1, ctypes.byref(fbo))
glBindFramebufferEXT
(GL_FRAMEBUFFER_EXT, fbo)

# allocate a texture and add to the frame buffer
tex
= image.Texture.create_for_size(GL_TEXTURE_2D, 1024, 1024, GL_RGB)
glBindTexture
(GL_TEXTURE_2D, tex.id)
glFramebufferTexture2DEXT
(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
GL_TEXTURE_2D
, tex.id, 0)
status
= glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT)
assert status == GL_FRAMEBUFFER_COMPLETE_EXT

# now render
glBindFramebufferEXT
(GL_FRAMEBUFFER_EXT, fbo)

# Snipped. This is where I render my map. See lib/map/display.py for full code.

# clean up
glDeleteFramebuffersEXT
(1, ctypes.byref(fbo))
self.offscreen_map = rabbyt.Sprite(tex, shape=(0,map_w,map_h,0),
tex_shape
=(0,1,1,0))
Using this method requires a certain OpenGL extension that older hardware might not have. You are also limited to a max texture resolution. Which could be an issue when it comes to large maps, even on newer hardware.

A method that avoids both of these problems is to split up your prerendering of the terrain into sections of 512x512 textures. That avoids the sharpness problem. As long as your section resolutions are smaller than your screen size you can render them to the backbuffer and copy that to a texture, which avoids GL extention problems.

This is how I'm doing it:

chunk_res = 2048
chunk_tex_res
= 512
num_chunks
= (map_w//chunk_res+1, map_h//chunk_res+1)

glClearColor
(0,0,0,0)
for x in range(num_chunks[0]):
for y in range(num_chunks[1]):
tex
= image.Texture.create_for_size(GL_TEXTURE_2D,
chunk_tex_res
, chunk_tex_res, GL_RGB)

l
= x*chunk_res
t
= y*chunk_res+chunk_res
r
= x*chunk_res+chunk_res
b
= y*chunk_res
glMatrixMode
(GL_PROJECTION)
glLoadIdentity
()
glViewport
(0,0,chunk_tex_res,chunk_tex_res)
glOrtho
(l, r, b, t, -1000, 1000)
glMatrixMode
(GL_MODELVIEW)
glLoadIdentity
()
glClear
(GL_COLOR_BUFFER_BIT)

# (Snipped) Render your map here.

glBindTexture
(GL_TEXTURE_2D, tex.id)
glTexParameteri
(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
glTexParameteri
(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
glCopyTexImage2D
(GL_TEXTURE_2D, 0, GL_RGB, 0, 0,
chunk_tex_res
, chunk_tex_res, 0)

self.offscreen_chunks[(x,y)] = rabbyt.Sprite(tex,
x
=x*chunk_res, y=y*chunk_res,
shape
=(0,chunk_res, chunk_res, 0))

glViewport
(0,0,self.scene.view.width,self.scene.view.height)

No comments:

Post a Comment