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. There should be no borders between 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. (Well, recent at the time of writing.)











2011-06-07

Minecraft mapping - Tiled rendering

This is the fourth in a series of articles about my attempts to write a Minecraft mapping program using Python. Last week, we made a simple texture and rendered it with OpenGL. For the amount of work involved, it wasn't terribly impressive! Today we're going to change the fragment shader to do the tiled rendering that we described before.
The fragment shader needs to calculate what colour each pixel should be. As input, it takes the texture coordinates that were output by the vertex shader. We have a single great big square with texture coordinate (0,0) in the bottom left and texture coordinate (1,1) in the top right. We want to texture this with a 512×512 grid of square textures drawn from our 16×16 grid of textures in our texture atlas.
The fragment shader needs to do two things with the texture coordinate. One is to look up our 512×512 texture that is our Minecraft map region - it can do that directly with no extra calculation. The other is to take that texture coordinate, consider it as a point in a grid of 512×512 cells, and figure out where it falls within its cell. We call this position theta. A theta of (0,0) corresponds to the bottom left of the cell, and (1,1) to the top right of the cell.
Given the sample taken from our map texture, we extract the red value to get a code between 0 and 255 that indicates a cell in our texture atlas. We call the position of that cell phi. The bottom-left cell in the texture atlas has phi=(0,0), the top-right cell has phi=(15, 15).
Finally, combining phi and theta identifies the position to sample from the texture atlas for the fragment we're rendering. Here's the new shader:
fragment_shader = '''\
#version 130

const float TILE_COUNT = 512.0;
const float INV_TILE_COUNT = 1.0 / TILE_COUNT;

uniform sampler2D texture_atlas;
uniform usampler2D map_texture;

in vec2 texcoord;
out vec4 fragcolor;

void main()
{
    vec2 theta;

    
    theta = (mod(texcoord, INV_TILE_COUNT) * TILE_COUNT);
    uvec4 map_sample = texture(map_texture, texcoord);
    uint uphi = map_sample.x % 16u;
    uint vphi = 15u - (map_sample.x / 16u);
    vec2 phi = vec2(uphi, vphi);
    vec2 atlas_point = phi / 16.0 + theta / 16.0;

    fragcolor = texture2D(texture_atlas, atlas_point);
}
'''
The complete source file is here:opengl2.py There are a few other changes. I've also tried enabled mip-mapping on the texture atlas, while limiting it to four levels, because beyond that the mip-maps would start blending together unrelated tiles. Unfortunately, I think I've still not quite got it working right, but that won't be immediately obvious. I might talk about it more in a later article, but it's going to need some work.
Here's the end result. (Click it to view it full size.) As you'll note, for now we've used the diagnostic texture for both the map texture and the texture atlas. On Friday we'll add some controls to zoom and pan so that we can actually see the whole thing and next week we'll start reading from Minecraft files.




2011-06-03

Minecraft mapping – Rendering with PyOpenGL

