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
Don't get spied on – We respect your privacy and provide numerous options to protect it. 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