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:
In the next tutorial, I’ll go over winding order and how to draw large numbers of primitives.
Great tutorial! I’ve always wanted to get into 3D programming, and the complexity always sapped my motivation. It’s great to be able to draw something with just a few lines of code. Thanks!
On OSX Lion pyglet had an issue with quicktime ‘a no suitable image found.’ error.
This fixed it:
or more permanently
defaults write com.apple.versioner.python Prefer-32-Bit -bool yes
Someone in the thread mentions “I don’t like to write to my defaults”, if you’re using TextMate you can (Bundles -> Bundle Editor -> Edit Commands -> Python -> Run Script (Terminal) -> ++ (Clone this script) -> Give it a name “Run Script (Terminal) 32-bit Python” -> edit the line where it says “do script” and place your export VERSIONER… line in there followed by a ;
Love this! Can’t wait for part 2!
I want to try to follow the tutorial using Linux Mint.
Which shouldn’t be a problem, knowing that Python is platform independent.
but I get the following error : “failed to create drawable”, when I try to import “pyglet.gl”
Has anyone any idea what is causing the error or what it means?
i would appreciate about ten more of these tutorials. Very awesome and thanks!
Do you have GL libs installed? What’s your video card driver?
If you install python-pip with “sudo apt-get install python-pip”, then you can easily install many libraries that are used with python with a single command each.
Once you have pip installed, you can install pyglet with:
“sudo pip install pyglet”.
Sorry – just to clarify, once you have pyglet installed as in my comment above, the code in this tutorial should just work (the error is telling you that your computer doesn’t have pyglet installed).
Oh, one more thing – if it’s erroring on “import pyglet.gl”, but not on “import pyglet”, which is the line above, then you may have an earlier version of pyglet (I’m not an expert on that library, but just trying to think out the issue a bit). I have pyglet version 1.2.3 installed on my computer and the code listed here works (I’m running the Slax linux distro, which has a lot less packages available than Ubuntu/Debian based distros like Mint). If you have pyglet installed using apt-get or something like that, you may want to uninstall it and use python-pip instead to install, as above.
How often will you be posting?
Probably once every week or two.
Never thought that I would actually learn something at greendale 😀
Do you have any idea when the next part will be up?
Awesome post, looking forward to part 2.
Why are you teaching how to use the deprecated and arcane fixed-function pipeline? Why are you using immediate mode? Both are complete no-nos in modern graphics programming. Please, at least teach something useful.
I was actually kind of enamored with this post as well until I started into this online book: http://www.arcsynthesis.org/gltut/index.html
Seeing as the minute you get into doing _real_ 3D work, the fixed function pipeline pretty much goes out the window, I agree.