Last time we made ourselves a simple texture using Pygame. Today we're going to render it as-is on the screen using PyOpenGL. Simple, right?
This will probably be a little steep if you don't know any OpenGL. However, fear not. Read An intro to modern OpenGL by Joe Groff. It's great, and it's how I learned to write GLSL shaders. Well, I say learned to write them – I've pretty much only written the one that you're going to see in these articles. The tutorial's in C, but I've translated the early parts into Python: Python OpenGL tutorial Lastly, don't worry if the 3D maths is scary – we won't be needing it since we're sticking solidly to 2D. (That said, I am quite scared of the maths in the ambient occlusion bit. But we'll worry about that when we come to it.)
In addition to Python and Pygame, you're going to need PyOpenGL and NumPy. I installed them on Ubuntu using:
sudo apt-get install python-opengl
sudo apt-get install python-numpy
(This should all work fine on Windows and Mac, but you'll need to download and install all these things separately.)
#!/usr/bin/env python

# Copyright 2011, Annette Wilson
# Licensed under the MIT license:
# http://www.opensource.org/licenses/MIT
#
# Minecraft mapping - Rendering something with OpenGL
#
# With great thanks to Joe Groff:
# http://duriansoftware.com/joe/An-intro-to-modern-OpenGL.-Chapter-1:-The-Graphics-Pipeline.html

from OpenGL.GL import *
import pygame, pygame.image, pygame.key
from pygame.locals import *
from opengl_tools import *

vertex_shader='''\
#version 130

uniform vec2 screen_dimensions;
uniform vec2 cam_position;
uniform float zoom;
uniform float texture_dimension;
uniform float map_dimension;

in vec4 position;
out vec2 texcoord;

void main()
{
    gl_Position.xy =
        (
            (position.xy / 2.0 + 0.5)
            * texture_dimension
            - cam_position
        )
        * 2.0
        * zoom
        / screen_dimensions;
    gl_Position.zw = vec2(0.0, 1.0);
    texcoord = position.xy * 0.5 + 0.5;
}
'''
Our square sits between (-1,-1,0) and (1,1,0) in world space. Screen space has (-1,-1) as the bottom-left of the screen and (1,1) as the top-right. If the texture is texture_dimension pixels across, and cam_position is a set of pixel coordinates in texture, this will position the square so that the given pixel is in the centre of the screen. We also use the original world space coordinates as the texture coordinates, but we multiply by 0.5 and add 0.5 because we want the texture coordinates to range from 0 to 1. So the bottom-left of our texture will have texture coordinates (0,0) and the top-right will have (1,1).
fragment_shader = '''\
#version 130

uniform sampler2D texture_atlas;
uniform usampler2D map_texture;

in vec2 texcoord;
out vec4 fragcolor;

void main()
{
    fragcolor = texture2D(texture_atlas, texcoord);
}
'''
The fragment shader is even more trivial. We just look up the texture atlas texture. Next time we will develop this to do something more interesting.
class Resources(object):
    pass

def make_resources():
    minecraft_map = pygame.Surface((512,512))
    atlas = pygame.image.load('numbered_texture_atlas.png')
    vertex_buffer_data = float_array(
        -1.0, -1.0, 0.0, 1.0,
         1.0, -1.0, 0.0, 1.0,
        -1.0,  1.0, 0.0, 1.0,
         1.0,  1.0, 0.0, 1.0)
    element_buffer_data = short_array(
        0,1,2,3)
    resources = Resources()
    resources.vertex_buffer = make_buffer(
        GL_ARRAY_BUFFER,
        vertex_buffer_data,
        vertex_buffer_data.nbytes)
    resources.element_buffer = make_buffer(
        GL_ELEMENT_ARRAY_BUFFER,
        element_buffer_data,
        element_buffer_data.nbytes)
    resources.map_texture = make_texture(
        image=minecraft_map, interpolate=False,
        alpha=True, integer=True)
    resources.texture_atlas = make_texture(
        image=atlas, interpolate=False, alpha=True)
    resources.program = assemble_shader_program(
        vertex_shader,
        fragment_shader,
        uniform_names=[
            'screen_dimensions',
            'cam_position',
            'zoom',
            'texture_dimension',
            'texture_atlas',
            'map_texture'],
        attribute_names=[
            'position'])
    return resources
This sets up all the resources we need:
  • map_texture is a placeholder for our map data. We'll make use of this next time.
  • texture_atlas is the texture atlas that we created last time.
  • vertex_buffer contains the four vertices of our square.
  • element_buffer is a list of indices into vertex_buffer_data, describing the order they should be connected up in a triangle strip.
  • program is our shader program, combining the vertex and fragment shaders.
This makes use of various helper functions we define in opengl_tools.py.
def render(resources, position, zoom, screen_dimensions):
    screen_w, screen_h = screen_dimensions
    glViewport(0,0,screen_w,screen_h)
    glClearColor(0.4, 0.4, 0.4, 1.0)
    glClear(GL_COLOR_BUFFER_BIT)

    glUseProgram(resources.program.program)
    uniforms = resources.program.uniforms
    glUniform2f(uniforms['screen_dimensions'], screen_w, screen_h)
    glUniform2f(uniforms['cam_position'], position[0], position[1])
    glUniform1f(uniforms['zoom'], zoom)
    glUniform1f(uniforms['texture_dimension'], 512.0) 

    glActiveTexture(GL_TEXTURE0)
    glBindTexture(GL_TEXTURE_2D, resources.map_texture)
    glUniform1i(resources.program.uniforms['map_texture'], 0)

    glActiveTexture(GL_TEXTURE1)
    glBindTexture(GL_TEXTURE_2D, resources.texture_atlas)
    glUniform1i(resources.program.uniforms['texture_atlas'], 1)

    glBindBuffer(GL_ARRAY_BUFFER, resources.vertex_buffer)
    glVertexAttribPointer(
        resources.program.attributes['position'],
        4, # size
        GL_FLOAT, # type
        GL_FALSE, # normalized?
        ctypes.sizeof(GLfloat)*4, # stride
        None # offset
        )
    position_attribute = resources.program.attributes['position']
    glEnableVertexAttribArray(position_attribute)
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, resources.element_buffer)
    glDrawElements(
        GL_TRIANGLE_STRIP,
        4,
        GL_UNSIGNED_SHORT,
        None)
    glDisableVertexAttribArray(position_attribute)
    pygame.display.flip()
Given the simplicity of the shaders, rendering is a somewhat gruesome affair. We need to provide a value for each of the uniforms that we defined in the shaders, bind textures to slots, connect up and enable a vertex array on each attribute and finally draw our triangle strip.
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.0, 256.0]
    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
        render(resources, position, zoom, screen_dimensions)
        frames += 1

