├── .gitignore ├── LICENSE ├── coil.stl ├── core.stl ├── example.py ├── images ├── combined_array.JPG ├── combined_array_isotropic.JPG ├── combined_array_nonisotropic.JPG └── combined_stl.JPG ├── openglvoxelizer.py └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__ 2 | *.npy -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 3D Printing for Microfluidics, 2023 Computed Axial Lithography 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /coil.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computed-axial-lithography/OpenGL-voxelizer/264b7b01702dbc27c21d7520ce3acd878ca7111e/coil.stl -------------------------------------------------------------------------------- /core.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computed-axial-lithography/OpenGL-voxelizer/264b7b01702dbc27c21d7520ce3acd878ca7111e/core.stl -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import openglvoxelizer 2 | 3 | vox = openglvoxelizer.Voxelizer() 4 | vox.addMeshes({ 5 | r'coil.stl':'print_body', 6 | r'core.stl':'attenuating_body', 7 | }) 8 | 9 | print_body = vox.voxelize('print_body',xy_voxel_size=0.01,z_voxel_size=0.1,voxel_value=1) 10 | attenuating_body = vox.voxelize('attenuating_body',xy_voxel_size=0.01,z_voxel_size=0.1,voxel_value=2) 11 | 12 | combined = print_body + attenuating_body 13 | 14 | import numpy as np 15 | np.save(r'combined_array.npy',combined) 16 | np.save(r'print_body_array.npy',print_body) 17 | np.save(r'attenuating_body_array.npy',attenuating_body) -------------------------------------------------------------------------------- /images/combined_array.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computed-axial-lithography/OpenGL-voxelizer/264b7b01702dbc27c21d7520ce3acd878ca7111e/images/combined_array.JPG -------------------------------------------------------------------------------- /images/combined_array_isotropic.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computed-axial-lithography/OpenGL-voxelizer/264b7b01702dbc27c21d7520ce3acd878ca7111e/images/combined_array_isotropic.JPG -------------------------------------------------------------------------------- /images/combined_array_nonisotropic.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computed-axial-lithography/OpenGL-voxelizer/264b7b01702dbc27c21d7520ce3acd878ca7111e/images/combined_array_nonisotropic.JPG -------------------------------------------------------------------------------- /images/combined_stl.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computed-axial-lithography/OpenGL-voxelizer/264b7b01702dbc27c21d7520ce3acd878ca7111e/images/combined_stl.JPG -------------------------------------------------------------------------------- /openglvoxelizer.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import time 4 | 5 | import numpy as np 6 | import pyglet 7 | import trimesh 8 | from PIL import Image 9 | import tqdm 10 | from OpenGL.GL import * 11 | from OpenGL.GL import shaders 12 | from OpenGL.arrays import vbo 13 | 14 | EPSILON = 0.0001 15 | 16 | def orthoMatrix(left, right, bottom, top, zNear, zFar, dtype): 17 | ''' 18 | Return the following matrix 19 | | 2 -(right+left) | 20 | | ---------- 0 0 ------------- | 21 | | right-left right-left | 22 | | | 23 | | 2 -(top+bottom) | 24 | | 0 ---------- 0 ------------- | 25 | | top-bottom top-bottom | 26 | | | 27 | | -2 -(zFar+zNear) | 28 | | 0 0 ---------- ------------- | 29 | | zFar-zNear zFar-zNear | 30 | | | 31 | | | 32 | | 0 0 0 1 | 33 | ''' 34 | M = np.identity(4, dtype=dtype) 35 | M[0,0] = 2 / (right - left) 36 | M[1,1] = 2 / (top - bottom) 37 | M[2,2] = -2 / (zFar - zNear) 38 | M[0,3] = -(right + left) / (right - left) 39 | M[1,3] = -(top + bottom) / (top - bottom) 40 | M[2,3] = -(zFar + zNear) / (zFar - zNear) 41 | return M.T 42 | 43 | def translationMatrix(direction, dtype): 44 | """Return matrix to translate by direction vector. 45 | 46 | If direction is [x, y, z], return the following matrix 47 | 48 | | 1 0 0 x | 49 | | | 50 | | 0 1 0 y | 51 | | | 52 | | 0 0 1 z | 53 | | | 54 | | 0 0 0 1 | 55 | 56 | """ 57 | M = np.identity(4, dtype=dtype) 58 | M[:3, 3] = direction[:3] 59 | return M.T 60 | 61 | class Bounds(): 62 | xmin = np.nan 63 | xmax = np.nan 64 | ymin = np.nan 65 | ymax = np.nan 66 | zmin = np.nan 67 | zmax = np.nan 68 | length_x = np.nan 69 | length_y = np.nan 70 | length_z = np.nan 71 | 72 | class BodyMesh(): 73 | def __init__(self,mesh:trimesh.Trimesh): 74 | self.mesh = mesh 75 | 76 | self.bounds = Bounds() 77 | self.bounds.xmin = self.mesh.bounds[0][0] 78 | self.bounds.xmax = self.mesh.bounds[1][0] 79 | self.bounds.ymin = self.mesh.bounds[0][1] 80 | self.bounds.ymax = self.mesh.bounds[1][1] 81 | self.bounds.zmin = self.mesh.bounds[0][2] 82 | self.bounds.zmax = self.mesh.bounds[1][2] 83 | 84 | 85 | self.bounds.length_x = np.abs(self.bounds.xmax - self.bounds.xmin) 86 | self.bounds.length_y = np.abs(self.bounds.ymax - self.bounds.ymin) 87 | self.bounds.length_z = np.abs(self.bounds.zmax - self.bounds.zmin) 88 | 89 | self.num_of_verts = self.mesh.triangles.shape[0] * 3 90 | 91 | class Voxelizer(): 92 | def __init__(self,): 93 | self.meshes = {} 94 | self.voxel_arrays = {} 95 | self.global_bounds = Bounds() 96 | 97 | def addMeshes(self,stl_struct:dict): 98 | """ 99 | Add mesh files to be voxelized. After adding meshes, the global bounding box of all meshes is calculated and used for subsequent voxelization such that all voxel arrays have the same physical bounds. 100 | 101 | If a mesh is added in a later call of addMeshes(), the global bounding box is updated. If the new mesh has a larger bounding box, previously voxelized meshes should be re-voxelized. 102 | 103 | Parameters 104 | ---------- 105 | stl_struct : dict 106 | dict with file name, body name pairs for each mesh file. The body name is only an identifier for the user and accepts arbitrary strings. 107 | 108 | Example: {'mymesh.stl': 'print_body'} 109 | """ 110 | for filename, body_name in stl_struct.items(): 111 | mesh = trimesh.load_mesh(filename) 112 | self.meshes[body_name] = BodyMesh(mesh) 113 | self.voxel_arrays[body_name] = None 114 | 115 | # after adding all meshes, update the global max and min bounds 116 | self._updateBounds() 117 | 118 | def voxelize(self,body_name:str,xy_voxel_size:float,voxel_value:float,voxel_dtype:str='uint8',z_voxel_size=None,square_xy:bool=True,store_voxel_array:bool=False,slice_save_path:str=None): 119 | """ 120 | Parameters 121 | ---------- 122 | body_name : str 123 | name of the mesh to be voxelized 124 | 125 | xy_voxel_size : float 126 | side length of square voxel cross section in x-y plane in voxelized array in same units as the mesh file 127 | 128 | voxel_value : float 129 | value of voxels in voxelized array 130 | 131 | Only homogeneous voxel values are implemented 132 | 133 | voxel_dtype : str, optional 134 | datatype of voxel array 135 | 136 | z_voxel_size : float, optional 137 | thickness of each slice in voxelized array in same units as the mesh file, defaults to z_voxel_size = xy_voxel_size. This allows for non-isotropic voxel size in the z-axis 138 | 139 | square_xy : bool, optional 140 | if the resulting voxel array should have equal number of voxels in x and y dimensions (i.e. square in x-y) 141 | 142 | store_voxel_array : bool, optional 143 | store the voxel array in the Voxelizer object. Retrieve with voxelizer_object.voxel_arrays[body_name] 144 | 145 | slice_save_path : str, optional 146 | file path to directory in which to save .png images of each slice 147 | 148 | Returns 149 | ------- 150 | voxel_array : np.ndarray 151 | voxelized array of the selected mesh 152 | """ 153 | 154 | slicer = OpenGLSlicer() 155 | slicer.begin() 156 | 157 | # slice the selected mesh 158 | voxel_array = slicer.slice(self.global_bounds,self.meshes[body_name],xy_voxel_size,z_voxel_size,square_xy,slice_save_path=slice_save_path,value=voxel_value,dtype=voxel_dtype) 159 | 160 | if store_voxel_array == True: 161 | self.voxel_arrays[body_name] = voxel_array 162 | 163 | # close the OpenGL window 164 | slicer.quit() 165 | 166 | return voxel_array 167 | 168 | def _updateBounds(self,): 169 | """Identify max and min bounds of every mesh and set global bounding box size from these""" 170 | for _, _mesh in self.meshes.items(): 171 | self.global_bounds.xmin = np.nanmin([_mesh.bounds.xmin,self.global_bounds.xmin]) 172 | self.global_bounds.xmax = np.nanmax([_mesh.bounds.xmax,self.global_bounds.xmax]) 173 | self.global_bounds.ymin = np.nanmin([_mesh.bounds.ymin,self.global_bounds.ymin]) 174 | self.global_bounds.ymax = np.nanmax([_mesh.bounds.ymax,self.global_bounds.ymax]) 175 | self.global_bounds.zmin = np.nanmin([_mesh.bounds.zmin,self.global_bounds.zmin]) 176 | self.global_bounds.zmax = np.nanmax([_mesh.bounds.zmax,self.global_bounds.zmax]) 177 | 178 | self.global_bounds.length_x = np.abs(self.global_bounds.xmax - self.global_bounds.xmin) 179 | self.global_bounds.length_y = np.abs(self.global_bounds.ymax - self.global_bounds.ymin) 180 | self.global_bounds.length_z = np.abs(self.global_bounds.zmax - self.global_bounds.zmin) 181 | 182 | class ShaderProgram(): 183 | 184 | _glsl_vert = ''' 185 | #version 330 core 186 | layout (location = 0) in vec3 vert; 187 | 188 | uniform mat4 model; 189 | uniform mat4 proj; 190 | 191 | void main() { 192 | gl_Position = proj * model * vec4(vert, 1.0f); 193 | } 194 | ''' 195 | _glsl_frag = ''' 196 | #version 330 core 197 | out vec4 FragColor; 198 | 199 | void main() 200 | { 201 | FragColor = vec4(1.0); 202 | } 203 | ''' 204 | def __init__(self,): 205 | self._compileProgram() 206 | 207 | def _compileShaders(self,): 208 | self.vert_shader = shaders.compileShader(self._glsl_vert, GL_VERTEX_SHADER) 209 | self.frag_shader = shaders.compileShader(self._glsl_frag, GL_FRAGMENT_SHADER) 210 | 211 | def _compileProgram(self,): 212 | self._compileShaders() 213 | self.id = shaders.compileProgram(self.vert_shader,self.frag_shader) 214 | 215 | def use(self): 216 | glUseProgram(self.id) 217 | 218 | def delete(self): 219 | glDeleteProgram(self.id) 220 | 221 | def setInt(self, name, value): 222 | glUniform1i(glGetUniformLocation(self.id, name), int(value)) 223 | 224 | def setMat4(self, name, arr): 225 | glUniformMatrix4fv(glGetUniformLocation(self.id, name), 1, GL_FALSE, arr) 226 | 227 | def get_uniform_location(self, name): 228 | return glGetUniformLocation(self.id, name) 229 | 230 | class OpenGLSlicer(): 231 | 232 | def __init__(self,): 233 | self.VAO, self.vertVBO, self.maskVAO, self.maskVBO = 0, 0, 0, 0 234 | self.slice_fbo, self.slice_tex, self.slice_buf = 0, 0, 0 235 | self.width, self.height = None, None 236 | 237 | self.shader = None 238 | 239 | def begin(self,): 240 | """Begin the pyglet window which will render each slice""" 241 | self.window = pyglet.window.Window() 242 | self.window.set_vsync(False) 243 | self.window.set_mouse_visible(False) 244 | self.window.switch_to() 245 | 246 | def quit(self,): 247 | """Close the pyglet slice rendering window""" 248 | self.window.close() 249 | 250 | def _drawWindow(self,): 251 | """Draw slice to window""" 252 | self.window.switch_to() 253 | self.window.flip() 254 | self.window.clear() 255 | 256 | def slice(self,bounds:Bounds,mesh:BodyMesh,xy_voxel_size:float,z_voxel_size:None,square_xy:bool=True,slice_save_path:str=None,value=1.0,dtype:str='uint8'): 257 | """ 258 | Parameters 259 | ---------- 260 | bounds : Bounds 261 | 262 | mesh : BodyMesh 263 | mesh object to be voxelized 264 | 265 | layer_thickness : float 266 | thickness of each slice in voxelized array in same units as the mesh file 267 | 268 | xy_voxel_size : float 269 | side length of square voxel cross section in x-y plane in voxelized array in same units as the mesh file 270 | 271 | z_voxel_size : float, optional 272 | thickness of each slice in voxelized array in same units as the mesh file, defaults to z_voxel_size = xy_voxel_size. This allows for non-isotropic voxel size in the z-axis 273 | 274 | square_xy : bool, optional 275 | if the resulting voxel array should have equal number of voxels in x and y dimensions (i.e. square in x-y) 276 | 277 | slice_save_path : str, optional 278 | file path to directory in which to save .png images of each slice 279 | 280 | value : np.uint8, optional 281 | value of voxels in voxelized array 282 | 283 | Only homogeneous voxel values are implemented 284 | 285 | Returns 286 | ------- 287 | voxel_array : np.ndarray 288 | voxelized array of the selected mesh 289 | """ 290 | # check if z_voxel_size is specified, if not it should be equal to xy_voxel_size for isotropic voxels 291 | if z_voxel_size == None: 292 | z_voxel_size = xy_voxel_size 293 | 294 | # update bounds 295 | self.bounds = bounds 296 | 297 | 298 | 299 | # preallocate voxel_array for the slicer 300 | length_x_voxels = int(self.bounds.length_x/xy_voxel_size) 301 | length_y_voxels = int(self.bounds.length_y/xy_voxel_size) 302 | length_z_voxels = int(np.floor(self.bounds.length_z/z_voxel_size)) 303 | self.slicer_bounds = Bounds() 304 | 305 | if square_xy: 306 | # calculate diagonal such that the bounds will fit within the circle inscribed in the square grid 307 | bound_corner_vectors = np.array([[self.bounds.xmin,self.bounds.ymin], 308 | [self.bounds.xmin,self.bounds.ymax], 309 | [self.bounds.xmax,self.bounds.ymin], 310 | [self.bounds.xmax,self.bounds.ymax]]) 311 | norms = np.linalg.norm(bound_corner_vectors,axis=1) 312 | norm_max = np.max(norms,axis=0) 313 | diagonal_length = 2*norm_max 314 | diagonal_length_voxels = int(diagonal_length/xy_voxel_size) 315 | slicer_length_x = diagonal_length_voxels 316 | slicer_length_y = diagonal_length_voxels 317 | self.slicer_bounds.xmin = -norm_max 318 | self.slicer_bounds.xmax = norm_max 319 | self.slicer_bounds.ymin = -norm_max 320 | self.slicer_bounds.ymax = norm_max 321 | else: 322 | slicer_length_x = length_x_voxels 323 | slicer_length_y = length_y_voxels 324 | self.slicer_bounds.xmin = bounds.xmin 325 | self.slicer_bounds.xmax = bounds.xmax 326 | self.slicer_bounds.ymin = bounds.ymin 327 | self.slicer_bounds.ymax = bounds.ymax 328 | 329 | voxel_array = np.zeros((slicer_length_y,slicer_length_x,length_z_voxels),dtype=dtype) 330 | 331 | 332 | # prepare the OpenGL window for rendering at the selected x-y grid size i.e. the window size in pixels is the same as the length in x-y voxels 333 | self.prepareSlice(slicer_length_x,slicer_length_y) 334 | 335 | self._makeMasks(mesh) 336 | 337 | # setup OpenGL shader 338 | self.shader = ShaderProgram() 339 | 340 | # each "slice" is a view mesh cross section at the center of each voxel thickness i.e. layer_thickness/2. First slice is at layer_thickness/2 341 | translation = z_voxel_size/2 342 | 343 | 344 | for i in tqdm.tqdm(range(length_z_voxels)): 345 | 346 | self._draw(translation-EPSILON,mesh) 347 | 348 | if slice_save_path is not None: 349 | array = self._renderSlice(translation-EPSILON,mesh,os.path.join(slice_save_path, f'out{i+1:04d}.png')) 350 | else: 351 | array = self._renderSlice(translation-EPSILON,mesh) 352 | 353 | # on the interior of the cross section (where there is solid), record this as the input voxel value 354 | array[array == 255] = value 355 | 356 | # insert the cross section at the corresponding layer of the input 357 | voxel_array[:,:,i] = array 358 | 359 | # increment slicing plane by one layer thickness 360 | translation += z_voxel_size 361 | 362 | self._drawWindow() 363 | 364 | 365 | return voxel_array 366 | 367 | def _makeMasks(self,body_mesh:BodyMesh): 368 | # make VAO for drawing our mesh 369 | self.VAO = glGenVertexArrays(1) 370 | glBindVertexArray(self.VAO) 371 | 372 | # trimesh 373 | vertVBO = vbo.VBO(data=body_mesh.mesh.triangles.astype( 374 | GLfloat).tobytes(), usage='GL_STATIC_DRAW', target='GL_ARRAY_BUFFER') 375 | 376 | vertVBO.bind() 377 | vertVBO.copy_data() 378 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 379 | 3 * sizeof(GLfloat), vertVBO) 380 | glEnableVertexAttribArray(0) 381 | glBindVertexArray(0) 382 | 383 | # a mask vertex array for stencil buffer to subtract 384 | # uses global bounds of all meshes 385 | maskVert = np.array( 386 | [[self.slicer_bounds.xmin, self.slicer_bounds.ymin, 0], 387 | [self.slicer_bounds.xmax, self.slicer_bounds.ymin, 0], 388 | [self.slicer_bounds.xmax, self.slicer_bounds.ymax, 0], 389 | 390 | [self.slicer_bounds.xmin, self.slicer_bounds.ymin, 0], 391 | [self.slicer_bounds.xmax, self.slicer_bounds.ymax, 0], 392 | [self.slicer_bounds.xmin, self.slicer_bounds.ymax, 0]], dtype=GLfloat 393 | ) 394 | 395 | # make VAO for drawing mask 396 | self.maskVAO = glGenVertexArrays(1) 397 | glBindVertexArray(self.maskVAO) 398 | maskVBO = vbo.VBO(data=maskVert.tobytes(), 399 | usage='GL_STATIC_DRAW', target='GL_ARRAY_BUFFER') 400 | maskVBO.bind() 401 | maskVBO.copy_data() 402 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 403 | 3 * sizeof(GLfloat), maskVBO) 404 | glEnableVertexAttribArray(0) 405 | maskVBO.unbind() 406 | glBindVertexArray(0) 407 | 408 | def prepareSlice(self,width,height): 409 | self.width = width 410 | self.height = height 411 | self.slice_fbo = glGenFramebuffers(1) 412 | self.slice_tex = glGenTextures(1) 413 | self.slice_buf = glGenRenderbuffers(1) 414 | 415 | glBindTexture(GL_TEXTURE_2D, self.slice_tex) 416 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.width, 417 | self.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, None) 418 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) 419 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) 420 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) 421 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) 422 | glBindTexture(GL_TEXTURE_2D, 0) 423 | 424 | def _setModelLocation(self,translation): 425 | proj = orthoMatrix(self.slicer_bounds.xmin, self.slicer_bounds.xmax, 426 | self.slicer_bounds.ymin, self.slicer_bounds.ymax, 427 | -self.bounds.zmax, self.bounds.zmax, GLfloat) 428 | 429 | self.shader.setMat4("proj", proj) 430 | 431 | model = translationMatrix([0, 0, 0+translation], GLfloat) 432 | self.shader.setMat4("model", model) 433 | 434 | def _draw(self,translation,body_mesh:BodyMesh): 435 | 436 | glEnable(GL_STENCIL_TEST) 437 | glClearColor(0., 0., 0., 1.) 438 | glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) 439 | glBindVertexArray(self.VAO) 440 | self.shader.use() 441 | 442 | self._setModelLocation(translation) 443 | 444 | glEnable(GL_CULL_FACE) 445 | glCullFace(GL_FRONT) 446 | glStencilFunc(GL_ALWAYS, 0, 0xFF) 447 | glStencilOp(GL_KEEP, GL_KEEP, GL_INCR) 448 | glDrawArrays(GL_TRIANGLES, 0, body_mesh.num_of_verts) 449 | 450 | glCullFace(GL_BACK) 451 | glStencilOp(GL_KEEP, GL_KEEP, GL_DECR) 452 | glDrawArrays(GL_TRIANGLES, 0, body_mesh.num_of_verts) 453 | glDisable(GL_CULL_FACE) 454 | 455 | glClear(GL_COLOR_BUFFER_BIT) 456 | glBindVertexArray(self.maskVAO) 457 | glStencilFunc(GL_NOTEQUAL, 0, 0xFF) 458 | glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP) 459 | glDrawArrays(GL_TRIANGLES, 0, 6) 460 | glDisable(GL_STENCIL_TEST) 461 | 462 | def _renderSlice(self, translation, body_mesh:BodyMesh, filename=None): 463 | glEnable(GL_STENCIL_TEST) 464 | glViewport(0, 0, self.width, self.height) 465 | glBindFramebuffer(GL_FRAMEBUFFER, self.slice_fbo) 466 | glFramebufferTexture2D( 467 | GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self.slice_tex, 0) 468 | glBindRenderbuffer(GL_RENDERBUFFER, self.slice_buf) 469 | glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_STENCIL, 470 | self.width, self.height) 471 | glFramebufferRenderbuffer( 472 | GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, self.slice_buf) 473 | 474 | glClearColor(0.0, 0.0, 0.0, 1.0) 475 | glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) 476 | glBindVertexArray(self.VAO) 477 | self.shader.use() 478 | 479 | self._setModelLocation(translation) 480 | 481 | glEnable(GL_CULL_FACE) 482 | glCullFace(GL_FRONT) 483 | glStencilFunc(GL_ALWAYS, 0, 0xFF) 484 | glStencilOp(GL_KEEP, GL_KEEP, GL_INCR) 485 | glDrawArrays(GL_TRIANGLES, 0, body_mesh.num_of_verts) 486 | 487 | glCullFace(GL_BACK) 488 | glStencilOp(GL_KEEP, GL_KEEP, GL_DECR) 489 | glDrawArrays(GL_TRIANGLES, 0, body_mesh.num_of_verts) 490 | glDisable(GL_CULL_FACE) 491 | 492 | glClear(GL_COLOR_BUFFER_BIT) 493 | glBindVertexArray(self.maskVAO) 494 | glStencilFunc(GL_NOTEQUAL, 0, 0xFF) 495 | glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP) 496 | glDrawArrays(GL_TRIANGLES, 0, 6) 497 | glDisable(GL_STENCIL_TEST) 498 | 499 | data = glReadPixels(0, 0, self.width, self.height, 500 | GL_RED, GL_UNSIGNED_BYTE) 501 | 502 | data_numpy = np.frombuffer(data,dtype=np.uint8).reshape((self.height,self.width)) 503 | 504 | if filename is not None: 505 | image = Image.frombytes('L', (self.width, self.height), data, 'raw', 'L', 0, -1) 506 | image.save(filename) 507 | 508 | glBindFramebuffer(GL_FRAMEBUFFER, 0) 509 | glDisable(GL_STENCIL_TEST) 510 | glViewport(0, 0, self.width, self.height) 511 | 512 | return np.copy(data_numpy) -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # OpenGL Voxelizer 2 | 3 | This is a standalone voxelizer based on OpenGL. It is based on [OpenGL-STL-slicer](https://github.com/3D-Printing-for-Microfluidics/OpenGL-STL-slicer). 4 | 5 | ## Requirements 6 | - numpy 7 | - [tqdm](https://anaconda.org/conda-forge/tqdm) 8 | - [pyopengl](https://anaconda.org/anaconda/pyopengl) 9 | - [pyglet](https://anaconda.org/conda-forge/pyglet) 10 | - [trimesh](https://anaconda.org/conda-forge/trimesh) 11 | 12 | 13 | ## Notes 14 | 15 | The voxelizer uses the .stl file coordinates to determine the position of the voxelized mesh in the voxel array. When using the voxelizer for VAM, the mesh should be oriented in CAD space such that the axis which is to be along the rotation axis of the vial is the z-axis. 16 | The minimum bound of an object should be equal to zero in the z-axis, e.g., `min(mesh_z_coordinates)=0`. 17 | The x and y position in space is also considered. This is especially important when more 2 or more meshes that are somehow related in physical space are voxelized. 18 | Ideally, the center of the mesh should be near `(x,y) = (0,0)` and `min(mesh_z_coordinates)=0`. This will ensure that the voxel array size is not expanded to fit meshes that are located at a large `(x,y)` offset from the origin. 19 | 20 | ## Example 21 | 22 | The coil and core meshes have been exported from CAD as separate .stl files but since they were drawn within the same physical coordinate space, they have the same origin. Thus, when they are voxelized as in the example, the voxel arrays reflect their respective positions. 23 | 24 | ![combined_stl](images/combined_stl.JPG) 25 | ``` python 26 | import openglvoxelizer 27 | 28 | vox = openglvoxelizer.Voxelizer() 29 | vox.addMeshes({ 30 | r'coil.stl':'print_body', 31 | r'core.stl':'attenuating_body', 32 | }) 33 | 34 | print_body = vox.voxelize('print_body',xy_voxel_size=0.01,z_voxel_size=0.01,voxel_value=1) 35 | attenuating_body = vox.voxelize('attenuating_body',xy_voxel_size=0.01,z_voxel_size=0.01,voxel_value=2) 36 | 37 | combined = print_body + attenuating_body 38 | ``` 39 | 40 | The voxel arrays can be saved to the disk with numpy 41 | ``` python 42 | import numpy as np 43 | np.save(r'combined_array.npy',combined) 44 | np.save(r'print_body_array.npy',print_body) 45 | np.save(r'attenuating_body_array.npy',attenuating_body) 46 | ``` 47 | 48 | Note that the voxel value for the coil was set to be 1 and the core was set to be 2. This is why the colors of voxels in each body appear differently: voxel_value = 1 is blue, voxel_value = 2 is red. The array was visualized using [Tomviz](https://tomviz.org/). 49 | 50 | ![combined_array](images/combined_array_isotropic.JPG) 51 | 52 | Non-isotropic voxel sizes in x-y and z dimensions are also supported. Below, the z voxel size is 10 times larger than the x-y voxel size. 53 | 54 | ``` python 55 | print_body = vox.voxelize('print_body',xy_voxel_size=0.01,z_voxel_size=0.1,voxel_value=1) 56 | attenuating_body = vox.voxelize('attenuating_body',xy_voxel_size=0.01,z_voxel_size=0.1,voxel_value=2) 57 | 58 | combined = print_body + attenuating_body 59 | ``` 60 | 61 | ![combined_array](images/combined_array_nonisotropic.JPG) --------------------------------------------------------------------------------