Ever wanted to use GLSL with Pyglet, but became lost in the ctypes circumlocutions required to compile and link shaders? If so, then here is the class for you – a basic, simple GLSL wrapper class, which takes the leg work out of using shaders.
This provides most of what you will need for simple GLSL programs, allowing you to compile and link shaders, bind and unbind them, and set uniforms (integer, float and matrix). It doesn’t support custom attributes, but that would be a trivial addition.
#
# Copyright Tristam Macdonald 2008.
#
# Distributed under the Boost Software License, Version 1.0
# (see http://www.boost.org/LICENSE_1_0.txt)
#
from pyglet.gl import *
class Shader:
# vert, frag and geom take arrays of source strings
# the arrays will be concattenated into one string by OpenGL
def __init__(self, vert = [], frag = [], geom = []):
# create the program handle
self.handle = glCreateProgram()
# we are not linked yet
self.linked = False
# create the vertex shader
self.createShader(vert, GL_VERTEX_SHADER)
# create the fragment shader
self.createShader(frag, GL_FRAGMENT_SHADER)
# the geometry shader will be the same, once pyglet supports the extension
# self.createShader(frag, GL_GEOMETRY_SHADER_EXT)
# attempt to link the program
self.link()
def createShader(self, strings, type):
count = len(strings)
# if we have no source code, ignore this shader
if count < 1:
return
# create the shader handle
shader = glCreateShader(type)
# convert the source strings into a ctypes pointer-to-char array, and upload them
# this is deep, dark, dangerous black magick - don't try stuff like this at home!
src = (c_char_p * count)(*strings)
glShaderSource(shader, count, cast(pointer(src), POINTER(POINTER(c_char))), None)
# compile the shader
glCompileShader(shader)
temp = c_int(0)
# retrieve the compile status
glGetShaderiv(shader, GL_COMPILE_STATUS, byref(temp))
# if compilation failed, print the log
if not temp:
# retrieve the log length
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, byref(temp))
# create a buffer for the log
buffer = create_string_buffer(temp.value)
# retrieve the log text
glGetShaderInfoLog(shader, temp, None, buffer)
# print the log to the console
print buffer.value
else:
# all is well, so attach the shader to the program
glAttachShader(self.handle, shader);
def link(self):
# link the program
glLinkProgram(self.handle)
temp = c_int(0)
# retrieve the link status
glGetProgramiv(self.handle, GL_LINK_STATUS, byref(temp))
# if linking failed, print the log
if not temp:
# retrieve the log length
glGetProgramiv(self.handle, GL_INFO_LOG_LENGTH, byref(temp))
# create a buffer for the log
buffer = create_string_buffer(temp.value)
# retrieve the log text
glGetProgramInfoLog(self.handle, temp, None, buffer)
# print the log to the console
print buffer.value
else:
# all is well, so we are linked
self.linked = True
def bind(self):
# bind the program
glUseProgram(self.handle)
def unbind(self):
# unbind whatever program is currently bound - not necessarily this program,
# so this should probably be a class method instead
glUseProgram(0)
# upload a floating point uniform
# this program must be currently bound
def uniformf(self, name, *vals):
# check there are 1-4 values
if len(vals) in range(1, 5):
# select the correct function
{ 1 : glUniform1f,
2 : glUniform2f,
3 : glUniform3f,
4 : glUniform4f
# retrieve the uniform location, and set
}[len(vals)](glGetUniformLocation(self.handle, name), *vals)
# upload an integer uniform
# this program must be currently bound
def uniformi(self, name, *vals):
# check there are 1-4 values
if len(vals) in range(1, 5):
# select the correct function
{ 1 : glUniform1i,
2 : glUniform2i,
3 : glUniform3i,
4 : glUniform4i
# retrieve the uniform location, and set
}[len(vals)](glGetUniformLocation(self.handle, name), *vals)
# upload a uniform matrix
# works with matrices stored as lists,
# as well as euclid matrices
def uniform_matrixf(self, name, mat):
# obtian the uniform location
loc = glGetUniformLocation(self.Handle, name)
# uplaod the 4x4 floating point matrix
glUniformMatrix4fv(loc, 1, False, (c_float * 16)(*mat))
[/sourcecode]
Note that there isn't anything restricting this class to use with Pyglet - a simple change to the import statement should allow it to be used with any setup based on OpenGL/ctypes.
I hope someone finds this useful, and I would love to hear if you use it to make anything cool/interesting.
This is the cleanest implementation I have seen so far! After adding custom attributes, this would make a good replacement for pyglet/experimental/shader.py.
Code that illustrate general GPU computation, a-la http://29a.ch/mandelbrot/mandelbrot.py.txt, would make for a good demo.
Thanks! I have a sample program in the works, check back in a couple of days.
Pingback: Conway’s Game of Life in GLSL/Pyglet « swiftcoding
Yo!
Great code! I’ve just started using python (and pyglet) and did a google to see if using shaders with python would be hard, luckily for me you made it really simple with this example code.
Thanks for saving a python-n00b for hours of trying and failing :>
I finally put up a website http://www.pythonstuff.org with
GLSL Stuff using your great library.
Feedback appreciated 🙂
Send comments on GLSL/Pyglet on http://www.pythonstuff.org
to
pythonian_at_inode_dot_at !
There’s a missing import in the shader module.
File “/shader.py”, line 40, in createShader
src = (c_char_p * count)(*strings)
NameError: global name ‘c_char_p’ is not defined
Resolved by adding
from ctypes import *
Cheers,
Adam
Adam Griffiths,
File “C:\Python33\lib\site-packages\shader.py”, line 41, in createShader
src = (c_char_p * count)(*strings)
TypeError: bytes or integer address expected instead of str instance
PS I try to use python33
I would be really very surprised if the code works as-is under python 3. You’ll probably have to convert all the strings to bytearrays.
I tried to make this work under Python 3 but couldn’t get it done. There is an invalid value GLException with a standard vertex and fragment shader. Can you help me by updating your code to be compatible with Python 3? Thanks in advance!
I still couldn’t figure out what to change in your code to make it python 3 compatible, the following lines won’t work under python3:
src = (c_char_p * count)(*strings)
glShaderSource(shader, count, cast(pointer(src), POINTER(POINTER(c_char))), None)
I struggled a lot to find out from the exceptions that I actually solved this problem, but it arose at another part of the Shader class. The uniformi and uniformf methods are also using strings to locate the uniform variable, so these have to be also encoded to ascii.
These lines have to be added or modified to make this useful program work under python3:
– before line 40 add the following:
strings = [s.encode(‘ascii’) for s in strings]
– modify line 106 and 119 to:
}[len(vals)](glGetUniformLocation(self.handle, name.encode(‘ascii’)), *vals)
So is there any way to make this work with Python 3…?
Yes, but it will take a little effort. There are trivial items to fix (i.e. print statement syntax), but the more complicated part is the handling of ctypes strings in python 3 – I think you need to adapt those to the buffer type instead.
The changes Nexor (above) identified were sufficient to make your code work for me in python 3 (in addition to the print statements). Thanks, Nexor! And, thanks Tristam!