if __name__ == '__main__':
    main()
And this is the output. Not thrilling, I'll admit, but I wanted to get as much of the tedious stuff out of the way to avoid confusing things too much when we do more with the shaders. You can download the two files from github. Next time: tiled map rendering.







2011-05-31

Minecraft mapping – Diagnostic texture

(Part of a series on rendering a top-down Minecraft map.)

As we will soon see, terrain.png isn't great for diagnosing problems if something goes wrong. For one thing, it has lots of gaps, so if we accidentally get the wrong textures there's a good chance we'll just end up rendering blank space and not being able to figure out how our calculation is wrong. Also, many of the textures in it don't have an obvious orientation, so if we flip them or rotate them we might not notice. To avoid these problems, it's useful to make ourselves a diagnostic texture without these problems.

Rather than spend hours of tedious work in a paint package, we can do this programmatically with Pygame. On Ubuntu, Python 2.6 was already installed and I installed Pygame with:
sudo apt-get install python-pygame
We're making a 512×512 texture that's a 16×16 grid of 32×32 pixel squares. Each square has a number between 0 and 255 printed in the middle and a solid grey background with intensity equal to the value printed on it. In Pygame, you render text to a new surface and then blit that surface onto your target. Also note that we need to initialize Pygame before we do any font rendering. Other than that, there's not a lot to comment on here.
import pygame

