Showing posts with label python. Show all posts
Showing posts with label python. Show all posts

Tuesday, March 1, 2011

PyOpenGL Geometry Shaders - Python and OpenGL Geometry Shader

Geometry Shaders using PyOpenGL - Implementation



Extending our shader from a previous post, Using GLSL in Python PyOpenGL we can simply add geometry shaders if your video card supports such a beast.



Geometry Shader Class in Python



The following is a shader class written in Python. Its based on our previous shader class except weve made some additions to support Geometry Shaders. We will start with the includes




from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GL.ARB.framebuffer_object import *
from OpenGL.GL.EXT.framebuffer_object import *
from OpenGL.GL.ARB.vertex_buffer_object import *
from OpenGL.GL.ARB.geometry_shader4 import *
from OpenGL.GL.EXT.geometry_shader4 import *


Not sure how many of these are actually used in this class, but they are there in case. The file contains other opengl related classes so they are there. Next I will show the class itself




class shader(node) :
def __init__(self, filename):
node.__init__(self)
self.filename = filename
self.load()

def load(self, debug=False):
fh = open(self.filename)
self.source = {'vertex': '', 'fragment':'', 'geometry':''}
write = None
for line in fh :
if line == '[[vertex-program]]\n' :
write = 'vertex'
elif line == '[[fragment-program]]\n' :
write = 'fragment'
elif line == '[[geometry-program]]\n' :
write = 'geometry'
else :
self.source[write] += line

self.draw = self.init
if debug :
print self.source['vertex']
print self.source['fragment']
print self.source['geometry']



def init(self):
##compile and link shader
self.vs = self.fs = self.gs = 0

self.vs = glCreateShader(GL_VERTEX_SHADER)
self.fs = glCreateShader(GL_FRAGMENT_SHADER)
self.gs = glCreateShader(GL_GEOMETRY_SHADER_EXT)

glShaderSource(self.vs, self.source['vertex'])
glShaderSource(self.fs, self.source['fragment'])
glShaderSource(self.gs, self.source['geometry'])

glCompileShader(self.vs)
log = glGetShaderInfoLog(self.vs)
if log: print 'Vertex Shader: ', log

glCompileShader(self.gs)
log = glGetShaderInfoLog(self.gs)
if log: print 'Geometry Shader: ', log

glCompileShader(self.fs)
log = glGetShaderInfoLog(self.fs)
if log: print 'Fragment Shader: ', log

self.prog = glCreateProgram()

glAttachShader(self.prog, self.vs)
glAttachShader(self.prog, self.fs)
glAttachShader(self.prog, self.gs)

glLinkProgram(self.prog)

glUseProgram(self.prog)

self.draw = self.use

def use(self):
glUseProgram(self.prog)
uniform_location = glGetUniformLocation(self.prog, "time")
glUniform1i(uniform_location, pygame.time.get_ticks())

def end(self):
glUseProgram(0)


Explaination of OpenGL Geometry Shader GL_GEOMETRY_SHADER_EXT



First difference is in the shader.load function. We have added code to load the actual source code for the shader. Secondly we create the shader with:



self.gs = glCreateShader(GL_GEOMETRY_SHADER_EXT)


GL_GEOMETRY_SHADER_EXT seems to be the only one defined for me. We contine normally now. The main additions are to the shader source code.



OpenGL Geometry Shader source code



Each section of the code is marked by delimters [[vertex-program]], [[fragment-program]], and [[geometry-program]]. Below is the source code




[[vertex-program]]
uniform int time;

