License Public Domain
Lines 327
Keywords
OpenGL (15) PyOpenGL (9) Volume Rendering (9) wxPython (7) wxWidgets (7)
Included in these Libraries
Permissions
Owner: Stou S.
Viewable by Everyone
Editable by All Siafoo Users
Hide
Siafoo is here to make coding less frustrating and to save you time. Join Siafoo Now or Learn More

wxPython and PyOpenGL Volume Rendering Skeleton Atom Feed 0

In Brief A very quick and dirty skeleton for prototyping GLSL shaders. It consists of a very simple self contained volume renderer, that uses many slices to create the illusion of 3D.... more
# 's
  1'''
2A quick and dirty skeleton for prototyping GLSL shaders. It consists of a
3self contained slice-based volume renderer.
4'''
5
6import numpy, sys, wx
7
8from OpenGL.GL import *
9from OpenGL.GLU import *
10
11from numpy import array
12from transfer_function import TransferFunctionWidget
13from wx.glcanvas import GLCanvas
14
15# The skeleton
16def box_side(w=1.0, z=0.0):
17 return [[0.0, 0.0, z], [w, 0.0, z], [w, 0.0, z], [w, w, z],
18 [w, w, z], [0.0, w, z], [0.0, w, z], [0.0, 0.0, z]]
19
20def gen_plane(t, p=0.0, w=1.0):
21 ''' Creates front facing planes '''
22 try:
23 return { 'yz': [(p, 0, 0), (p, w, 0), (p, w, w), (p, 0, w)],
24 'xz': [(0, p, w), (w, p, w), (w, p, 0), (0, p, 0)],
25 'xy': [(0, 0, p), (w, 0, p), (w, w, p), (0, w, p)]}[t]
26 except KeyError:
27 raise Exception, 'What kind of planes do you want?'
28
29box = [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0],
30 [1.0, 0.0, 0.0], [1.0, 0.0, 1.0],
31 [1.0, 1.0, 0.0], [1.0, 1.0, 1.0],
32 [0.0, 1.0, 0.0], [0.0, 1.0, 1.0]]
33box.extend(box_side())
34box.extend(box_side(z=1.0))
35
36plane_count = 1000
37
38def compile_program(vertex_src, fragment_src):
39 '''
40 Compile a Shader program given the vertex
41 and fragment sources
42 '''
43
44 program = glCreateProgram()
45
46 shaders = []
47
48 for shader_type, src in ((GL_VERTEX_SHADER, vertex_src),
49 (GL_FRAGMENT_SHADER, fragment_src)):
50 shader = glCreateShader(shader_type)
51 glShaderSource(shader, src)
52 glCompileShader(shader)
53
54 shaders.append(shader)
55
56 status = glGetShaderiv(shader, GL_COMPILE_STATUS)
57
58 if not status:
59 if glGetShaderiv(shader, GL_INFO_LOG_LENGTH) > 0:
60 log = glGetShaderInfoLog(shader)
61 print >> sys.stderr, log.value
62 glDeleteShader(shader)
63 raise ValueError, 'Shader compilation failed'
64
65 glAttachShader(program, shader)
66
67 glLinkProgram(program)
68
69 for shader in shaders:
70 glDeleteShader(shader)
71
72 return program
73
74class TransferGraph(wx.Dialog):
75
76 def __init__(self, parent, id=wx.ID_ANY, title="", pos=wx.DefaultPosition,
77 size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE):
78
79 wx.Dialog.__init__(self, parent, id, title, pos, size, style)
80 self.mainPanel = wx.Panel(self, -1)
81
82 # Create some CustomCheckBoxes
83 self.t_function = TransferFunctionWidget(self.mainPanel, -1, "", size=wx.Size(300, 150))
84
85 # Layout the items with sizers
86 mainSizer = wx.BoxSizer(wx.VERTICAL)
87 mainSizer.Add(self.mainPanel, 1, wx.EXPAND)
88
89 self.SetSizer(mainSizer)
90 mainSizer.Layout()
91
92class VolumeRenderSkeleton(GLCanvas):
93 def __init__(self, parent):
94 GLCanvas.__init__(self, parent, -1, attribList=[wx.glcanvas.WX_GL_DOUBLEBUFFER])
95
96 self.t_graph = TransferGraph(self)
97
98 wx.EVT_PAINT(self, self.OnDraw)
99 wx.EVT_SIZE(self, self.OnSize)
100 wx.EVT_MOTION(self, self.OnMouseMotion)
101 wx.EVT_LEFT_DOWN(self, self.OnMouseLeftDown)
102 wx.EVT_LEFT_UP(self, self.OnMouseLeftUp)
103 wx.EVT_ERASE_BACKGROUND(self, lambda e: None)
104 wx.EVT_CLOSE(self, self.OnClose)
105 wx.EVT_CHAR(self, self.OnKeyDown)
106
107 self.SetFocus()
108
109 # So we know when new values are added / changed on the tgraph
110 self.t_graph.Connect(-1, -1, wx.wxEVT_COMMAND_SLIDER_UPDATED, self.OnTGraphUpdate)
111
112 self.init = False
113 self.rotation_y = 0.0
114 self.rotation_x = 0.0
115 self.prev_y = 0
116 self.prev_x = 0
117 self.mouse_down = False
118
119 self.width = 400
120 self.height = 400
121
122 self.fragment_shader_src = '''
123 uniform sampler1D TransferFunction;
124 uniform sampler3D VolumeData;
125 void main(void)
126 {
127 gl_FragColor = vec4(1.0, 0.0, 0.0, 0.0);
128 }
129 '''
130
131 self.vertex_shader_src = '''
132 void main(void)
133 {
134 gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
135 }
136 '''
137 self.fragment_src_file = 'earth.f.c'
138 self.vertex_src_file = 'earth.v.c'
139 self.lighting = False
140 self.light_count = 1
141
142 # List of textures that need to be freed
143 self.texture_list = []
144 # List of VBOs that need to be freed
145 self.buffers_list = []
146 # This is the transfer graph
147 self.t_graph.Show()
148
149 def OnTGraphUpdate(self, event):
150 self.UpdateTransferFunction()
151 self.Refresh()
152
153 def OnDraw(self, event):
154 self.SetCurrent()
155 if not self.init:
156 self.InitGL()
157 self.init = True
158
159 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
160 glLoadIdentity()
161
162 glTranslate(0.0, 0.0, -2.0)
163 glRotate(self.rotation_y, 0.0, 1.0, 0.0)
164 glRotate(self.rotation_x, 1.0, 0.0, 0.0)
165 glTranslate(-0.5, -0.5, -0.5)
166
167 glEnable(GL_BLEND)
168 glEnable(GL_POLYGON_SMOOTH)
169
170
171 # Draw the box
172 glUseProgram(0)
173 glColor(0.0, 1.0, 0.0)
174 glDisable(GL_LIGHTING)
175 glVertexPointerf(box)
176 glDrawArrays(GL_LINES, 0, len(box))
177
178 # Draw the slice planes
179 glUseProgram(self.program)
180
181 self.SetupUniforms()
182
183 # Choose the correct set of planes
184 if self.rotation_y < 45.0 or self.rotation_y >= 315.0:
185 vertex_vbo = self.planes_vbo['xy'][0]
186 elif self.rotation_y >= 45.0 and self.rotation_y < 135.0:
187 vertex_vbo = self.planes_vbo['yz'][1]
188 elif self.rotation_y >= 135.0 and self.rotation_y < 225.0:
189 vertex_vbo = self.planes_vbo['xy'][1]
190 elif self.rotation_y >= 225.0 and self.rotation_y < 315.0:
191 vertex_vbo = self.planes_vbo['yz'][0]
192
193 # Render the planes using VBOs
194 glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo)
195 glVertexPointer(3, GL_FLOAT, 0, None)
196 glDrawArrays(GL_QUADS, 0, 4*plane_count)
197 glBindBuffer(GL_ARRAY_BUFFER, 0)
198
199 self.SwapBuffers()
200 return
201
202 def InitGL(self):
203
204 # Load the Shader sources from the files
205 self.LoadShaderSources()
206
207 glEnable(GL_DEPTH_TEST)
208 glEnable(GL_BLEND)
209 glEnableClientState(GL_VERTEX_ARRAY)
210
211 glDepthFunc(GL_LESS)
212 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
213 glShadeModel(GL_SMOOTH)
214
215 glClearColor(0.0, 0.0, 0.0, 1.0)
216 glClearDepth(1.0)
217
218 glMatrixMode(GL_PROJECTION)
219 glLoadIdentity()
220 gluPerspective(45.0, self.width/float(self.height), 0.1, 1000.0)
221
222 glMatrixMode(GL_MODELVIEW)
223 glLoadIdentity()
224
225 self.SetupLighting()
226 self.LoadVolumeData()
227 self.LoadTransferFunction((self.t_graph.t_function.get_map() / array([255.0, 255.0, 255.0, 1.0])).flatten())
228 self.program = compile_program(self.vertex_shader_src, self.fragment_shader_src)
229 self.BuildGeometry()
230
231 def SetupLighting(self):
232 '''
233 Initialize default lighting
234 '''
235
236 glLight(GL_LIGHT0, GL_AMBIENT, (1.0, 1.0, 1.0))
237 glLight(GL_LIGHT0, GL_DIFFUSE, (1.0, 1.0, 1.0))
238 glLight(GL_LIGHT0, GL_SPECULAR, (1.0, 1.0, 1.0))
239 glLight(GL_LIGHT0, GL_POSITION, (-1.0, -1.0, -1.0))
240 glEnable(GL_LIGHT0)
241
242 def SetupUniforms(self):
243
244 # Init the texture units
245 glActiveTexture(GL_TEXTURE0)
246 glBindTexture(GL_TEXTURE_1D, self.transfer_function)
247 glUniform1i(glGetUniformLocation(self.program, "TransferFunction"), 0)
248 glUniform1i(glGetUniformLocation(self.program, "EnableLighting"),
249 self.lighting)
250 glUniform1i(glGetUniformLocation(self.program, "NumberOfLights"),
251 self.light_count)
252
253 def BuildGeometry(self):
254 self.planes_vbo = { 'xy':None, 'xz':None, 'yz':None }
255
256 increment = 1.0 / (plane_count)
257
258 for k in self.planes_vbo.keys():
259 fwd = [gen_plane(p=(i*increment), t=k) for i in range(plane_count + 1)]
260
261 rev = []
262 rev.extend(fwd)
263 rev.reverse()
264
265 data = (array(fwd, dtype=numpy.float32).flatten(),
266 array(rev, dtype=numpy.float32).flatten())
267
268 self.planes_vbo[k] = []
269
270 for i in range(2):
271 self.planes_vbo[k].append(glGenBuffers(1))
272 glBindBuffer(GL_ARRAY_BUFFER, self.planes_vbo[k][i])
273 glBufferData(GL_ARRAY_BUFFER, data[i], GL_STATIC_DRAW_ARB)
274
275 def LoadTransferFunction(self, data):
276 # Create Texture
277 self.transfer_function = glGenTextures(1)
278 glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
279 glBindTexture(GL_TEXTURE_1D, self.transfer_function)
280 glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP)
281 glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
282 glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
283 glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, 256, 0, GL_RGBA, GL_FLOAT, data)
284 return
285
286 def UpdateTransferFunction(self):
287 data = (self.t_graph.t_function.get_map() / array([255.0, 255.0, 255.0, 1.0])).flatten()
288 glBindTexture(GL_TEXTURE_1D, self.transfer_function)
289 glTexSubImage1D(GL_TEXTURE_1D, 0, 0, 256, GL_RGBA, GL_FLOAT, data)
290
291 def LoadVolumeData(self):
292 pass
293
294 def LoadShaderSources(self):
295 try:
296 self.fragment_shader_src = open(self.fragment_src_file).read()
297 except IOError, e:
298 print 'Fragment source not found, using default'
299
300 try:
301 self.vertex_shader_src = open(self.vertex_src_file).read()
302 except IOError, e:
303 print 'Vertex source not found, using default'
304
305
306 def OnSize(self, event):
307 try:
308 self.width, self.height = event.GetSize()
309 except:
310 self.width = event.GetSize().width
311 self.height = event.GetSize().height
312
313 self.Refresh()
314 self.Update()
315
316 def OnMouseMotion(self, event):
317
318 x = event.GetX()
319 y = event.GetY()
320
321 if self.mouse_down:
322 self.rotation_y += (x - self.prev_x)/2.0
323 self.rotation_y %= 360.0
324# self.rotation_x -= ((y - self.prev_y)/2.0
325# self.rotation_x %= 360.0
326 self.prev_x = x
327 self.prev_y = y
328 self.Refresh()
329 self.Update()
330
331 def OnMouseLeftDown(self, event):
332 self.mouse_down = True
333 self.prev_x = event.GetX()
334 self.prev_y = event.GetY()
335
336 def OnMouseLeftUp(self, event):
337 self.mouse_down = False
338
339 def OnKeyDown(self, event):
340
341 if event.GetKeyCode() == ord('r'):
342 try:
343 print 'Compiling shaders...',
344 self.LoadShaderSources()
345 program = compile_program(self.vertex_shader_src,
346 self.fragment_shader_src)
347 self.program = program
348 self.Refresh()
349 except Exception, e:
350 print 'FAILED'
351 print e
352 print 'Done'
353 elif event.GetKeyCode() == ord('l'):
354 self.lighting = not self.lighting
355 print 'Lighting', self.lighting
356 self.Refresh()
357
358 def OnClose(self):
359
360 for t in self.texture_list:
361 glDeleteTextures(t)
362
363 for b in self.buffers_list:
364 glDeleteBuffers(1, b)
365
366if __name__ == '__main__':
367 app = wx.App()
368 frame = wx.Frame(None, -1, 'Volume Rendering Skeleton', wx.DefaultPosition, wx.Size(600, 600))
369 canvas = VolumeRenderSkeleton(frame)
370
371 frame.Show()
372 app.MainLoop()

A very quick and dirty skeleton for prototyping GLSL shaders. It consists of a very simple self contained volume renderer, that uses many slices to create the illusion of 3D.

Keys:

r - Reload shader sources

l - Toggle lighting

Warning

You must first download wxPython Transfer Function Widget

For a basic example of using this code see the Volume Rendering MRI data using PyOpenGL snippet.

Images created using MRI data (i.e. 3D Texture) and the above code:

http://www.siafoo.net/image/33?w=300 http://www.siafoo.net/image/40?w=300

Programmatically generated images of a sphere with an earth texture. Note that there is no geometry here except for the slice-planes (i.e. the sphere is). Also the colorful rings in the first image are due to the transfer function.

http://www.siafoo.net/image/34?w=300 http://www.siafoo.net/image/39?w=300