glitter Example: OpenCL
Summary
This program will open a GLUT window and render an animated particle simulation. The particle simulation is computed in OpenCL, and the results are written directly to an OpenGL array buffer.
Front matter
Module docstring
The module docstring is used as a description of this example in the generated documentation:
"""Basic OpenGL/OpenCL interoperability example.
@author: Stephan Wenger
@date: 2012-02-29
"""
Imports
We use numpy for creating the particle data:
import numpy
PyOpenCL (documentation, PyPi, download) is a convenient Python wrapper for OpenCL:
import pyopencl as cl
For creating an OpenCL context that shares data with OpenGL, we need
get_gl_sharing_context_properties
:
from pyopencl.tools import get_gl_sharing_context_properties
We can usually import classes and functions contained in glitter submodules directly from glitter:
from glitter import ArrayBuffer, VertexArray, get_default_program
Modules with external dependencies other than numpy, such as platform dependent parts like methods for the generation of an OpenGL context, however, have to be imported from their respective submodules:
from glitter.contexts.glut import GlutWindow, main_loop
OpenCL
OpenCL kernel
The OpenCL kernel (adapted from a CMSoft tutorial) computes a single iteration of a simple Eulerian particle simulation. Kernel code can be loaded from a file or defined inline as a Python string:
kernel_code = """
__kernel void animate(__global float4* positions,
__global float4* colors,
__global float4* velocities,
__global float4* initial_positions,
__global float4* initial_velocities,
float dt) {
unsigned int i = get_global_id(0);
float4 p = positions[i];
float4 v = velocities[i];
float life = velocities[i].w;
life -= dt;
if (life <= 0.0) {
p = initial_positions[i];
v = initial_velocities[i];
life = 1.0;
}
v.z -= 9.8 * dt;
p.xyz += v.xyz * dt;
v.w = life;
positions[i] = p;
velocities[i] = v;
colors[i].w = life;
}
"""
OpenCL interaction
The CLCode
class will comprise all OpenCL interaction:
class CLCode(object):
Initialization
The constructor receives OpenGL buffers for positions and colors as well as a numpy array of velocities and a timestep for the simulation: def __init__(self, gl_positions, gl_colors, velocities, dt=0.001):
First, we have to initialize the OpenCL context. That means we have to get a list of available platforms and select one:
platform = cl.get_platforms()[0]
Then, we can create a context. Passing
get_gl_sharing_context_properties()
as a property
ensures that we share state with the active OpenGL context:
self.ctx = cl.Context(properties=[(cl.context_properties.PLATFORM, platform)] +
get_gl_sharing_context_properties(), devices=None)
A command queue is necessary for serializing OpenCL commands:
self.queue = cl.CommandQueue(self.ctx)
Finally, we can compile the kernel:
self.program = cl.Program(self.ctx, kernel_code).build()
The constructor parameters are stored for later use:
self.gl_positions = gl_positions
self.gl_colors = gl_colors
self.velocities = velocities
The dt
value will later be passed to an OpenCL kernel as
a 32-bit float. We therefore wrap it in a numpy float32
object:
self.dt = numpy.float32(dt)
Next, we generate OpenCL buffers. The positions and colors are
contained in OpenGL buffers, which we wrap in PyOpenCL's
GLBuffer<(/code> class:
self.cl_positions = cl.GLBuffer(self.ctx, cl.mem_flags.READ_WRITE, self.gl_positions._id)
self.cl_colors = cl.GLBuffer(self.ctx, cl.mem_flags.READ_WRITE, self.gl_colors._id)
Note how we had to extract the _id
s from the
ArrayBuffer
objects. In pure glitter code, you
should never (have to) access this value; however for interaction
with other OpenGL-related libraries, this cannot always be avoided.
The velocities are given as a numpy array, which is simply uploaded
into a new OpenCL Buffer
object along with the initial
values of the positions and colors:
self.cl_velocities = cl.Buffer(self.ctx, cl.mem_flags.READ_ONLY |
cl.mem_flags.COPY_HOST_PTR, hostbuf=velocities)
self.cl_initial_positions = cl.Buffer(self.ctx, cl.mem_flags.READ_ONLY |
cl.mem_flags.COPY_HOST_PTR, hostbuf=self.gl_positions.data)
self.cl_initial_velocities = cl.Buffer(self.ctx, cl.mem_flags.READ_ONLY |
cl.mem_flags.COPY_HOST_PTR, hostbuf=self.velocities)
Execution
The execute
function executes the OpenCL kernel several
times in a row:
def execute(self, sub_intervals):
First, we have to make sure that OpenGL is done using the buffer objects:
cl.enqueue_acquire_gl_objects(self.queue, [self.cl_positions, self.cl_colors])
Now, we can safely call the kernel. Its arguments are buffer objects:
args = (self.cl_positions, self.cl_colors, self.cl_velocities,
self.cl_initial_positions, self.cl_initial_velocities, self.dt)
The kernel will be executed several times with a small step size.
This increases the accuracy with respect to a single step with a
large step size. However, it is not necessary to display all the
intermediate results.
for i in xrange(0, sub_intervals):
In each step, the animate
kernel function is called.
Its arguments are the queue object that schedules its execution,
the global and local block sizes, and any arguments that will be
passed to the actual kernel.
self.program.animate(self.queue, [len(self.gl_positions)], None, *args)
Finally, we allow OpenGL to access the buffers again:
cl.enqueue_release_gl_objects(self.queue, [self.cl_positions, self.cl_colors])
Main class
We wrap all the OpenGL interaction in a class. The class will contain an
__init__()
method to set up all OpenGL objects, any required
callback methods, as well as a run()
method to trigger execution
of the GLUT main loop.
class OpenCLExample(object):
Initialization
When a OpenCLExample
instance is created, we need to
initialize a few OpenGL objects.
def __init__(self):
First, we create a window; this also creates an OpenGL context.
self.window = GlutWindow(double=True, alpha=True, depth=True)
Then, we set the GLUT display and keyboard callback functions which
will be defined later.
self.window.display_callback = self.display
self.window.keyboard_callback = self.keyboard
Here, we generate numpy arrays to hold the positions, colors, and
velocities of the particles:
num = 200000
positions = numpy.empty((num, 4), dtype=numpy.float32)
colors = numpy.empty((num, 4), dtype=numpy.float32)
velocities = numpy.empty((num, 4), dtype=numpy.float32)
So far, the array contents are undefined. We have to initialize them with meaningful values:
positions[:, 0] = numpy.sin(numpy.arange(0, num) * 2 * numpy.pi / num) * (numpy.random.random_sample((num,)) / 3 + 0.2)
positions[:, 1] = numpy.cos(numpy.arange(0, num) * 2 * numpy.pi / num) * (numpy.random.random_sample((num,)) / 3 + 0.2)
positions[:, 2:] = 0, 1
colors[:] = 0, 1, 0, 1
velocities[:, :2] = 2 * positions[:, :2]
velocities[:, 2] = 3
velocities[:, 3] = numpy.random.random_sample((num,))
Instead of simply generating a vertex array from the position and color
data, we first generate array buffers for them:
gl_positions = ArrayBuffer(data=positions, usage="DYNAMIC_DRAW")
gl_colors = ArrayBuffer(data=colors, usage="DYNAMIC_DRAW")
These array buffers will later also be used by OpenCL. We do not need to
wrap velocities
in this way, as it will only be used by
OpenCL and can be wrapped in an OpenCL buffer directly.
We now create a vertex array that will pass the position and color data
to the shader. The vertex array constructor accepts
ArrayBuffer
instances:
self.vao = VertexArray(gl_positions, gl_colors)
In the OpenGL core profile, there is no such thing as a "standard pipeline"
any more. We use the minimalistic defaultpipeline
from the
glitter.convenience
module to create a shader program instead:
self.shader = get_default_program()
Here, we create the CLCode
object that manages OpenCL
interaction. It is passed the OpenGL buffer objects as well as a numpy
array of velocities.
self.clcode = CLCode(gl_positions, gl_colors, velocities)
Callback functions
Display function
Here we define the display function. It will be called by GLUT whenever the
screen has to be redrawn.
def display(self):
First we clear the default framebuffer:
self.window.clear()
To draw the vertex array, we use:
self.vao.draw()
After all rendering commands have been issued, we swap the back buffer to
the front, making the rendered image visible all at once:
self.window.swap_buffers()
Timer function
The animation is controlled by a GLUT timer. The timer callback animates the
particle system, schedules the next timer event, and causes a screen redraw:
def timer(self):
We first tell an instance of the CLCode
class to execute the
OpenCL kernel:
self.clcode.execute(10)
The following line schedules the next timer event to execute after one millisecond.
self.window.add_timer(1, self.timer)
Finally, we tell GLUT to redraw the screen.
self.window.post_redisplay()
Keyboard function
To further illustrate the concept of GLUT callbacks, here's a keyboard
handler that will simply make the program exit when any key is pressed:
def keyboard(self, key, x, y):
raise SystemExit
Running
We will call the run()
method later to run the OpenGL code.
def run(self):
To start the animation, we call the timer once; all subsequent timer
calls will be scheduled by the timer function itself.
self.timer()
The default program is bound by using a with
statement. At
the same time, we can pass in additional uniform variables, such as the
modelview matrix:
with self.shader(modelview_matrix=((1, 0, 0, 0), (0, 0, 1, 0), (0, 1, 0, 0), (0, 0, 0, 2))):
With the shader bound, we enter the GLUT main loop.
main_loop()
When the main loop exits, control is handed back to the script,
unless SystemExit
has been raised by the keyboard
handler.
Main section
Finally, if this program is being run from the command line, we instanciate
the main class and run it.
if __name__ == "__main__":
OpenCLExample().run()