3D Programming in Python – Part 1

In this series of posts, I’m going to be covering the basics of 3D programming in Python. Let’s go over what tools we will be using (and not using), and why.

First, we’ll be using OpenGL. OpenGL (Open Graphics Library) is a cross-platform API for writing 2D and 3D applications. Essentially, it’s a set of functions you can call that will tell your GPU what to draw on the screen. Now, we want to do all of our programming for this tutorial in Python, so we’ll be using the pyglet library to call all of our OpenGL functions. There are other libraries available, such as PyOpenGL, but I personally prefer pyglet because of it’s documentation and extensive programming guide.

If you’ve looked at any amount of documentation or any other tutorials or books on programming with OpenGL, you may have come across libraries such as GLU and GLUT. We won’t be using any of these support libraries, that way you’ll have a better understanding of how OpenGL works at it’s core.

I should note that for these tutorials, you should already have pyglet on your machine and know how to run python scripts. An installation guide from the pyglet website is located here.

Let’s get started. In this tutorial, I’ll take you through the basics of drawing OpenGL primitives. But first, let’s create a pyglet window with an OpenGL context:

import pyglet

win = pyglet.window.Window()

@win.event
def on_draw():
        win.clear()

pyglet.app.run()

If all goes well, you should get a window that is completely black. So what’s going on here? Line 1 imports the pyglet package. Line 3 creates a pyglet window with an active OpenGL context.

Lines 5-7 are overriding the on_draw() function for the window you just created. This allows you to define what you want to happen every time the window needs to redraw itself, and this is where all of the actual OpenGL drawing functions will be called. Right now all it does is clear the window, so all you get is a black screen.

Finally, line 9 calls the function pyglet.app.run(). This starts the event loop (the thing that is always checking to see if the user has performed any actions, such as pressing a key or clicking the mouse). When you close the window that you created, the run method will return, and the application will be finished running.

Alright, so let’s draw some stuff. We’ll start off with a couple points.

import pyglet
from pyglet.gl import *

win = pyglet.window.Window()

@win.event
def on_draw():

        # Clear buffers
        glClear(GL_COLOR_BUFFER_BIT)

        # Draw some stuff
        glBegin(GL_POINTS)
        glVertex2i(50, 50)
        glVertex2i(75, 100)
        glVertex2i(100, 150)
        glEnd()

pyglet.app.run()

There are a few things that have changed from our first little bit of code. First, in line 2 we’ve added:

from pyglet.gl import *

Which allows us to access all sorts of OpenGL stuff without having to write pyglet.gl.something_we_want_to_use every time. In this example specifically, we only have to write GL_COLOR_BUFFER_BIT instead of pyglet.gl.GL_COLOR_BUFFER_BIT and GL_POINTS instead of pyglet.gl.GL_POINTS. It also allows us to call OpenGL functions. pyglet is set up so that all function names and constants are identical to their C counterparts, so any function that is defined in the OpenGL documentation can be called using that same syntax

We’ve also changed our on_draw() function to:

        # Clear buffers
        glClear(GL_COLOR_BUFFER_BIT)

        # Draw some stuff
        glBegin(GL_POINTS)
        glVertex2i(50, 50)
        glVertex2i(75, 100)
        glVertex2i(100, 150)
        glEnd()

There are two things going on here. First, we have this function on line 10 called glClear being called on the color buffer. This is telling the OpenGL context to clear all information from the color buffer. What is the color buffer? It’s the thing in memory that keeps track of what color is supposed to be displayed on each pixel of your screen. So clearing it gives us a blank palette so start drawing on. Now, when we move on to more advanced rendering, there will be more buffers we’ll have to clear, but for now, clearing the color buffer will be sufficient.

Next, we call a bunch of OpenGL functions that draw some points. First, the glBegin function tells OpenGL what kind of primitive to draw. In this case, we’ll be drawing points. The next 3 lines draw our points. Lets look at the syntax of this function really quick. glVertex2i means we’ll be defining a vertex using two integers. If we wanted to create a 3D vertex, we’d call glVertex3i and pass it three integers instead of two. If we wanted to create these vertices using floats instead of integers, we’d used glVertex2f or glVertex3f. The last function, glEnd, tells OpenGL that we’re finished drawing our points.

So, with that code you should get something that looks like this:

Pretty cool huh? Yeah, not really, but don’t worry it’s only uphill from here.

Let’s move on to lines:

import pyglet
from pyglet.gl import *

win = pyglet.window.Window()

@win.event
def on_draw():

        # Clear buffers
        glClear(GL_COLOR_BUFFER_BIT)

        # Draw some stuff
        glBegin(GL_LINES)
        glVertex2i(50, 50)
        glVertex2i(75, 100)
        glVertex2i(100, 150)
        glVertex2i(200, 200)
        glEnd()

pyglet.app.run()