void main(void) {
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

[[geometry-program]]
#version 150
#extension GL_EXT_geometry_shader4 : enable

layout(triangles) in;
layout(line_strip, max_vertices = 20) out;

void emit(vec4 vertex) {
gl_Position = vertex;
EmitVertex();
}

void main(void) {
vec4 avg = vec4(0., 0., 0., 0.);
int i;
for(i=0; i< gl_VerticesIn; i++){
avg += gl_PositionIn[i];
}
avg /= (gl_VerticesIn * 1.0);

vec4 mid1 = (gl_PositionIn[0] + gl_PositionIn[1]) / 2.0;
vec4 mid2 = (gl_PositionIn[1] + gl_PositionIn[2]) / 2.0;
vec4 mid3 = (gl_PositionIn[2] + gl_PositionIn[0]) / 2.0;

emit(gl_PositionIn[0]);
emit(mid1);
emit(mid3);
emit(mid2);
emit(mid1);
emit(gl_PositionIn[1]);
emit(mid2);
emit(gl_PositionIn[2]);
emit(mid3);
emit(gl_PositionIn[0]);

EndPrimitive();
}


[[fragment-program]]
uniform sampler2D texture_0;

void main (void) {
vec4 cvec = texture2D(texture_0, gl_TexCoord[0].xy);
gl_FragColor = cvec;
gl_FragColor.a = 1.0;
}


Caveats with OpenGL Geometry Program, it always uses triangles



Something that took me awhile to figure out is that the geometry shader always works in triangles. I couldn't figure out why quads was not defined in GLSL. I was using GL_QUADS so I naturally figured it would exist. It does not. Internal OpenGL pipeline converts to triangles here. Maybe this is common knowledge, maybe it is not. Someone can feel free to correct me.



Other interesting things are the extra lines there




#version 150
#extension GL_EXT_geometry_shader4 : enable

layout(triangles) in;
layout(line_strip, max_vertices = 20) out;


The top comments are required for me. The first function layout() show the input, triangles as mentioned earlier. The next specifies output, which is line strip. I did this to show the effect of the shader, it will draw line strips instead of triangle_strip which would make more sense in the real world perhaps.

Monday, September 13, 2010

Parsing Wavefront .obj using Python

Wavefront OBJ File Format Parsing in Python






I thought I would share some simple parsing information about the wavefront .OBJ file format using python. The thing I like about this format is that it is stored in plain text, and easy to use if you are writing simple 3D game engines, or just 3D modeling programs.

You can use this parser to load wavefront files using python, and possibly to view the wavefront obj file.

Overview of the wavefront .OBJ file format



Based on http://en.wikipedia.org/wiki/Obj

Basically, our approach is to go line by line through the file. If the line starts with a "v", we are dealing with a vertex. If the line starts with a "vt" then we are dealing with a texture coordinate (u, v, optionally w). "n" means normal. "f" means its a face index. These are a bit special, but not too difficult to grasp. Our exported models that are from blender will all have normal vectors, and texture coordinates (make sure you specify the texture coordinates in blender or there will be none). The "f" lines will look like this:


f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3


where "v1" is the vertex array index, "vt1" is the texture coordinate index, and "vn1" is the normals array index. This particular face is a triangle. I recommend storing triangles, and quads in 2 different arrays. They can both reference your vertex/texture coordinate/normals array.

Very important note before we begin



The index format in the .OBJ Wavefront file format is 1 based, not 0 based. Thus, we should subtract 1 from the actual number in order to get a 0 based index, and make it compatible with python lists or any array type.


#do the loading of the obj file
def load_obj(filename) :
V = [] #vertex
T = [] #texcoords
N = [] #normals
F = [] #face indexies

fh = open(filename)
for line in fh :
if line[0] == '#' : continue

line = line.strip().split(' ')
if line[0] == 'v' : #vertex
V.append(line[1:])
elif line[0] == 'vt' : #tex-coord
T.append(line[1:])
elif line[0] == 'vn' : #normal vector
N.append(line[1:])
elif line[0] == 'f' : #face
face = line[1:]
if len(face) != 4 :
print line
#raise Exception('not a quad!')
continue
for i in range(0, len(face)) :
face[i] = face[i].split('/')
# OBJ indexies are 1 based not 0 based hence the -1
# convert indexies to integer
for j in range(0, len(face[i])) : face[i][j] = int(face[i][j]) - 1
F.append(face)

return V, T, N, F


Please not this program will give you string representations. You should loop through these arrays again and convert them to the proper format (int, float, etc).

Please leave your comments

Thursday, August 26, 2010

Compiling boost C++ development libraries in OSX

Compiling boost c++ libraries and development headers for Mac OSX



Hi Everyone,

So I am starting to play with OpenCL. And as you all know I do love python. I have been playing with pyOpenCL package for awhile and wanted to get it set up on my Mac using OSX.

It depends on an installation of Boost C++ Libraries http://www.boost.org/

first we set up a directory, and in a terminal we need to download the boost files:


wget http://downloads.sourceforge.net/project/boost/boost/1.44.0/boost_1_44_0.tar.bz2?r=http%3A%2F%2Fsourceforge.net%2Fprojects%2Fboost%2Ffiles%2Fboost%2F1.44.0%2F

bzip2 -d boost_1_44_0.tar.bz2

tar -xvf boost_1_44_0.tar

cd boost_1_44_0


Configuration and Compilation



Great now we have extracted it, lets attempt to configure, compile, and install this package. Since I have build many applications on my MacOSX, I already have lots of build tools/libraries installed (eg. gcc, xorg, wine).

I was able to compile by typing at the console:


sh bootstrap.sh

./bjam


And then I waited... Its a shame there is no configure and make so I could use "make -j 4" and utilize all 4 of my cores for compiling. Maybe bjam has that option, I am not sure and too lazy to look into the documentation. Also I believe that is a bit out of the scope of this post for now.

After waiting for it to finish, we install the libraries by typing


sudo ./bjam install


And then we wait some more...

Finally after it is installed we can continue to try and install pyOpenCL.

Note: openCL unfortunately is not supported on my mac (Mac OS X 10.5.8 Leopard) and no amount of wishing will make it so :( However hopefully you are able to install this package (pyOpenCL) using Linux or Windows, or even Snow Leopard. Good luck!

Wednesday, June 23, 2010

Lambda functions in Python

Using lambda functions in python. Defining a lambda function


Lambda functions in python can be somewhat confusing. Here is a brief introduction



Lambda functions are handy functions that can be defined in 1 line, and thrown away afterwards. Here is a small example of what the syntax for a normal function looks like, and what the syntax for a lambda function looks like:


def f(x): return x * x


And here is the same function using the lambda syntax:


f = lambda x : x * x


Lambda functions really come in handy when used with python functions such as filter(), map() and reduce(). Here are some examples.

First we have filter. It will return a copy of a list where the function you pass returns True. This example will return only even numbers (because any number mod 2 will be equal to 0 if it is even).


array = [1, 2, 3, 4, 5, 6] #create an array of integers
print filter(lambda x: x % 2 == 0, array)


Output:


[2, 4, 6] # a list containing all even numbers


Next we will show map. It is basically a mapping of one list to another using a function (or a lambda function):


array = [1, 2, 3, 4, 5, 6] #create an array of integers
print map(lambda x: x * x, array)


Output:


[1, 4, 9, 16, 25, 36] # a list containing squares of 1, 2, 3, etc


And finally we have reduce. What this will do is convert the list into a single value. Our function will add element 0 and element 1. That result is then added to element 3, that result is then added to element 4, etc. Eg. (((((1+2) + 3) + 4) + 5) + 6) = 21


array = [1, 2, 3, 4, 5, 6] #create an array of integers
print reduce(lambda x, y: x + y, array)


Output:


21


And that concludes our short tutorial on creating and using lambda functions

Python: Defining a function.

Defining a function in python


Python function definitions are simple. Defining a function in python is very similar to many other languages out there. Lets try it out:




def hello(x) :
print "Hello", x

hello("Bill")


Output is as follows



Hello Bill


Notice we used print with a comma. This allows concatenation of strings in a single line statement.

An interesting property of functions in python is that you are able to assign them to variables.


variable1 = hello # assigns the variable "variable1" to the function we created above "hello"
variable1("Sarah")


Will output



Hello Sarah


That is all for now. Thanks and please add comments.

Thursday, April 1, 2010

Programming Games in Python

Programming Linux Games - Prototyping in Python



Hi Everyone,


Its been awhile since I've posted on topics related to game programming. Basically I am here to talk about programming games in Python programming language. I have programmed in many languages including C and C++ and even developed graphics engines in these languages. Eventually I get to a point where the code is large and basically I sit pondering where to go next in order to add features, and also get lost in semantics of the languages and how I will accomplish these goals.

Next I started trying out python, in order to prototype new features. I have written rather large programs in python before, and the one thing I got out of it is that its very fast for writing programs, and also very type safe. So if you try to compare and int with a float, it will raise an exception. This sounds like a headache, but really its a dream for debugging later on!

Some problems you may be thinking is 1) Python is slow. Right you are, python is rather slow compared to languages like C++ and C, but only in specific cases. For example, iterating through arrays can be much faster in C and C++. Memory usage is also less because you are working with more primitive data types. Also python does constant checking of variable types, etc.

When it comes to games, I feel that today's processors are very fast. Not only that, but many of the heaviest tasks are getting put onto the GPU, which has limited choice in programming languages, and they are mostly low level. Tasks like physics, rasterization are now completely done on the GPU, and now the trend seems to be even using the GPU for general processing tasks (GPGPU).

Back to the point, python's speed limitations no longer apply considering we are pushing all the data to the GPU and allowing it to do all the work with its languages. There are some things left to the CPU, like event handling, playing sounds, and some basic math (debatable as to perform on GPU or CPU) which I think my quad core can handle quite easily thank you!

Further more, threading in python is very easy however you still need to be very careful as to how you set up variable access etc.

As I will show in future blog posts, using python for game development should become a reality because its so type safe, it allows you to program things quickly (functionally) and then allows you to push all the data to the GPU very easily. Currently I am working on a GPGPU processing program written in python, and it should come in under 1000 lines of code!

Wednesday, March 3, 2010

Using GLSL programs in python pyopengl

Here is an easy straightforward example of using GLSL shader programs with pyopengl.

To use this you will need to import the following:


from OpenGL.GL import *
from OpenGL.GLU import *


Thus we will need pyopengl package installed.



##compile and link shader
vs = fs = 0

vs = glCreateShader(GL_VERTEX_SHADER)
fs = glCreateShader(GL_FRAGMENT_SHADER)

vs_source = """
void main(void) {
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
"""
fs_source = """
void main (void) {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
"""
glShaderSource(vs, vs_source)
glShaderSource(fs, fs_source)

glCompileShader(vs)
log = glGetShaderInfoLog(vs)
if log: print 'Vertex Shader: ', log

glCompileShader(fs)
log = glGetShaderInfoLog(fs)
if log: print 'Fragment Shader: ', log

prog = glCreateProgram()
glAttachShader(prog, vs)
glAttachShader(prog, fs)

glLinkProgram(prog)
glUseProgram(prog)


glShaderSource(shader_id, text) is a handy python function that will load our shader for us into opengl.


I also have a handy shader class I use in my engine program. Here it is:


class shader(node) :
def __init__(self, filename):
node.__init__(self)
fh = open(filename)
self.source = {'vertex': '', 'fragment':''}
write = None
for line in fh :
if line == '[[vertex-program]]\n' :
write = 'vertex'
elif line == '[[fragment-program]]\n' :
write = 'fragment'
else :
self.source[write] += line

self.draw = self.init

def init(self):
##compile and link shader
self.vs = self.fs = 0

self.vs = glCreateShader(GL_VERTEX_SHADER)
self.fs = glCreateShader(GL_FRAGMENT_SHADER)

glShaderSource(self.vs, self.source['vertex'])
glShaderSource(self.fs, self.source['fragment'])

glCompileShader(self.vs)
log = glGetShaderInfoLog(self.vs)
if log: print 'Vertex Shader: ', log

glCompileShader(self.fs)
log = glGetShaderInfoLog(self.fs)
if log: print 'Fragment Shader: ', log

self.prog = glCreateProgram()
glAttachShader(self.prog, self.vs)
glAttachShader(self.prog, self.fs)

glLinkProgram(self.prog)
self.use()
self.draw = self.use

def use(self):
glUseProgram(self.prog)

def end(self):
glUseProgram(0)

Wavefront Obj file format, opengl vertex arrays format, and uv texture coords

Loading Wavefront files using Python


Loading .obj files into numpy arrays, then using OpenGL to draw them



Note: for information on parsing wavefront .obj format click here

Our task here is to load wavefront .obj files into numpy arrays and use them in OpenGL, or pyOpenGL of course.

I am here to discuss something that has always bothered me in openGL. The fact that the vertex arrays and glDrawElements uses only 1 pointer to draw the elements, when in reality, it should use 2 (at least). This could be solved by writing special GLSL vertex shaders and using vertex textures, but that is overly complicated.

Understanding the problem:

The problem with indexing a vertex is the texture coordinate. If you are going to have UV islands, you will see that each vertex at that point, will have more than 1 texture coordinate. This means that there is no 1:1 mapping between a vertex, and a texture coordinate, and this breaks the 1 array indexing multiple verticies in an array, and also breaks the glDrawElements method.

The solution?

We basically unravel the arrays, reusing as many vertex:uv mappings as we can, but create new indexes when they differ.


Visual Examples:
 
v1 v2 v3
--- ---
| | | - 2 quads connected
--- ---
v4 v5 v6

v1 v2 v2 v3
--- ---
| | | | - UV coordinates NOT connected
--- ---
v4 v5 v5 v6


In this example may you see that v2 and v5 don't share UV coordinates, so they will need to have 4 indexies into our index array (because the index array indexes both UV coordinates and Vertex coordinates)

How will we take care of this?

What we are going to do is write a python parser for OBJ files, parse it into the traditional array structure that is laid out in the file. Then we are going to unravel it and create secondary arrays that will work with the glDrawElements.

Code:


from numpy import *
import random


#util to unravel
indexies = dict()
counter = -1
def get_index(key) :
global indexies, counter
if key not in indexies :
counter += 1
indexies[key] = counter
return [False, counter]
else :
return [True, indexies[key]]

#do the loading of the obj file
def load_obj(filename) :
V = [] #vertex
T = [] #texcoords
N = [] #normals
F = [] #face indexies

fh = open(filename)
for line in fh :
if line[0] == '#' : continue

line = line.strip().split(' ')
if line[0] == 'v' : #vertex
V.append(line[1:])
elif line[0] == 'vt' : #tex-coord
T.append(line[1:])
elif line[0] == 'vn' : #normal vector
N.append(line[1:])
elif line[0] == 'f' : #face
face = line[1:]
if len(face) != 4 :
print line
#raise Exception('not a quad!')
continue
for i in range(0, len(face)) :
face[i] = face[i].split('/')
# OBJ indexies are 1 based not 0 based hence the -1
# convert indexies to integer
for j in range(0, len(face[i])) : face[i][j] = int(face[i][j]) - 1
F.append(face)

#Now we lay out all the vertex/texcoord/normal data into a flat array
#and try to reuse as much as possible using a hash key

V2 = []
T2 = []
N2 = []
C2 = []
F2 = []

for face in F :
for index in face :
#print V[index[0]], T[index[1]], N[index[2]]
key = '%s%s%s%s%s' % (V[index[0]][0], V[index[0]][1], V[index[0]][2], T[index[1]][0], T[index[1]][1])
idx = get_index(key)

if not idx[0] :
V2.append([float(V[index[0]][0]), float(V[index[0]][1]), float(V[index[0]][2])])
T2.append([float(T[index[1]][0]), float(T[index[1]][1])])
N2.append([float(N[index[2]][0]), float(N[index[2]][1]), float(N[index[2]][2])])
C2.append([random.random(), random.random(), random.random()])

F2.append(idx[1])

print len(V) * 3 * 4, 'bytes compared to', len(V2) * 3 * 4, 'bytes'

#return numpy arrays
return [
array(V2, dtype=float32),
array(T2, dtype=float32),
array(N2, dtype=float32),
array(C2, dtype=float32),
array(F2, dtype=uint32)
]