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.







4 comments:

  1. where do you get the opengl_tools module from? I've got pyopengl installed, but that module is missing.

    ReplyDelete
    Replies
    1. Sorry to leave your comment in moderation so long. There's a link at the bottom of the article to the opengl_tools.py file.

      Delete
  2. Sorry, but neither of the links at the bottom work anymore, for me at least. Are they hosted anywhere else?

    ReplyDelete
    Replies
    1. Ack, you're right. I forgot to renew my domain and lost control of it a few months ago, and I've not gotten around to sorting out a replacement yet. I've put those files on github here: https://github.com/weeble/clockworkcodex_ogl I'll update the article shortly.

      Sorry to leave your comment in moderation so long. I *think* I've turned on email notifications properly this time. Fingers-crossed.

      Delete