You’ll notice not much has changed. Our glBegin function now passes GL_LINES instead of GL_POINTS to tell OpenGL we’re going to be drawing lines instead of points. We’ve also added another vertex. This is so that we’ll get two lines. When we tell OpenGL that we’re going to be drawing GL_LINES, OpenGL waits for you to define two vertices, and once you do, it will draw a line between them. So, in this example, we’ll get a line between the points (50, 50) and (75, 100), and we’ll also get a line between the points (100, 150) and (200, 200).

With our modified code, we get this:

Another way of drawing lines is by using GL_LINE_STRIP instead of GL_LINES. When you do this, OpenGL will wait for you to define your first two vertices and then draw a line between them. From there, a line will be drawn to any subsequently defined vertex from the last vertex that was defined. So, in this example, the first line would be drawn from (50, 50) to (75, 100), the second line from (75, 100) to (100, 150), and the third line from (100, 150) to (200, 200) and would look like this:

Lastly, if you want to make a closed loop, you can use GL_LINE_LOOP instead of GL_LINE_STRIP. This does the exact same thing as GL_LINE_STRIP, except it will draw a line from the last defined vertex to the first defined vertex, thus closing this loop. In our example, we get this:

Okay, so now that we’ve mastered drawing lines, let’s move on to triangles.

import pyglet
from pyglet.gl import *

win = pyglet.window.Window()

@win.event
def on_draw():

        # Clear buffers
        glClear(GL_COLOR_BUFFER_BIT)

        # Draw outlines only
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)

        # Draw some stuff
        glBegin(GL_TRIANGLES)
        glVertex2i(50, 50)
        glVertex2i(75, 100)
        glVertex2i(200, 200)
        glEnd()

pyglet.app.run()

Again, this code looks pretty much identical to what we’ve been using already. You will notice, however, that we’ve added a line that calls glPolygonMode. I won’t go too in depth with the first variable that we’re passing, GL_FRONT_AND_BACK, because I’ll probably make a whole separate post to describe winding order and front and back faces. However, the second variable we’re passing, GL_LINE, is pretty straightforward. It just tells OpenGL that everything we draw is going to be outlines. By default, this is set to GL_FILL, but we haven’t needed to change it up to this point because you can’t fill in a line.

Calling glBegin with our primitive GL_TRIANGLES tells OpenGL that every set of three vertices defines a triangle. With glPolygonMode set to GL_LINE, for every set of three vertices we define, we get the outline of a triangle drawn on the screen. This code gives us the following image:

Next, we’ll look at a triangle strip.

import pyglet
from pyglet.gl import *

win = pyglet.window.Window()

@win.event
def on_draw():

        # Clear buffers
        glClear(GL_COLOR_BUFFER_BIT)

        # Draw outlines only
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)

        # Draw some stuff
        glBegin(GL_TRIANGLE_STRIP)
        glVertex2i(50, 50)
        glVertex2i(75, 100)
        glVertex2i(200, 200)
        glVertex2i(50, 250)
        glEnd()

pyglet.app.run()

Triangle strips act in a way similar to line strips. When we call glBegin(GL_TRIANGLE_STRIP), OpenGL will draw a triangle using the first three vertices that are defined. From there, a triangle will be drawn for every subsequently defined vertex using the two previously defined vertices. So in this example, the first triangle is drawn using the vertices defined on lines 17, 18, and 19. The second triangle is then drawn using the vertices defined on lines 18, 19, and 20, and we get the following image:

One last way of drawing triangles is by using GL_TRIANGLE_FAN. This allows you to draw a bunch of triangles around a central point.

import pyglet
from pyglet.gl import *

win = pyglet.window.Window()

@win.event
def on_draw():

        # Clear buffers
        glClear(GL_COLOR_BUFFER_BIT)

        # Draw outlines only
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)

        # Draw some stuff
        glBegin(GL_TRIANGLE_FAN)
        glVertex2i(200, 200)
        glVertex2i(200, 300)
        glVertex2i(250, 250)
        glVertex2i(300, 200)
        glVertex2i(250, 150)
        glVertex2i(200, 100)
        glEnd()

pyglet.app.run()

The first vertex we define, (200, 200), defines the origin of our triangle fan. After the first triangle is define using the next two vertices, we then go down the list of vertices we’ve created, and define triangles using our current vertex, the previous vertex, and the origin vertex. So in this example, our first triangle is defined using the vertices on lines 17, 18, and 19. Our next triangle is defined using the vertices on lines 17, 19, and 20, the next one using the vertices on lines 17, 20, and 21, and the last one using the vertices on lines 17, 21, and 22. With this, we get the following image:

There are three other primitive types that I won’t be going over, mainly because they are relatively intuitive, and if you’ve been able to follow everything up to this point, you should be able to figure out how to use them pretty easily. Here is a recap list of all of the types we’ve gone over, as well as the three that we haven’t:

  • GL_POINTS
  • GL_LINES
  • GL_LINE_STRIP
  • GL_LINE_LOOP
  • GL_TRIANGLES
  • GL_TRIANGLE_STRIP
  • GL_TRIANGLE_FAN
  • GL_QUADS
  • GL_QUAD_STRIP
  • GL_POLYGON

In the next tutorial, I’ll go over winding order and how to draw large numbers of primitives.

Tagged , ,