2011-06-10

Minecraft mapping - Zooming and panning

Last time we did some tiled rendering. However, it was pretty awkward to look at the results because we needed to edit the code and re-run it if we wanted to move around. Today we add zooming and panning controls.

Before we start looking at the details, I should point out that all the code is now available on github. That includes the previous two examples "opengl1.py" and "opengl2.py", as well as today's, "zoompan.py". If you look in there you can see everything I've written for this project, but be aware that most of it is very haphazard. I'm brushing off and tidying up the corresponding parts as I write each article. Feel free to ask questions about any of the code in there, but please don't beat me up too much for my coding style!

Most of the changes this time are to the main loop:

def main(): 
    video_flags = OPENGL|DOUBLEBUF
    pygame.init()
    screen_dimensions = 800, 600
    surface = pygame.display.set_mode(
        screen_dimensions, video_flags)
    resources = make_resources()
    frames = 0
    done = 0
    zoom = 1.0
    position = [256, 8192 - 256]
    dragging = False
    draglast = 0,0

    while not done:
        while 1:
            event = pygame.event.poll()
            if event.type == NOEVENT:
                break
            if event.type == KEYDOWN:
                pass
            if event.type == QUIT:
                done = 1
            if event.type == MOUSEMOTION:
                if dragging:
                    mx,my = event.pos
                    lx,ly = draglast
                    dx = (mx - lx)/zoom
                    dy = (my - ly)/zoom
                    position[0] -= dx
                    position[1] += dy
                    draglast = mx, my
            if event.type == MOUSEBUTTONDOWN:
                if event.button == 1:
                    draglast = event.pos
                    dragging = True
                if event.button == 4:
                    zoom *= 2.0
                    if zoom > 16:
                        zoom = 16.0
                if event.button == 5:
                    zoom /= 2.0
                    if zoom < 1.0/32.0:
                        zoom = 1.0/32.0
                    x,y = position
                    x = math.floor(x * zoom + 0.5) / zoom
                    y = math.floor(y * zoom + 0.5) / zoom
                    position = [x,y]
            if event.type == MOUSEBUTTONUP:
                if event.button == 1:
                    dragging = False
        render(resources, position, zoom, screen_dimensions)
        frames += 1

if __name__ == '__main__':
    main()

As you'll probably have noticed, all the code for rendering from a different position and zoom factor was already there. All we need to do is add handling for the mouse events that appropriately updates the camera position and zoom factor. Mouse buttons 4 and 5 are triggered by the scroll wheel. When it rolls up we double the zoom factor, and when it rolls down we halve it.

Mouse button 1 is the left (or primary) button. When it is pressed down we enter dragging mode and when it is released we leave dragging mode. Whenever the mouse moves in dragging mode, we track how far it moved and update the camera position, taking into account the zoom factor. Note that for mouse events the y coordinate increases down the screen, while our camera y increases up the screen.

One last thing to note is that when zooming out, we apply rounding to the camera position. This is so that every pixel matches up perfectly with a texel. When I didn't have this, sometimes if you zoomed in, panned around and then zoomed out you'd see odd pixels at the boundaries between tiles.

The other minor changes this time are to use mipmapping. This involves some changes to how we set up our texture atlas:

    glTexParameteri(
        GL_TEXTURE_2D,
        GL_TEXTURE_MIN_FILTER,
        GL_NEAREST_MIPMAP_NEAREST)
    glTexParameteri(
        GL_TEXTURE_2D,
        GL_GENERATE_MIPMAP,
        GL_TRUE);

As I understand it, GL_GENERATE_MIPMAP is deprecated, but the alternative – the glGenerateMipmap function – isn't in PyOpenGL's OpenGL.GL module. I didn't really look into it much. I assume that it's either from a newer version of OpenGL than PyOpenGL supports, or it's in some obscure submodule.

I'm using GL_NEAREST_MIPMAP_NEAREST as the minification function. This means that we don't do any interpolation - we just pick the closest mipmap level and in that mipmap level pick the closest texel. This is fine because our zoom levels are powers of 2 and we keep the texels and pixels lined up. It also avoids some ways for things to go wrong.

The shader also needs a change:

fragcolor = textureGrad(
    texture_atlas, atlas_point,
    vec2(1/512.0/zoom,0), vec2(0,1/512.0/zoom));

Instead of using the texture function, we use textureGrad. This lets us give the GPU a hand in choosing a mipmap level. Without it, mipmap levels are chosed by looking at how much the texture coordinates are changing between one screen pixel and the next. This works fine in the middle of our tiles, but fails at the edges, because our texture coordinates jump suddenly between one tile and another. You can see in the image on the right inappropriate mipmap levels on the borders of tiles.

The extra parameters for textureGrad allow us to specify the rate of change of the texture coordinates per pixel both horizontally and vertically. We can calculate them based on the zoom level.

You can get the current version of the code from github. Next week, we'll start looking at reading Minecraft maps. We'll be using an NBT parser for Python with some extra work to read the region files used by recent versions of Minecraft.

No comments:

Post a Comment