def make_numbered_texture_atlas():
    surface = pygame.Surface((512,512))
    font = pygame.font.SysFont("arial",14,bold=True)
    for i in xrange(256):
        y = 15 - (i // 16)
        x = i % 16
        fg = (255,255,255) if i<=127 else (0,0,0)
        bg = (i,i,i)
        textimage = font.render(str(i), True, fg)
        w,h = textimage.get_size()
        surface.fill(bg, (32*x, 32*y, 32, 32))
        surface.blit(textimage, (32*x+16-w//2, 32*y+16-h//2))
    return surface

def main():
    pygame.init()
    pygame.image.save(
        make_numbered_texture_atlas(),
        "numbered_texture_atlas.png")

if __name__ == "__main__":
    main()
Next time we'll use PyOpenGL to render something on-screen.

2011-05-27

Minecraft mapping – 2D rendering strategy

This is the first in a series of articles about writing a Minecraft mapping tool in Python. I'm aiming for somewhere between a tutorial and a general discussion of the things that I think are cool or interesting. Today I'll describe how we're going to render the map.
To understand what we're doing, you'll probably want to be familiar with Minecraft. To recap briefly, playing Minecraft involves exploring and building in a world of axis-aligned 1m side cubic blocks. Each cube might be filled with one of various materials, such as earth, air, stone, water or sand. The world extends indefinitely in both horizontal directions and has a vertical extent of 128m, with mostly impenetrable rock blocking access to the lower boundary and merely empty space into which blocks may not be placed above the upper boundary.
In subsequent posts we'll talk more about exactly how we're going to take this 3D world and flatten it into a 2D map. For now, we're going to suppose that we have a 2D grid of square cells and in each cell we have the Minecraft block ID that describes the content of that cell and we want to render the corresponding Minecraft block texture in it. (We will also ignore the fact that some Minecraft blocks are partially transparent for now.)
There are various ways we could do this:
  1. Load the Minecraft textures into a Pygame Surface, loop through the map of block IDs and for each one blit the corresponding texture onto the corresponding position on-screen. Pretty easy, possibly quite slow, and not enough fun.
  2. Load the Minecraft textures into an OpenGL texture as a "texture atlas", loop through the map of block IDs and create a pair of triangles for each with appropriate texture coordinates to apply the right texture from the atlas. This is a bit more tricky, but once we've created the vertices and indices that make up the triangles, it should be pretty fast, because we can render everything frame after frame without needing to send any large amount of data to the graphics card. However, it's still not very fun.
  3. Load the Minecraft textures into one OpenGL texture as above, but load the block ID map into another texture. Render one big quad and write a pixel shader that first looks up the block ID map and then uses the value it finds there to look up the block texture atlas. This is still quite tricky, but it also lets us do some other interesting stuff, and it's fun. Since I have very little experience writing shaders, I thought this would be a great idea to try.

To get a bit more concrete, we're going to feed in two textures. One is a texture atlas containing a 16x16 grid of textures – so 256×256 pixels if each texture is 16x16 - which will be very similar to the terrain.png file from Minecraft. You can see mine on the right. (As I mentioned before, I'm using the awesome Painterly Pack. All the cool textures are from that. Only a few special top-down views in the bottom-right were drawn by me.) The other texture that we will feed in will be a specially encoded 32-bit RGBA format texture where the values in the red channel (between 0 and 255) specify one of the textures in the texture atlas. (Later on we'll use the other channels for various other things.) We'll number the texture atlas textures from 0 in the top left through to 15 in the top right and in rows down to 255 in the bottom right. The dimensions of this texture will depend on what size of map we want to render in one go. A Minecraft region is 512×512 and this is a convenient enough size for us, so we'll go with that for now. The next article will be some preparatory groundwork – creating a useful diagnostic texture which we can use in place of terrain.png to properly understand what's going on and more easily spot when we've made a mistake. After that, we'll move on to doing OpenGL using PyOpenGL.




2011-05-24

Minecraft mapping with Python, Pygame and OpenGL


When playing Minecraft, I love doing stuff underground: mining, spelunking and digging great big tunnels. I really wanted to map out the mines I have dug and the caverns I have explored. There are a lot of great Minecraft mapping tools, but not many of them that seem oriented to underground mapping. I also thought it would be fun to write my own, and I have a few ideas for distinct directions to develop it in when it gets far enough. This series of articles will cover all the code that I've written and will span a range of topics including Pygame, OpenGL, Numpy and the structure of Minecraft maps.
You can see some teaser images of the sort of things I have working right now over on the right. The textures are from the Painterly Pack, which I thoroughly recommend as a Minecraft texture pack.
Here's the rough plan for the series:
  1. 2D rendering strategy
  2. Pygame – Making a diagnostic texture
  3. PyOpenGL – Rendering with a pixel shader
  4. PyOpenGL – Tiled rendering with a pixel shader
  5. Pygame – Zooming and panning
  6. NBT – Reading Minecraft files
  7. Numpy – Flattening a Minecraft map
  8. Ambient occlusion part 1 and part 2
  9. Rotating and flipping to render mine-cart tracks
  10. Lighting (Never finished this one.)
The tentative schedule is going to be Tuesdays and Fridays. This Friday we'll start with an overview of the basic rendering strategy for our maps.