├── README.md ├── binvox_rw.py ├── blender_utils.py ├── examples ├── 0.binvox ├── 0.off ├── 0.txt ├── render_binvox.sh ├── render_off.sh ├── render_off_txt.sh └── render_txt.sh ├── import_off.py ├── mesh.py ├── obj_to_off.py ├── off_to_obj.py ├── point_cloud.py ├── render_binvox.py ├── render_off.py ├── render_off_txt.py ├── render_txt.py ├── screenshot.jpg ├── txt_to_ply.py ├── write_binvox.py └── write_txt.py /README.md: -------------------------------------------------------------------------------- 1 | # Visualizing 3D Data with Blender in Python 2 | 3 | This repository contains various Python scripts for visualizing 3D data, 4 | in the form of triangular meshes, point clouds or occupancy grids, 5 | using [Blender](https://www.blender.org/)'s [Python API](https://docs.blender.org/api/current/). 6 | 7 | **Acknowledgement: The code is based in some parts on work by 8 | [Simon Donné](https://avg.is.tuebingen.mpg.de/person/sdonne).** 9 | 10 | If you use this code, consider citing: 11 | 12 | @inproceedings{Stutz2018CVPR, 13 | title = {Learning 3D Shape Completion from Laser Scan Data with Weak Supervision }, 14 | author = {Stutz, David and Geiger, Andreas}, 15 | booktitle = {IEEE Conference on Computer Vision and Pattern Recognition (CVPR)}, 16 | publisher = {IEEE Computer Society}, 17 | year = {2018} 18 | } 19 | @misc{Stutz2017, 20 | author = {David Stutz}, 21 | title = {Learning Shape Completion from Bounding Boxes with CAD Shape Priors}, 22 | month = {September}, 23 | year = {2017}, 24 | institution = {RWTH Aachen University}, 25 | address = {Aachen, Germany}, 26 | howpublished = {http://davidstutz.de/}, 27 | } 28 | 29 | ![Example of visualizations.](screenshot.jpg?raw=true "Example of visualizations.") 30 | 31 | ## Requirements 32 | 33 | The code was tested with [Blender](https://www.blender.org/) 2.78 or higher. 34 | Python should be installed locally and should also ship Python 3.5 with 35 | a custom NumPy installation. 36 | 37 | All remaining requirements, essentially [dimatura/binvox-rw-py](https://github.com/dimatura/binvox-rw-py) 38 | and [alextsui05/blender-off-addon](https://github.com/alextsui05/blender-off-addon) 39 | are included. 40 | 41 | ## File Formats 42 | 43 | The following file formats are assumed: 44 | 45 | * [OFF](https://en.wikipedia.org/wiki/OFF_(file_format)): file format for triangular meshes. 46 | * [BINVOX](http://www.patrickmin.com/binvox/): file format for occupancy grids. 47 | * A custom TXT format for point clouds. 48 | 49 | The provided OFF files can be visualized using [MeshLab](http://www.meshlab.net/). 50 | Scripts for converting OBJ files to OFF files as well as writing BINVOX and TXT files 51 | are provided. 52 | 53 | The custom TXT file format looks as follows: 54 | 55 | n_points 56 | p1_x p1_y p2_z 57 | p2_x p2_y p2_z 58 | ... 59 | 60 | Use the following Python scripts for conversion: 61 | 62 | * `off_to_obj.py` and `obj_to_off.py` to conversion between OFF and OBJ. 63 | * `txt_to_ply.py` to convert a TXT point cloud to PLY format. 64 | 65 | Examples of using the included Python code for writing TXT or BINVOX files 66 | are provided in `write_txt.py` and `write_binvox.py`. 67 | 68 | ## Examples 69 | 70 | `examples/` includes some examples; these should be rune out of the root folder: 71 | 72 | ./examples/render_off.sh 73 | ./examples/render_binvox.sh 74 | ./examples/render_txt.sh 75 | 76 | All examples create `examples/0.png`. 77 | 78 | ## Usage 79 | 80 | The provided examples essentially use the functionality provided in `blender_utils.py`; 81 | the general approach is as follows: 82 | 83 | # Import utils. 84 | from blender_utils import * 85 | 86 | # Initialize and set camera target. 87 | # Check the function to adapt the light sources or camera(s). 88 | camera_target = initialize() 89 | 90 | # Create a material. 91 | # First parameter is the name, second is the diffuse color in rgb, 92 | # third argument is the alpha channel and last whether to cast/receive shadow. 93 | off_material = make_material('BRC_Material_Mesh', (0.66, 0.45, 0.23), 0.8, True) 94 | 95 | # Load an OFF file, alternatively, use load_binvox or load_txt. 96 | # In any case, we need the previously defined material. 97 | # The mesh provided in examples/0.off is scaled to [0,32]^3, 98 | # so we scale it by 1/32 = 0.03125 and then translate it to the 99 | # origin using (-0.5, -0.5, -0.5). 100 | # Finally we switch y and z axes. 101 | load_off(args.off, off_material, (-0.5, -0.5, -0.5), 0.03125, 'xzy') 102 | 103 | # Set rotation of camera around the origin, implicitly defines the locatio 104 | # together with the distance. 105 | rotation = (5, 0, -55) 106 | distance = 0.5 107 | 108 | # Render all loaded objects using the camera_target obtained above and 109 | # the output file. 110 | render(camera_target, output_file, rotation, distance) 111 | 112 | ## License 113 | 114 | License for source code corresponding to: 115 | 116 | D. Stutz, A. Geiger. **Learning 3D Shape Completion from Laser Scan Data with Weak Supervision.** IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 2018. 117 | 118 | Note that the source code is based on the following projects for which separate licenses apply: 119 | 120 | * [ModelNet](http://modelnet.cs.princeton.edu/) 121 | * [alextsui05/blender-off-addon](https://github.com/alextsui05/blender-off-addon) 122 | * [dimatura/binvox-rw-py](https://github.com/dimatura/binvox-rw-py) 123 | 124 | Copyright (c) 2018 David Stutz, Max-Planck-Gesellschaft 125 | 126 | **Please read carefully the following terms and conditions and any accompanying documentation before you download and/or use this software and associated documentation files (the "Software").** 127 | 128 | The authors hereby grant you a non-exclusive, non-transferable, free of charge right to copy, modify, merge, publish, distribute, and sublicense the Software for the sole purpose of performing non-commercial scientific research, non-commercial education, or non-commercial artistic projects. 129 | 130 | Any other use, in particular any use for commercial purposes, is prohibited. This includes, without limitation, incorporation in a commercial product, use in a commercial service, or production of other artefacts for commercial purposes. 131 | 132 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 133 | 134 | You understand and agree that the authors are under no obligation to provide either maintenance services, update services, notices of latent defects, or corrections of defects with regard to the Software. The authors nevertheless reserve the right to update, modify, or discontinue the Software at any time. 135 | 136 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. You agree to cite the corresponding papers (see above) in documents and papers that report on research using the Software. 137 | -------------------------------------------------------------------------------- /binvox_rw.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2012 Daniel Maturana 2 | # This file is part of binvox-rw-py. 3 | # 4 | # binvox-rw-py is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # binvox-rw-py is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with binvox-rw-py. If not, see . 16 | # 17 | 18 | """ 19 | Binvox to Numpy and back. 20 | 21 | 22 | >>> import numpy as np 23 | >>> import binvox_rw 24 | >>> with open('chair.binvox', 'rb') as f: 25 | ... m1 = binvox_rw.read_as_3d_array(f) 26 | ... 27 | >>> m1.dims 28 | [32, 32, 32] 29 | >>> m1.scale 30 | 41.133000000000003 31 | >>> m1.translate 32 | [0.0, 0.0, 0.0] 33 | >>> with open('chair_out.binvox', 'wb') as f: 34 | ... m1.write(f) 35 | ... 36 | >>> with open('chair_out.binvox', 'rb') as f: 37 | ... m2 = binvox_rw.read_as_3d_array(f) 38 | ... 39 | >>> m1.dims==m2.dims 40 | True 41 | >>> m1.scale==m2.scale 42 | True 43 | >>> m1.translate==m2.translate 44 | True 45 | >>> np.all(m1.data==m2.data) 46 | True 47 | 48 | >>> with open('chair.binvox', 'rb') as f: 49 | ... md = binvox_rw.read_as_3d_array(f) 50 | ... 51 | >>> with open('chair.binvox', 'rb') as f: 52 | ... ms = binvox_rw.read_as_coord_array(f) 53 | ... 54 | >>> data_ds = binvox_rw.dense_to_sparse(md.data) 55 | >>> data_sd = binvox_rw.sparse_to_dense(ms.data, 32) 56 | >>> np.all(data_sd==md.data) 57 | True 58 | >>> # the ordering of elements returned by numpy.nonzero changes with axis 59 | >>> # ordering, so to compare for equality we first lexically sort the voxels. 60 | >>> np.all(ms.data[:, np.lexsort(ms.data)] == data_ds[:, np.lexsort(data_ds)]) 61 | True 62 | """ 63 | 64 | import numpy as np 65 | 66 | class Voxels(object): 67 | """ Holds a binvox model. 68 | data is either a three-dimensional numpy boolean array (dense representation) 69 | or a two-dimensional numpy float array (coordinate representation). 70 | 71 | dims, translate and scale are the model metadata. 72 | 73 | dims are the voxel dimensions, e.g. [32, 32, 32] for a 32x32x32 model. 74 | 75 | scale and translate relate the voxels to the original model coordinates. 76 | 77 | To translate voxel coordinates i, j, k to original coordinates x, y, z: 78 | 79 | x_n = (i+.5)/dims[0] 80 | y_n = (j+.5)/dims[1] 81 | z_n = (k+.5)/dims[2] 82 | x = scale*x_n + translate[0] 83 | y = scale*y_n + translate[1] 84 | z = scale*z_n + translate[2] 85 | 86 | """ 87 | 88 | def __init__(self, data, dims, translate, scale): 89 | self.data = data 90 | self.dims = dims 91 | self.translate = translate 92 | self.scale = scale 93 | 94 | def clone(self): 95 | data = self.data.copy() 96 | dims = self.dims[:] 97 | translate = self.translate[:] 98 | return Voxels(data, dims, translate, self.scale) 99 | 100 | def write(self, fp): 101 | write(self, fp) 102 | 103 | def read_header(fp): 104 | """ Read binvox header. Mostly meant for internal use. 105 | """ 106 | line = fp.readline().strip() 107 | if not line.startswith(b'#binvox'): 108 | raise IOError('Not a binvox file') 109 | dims = list(map(int, fp.readline().strip().split(b' ')[1:])) 110 | translate = list(map(float, fp.readline().strip().split(b' ')[1:])) 111 | scale = list(map(float, fp.readline().strip().split(b' ')[1:]))[0] 112 | line = fp.readline() 113 | return dims, translate, scale 114 | 115 | def read_as_3d_array(fp): 116 | """ Read binary binvox format as array. 117 | 118 | Returns the model with accompanying metadata. 119 | 120 | Voxels are stored in a three-dimensional numpy array, which is simple and 121 | direct, but may use a lot of memory for large models. (Storage requirements 122 | are 8*(d^3) bytes, where d is the dimensions of the binvox model. Numpy 123 | boolean arrays use a byte per element). 124 | 125 | Doesn't do any checks on input except for the '#binvox' line. 126 | """ 127 | dims, translate, scale = read_header(fp) 128 | raw_data = np.frombuffer(fp.read(), dtype=np.uint8) 129 | # if just using reshape() on the raw data: 130 | # indexing the array as array[i,j,k], the indices map into the 131 | # coords as: 132 | # i -> x 133 | # j -> z 134 | # k -> y 135 | # if fix_coords is true, then data is rearranged so that 136 | # mapping is 137 | # i -> x 138 | # j -> y 139 | # k -> z 140 | values, counts = raw_data[::2], raw_data[1::2] 141 | data = np.repeat(values, counts).astype(np.bool) 142 | data = data.reshape(dims) 143 | 144 | return Voxels(data, dims, translate, scale) 145 | 146 | def read_as_coord_array(fp, fix_coords=True): 147 | """ Read binary binvox format as coordinates. 148 | 149 | Returns binvox model with voxels in a "coordinate" representation, i.e. an 150 | 3 x N array where N is the number of nonzero voxels. Each column 151 | corresponds to a nonzero voxel and the 3 rows are the (x, z, y) coordinates 152 | of the voxel. (The odd ordering is due to the way binvox format lays out 153 | data). Note that coordinates refer to the binvox voxels, without any 154 | scaling or translation. 155 | 156 | Use this to save memory if your model is very sparse (mostly empty). 157 | 158 | Doesn't do any checks on input except for the '#binvox' line. 159 | """ 160 | dims, translate, scale = read_header(fp) 161 | raw_data = np.frombuffer(fp.read(), dtype=np.uint8) 162 | 163 | values, counts = raw_data[::2], raw_data[1::2] 164 | 165 | sz = np.prod(dims) 166 | index, end_index = 0, 0 167 | end_indices = np.cumsum(counts) 168 | indices = np.concatenate(([0], end_indices[:-1])).astype(end_indices.dtype) 169 | 170 | values = values.astype(np.bool) 171 | indices = indices[values] 172 | end_indices = end_indices[values] 173 | 174 | nz_voxels = [] 175 | for index, end_index in zip(indices, end_indices): 176 | nz_voxels.extend(range(index, end_index)) 177 | nz_voxels = np.array(nz_voxels) 178 | # TODO are these dims correct? 179 | # according to docs, 180 | # index = x * wxh + z * width + y; // wxh = width * height = d * d 181 | 182 | x = nz_voxels / (dims[0]*dims[1]) 183 | zwpy = nz_voxels % (dims[0]*dims[1]) # z*w + y 184 | z = zwpy / dims[0] 185 | y = zwpy % dims[0] 186 | 187 | data = np.vstack((x, y, z)) 188 | 189 | #return Voxels(data, dims, translate, scale) 190 | return Voxels(np.ascontiguousarray(data), dims, translate, scale) 191 | 192 | def dense_to_sparse(voxel_data, dtype=np.int): 193 | """ From dense representation to sparse (coordinate) representation. 194 | No coordinate reordering. 195 | """ 196 | if voxel_data.ndim!=3: 197 | raise ValueError('voxel_data is wrong shape; should be 3D array.') 198 | return np.asarray(np.nonzero(voxel_data), dtype) 199 | 200 | def sparse_to_dense(voxel_data, dims, dtype=np.bool): 201 | if voxel_data.ndim!=2 or voxel_data.shape[0]!=3: 202 | raise ValueError('voxel_data is wrong shape; should be 3xN array.') 203 | if np.isscalar(dims): 204 | dims = [dims]*3 205 | dims = np.atleast_2d(dims).T 206 | # truncate to integers 207 | xyz = voxel_data.astype(np.int) 208 | # discard voxels that fall outside dims 209 | valid_ix = ~np.any((xyz < 0) | (xyz >= dims), 0) 210 | xyz = xyz[:,valid_ix] 211 | out = np.zeros(dims.flatten(), dtype=dtype) 212 | out[tuple(xyz)] = True 213 | return out 214 | 215 | #def get_linear_index(x, y, z, dims): 216 | #""" Assuming xzy order. (y increasing fastest. 217 | #TODO ensure this is right when dims are not all same 218 | #""" 219 | #return x*(dims[1]*dims[2]) + z*dims[1] + y 220 | 221 | def write(voxel_model, fp): 222 | """ Write binary binvox format. 223 | 224 | Note that when saving a model in sparse (coordinate) format, it is first 225 | converted to dense format. 226 | 227 | Doesn't check if the model is 'sane'. 228 | 229 | """ 230 | if voxel_model.data.ndim==2: 231 | # TODO avoid conversion to dense 232 | dense_voxel_data = sparse_to_dense(voxel_model.data, voxel_model.dims) 233 | else: 234 | dense_voxel_data = voxel_model.data 235 | 236 | fp.write('#binvox 1\n') 237 | fp.write('dim '+' '.join(map(str, voxel_model.dims))+'\n') 238 | fp.write('translate '+' '.join(map(str, voxel_model.translate))+'\n') 239 | fp.write('scale '+str(voxel_model.scale)+'\n') 240 | fp.write('data\n') 241 | 242 | voxels_flat = dense_voxel_data.flatten() 243 | 244 | # keep a sort of state machine for writing run length encoding 245 | state = voxels_flat[0] 246 | ctr = 0 247 | for c in voxels_flat: 248 | if c==state: 249 | ctr += 1 250 | # if ctr hits max, dump 251 | if ctr==255: 252 | fp.write(chr(state)) 253 | fp.write(chr(ctr)) 254 | ctr = 0 255 | else: 256 | # if switch state, dump 257 | fp.write(chr(state)) 258 | fp.write(chr(ctr)) 259 | state = c 260 | ctr = 1 261 | # flush out remainders 262 | if ctr > 0: 263 | fp.write(chr(state)) 264 | fp.write(chr(ctr)) 265 | 266 | if __name__ == '__main__': 267 | import doctest 268 | doctest.testmod() 269 | -------------------------------------------------------------------------------- /blender_utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | 5 | class LogLevel: 6 | """ 7 | Defines color of log message. 8 | """ 9 | 10 | INFO = '\033[94m' 11 | """ (string) Blue. """ 12 | WARNING = '\033[93m' 13 | """ (string) Yellow. """ 14 | ERROR = '\033[91m\033[1m' 15 | """ (string) Red. """ 16 | ENDC = '\033[0m' 17 | """ (string) End of color. """ 18 | 19 | 20 | def log(output,level=LogLevel.INFO): 21 | """ 22 | Log message. 23 | 24 | :param output: message 25 | :param level: LogLevel 26 | """ 27 | 28 | sys.stderr.write(level) 29 | sys.stderr.write(str(output)) 30 | sys.stderr.write(LogLevel.ENDC) 31 | sys.stderr.write("\n") 32 | sys.stderr.flush() 33 | 34 | 35 | # This makes sure that Blender's NumPy is loaded first. 36 | # The path needs to be adapted before usage. 37 | # Example: 38 | # blender_package_path = '~/blender-2.79-linux-glibc219-x86_64/2.79/python/lib/python3.5/site-packages/' 39 | if not 'BLENDER_PACKAGE_PATH' in globals(): 40 | BLENDER_PACKAGE_PATH = None 41 | BLENDER_PACKAGE_PATH = '/BS/dstutz/work/dev-box/blender-2.79-linux-glibc219-x86_64/2.79/python/lib/python3.5/site-packages/' 42 | if BLENDER_PACKAGE_PATH is None: 43 | log('Open blender_utils.py and set BLENDER_PACKAGE_PATH before usage, check documentation!', LogLevel.ERROR) 44 | exit() 45 | if not os.path.exists(BLENDER_PACKAGE_PATH): 46 | log('The set BLENDER_PACKAGE_PATH does not exist, check documentation!', LogLevel.ERROR) 47 | exit() 48 | 49 | sys.path.insert(1, os.path.realpath(BLENDER_PACKAGE_PATH)) 50 | 51 | import bpy 52 | import bmesh 53 | import math 54 | import numpy as np 55 | 56 | import binvox_rw 57 | import import_off 58 | import_off.register() 59 | 60 | sphere_base_mesh = None 61 | cube_base_mesh = None 62 | 63 | 64 | def initialize(width=512, height=448): 65 | """ 66 | Setup scene, camer and lighting. 67 | 68 | :param width: width of rendered image 69 | :param height: height of rendered image 70 | :return: camera target 71 | """ 72 | 73 | # First, the base meshes (sphere and cube) are set, 74 | # these are later used to display point clouds or occupancy grids. 75 | bpy.ops.mesh.primitive_ico_sphere_add() 76 | global sphere_base_mesh 77 | sphere_base_mesh = bpy.context.scene.objects.active.data.copy() 78 | for face in sphere_base_mesh.polygons: 79 | face.use_smooth = True 80 | 81 | bpy.ops.mesh.primitive_cube_add() 82 | global cube_base_mesh 83 | cube_base_mesh = bpy.context.scene.objects.active.data.copy() 84 | 85 | # Delete current scene, except for the camera and the lamp 86 | for obj in bpy.data.objects: 87 | if str(obj.name) in ['Camera']: 88 | continue 89 | obj.select = True 90 | bpy.ops.object.delete() 91 | 92 | scene = bpy.context.scene 93 | 94 | # Setup the camera, location can also be influenced later, 95 | # these are only defaults. 96 | cam = scene.objects['Camera'] 97 | cam.location = (0, 3.0, 1.0) 98 | cam.data.lens = 35 99 | cam.data.sensor_width = 32 100 | cam.data.sensor_height = 32 101 | cam_constraint = cam.constraints.new(type='TRACK_TO') 102 | cam_constraint.track_axis = 'TRACK_NEGATIVE_Z' 103 | cam_constraint.up_axis = 'UP_Y' 104 | 105 | def parent_obj_to_camera(b_camera): 106 | """ 107 | Utility function defining the target of the camera as the origin. 108 | 109 | :param b_camera: camera object 110 | :return: origin object 111 | """ 112 | 113 | origin = (0, 0, 0) 114 | b_empty = bpy.data.objects.new('Empty', None) 115 | b_empty.location = origin 116 | b_camera.parent = b_empty # setup parenting 117 | 118 | scn = bpy.context.scene 119 | scn.objects.link(b_empty) 120 | scn.objects.active = b_empty 121 | return b_empty 122 | 123 | # Sets up the camera and defines its target. 124 | camera_target = parent_obj_to_camera(cam) 125 | cam_constraint.target = camera_target 126 | 127 | # For nicer visualization, several light locations are defined. 128 | # See the documentation for details, these should be edited based 129 | # on preferences. 130 | locations = [ 131 | (-0.98382, 0.445997, 0.526505), 132 | (-0.421806, -0.870784, 0.524944), 133 | (0.075576, -0.960128, 0.816464), 134 | (0.493553, -0.57716, 0.928208), 135 | (0.787275, -0.256822, 0.635172), 136 | (1.01032, 0.148764, 0.335078) 137 | ] 138 | 139 | for i in range(len(locations)): 140 | # We only use point spot lamps centered at the given locations 141 | # and without any specific rotation (see euler angles below). 142 | lamp_data = bpy.data.lamps.new(name='Point Lamp ' + str(i), type='POINT') 143 | lamp_data.shadow_method = 'RAY_SHADOW' 144 | lamp_data.shadow_ray_sample_method = 'CONSTANT_QMC' 145 | lamp_data.use_shadow = True 146 | lamp_data.shadow_soft_size = 1e6 147 | lamp_data.distance = 2 148 | lamp_data.energy = 0.1 149 | lamp_data.use_diffuse = True 150 | lamp_data.use_specular = True 151 | lamp_data.falloff_type = 'CONSTANT' 152 | 153 | lamp_object = bpy.data.objects.new(name='Spot Lamp ' + str(i), object_data=lamp_data) 154 | scene.objects.link(lamp_object) 155 | lamp_object.location[0] = locations[i][0] 156 | lamp_object.location[1] = locations[i][1] 157 | lamp_object.location[2] = locations[i][2] 158 | lamp_object.rotation_euler[0] = 0 159 | lamp_object.rotation_euler[1] = 0 160 | lamp_object.rotation_euler[2] = 0 161 | lamp_object.parent = camera_target 162 | 163 | # This tries to use CUDA rendering if possible. 164 | try: 165 | if (2, 78, 0) <= bpy.app.version: 166 | # https://blender.stackexchange.com/questions/5281/blender-sets-compute-device-cuda-but-doesnt-use-it-for-actual-render-on-ec2 167 | bpy.context.user_preferences.addons['cycles'].preferences.compute_device_type = 'CUDA' 168 | bpy.context.user_preferences.addons['cycles'].preferences.devices[0].use = True 169 | else: 170 | bpy.context.user_preferences.system.compute_device_type = 'CUDA' 171 | except TypeError: 172 | pass 173 | 174 | scene.render.use_file_extension = False 175 | scene.render.resolution_x = width 176 | scene.render.resolution_y = height 177 | scene.render.resolution_percentage = 100 178 | scene.render.use_antialiasing = True 179 | scene.render.use_shadows = True 180 | world = bpy.context.scene.world 181 | world.zenith_color = [1.0, 1.0, 1.0] 182 | world.horizon_color = [1.0, 1.0, 1.0] 183 | scene.render.alpha_mode = 'SKY' 184 | world.light_settings.use_environment_light = True 185 | world.light_settings.environment_color = 'PLAIN' 186 | world.light_settings.environment_energy = 0.5 187 | 188 | return camera_target 189 | 190 | 191 | def make_material(name, diffuse, alpha, shadow=False): 192 | """ 193 | Creates a material with the given diffuse and alpha. If shadow is true the 194 | object casts and receives shadows. 195 | 196 | :param name: name of material 197 | :param diffuse: diffuse color (in rgb) 198 | :param alpha: alpha (float in [0,1]) 199 | :param shadow: whether to cast/receive shadows 200 | :return: material 201 | """ 202 | 203 | material = bpy.data.materials.new(name) 204 | material.diffuse_color = diffuse 205 | material.diffuse_shader = 'LAMBERT' 206 | material.diffuse_intensity = 1 207 | material.specular_color = (1, 1, 1) 208 | material.specular_shader = 'COOKTORR' 209 | material.specular_intensity = 2 210 | material.alpha = alpha 211 | material.use_transparency = True 212 | material.ambient = 1.0 213 | 214 | material.use_cast_shadows = shadow 215 | material.use_shadows = shadow 216 | 217 | return material 218 | 219 | 220 | def load_off(off_file, material, offset=(0, 0, 0), scale=1, axes='xyz'): 221 | """ 222 | Loads a triangular mesh from an OFF file. For pre-processing, mesh.py can be used; 223 | the function still allows to define an offset (to translate the mesh) and a scale. 224 | 225 | The axes parameter defines the order of the axes. Using xzy, for example, assumes 226 | that the first coordinate is x, the second is z and the third is y. 227 | 228 | **Note that first, the axes are swapper, then the OFF is scaled and finally translated.** 229 | 230 | :param off_file: path to OFF file 231 | :param material: previously defined material 232 | :param offset: offset after scaling 233 | :param scale: scaling 234 | :param axes: axes definition 235 | """ 236 | 237 | # This used import_off.py, see README for license. 238 | bpy.ops.import_mesh.off(filepath=off_file) 239 | 240 | assert len(offset) == 3 241 | assert scale > 0 242 | assert len(axes) == 3 243 | 244 | x_index = axes.find('x') 245 | y_index = axes.find('y') 246 | z_index = axes.find('z') 247 | 248 | assert x_index >= 0 and x_index < 3 249 | assert y_index >= 0 and y_index < 3 250 | assert z_index >= 0 and z_index < 3 251 | assert x_index != y_index and x_index != z_index and y_index != z_index 252 | 253 | for obj in bpy.context.scene.objects: 254 | 255 | # obj.name contains the group name of a group of faces, see http://paulbourke.net/dataformats/obj/ 256 | # every mesh is of type 'MESH', this works not only for ShapeNet but also for 'simple' 257 | # obj files 258 | if obj.type == 'MESH' and not 'BRC' in obj.name: 259 | 260 | # change color 261 | # this is based on https://stackoverflow.com/questions/4644650/blender-how-do-i-add-a-color-to-an-object 262 | # but needed changing a lot of attributes according to documentation 263 | obj.data.materials.append(material) 264 | 265 | for vertex in obj.data.vertices: 266 | # make a copy, otherwise axes switching does not work 267 | vertex_copy = (vertex.co[0], vertex.co[1], vertex.co[2]) 268 | 269 | # First, swap the axes, then scale and offset. 270 | vertex.co[0] = vertex_copy[x_index] 271 | vertex.co[1] = vertex_copy[y_index] 272 | vertex.co[2] = vertex_copy[z_index] 273 | 274 | vertex.co[0] = vertex.co[0] * scale + offset[0] 275 | vertex.co[1] = vertex.co[1] * scale + offset[1] 276 | vertex.co[2] = vertex.co[2] * scale + offset[2] 277 | 278 | obj.name = 'BRC_' + obj.name 279 | 280 | 281 | def load_txt(txt_file, radius, material, offset=(0, 0, 0), scale=1, axes='xyz'): 282 | """ 283 | Load a point cloud from txt file, see the documentation for the format. 284 | Additionally, the radius of the points, an offset and a scale can be defined, for details 285 | on the parameters also see load_off. 286 | 287 | :param txt_file: path to TXT file 288 | :param radius: radius of rendered points/spheres 289 | :param material: previously defined material 290 | :param offset: offset 291 | :param scale: scale 292 | :param axes: axes definition 293 | :return: 294 | """ 295 | 296 | global sphere_base_mesh 297 | 298 | assert len(offset) == 3 299 | assert scale > 0 300 | assert len(axes) == 3 301 | 302 | x_index = axes.find('x') 303 | y_index = axes.find('y') 304 | z_index = axes.find('z') 305 | 306 | assert x_index >= 0 and x_index < 3 307 | assert y_index >= 0 and y_index < 3 308 | assert z_index >= 0 and z_index < 3 309 | assert x_index != y_index and x_index != z_index and y_index != z_index 310 | 311 | voxel_file = open(txt_file, 'r') 312 | voxel_lines = voxel_file.readlines() 313 | voxel_file.close() 314 | 315 | mesh = bmesh.new() 316 | for line in voxel_lines: 317 | vals = line.split(' ') 318 | if not line.startswith('#') and line.strip() != '' and len(vals) >= 3: 319 | location = ( 320 | float(vals[x_index]) * scale + offset[0], 321 | float(vals[y_index]) * scale + offset[1], 322 | float(vals[z_index]) * scale + offset[2] 323 | ) 324 | 325 | m = sphere_base_mesh.copy() 326 | for vertex in m.vertices: 327 | vertex.co[0] = vertex.co[0] * radius + location[0] 328 | vertex.co[1] = vertex.co[1] * radius + location[1] 329 | vertex.co[2] = vertex.co[2] * radius + location[2] 330 | 331 | mesh.from_mesh(m) 332 | 333 | mesh2 = bpy.data.meshes.new('Mesh') 334 | mesh.to_mesh(mesh2) 335 | 336 | obj = bpy.data.objects.new('BRC_Point_Cloud', mesh2) 337 | obj.data.materials.append(material) 338 | obj.active_material_index = 0 339 | obj.active_material = material 340 | 341 | bpy.context.scene.objects.link(obj) 342 | 343 | 344 | def load_binvox(binvox_file, radius, material, offset, scale, axes): 345 | """ 346 | Load a binvox file, see binvox_rw.py for format. Again, radius of the cubes, material, offset and scale 347 | can be defined as in load_off. 348 | 349 | :param binvox_file: path to binvox file 350 | :param radius: radius, i.e. side length, of cubes 351 | :param material: previously defined material 352 | :param offset: offset 353 | :param scale: scale 354 | :param axes: axes definition 355 | :return: 356 | """ 357 | 358 | global cube_base_mesh 359 | 360 | assert len(offset) == 3 361 | assert len(scale) == 3 362 | assert len(axes) == 3 363 | 364 | x_index = axes.find("x") 365 | y_index = axes.find("y") 366 | z_index = axes.find("z") 367 | 368 | assert x_index >= 0 and x_index < 3 369 | assert y_index >= 0 and y_index < 3 370 | assert z_index >= 0 and z_index < 3 371 | assert x_index != y_index and x_index != z_index and y_index != z_index 372 | 373 | with open(binvox_file, 'rb') as f: 374 | model = binvox_rw.read_as_3d_array(f) 375 | 376 | points = np.where(model.data) 377 | locations = np.zeros((points[0].shape[0], 3), dtype=float) 378 | locations[:, 0] = (points[x_index][:] + 0.5) / model.data.shape[x_index] 379 | locations[:, 1] = (points[y_index][:] + 0.5) / model.data.shape[y_index] 380 | locations[:, 2] = (points[z_index][:] + 0.5) / model.data.shape[z_index] 381 | locations[:, 0] -= 0.5 382 | locations[:, 1] -= 0.5 383 | locations[:, 2] -= 0.5 384 | 385 | locations[:, 0] = locations[:, 0] * scale[0] + offset[0] 386 | locations[:, 1] = locations[:, 1] * scale[1] + offset[1] 387 | locations[:, 2] = locations[:, 2] * scale[2] + offset[2] 388 | 389 | mesh = bmesh.new() 390 | for i in range(locations.shape[0]): 391 | m = cube_base_mesh.copy() 392 | for vertex in m.vertices: 393 | vertex.co[0] = vertex.co[0] * radius + locations[i, 0] 394 | vertex.co[1] = vertex.co[1] * radius + locations[i, 1] 395 | vertex.co[2] = vertex.co[2] * radius + locations[i, 2] 396 | 397 | mesh.from_mesh(m) 398 | 399 | mesh2 = bpy.data.meshes.new('Mesh') 400 | mesh.to_mesh(mesh2) 401 | 402 | obj = bpy.data.objects.new('BRC_Occupancy', mesh2) 403 | obj.data.materials.append(material) 404 | obj.active_material_index = 0 405 | obj.active_material = material 406 | 407 | bpy.context.scene.objects.link(obj) 408 | 409 | 410 | def render(camera_target, output_file, rotation, distance): 411 | """ 412 | Render all loaded objects into the given object files. Additionally, the 413 | rotation of the camera around the origin and the distance can be defined. 414 | 415 | The first argument is the camera_target returned from initialize(). 416 | 417 | :param camera_target: returned by initialize() 418 | :param output_file: path to output file 419 | :param rotation: rotation of camera 420 | :param distance: distance to target 421 | """ 422 | 423 | bpy.context.scene.render.filepath = output_file 424 | 425 | camera_target.rotation_euler[0] = math.radians(rotation[0]) 426 | camera_target.rotation_euler[1] = math.radians(rotation[1]) 427 | camera_target.rotation_euler[2] = math.radians(rotation[2]) 428 | 429 | cam = bpy.context.scene.objects['Camera'] 430 | cam.location = (0, 3.0 * distance, 1.0 * distance) 431 | 432 | bpy.ops.render.render(animation=False, write_still=True) 433 | -------------------------------------------------------------------------------- /examples/0.binvox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidstutz/bpy-visualization-utils/9aec34f4b58d83992c3c3ce7eb6369d9055760bc/examples/0.binvox -------------------------------------------------------------------------------- /examples/0.txt: -------------------------------------------------------------------------------- 1 | 597 2 | 9.2312 6.01208 11.3019 3 | 8.41012 6.15688 11.2039 4 | 23.1434 5.82715 10.8433 5 | 8.70879 6.97059 11.2857 6 | 8.32762 7.41969 10.6109 7 | 22.8282 6.3528 11.6698 8 | 9.0674 7.835 11.2977 9 | 8.31103 8.02385 11.1044 10 | 23.2571 7.2552 11.7843 11 | 22.767 7.68042 11.1771 12 | 8.53898 8.80688 11.2816 13 | 8.31705 9.39399 10.383 14 | 22.8261 8.40071 11.6476 15 | 8.89255 9.69058 11.3031 16 | 8.30492 10.0097 10.8776 17 | 8.35123 10.8274 9.60242 18 | 7.42347 10.8874 9.61968 19 | 23.2385 9.31776 11.7839 20 | 22.7593 9.76443 11.1533 21 | 20.708 10.7657 9.84308 22 | 19.4961 10.7727 9.96082 23 | 18.3032 10.7767 10.0809 24 | 17.1305 10.7788 10.2018 25 | 15.9884 10.7862 10.3114 26 | 14.8671 10.7931 10.4196 27 | 13.7647 10.7985 10.5282 28 | 12.684 10.8047 10.633 29 | 11.6156 10.8054 10.7451 30 | 10.5727 10.8104 10.8478 31 | 9.25672 10.5927 11.3191 32 | 8.38344 10.6958 11.2543 33 | 7.56107 10.8301 11.1365 34 | 7.34353 11.4521 10.2108 35 | 23.787 10.3218 11.8 36 | 22.7691 10.4667 11.6775 37 | 22.0173 10.7603 11.3074 38 | 20.7928 10.7666 11.4119 39 | 19.5902 10.7715 11.5163 40 | 18.4103 10.7762 11.6191 41 | 17.2565 10.7831 11.716 42 | 16.1219 10.7882 11.8139 43 | 15.0112 10.7951 11.9068 44 | 13.9202 10.8013 11.9989 45 | 12.8463 10.8055 12.0925 46 | 11.7951 10.8119 12.1807 47 | 10.7595 10.8158 12.2712 48 | 9.74419 10.8209 12.3579 49 | 8.74601 10.8254 12.444 50 | 7.77235 10.8354 12.5196 51 | 7.30195 11.2418 11.9542 52 | 7.3212 12.0624 10.7226 53 | 7.34377 12.9115 9.44814 54 | 24.5944 10.7545 12.6665 55 | 23.3278 10.7575 12.7641 56 | 22.0937 10.7648 12.8526 57 | 20.8816 10.7709 12.9411 58 | 19.6913 10.776 13.0293 59 | 18.5216 10.7798 13.1179 60 | 17.376 10.7847 13.2027 61 | 16.2576 10.7934 13.2798 62 | 15.154 10.7982 13.3615 63 | 14.0728 10.8044 13.4394 64 | 13.008 10.8082 13.5194 65 | 11.9684 10.8163 13.5911 66 | 10.9423 10.8208 13.667 67 | 9.93943 10.829 13.7356 68 | 8.94804 10.8325 13.8102 69 | 7.97176 10.8343 13.8861 70 | 7.267 11.0425 13.6362 71 | 7.28484 11.8466 12.4507 72 | 7.30598 12.6782 11.2244 73 | 7.3304 13.5385 9.95556 74 | 24.6488 10.7622 14.2056 75 | 23.3955 10.7652 14.2867 76 | 22.1711 10.7707 14.3622 77 | 20.9707 10.7765 14.4357 78 | 19.7862 10.7779 14.5144 79 | 18.6333 10.785 14.5826 80 | 17.5061 10.7948 14.6453 81 | 9.02739 5.95223 22.2307 82 | 8.17622 6.01113 22.2035 83 | 14.2231 10.8079 14.8501 84 | 13.1742 10.8163 14.9093 85 | 12.1376 10.8202 14.9744 86 | 11.1221 10.8261 15.0351 87 | 10.1233 10.8311 15.0959 88 | 9.13883 10.8338 15.1591 89 | 8.17962 10.8429 15.2113 90 | 7.31298 10.9136 15.1681 91 | 7.24809 11.6362 14.1269 92 | 7.27094 12.4536 12.9425 93 | 7.29117 13.2938 11.7253 94 | 7.31686 14.1649 10.4629 95 | 22.9044 5.89981 22.7383 96 | 23.4597 10.7715 15.779 97 | 22.2509 10.7788 15.8364 98 | 21.0582 10.782 15.8988 99 | 19.8893 10.786 15.9588 100 | 18.7445 10.7912 16.0157 101 | 17.628 10.801 16.0644 102 | 16.5242 10.8058 16.1196 103 | 8.89927 6.42032 22.7846 104 | 8.12755 6.52781 22.6751 105 | 8.08216 7.13788 21.81 106 | 12.3011 10.8223 16.3333 107 | 11.297 10.8303 16.3784 108 | 10.3065 10.8354 16.4267 109 | 9.3319 10.8396 16.4754 110 | 8.37223 10.8425 16.5253 111 | 7.43748 10.8522 16.5639 112 | 7.21476 11.4341 15.7488 113 | 7.23465 12.234 14.6102 114 | 7.25703 13.0601 13.4341 115 | 7.27837 13.9107 12.2233 116 | 7.30208 14.7899 10.9716 117 | 7.32723 15.6982 9.67835 118 | 22.8335 6.4864 23.2685 119 | 22.518 6.93649 22.6534 120 | 23.521 10.7768 17.241 121 | 22.3221 10.7827 17.2859 122 | 21.1452 10.7884 17.3303 123 | 19.9817 10.7886 17.3816 124 | 18.8614 10.8023 17.4123 125 | 17.746 10.8063 17.4563 126 | 16.6507 10.81 17.4997 127 | 15.5755 10.8139 17.542 128 | 14.5212 10.819 17.5818 129 | 8.42792 7.2873 22.789 130 | 8.07037 7.68552 22.2483 131 | 11.4684 10.8342 17.6961 132 | 10.4867 10.8399 17.7318 133 | 9.52206 10.8459 17.7663 134 | 8.56805 10.8474 17.8066 135 | 7.63593 10.8535 17.8394 136 | 7.1855 11.2407 17.3183 137 | 7.20076 12.0222 16.2252 138 | 7.22154 12.8316 15.0929 139 | 7.2426 13.6657 13.926 140 | 7.26512 14.5268 12.7215 141 | 7.28924 15.4161 11.4773 142 | 7.31191 16.3322 10.1958 143 | 23.4358 7.41995 23.2964 144 | 22.5429 7.57468 23.1133 145 | 22.5266 8.19666 22.2592 146 | 23.585 10.7843 18.6691 147 | 22.3935 10.7875 18.7039 148 | 21.2278 10.7929 18.735 149 | 20.0912 10.8029 18.7587 150 | 18.9582 10.8021 18.7971 151 | 17.8585 10.8094 18.8234 152 | 16.7775 10.8159 18.85 153 | 15.7148 10.8218 18.8768 154 | 14.6702 10.8273 18.9037 155 | 13.644 10.8329 18.93 156 | 8.83798 8.13283 22.798 157 | 8.0863 8.25303 22.6576 158 | 8.08944 8.92855 21.7294 159 | 9.70691 10.8506 19.0353 160 | 8.76422 10.8553 19.06 161 | 7.83295 10.8565 19.0889 162 | 7.152 11.0488 18.8472 163 | 7.16734 11.8163 17.7922 164 | 7.18801 12.6109 16.6998 165 | 7.20883 13.4292 15.5748 166 | 7.23037 14.2729 14.4148 167 | 7.25187 15.1425 13.2194 168 | 7.27495 16.0406 11.9847 169 | 7.29975 16.9688 10.7086 170 | 7.3351 17.9371 9.37706 171 | 22.9676 8.4293 23.2733 172 | 22.5161 8.82294 22.7514 173 | 23.6354 10.7846 20.0777 174 | 22.4563 10.7881 20.0985 175 | 21.3077 10.7968 20.1118 176 | 20.1773 10.8038 20.1269 177 | 19.0542 10.8026 20.1529 178 | 17.9683 10.8121 20.1637 179 | 16.896 10.8179 20.1791 180 | 15.8418 10.8232 20.1949 181 | 14.8117 10.8323 20.2049 182 | 13.7905 10.8353 20.2231 183 | 12.7916 10.8416 20.2363 184 | 11.8073 10.8461 20.2516 185 | 8.36565 9.01627 22.7978 186 | 8.06247 9.4717 22.1893 187 | 8.94875 10.8568 20.2993 188 | 8.02858 10.8612 20.3133 189 | 7.19143 10.9207 20.2511 190 | 7.13468 11.6164 19.3127 191 | 7.15469 12.3961 18.2592 192 | 7.17541 13.1993 17.1739 193 | 7.1965 14.0268 16.0559 194 | 7.21697 14.8787 14.9048 195 | 7.23818 15.7574 13.7176 196 | 7.2592 16.6632 12.4936 197 | 7.28475 17.6021 11.2249 198 | 7.31326 18.5742 9.91148 199 | 22.5684 9.48431 23.1959 200 | 22.5168 10.1142 22.3601 201 | 22.5335 10.7978 21.4522 202 | 21.3973 10.8073 21.4517 203 | 20.2733 10.8118 21.4578 204 | 19.1657 10.8139 21.467 205 | 18.0787 10.8165 21.4752 206 | 17.0197 10.8247 21.4757 207 | 15.971 10.8276 21.4832 208 | 14.9406 10.8304 21.4905 209 | 13.9326 10.8365 21.4932 210 | 12.9448 10.8448 21.4929 211 | 11.9748 10.8539 21.4912 212 | 11.0129 10.8571 21.4972 213 | 8.78228 9.89059 22.8046 214 | 8.07377 10.0436 22.609 215 | 8.09839 10.7694 21.645 216 | 7.36815 10.9074 21.4697 217 | 7.1019 11.4214 20.79 218 | 7.12108 12.1867 19.7737 219 | 7.14096 12.9746 18.7272 220 | 7.16125 13.786 17.6496 221 | 7.18218 14.6222 16.5391 222 | 7.20254 15.4831 15.3958 223 | 7.22525 16.3726 14.2144 224 | 7.24558 17.2875 12.9994 225 | 7.26799 18.2333 11.7432 226 | 7.29455 19.2137 10.4411 227 | 23.087 10.4144 23.2919 228 | 22.4634 10.722 22.8904 229 | 21.4839 10.8169 22.7664 230 | 20.371 10.8219 22.7599 231 | 19.2811 10.8288 22.7509 232 | 18.2077 10.834 22.7441 233 | 17.1467 10.8351 22.7427 234 | 16.1128 10.8419 22.7338 235 | 15.092 10.8456 22.729 236 | 14.0901 10.8503 22.7229 237 | 13.109 10.8577 22.7133 238 | 12.1421 10.8635 22.7056 239 | 11.1864 10.8659 22.7026 240 | 10.2437 10.8661 22.7022 241 | 9.31417 10.8645 22.7043 242 | 8.25343 10.748 22.8565 243 | 7.5299 10.8849 22.6777 244 | 7.08476 11.2442 22.2087 245 | 7.09194 11.9863 21.2399 246 | 7.10932 12.7581 20.2323 247 | 7.1285 13.5538 19.1935 248 | 7.14711 14.3723 18.1249 249 | 7.16842 15.2177 17.0213 250 | 7.18918 16.088 15.8851 251 | 7.21129 16.9864 14.7123 252 | 7.23294 17.9122 13.5036 253 | 7.25542 18.8682 12.2556 254 | 7.27953 19.8565 10.9654 255 | 7.30553 20.879 9.63048 256 | 23.8615 10.8319 24.0727 257 | 22.6969 10.8246 24.0697 258 | 21.6121 10.8522 24.0229 259 | 20.482 10.841 24.0253 260 | 19.407 10.8513 24.0009 261 | 18.3476 10.8597 23.9789 262 | 17.3083 10.8693 23.9557 263 | 16.2879 10.8797 23.9316 264 | 15.2951 10.8971 23.8988 265 | 14.2946 10.8981 23.887 266 | 13.327 10.9103 23.8612 267 | 12.3707 10.9193 23.8396 268 | 11.4309 10.9289 23.8173 269 | 10.5037 10.9366 23.7977 270 | 9.58591 10.9402 23.7835 271 | 8.69525 10.9534 23.7571 272 | 7.81089 10.9605 23.7387 273 | 7.08138 11.0826 23.5742 274 | 7.06533 11.793 22.6622 275 | 7.08103 12.55 21.6907 276 | 7.09727 13.329 20.6909 277 | 7.1141 14.1309 19.6617 278 | 7.1349 14.96 18.5977 279 | 7.15701 15.8149 17.5004 280 | 7.17655 16.6933 16.3732 281 | 7.19732 17.5997 15.2098 282 | 7.21999 18.5363 14.0079 283 | 7.24258 19.5023 12.7681 284 | 7.26455 20.4988 11.4892 285 | 7.28962 21.531 10.1645 286 | 24.1855 11.6612 24.3307 287 | 23.0516 11.6701 24.2958 288 | 21.9391 11.6799 24.2602 289 | 20.8464 11.6901 24.2246 290 | 19.7749 11.7019 24.1875 291 | 18.72 11.7127 24.1519 292 | 17.6826 11.7233 24.1168 293 | 16.6621 11.7337 24.0825 294 | 15.6589 11.7443 24.0482 295 | 14.6701 11.7535 24.016 296 | 13.6981 11.7633 23.9834 297 | 12.7447 11.7752 23.9486 298 | 11.8043 11.7854 23.9161 299 | 10.8785 11.7955 23.8841 300 | 9.96624 11.8047 23.8534 301 | 9.06794 11.8138 23.8232 302 | 8.18465 11.8237 23.7923 303 | 7.31647 11.835 23.76 304 | 7.04952 12.3445 23.1114 305 | 7.06832 13.1119 22.1431 306 | 7.08587 13.9001 21.1486 307 | 7.10327 14.7109 20.1255 308 | 7.12181 15.5465 19.0712 309 | 7.14365 16.41 17.9816 310 | 7.16332 17.2976 16.8617 311 | 7.18458 18.2139 15.7055 312 | 7.20534 19.1582 14.514 313 | 7.22828 20.1346 13.2821 314 | 7.25107 21.1422 12.0107 315 | 7.27554 22.1844 10.6957 316 | 7.33256 23.2973 9.29211 317 | 23.6903 12.696 24.3227 318 | 22.5584 12.6962 24.2875 319 | 21.4476 12.6973 24.2518 320 | 20.3588 12.7004 24.2143 321 | 19.2869 12.7024 24.1787 322 | 18.2346 12.7056 24.1422 323 | 17.1987 12.7079 24.1073 324 | 16.1804 12.7106 24.0724 325 | 15.178 12.7127 24.0388 326 | 14.1929 12.7157 24.0047 327 | 13.2216 12.7171 23.9729 328 | 12.2681 12.7202 23.9396 329 | 11.33 12.7235 23.9065 330 | 10.4051 12.7256 23.8753 331 | 9.49434 12.7276 23.8447 332 | 8.59916 12.7307 23.8132 333 | 7.71692 12.7332 23.7828 334 | 7.04929 12.9089 23.5441 335 | 7.05569 13.6735 22.5952 336 | 7.07323 14.4697 21.6074 337 | 7.0893 15.2876 20.5926 338 | 7.10912 16.133 19.5439 339 | 7.12927 17.0037 18.4636 340 | 7.14997 17.9014 17.3501 341 | 7.17104 18.8269 16.2019 342 | 7.19093 19.7799 15.0195 343 | 7.2141 20.7665 13.7956 344 | 7.23679 21.7843 12.5329 345 | 7.25988 22.8357 11.2286 346 | 7.28439 23.9235 9.87905 347 | 24.345 13.7463 24.349 348 | 23.1944 13.7371 24.3133 349 | 22.0646 13.7286 24.2777 350 | 20.9571 13.7219 24.2406 351 | 19.869 13.7157 24.2038 352 | 18.7998 13.7099 24.1672 353 | 17.7485 13.7043 24.1312 354 | 16.7142 13.6984 24.0962 355 | 15.6966 13.6923 24.0622 356 | 14.697 13.6873 24.0275 357 | 13.7131 13.6821 23.9937 358 | 12.7439 13.6761 23.9615 359 | 11.7913 13.671 23.9289 360 | 10.8533 13.6656 23.8972 361 | 9.93085 13.661 23.8652 362 | 9.02364 13.6572 23.8328 363 | 8.13103 13.654 23.8003 364 | 7.25494 13.6534 23.7653 365 | 7.04136 14.2332 23.0488 366 | 7.05997 15.0385 22.0665 367 | 7.07666 15.8652 21.058 368 | 7.09597 16.7186 20.0169 369 | 7.11566 17.5978 18.9445 370 | 7.1363 18.5044 17.8385 371 | 7.15705 19.439 16.6985 372 | 7.17689 20.4016 15.5242 373 | 7.2006 21.3987 14.3079 374 | 7.22285 22.4263 13.0543 375 | 7.24499 23.4873 11.7601 376 | 23.8474 14.8035 24.3379 377 | 22.6967 14.7839 24.3034 378 | 21.5707 14.7676 24.2661 379 | 20.466 14.7528 24.228 380 | 19.3787 14.7373 24.1917 381 | 18.3109 14.7228 24.1553 382 | 17.2611 14.7086 24.1193 383 | 16.2279 14.694 24.0847 384 | 15.2118 14.6797 24.0507 385 | 14.2134 14.6663 24.0163 386 | 13.2309 14.6529 23.9828 387 | 12.2648 14.6402 23.9493 388 | 11.3135 14.6272 23.917 389 | 10.3775 14.6145 23.8849 390 | 9.45687 14.6026 23.8527 391 | 8.55057 14.5909 23.8211 392 | 7.6614 14.5821 23.7867 393 | 7.02769 14.7931 23.5014 394 | 7.04753 15.6076 22.5245 395 | 7.06574 16.444 21.5211 396 | 7.08326 17.3043 20.4891 397 | 7.10351 18.1928 19.4233 398 | 7.12226 19.1067 18.3271 399 | 7.14198 20.0495 17.1961 400 | 7.16437 21.0244 16.0267 401 | 7.18577 22.0291 14.8215 402 | 7.20938 23.0684 13.5748 403 | 7.23377 24.1425 12.2864 404 | 23.3452 15.8646 24.3283 405 | 22.1977 15.8366 24.2926 406 | 21.0758 15.813 24.2533 407 | 19.9707 15.7878 24.2167 408 | 18.8858 15.764 24.1797 409 | 17.8204 15.7414 24.1426 410 | 16.7712 15.7179 24.1075 411 | 15.7383 15.6938 24.0739 412 | 14.7255 15.6724 24.0385 413 | 13.7285 15.6507 24.0043 414 | 12.7483 15.6298 23.9702 415 | 11.7833 15.6089 23.9371 416 | 10.8333 15.5877 23.9051 417 | 9.89835 15.5669 23.8736 418 | 8.97827 15.5465 23.8426 419 | 8.07472 15.5283 23.8099 420 | 7.19489 15.5194 23.7676 421 | 7.03556 16.1768 22.9818 422 | 7.05306 17.0208 21.986 423 | 7.07146 17.8905 20.96 424 | 7.08959 18.7858 19.9039 425 | 7.10801 19.7083 18.8155 426 | 7.1296 20.6624 17.6901 427 | 7.14961 21.6445 16.5316 428 | 7.17095 22.659 15.3348 429 | 7.19627 23.7105 14.0945 430 | 24.009 16.9707 24.3536 431 | 22.8409 16.9315 24.3183 432 | 21.6989 16.8967 24.2796 433 | 20.5765 16.8623 24.2419 434 | 19.4739 16.8287 24.2045 435 | 18.391 16.7962 24.1673 436 | 17.3266 16.7644 24.1305 437 | 16.2777 16.7312 24.0964 438 | 15.2486 16.7002 24.0611 439 | 14.2368 16.67 24.0262 440 | 13.2422 16.6407 23.9913 441 | 12.262 16.6103 23.9587 442 | 11.298 16.5807 23.9263 443 | 10.3494 16.5515 23.8946 444 | 9.41746 16.5241 23.8619 445 | 8.5007 16.4975 23.8292 446 | 7.60105 16.474 23.7942 447 | 7.02098 16.7431 23.4414 448 | 7.04103 17.598 22.4498 449 | 7.05957 18.4763 21.4308 450 | 7.07608 19.3787 20.3837 451 | 7.09613 20.312 19.301 452 | 7.11643 21.2741 18.1848 453 | 7.13645 22.2658 17.0342 454 | 7.1567 23.289 15.847 455 | 7.18446 24.3536 14.6122 456 | 23.5025 18.0543 24.3421 457 | 22.3372 18.0062 24.3058 458 | 21.1958 17.9611 24.2679 459 | 20.0752 17.9174 24.23 460 | 18.9745 17.8748 24.1924 461 | 17.8942 17.834 24.1543 462 | 16.8288 17.7913 24.1197 463 | 15.7845 17.7516 24.0832 464 | 14.7575 17.7124 24.0474 465 | 13.7465 17.673 24.0132 466 | 12.7512 17.6334 23.9804 467 | 11.773 17.5951 23.9476 468 | 10.811 17.5576 23.915 469 | 9.86606 17.5223 23.8813 470 | 8.9367 17.4879 23.8477 471 | 8.02451 17.4565 23.8121 472 | 7.13172 17.4307 23.7717 473 | 7.03069 18.1764 22.9117 474 | 7.04677 19.0608 21.9023 475 | 7.06359 19.9723 20.862 476 | 7.0824 20.9133 19.7882 477 | 7.10186 21.8838 18.6808 478 | 7.12179 22.885 17.5383 479 | 7.14725 23.924 16.353 480 | 24.1828 19.2059 24.3644 481 | 22.9901 19.1412 24.333 482 | 21.8293 19.0852 24.2945 483 | 20.6898 19.0307 24.2563 484 | 19.5716 18.9781 24.2178 485 | 18.4731 18.9266 24.1797 486 | 17.3916 18.8746 24.1438 487 | 16.3297 18.8241 24.1078 488 | 15.2869 18.7755 24.0714 489 | 14.2606 18.7268 24.0365 490 | 13.2511 18.6787 24.0025 491 | 12.2598 18.6326 23.9678 492 | 11.2835 18.5862 23.9347 493 | 10.3243 18.5417 23.901 494 | 9.38098 18.4982 23.8675 495 | 8.45306 18.4558 23.8341 496 | 7.54671 18.4209 23.7942 497 | 7.01417 18.7483 23.3797 498 | 7.03331 19.6443 22.3741 499 | 7.0497 20.5641 21.3417 500 | 7.06943 21.5151 20.2743 501 | 7.08827 22.4942 19.1753 502 | 7.10885 23.5058 18.04 503 | 7.17718 24.6038 16.8116 504 | 23.6606 20.3047 24.3591 505 | 22.4771 20.2352 24.3223 506 | 21.3185 20.1697 24.2834 507 | 20.1818 20.1063 24.2442 508 | 19.065 20.0439 24.2059 509 | 17.9669 19.9821 24.1688 510 | 16.8885 19.9219 24.1318 511 | 15.8287 19.8627 24.0954 512 | 14.7875 19.8051 24.0591 513 | 13.7638 19.7484 24.0234 514 | 12.757 19.6925 23.9885 515 | 11.7662 19.6369 23.9548 516 | 10.7921 19.5826 23.9212 517 | 9.83455 19.5298 23.8877 518 | 8.89249 19.4778 23.8546 519 | 7.96691 19.4281 23.8206 520 | 7.08611 19.4099 23.7561 521 | 7.02154 20.2291 22.8439 522 | 7.03738 21.1571 21.8192 523 | 7.05659 22.1166 20.7599 524 | 7.07399 23.1035 19.6702 525 | 7.09548 24.1256 18.5419 526 | 24.3537 21.5009 24.3813 527 | 23.1453 21.4163 24.3469 528 | 21.9623 21.3356 24.3108 529 | 20.8064 21.2608 24.2711 530 | 19.671 21.1873 24.2323 531 | 18.5569 21.1162 24.193 532 | 17.4611 21.0454 24.1554 533 | 16.386 20.9771 24.1171 534 | 15.3283 20.9092 24.0802 535 | 14.288 20.8419 24.0446 536 | 13.2647 20.7752 24.01 537 | 12.2586 20.7097 23.976 538 | 11.2696 20.6455 23.9423 539 | 10.2972 20.5827 23.9089 540 | 9.34096 20.5212 23.8757 541 | 8.40113 20.4616 23.8422 542 | 7.47583 20.4024 23.8097 543 | 7.00829 20.812 23.315 544 | 7.02583 21.7505 22.2957 545 | 7.04254 22.7164 21.2466 546 | 7.05968 23.7122 20.1649 547 | 23.8286 22.625 24.3728 548 | 22.6223 22.5299 24.3382 549 | 21.4471 22.4436 24.2976 550 | 20.2936 22.3594 24.2573 551 | 19.1605 22.2765 24.2178 552 | 18.0477 22.1953 24.1789 553 | 16.9543 22.1154 24.1407 554 | 15.879 22.0363 24.1038 555 | 14.821 21.9573 24.0687 556 | 13.7822 21.8806 24.0333 557 | 12.7619 21.806 23.9977 558 | 11.7578 21.7318 23.9635 559 | 10.7703 21.6588 23.93 560 | 9.79963 21.5874 23.8966 561 | 8.8462 21.5185 23.8625 562 | 7.90671 21.4492 23.8304 563 | 7.0531 21.4568 23.7258 564 | 7.0113 22.3404 22.7751 565 | 7.02945 23.3168 21.7319 566 | 7.05142 24.3274 20.6525 567 | 23.3026 23.7564 24.363 568 | 22.1045 23.6558 24.3235 569 | 20.9312 23.5593 24.2827 570 | 19.7781 23.4638 24.2433 571 | 18.6464 23.3707 24.204 572 | 17.5349 23.2793 24.1654 573 | 16.4415 23.1882 24.1286 574 | 15.3683 23.0997 24.0915 575 | 14.3132 23.0124 24.0554 576 | 13.2757 22.9261 24.0203 577 | 12.2556 22.841 23.9861 578 | 11.2543 22.7588 23.9511 579 | 10.2687 22.6771 23.9176 580 | 9.30021 22.5975 23.8838 581 | 8.34758 22.5192 23.8507 582 | 7.41936 22.4518 23.809 583 | 6.99935 22.9327 23.2515 584 | 7.01634 23.9168 22.2169 585 | 18.1335 24.4742 24.1871 586 | 17.0173 24.367 24.1536 587 | 15.9274 24.2677 24.1153 588 | 14.8561 24.1698 24.078 589 | 13.8023 24.0724 24.0424 590 | 12.768 23.9782 24.0062 591 | 11.7484 23.8826 23.9731 592 | 10.7482 23.7908 23.9388 593 | 9.76624 23.7021 23.9035 594 | 8.79924 23.6135 23.8701 595 | 7.85001 23.5284 23.8354 596 | 7.03166 23.5743 23.6817 597 | 7.01943 24.535 22.6845 598 | 7.36098 24.5551 23.8087 599 | -------------------------------------------------------------------------------- /examples/render_binvox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$BLENDER" ]; then 3 | export BLENDER="blender" 4 | fi 5 | 6 | "$BLENDER" --background --python render_binvox.py -- --binvox examples/0.binvox --output examples/0.png 7 | -------------------------------------------------------------------------------- /examples/render_off.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$BLENDER" ]; then 3 | export BLENDER="blender" 4 | fi 5 | 6 | "$BLENDER" --background --python render_off.py -- --off examples/0.off --output examples/0.png 7 | -------------------------------------------------------------------------------- /examples/render_off_txt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$BLENDER" ]; then 3 | export BLENDER="blender" 4 | fi 5 | 6 | "$BLENDER" --background --python render_off_txt.py -- --off examples/0.off --txt examples/0.txt --output examples/0.png 7 | -------------------------------------------------------------------------------- /examples/render_txt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$BLENDER" ]; then 3 | export BLENDER="blender" 4 | fi 5 | 6 | "$BLENDER" --background --python render_txt.py -- --txt examples/0.txt --output examples/0.png -------------------------------------------------------------------------------- /import_off.py: -------------------------------------------------------------------------------- 1 | ##### 2 | # 3 | # Copyright 2014 Alex Tsui 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | ##### 18 | 19 | # 20 | # http://wiki.blender.org/index.php/Dev:2.5/Py/Scripts/Guidelines/Addons 21 | # 22 | import os 23 | import bpy 24 | import mathutils 25 | from bpy.props import (BoolProperty, 26 | FloatProperty, 27 | StringProperty, 28 | EnumProperty, 29 | ) 30 | from bpy_extras.io_utils import (ImportHelper, 31 | ExportHelper, 32 | unpack_list, 33 | unpack_face_list, 34 | axis_conversion, 35 | ) 36 | 37 | #if "bpy" in locals(): 38 | # import imp 39 | # if "import_off" in 40 | 41 | bl_info = { 42 | "name": "OFF format", 43 | "description": "Import-Export OFF, Import/export simple OFF mesh.", 44 | "author": "Alex Tsui, Mateusz Kłoczko", 45 | "version": (0, 3), 46 | "blender": (2, 74, 0), 47 | "location": "File > Import-Export", 48 | "warning": "", # used for warning icon and text in addons panel 49 | "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/" 50 | "Scripts/My_Script", 51 | "category": "Import-Export"} 52 | 53 | class ImportOFF(bpy.types.Operator, ImportHelper): 54 | """Load an OFF Mesh file""" 55 | bl_idname = "import_mesh.off" 56 | bl_label = "Import OFF Mesh" 57 | filename_ext = ".off" 58 | filter_glob = StringProperty( 59 | default="*.off", 60 | options={'HIDDEN'}, 61 | ) 62 | 63 | axis_forward = EnumProperty( 64 | name="Forward", 65 | items=(('X', "X Forward", ""), 66 | ('Y', "Y Forward", ""), 67 | ('Z', "Z Forward", ""), 68 | ('-X', "-X Forward", ""), 69 | ('-Y', "-Y Forward", ""), 70 | ('-Z', "-Z Forward", ""), 71 | ), 72 | default='Y', 73 | ) 74 | axis_up = EnumProperty( 75 | name="Up", 76 | items=(('X', "X Up", ""), 77 | ('Y', "Y Up", ""), 78 | ('Z', "Z Up", ""), 79 | ('-X', "-X Up", ""), 80 | ('-Y', "-Y Up", ""), 81 | ('-Z', "-Z Up", ""), 82 | ), 83 | default='Z', 84 | ) 85 | 86 | def execute(self, context): 87 | #from . import import_off 88 | 89 | keywords = self.as_keywords(ignore=('axis_forward', 90 | 'axis_up', 91 | 'filter_glob', 92 | )) 93 | global_matrix = axis_conversion(from_forward=self.axis_forward, 94 | from_up=self.axis_up, 95 | ).to_4x4() 96 | 97 | mesh = load(self, context, **keywords) 98 | if not mesh: 99 | return {'CANCELLED'} 100 | 101 | scene = bpy.context.scene 102 | obj = bpy.data.objects.new(mesh.name, mesh) 103 | scene.objects.link(obj) 104 | scene.objects.active = obj 105 | obj.select = True 106 | 107 | obj.matrix_world = global_matrix 108 | 109 | scene.update() 110 | 111 | return {'FINISHED'} 112 | 113 | class ExportOFF(bpy.types.Operator, ExportHelper): 114 | """Save an OFF Mesh file""" 115 | bl_idname = "export_mesh.off" 116 | bl_label = "Export OFF Mesh" 117 | filter_glob = StringProperty( 118 | default="*.off", 119 | options={'HIDDEN'}, 120 | ) 121 | check_extension = True 122 | filename_ext = ".off" 123 | 124 | axis_forward = EnumProperty( 125 | name="Forward", 126 | items=(('X', "X Forward", ""), 127 | ('Y', "Y Forward", ""), 128 | ('Z', "Z Forward", ""), 129 | ('-X', "-X Forward", ""), 130 | ('-Y', "-Y Forward", ""), 131 | ('-Z', "-Z Forward", ""), 132 | ), 133 | default='Y', 134 | ) 135 | axis_up = EnumProperty( 136 | name="Up", 137 | items=(('X', "X Up", ""), 138 | ('Y', "Y Up", ""), 139 | ('Z', "Z Up", ""), 140 | ('-X', "-X Up", ""), 141 | ('-Y', "-Y Up", ""), 142 | ('-Z', "-Z Up", ""), 143 | ), 144 | default='Z', 145 | ) 146 | use_colors = BoolProperty( 147 | name="Vertex Colors", 148 | description="Export the active vertex color layer", 149 | default=False, 150 | ) 151 | 152 | def execute(self, context): 153 | keywords = self.as_keywords(ignore=('axis_forward', 154 | 'axis_up', 155 | 'filter_glob', 156 | 'check_existing', 157 | )) 158 | global_matrix = axis_conversion(to_forward=self.axis_forward, 159 | to_up=self.axis_up, 160 | ).to_4x4() 161 | keywords['global_matrix'] = global_matrix 162 | return save(self, context, **keywords) 163 | 164 | def menu_func_import(self, context): 165 | self.layout.operator(ImportOFF.bl_idname, text="OFF Mesh (.off)") 166 | 167 | def menu_func_export(self, context): 168 | self.layout.operator(ExportOFF.bl_idname, text="OFF Mesh (.off)") 169 | 170 | def register(): 171 | bpy.utils.register_module(__name__) 172 | bpy.types.INFO_MT_file_import.append(menu_func_import) 173 | bpy.types.INFO_MT_file_export.append(menu_func_export) 174 | 175 | def unregister(): 176 | bpy.utils.unregister_module(__name__) 177 | bpy.types.INFO_MT_file_import.remove(menu_func_import) 178 | bpy.types.INFO_MT_file_export.remove(menu_func_export) 179 | 180 | def load(operator, context, filepath): 181 | # Parse mesh from OFF file 182 | # TODO: Add support for NOFF and COFF 183 | filepath = os.fsencode(filepath) 184 | file = open(filepath, 'r') 185 | first_line = file.readline().rstrip() 186 | use_colors = (first_line == 'COFF') 187 | colors = [] 188 | vcount, fcount, ecount = [int(x) for x in file.readline().split()] 189 | verts = [] 190 | facets = [] 191 | edges = [] 192 | i=0; 193 | while i 3: 220 | facets.append(tuple(ids[1:])) 221 | elif len(ids) == 3: 222 | edges.append(tuple(ids[1:])) 223 | except ValueError: 224 | i=i+1 225 | continue 226 | i=i+1 227 | 228 | # Assemble mesh 229 | off_name = bpy.path.display_name_from_filepath(filepath) 230 | mesh = bpy.data.meshes.new(name=off_name) 231 | mesh.from_pydata(verts,edges,facets) 232 | # mesh.vertices.add(len(verts)) 233 | # mesh.vertices.foreach_set("co", unpack_list(verts)) 234 | 235 | # mesh.faces.add(len(facets)) 236 | # mesh.faces.foreach_set("vertices", unpack_face_list(facets)) 237 | 238 | mesh.validate() 239 | mesh.update() 240 | 241 | if use_colors: 242 | color_data = mesh.vertex_colors.new() 243 | for i, facet in enumerate(mesh.polygons): 244 | for j, vidx in enumerate(facet.vertices): 245 | color_data.data[3*i + j].color = colors[vidx] 246 | 247 | return mesh 248 | 249 | def save(operator, context, filepath, 250 | global_matrix = None, 251 | use_colors = False): 252 | # Export the selected mesh 253 | APPLY_MODIFIERS = True # TODO: Make this configurable 254 | if global_matrix is None: 255 | global_matrix = mathutils.Matrix() 256 | scene = context.scene 257 | obj = scene.objects.active 258 | mesh = obj.to_mesh(scene, APPLY_MODIFIERS, 'PREVIEW') 259 | 260 | # Apply the inverse transformation 261 | obj_mat = obj.matrix_world 262 | mesh.transform(global_matrix * obj_mat) 263 | 264 | verts = mesh.vertices[:] 265 | facets = [ f for f in mesh.tessfaces ] 266 | # Collect colors by vertex id 267 | colors = False 268 | vertex_colors = None 269 | if use_colors: 270 | colors = mesh.tessface_vertex_colors.active 271 | if colors: 272 | colors = colors.data 273 | vertex_colors = {} 274 | for i, facet in enumerate(mesh.tessfaces): 275 | color = colors[i] 276 | color = color.color1[:], color.color2[:], color.color3[:], color.color4[:] 277 | for j, vidx in enumerate(facet.vertices): 278 | if vidx not in vertex_colors: 279 | vertex_colors[vidx] = (int(color[j][0] * 255.0), 280 | int(color[j][1] * 255.0), 281 | int(color[j][2] * 255.0)) 282 | else: 283 | use_colors = False 284 | 285 | # Write geometry to file 286 | filepath = os.fsencode(filepath) 287 | fp = open(filepath, 'w') 288 | 289 | if use_colors: 290 | fp.write('COFF\n') 291 | else: 292 | fp.write('OFF\n') 293 | 294 | fp.write('%d %d 0\n' % (len(verts), len(facets))) 295 | 296 | for i, vert in enumerate(mesh.vertices): 297 | fp.write('%.16f %.16f %.16f' % vert.co[:]) 298 | if use_colors: 299 | fp.write(' %d %d %d 255' % vertex_colors[i]) 300 | fp.write('\n') 301 | 302 | #for facet in facets: 303 | for i, facet in enumerate(mesh.tessfaces): 304 | fp.write('%d' % len(facet.vertices)) 305 | for vid in facet.vertices: 306 | fp.write(' %d' % vid) 307 | fp.write('\n') 308 | 309 | fp.close() 310 | 311 | return {'FINISHED'} 312 | -------------------------------------------------------------------------------- /mesh.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | 5 | def write_off(file, vertices, faces): 6 | """ 7 | Writes the given vertices and faces to OFF. 8 | 9 | :param vertices: vertices as tuples of (x, y, z) coordinates 10 | :type vertices: [(float)] 11 | :param faces: faces as tuples of (num_vertices, vertex_id_1, vertex_id_2, ...) 12 | :type faces: [(int)] 13 | """ 14 | 15 | num_vertices = len(vertices) 16 | num_faces = len(faces) 17 | 18 | assert num_vertices > 0 19 | assert num_faces > 0 20 | 21 | with open(file, 'w') as fp: 22 | fp.write('OFF\n') 23 | fp.write(str(num_vertices) + ' ' + str(num_faces) + ' 0\n') 24 | 25 | for vertex in vertices: 26 | assert len(vertex) == 3, 'invalid vertex with %d dimensions found (%s)' % (len(vertex), file) 27 | fp.write(str(vertex[0]) + ' ' + str(vertex[1]) + ' ' + str(vertex[2]) + '\n') 28 | 29 | for face in faces: 30 | assert face[0] == 3, 'only triangular faces supported (%s)' % file 31 | assert len(face) == 4, 'faces need to have 3 vertices, but found %d (%s)' % (len(face), file) 32 | 33 | for i in range(len(face)): 34 | assert face[i] >= 0 and face[i] < num_vertices, 'invalid vertex index %d (of %d vertices) (%s)' % (face[i], num_vertices, file) 35 | 36 | fp.write(str(face[i])) 37 | if i < len(face) - 1: 38 | fp.write(' ') 39 | 40 | fp.write('\n') 41 | 42 | # add empty line to be sure 43 | fp.write('\n') 44 | 45 | def read_off(file): 46 | """ 47 | Reads vertices and faces from an off file. 48 | 49 | :param file: path to file to read 50 | :type file: str 51 | :return: vertices and faces as lists of tuples 52 | :rtype: [(float)], [(int)] 53 | """ 54 | 55 | assert os.path.exists(file), 'file %s not found' % file 56 | 57 | with open(file, 'r') as fp: 58 | lines = fp.readlines() 59 | lines = [line.strip() for line in lines] 60 | 61 | # Fix for ModelNet bug were 'OFF' and the number of vertices and faces are 62 | # all in the first line. 63 | if len(lines[0]) > 3: 64 | assert lines[0][:3] == 'OFF' or lines[0][:3] == 'off', 'invalid OFF file %s' % file 65 | 66 | parts = lines[0][3:].split(' ') 67 | assert len(parts) == 3 68 | 69 | num_vertices = int(parts[0]) 70 | assert num_vertices > 0 71 | 72 | num_faces = int(parts[1]) 73 | assert num_faces > 0 74 | 75 | start_index = 1 76 | # This is the regular case! 77 | else: 78 | assert lines[0] == 'OFF' or lines[0] == 'off', 'invalid OFF file %s' % file 79 | 80 | parts = lines[1].split(' ') 81 | assert len(parts) == 3 82 | 83 | num_vertices = int(parts[0]) 84 | assert num_vertices > 0 85 | 86 | num_faces = int(parts[1]) 87 | assert num_faces > 0 88 | 89 | start_index = 2 90 | 91 | vertices = [] 92 | for i in range(num_vertices): 93 | vertex = lines[start_index + i].split(' ') 94 | vertex = [float(point.strip()) for point in vertex if point != ''] 95 | assert len(vertex) == 3 96 | 97 | vertices.append(vertex) 98 | 99 | faces = [] 100 | for i in range(num_faces): 101 | face = lines[start_index + num_vertices + i].split(' ') 102 | face = [index.strip() for index in face if index != ''] 103 | 104 | # check to be sure 105 | for index in face: 106 | assert index != '', 'found empty vertex index: %s (%s)' % (lines[start_index + num_vertices + i], file) 107 | 108 | face = [int(index) for index in face] 109 | 110 | assert face[0] == len(face) - 1, 'face should have %d vertices but as %d (%s)' % (face[0], len(face) - 1, file) 111 | assert face[0] == 3, 'only triangular meshes supported (%s)' % file 112 | for index in face: 113 | assert index >= 0 and index < num_vertices, 'vertex %d (of %d vertices) does not exist (%s)' % (index, num_vertices, file) 114 | 115 | assert len(face) > 1 116 | 117 | faces.append(face) 118 | 119 | return vertices, faces 120 | 121 | assert False, 'could not open %s' % file 122 | 123 | def write_obj(file, vertices, faces): 124 | """ 125 | Writes the given vertices and faces to OBJ. 126 | 127 | :param vertices: vertices as tuples of (x, y, z) coordinates 128 | :type vertices: [(float)] 129 | :param faces: faces as tuples of (num_vertices, vertex_id_1, vertex_id_2, ...) 130 | :type faces: [(int)] 131 | """ 132 | 133 | num_vertices = len(vertices) 134 | num_faces = len(faces) 135 | 136 | assert num_vertices > 0 137 | assert num_faces > 0 138 | 139 | with open(file, 'w') as fp: 140 | for vertex in vertices: 141 | assert len(vertex) == 3, 'invalid vertex with %d dimensions found (%s)' % (len(vertex), file) 142 | fp.write('v' + ' ' + str(vertex[0]) + ' ' + str(vertex[1]) + ' ' + str(vertex[2]) + '\n') 143 | 144 | for face in faces: 145 | assert len(face) == 3, 'only triangular faces supported (%s)' % file 146 | fp.write('f ') 147 | 148 | for i in range(len(face)): 149 | assert face[i] >= 0 and face[i] < num_vertices, 'invalid vertex index %d (of %d vertices) (%s)' % (face[i], num_vertices, file) 150 | 151 | # face indices are 1-based 152 | fp.write(str(face[i] + 1)) 153 | if i < len(face) - 1: 154 | fp.write(' ') 155 | 156 | fp.write('\n') 157 | 158 | # add empty line to be sure 159 | fp.write('\n') 160 | 161 | def read_obj(file): 162 | """ 163 | Reads vertices and faces from an obj file. 164 | 165 | :param file: path to file to read 166 | :type file: str 167 | :return: vertices and faces as lists of tuples 168 | :rtype: [(float)], [(int)] 169 | """ 170 | 171 | assert os.path.exists(file), 'file %s not found' % file 172 | 173 | with open(file, 'r') as fp: 174 | lines = fp.readlines() 175 | lines = [line.strip() for line in lines if line.strip()] 176 | 177 | vertices = [] 178 | faces = [] 179 | for line in lines: 180 | parts = line.split(' ') 181 | parts = [part.strip() for part in parts if part] 182 | 183 | if parts[0] == 'v': 184 | assert len(parts) == 4, \ 185 | 'vertex should be of the form v x y z, but found %d parts instead (%s)' % (len(parts), file) 186 | assert parts[1] != '', 'vertex x coordinate is empty (%s)' % file 187 | assert parts[2] != '', 'vertex y coordinate is empty (%s)' % file 188 | assert parts[3] != '', 'vertex z coordinate is empty (%s)' % file 189 | 190 | vertices.append([float(parts[1]), float(parts[2]), float(parts[3])]) 191 | elif parts[0] == 'f': 192 | assert len(parts) == 4, \ 193 | 'face should be of the form f v1/vt1/vn1 v2/vt2/vn2 v2/vt2/vn2, but found %d parts (%s) instead (%s)' % (len(parts), line, file) 194 | 195 | components = parts[1].split('/') 196 | assert len(components) >= 1 and len(components) <= 3, \ 197 | 'face component should have the forms v, v/vt or v/vt/vn, but found %d components instead (%s)' % (len(components), file) 198 | assert components[0].strip() != '', \ 199 | 'face component is empty (%s)' % file 200 | v1 = int(components[0]) 201 | 202 | components = parts[2].split('/') 203 | assert len(components) >= 1 and len(components) <= 3, \ 204 | 'face component should have the forms v, v/vt or v/vt/vn, but found %d components instead (%s)' % (len(components), file) 205 | assert components[0].strip() != '', \ 206 | 'face component is empty (%s)' % file 207 | v2 = int(components[0]) 208 | 209 | components = parts[3].split('/') 210 | assert len(components) >= 1 and len(components) <= 3, \ 211 | 'face component should have the forms v, v/vt or v/vt/vn, but found %d components instead (%s)' % (len(components), file) 212 | assert components[0].strip() != '', \ 213 | 'face component is empty (%s)' % file 214 | v3 = int(components[0]) 215 | 216 | #assert v1 != v2 and v2 != v3 and v3 != v2, 'degenerate face detected: %d %d %d (%s)' % (v1, v2, v3, file) 217 | if v1 == v2 or v2 == v3 or v1 == v3: 218 | print('[Info] skipping degenerate face in %s' % file) 219 | else: 220 | faces.append([v1 - 1, v2 - 1, v3 - 1]) # indices are 1-based! 221 | else: 222 | assert False, 'expected either vertex or face but got line: %s (%s)' % (line, file) 223 | 224 | return vertices, faces 225 | 226 | assert False, 'could not open %s' % file 227 | 228 | def write_ply(file, vertices, faces): 229 | """ 230 | Writes the given vertices and faces to PLY. 231 | 232 | :param vertices: vertices as tuples of (x, y, z) coordinates 233 | :type vertices: [(float)] 234 | :param faces: faces as tuples of (num_vertices, vertex_id_1, vertex_id_2, ...) 235 | :type faces: [(int)] 236 | """ 237 | 238 | num_vertices = len(vertices) 239 | num_faces = len(faces) 240 | 241 | assert num_vertices > 0 242 | assert num_faces > 0 243 | 244 | with open(file, 'w') as fp: 245 | fp.write('ply\n') 246 | fp.write('format ascii 1.0\n') 247 | fp.write('element vertex ' + str(len(vertices))) 248 | fp.write('property float x') 249 | fp.write('property float y') 250 | fp.write('property float z') 251 | fp.write('element face ' + str(len(faces))) 252 | fp.write('property list uchar int vertex_indices') 253 | fp.write('end_header') 254 | 255 | for vertex in vertices: 256 | assert len(vertex) == 3, 'invalid vertex with %d dimensions found (%s)' % (len(vertex), file) 257 | fp.write(str(vertex[0]) + ' ' + str(vertex[1]) + ' ' + str(vertex[2]) + '\n') 258 | 259 | for face in faces: 260 | assert len(face) == 3, 'only triangular faces supported (%s)' % file 261 | fp.write('3 ') 262 | 263 | for i in range(len(face)): 264 | assert face[i] >= 0 and face[i] < num_vertices, 'invalid vertex index %d (of %d vertices) (%s)' % (face[i], num_vertices, file) 265 | 266 | # face indices are 0-based 267 | fp.write(str(face[i])) 268 | if i < len(face) - 1: 269 | fp.write(' ') 270 | 271 | fp.write('\n') 272 | 273 | # add empty line to be sure 274 | fp.write('\n') 275 | 276 | class Mesh: 277 | """ 278 | Represents a mesh. 279 | """ 280 | 281 | def __init__(self, vertices = [[]], faces = [[]]): 282 | """ 283 | Construct a mesh from vertices and faces. 284 | 285 | :param vertices: list of vertices, or numpy array 286 | :type vertices: [[float]] or numpy.ndarray 287 | :param faces: list of faces or numpy array, i.e. the indices of the corresponding vertices per triangular face 288 | :type faces: [[int]] fo rnumpy.ndarray 289 | """ 290 | 291 | self.vertices = np.array(vertices, dtype = float) 292 | """ (numpy.ndarray) Vertices. """ 293 | 294 | self.faces = np.array(faces, dtype = int) 295 | """ (numpy.ndarray) Faces. """ 296 | 297 | assert self.vertices.shape[1] == 3 298 | assert self.faces.shape[1] == 3 299 | 300 | def extents(self): 301 | """ 302 | Get the extents. 303 | 304 | :return: (min_x, min_y, min_z), (max_x, max_y, max_z) 305 | :rtype: (float, float, float), (float, float, float) 306 | """ 307 | 308 | min = [0]*3 309 | max = [0]*3 310 | 311 | for i in range(3): 312 | min[i] = np.min(self.vertices[:, i]) 313 | max[i] = np.max(self.vertices[:, i]) 314 | 315 | return tuple(min), tuple(max) 316 | 317 | def switch_axes(self, axis_1, axis_2): 318 | """ 319 | Switch the two axes, this is usually useful for switching y and z axes. 320 | 321 | :param axis_1: index of first axis 322 | :type axis_1: int 323 | :param axis_2: index of second axis 324 | :type axis_2: int 325 | """ 326 | 327 | temp = np.copy(self.vertices[:, axis_1]) 328 | self.vertices[:, axis_1] = self.vertices[:, axis_2] 329 | self.vertices[:, axis_2] = temp 330 | 331 | def mirror(self, axis): 332 | """ 333 | Mirror given axis. 334 | 335 | :param axis: axis to mirror 336 | :type axis: int 337 | """ 338 | 339 | self.vertices[:, axis] *= -1 340 | 341 | def scale(self, scales): 342 | """ 343 | Scale the mesh in all dimensions. 344 | 345 | :param scales: tuple of length 3 with scale for (x, y, z) 346 | :type scales: (float, float, float) 347 | """ 348 | 349 | assert len(scales) == 3 350 | 351 | for i in range(3): 352 | self.vertices[:, i] *= scales[i] 353 | 354 | def translate(self, translation): 355 | """ 356 | Translate the mesh. 357 | 358 | :param translation: translation as (x, y, z) 359 | :type translation: (float, float, float) 360 | """ 361 | 362 | assert len(translation) == 3 363 | 364 | for i in range(3): 365 | self.vertices[:, i] += translation[i] 366 | 367 | @staticmethod 368 | def from_off(filepath): 369 | """ 370 | Read a mesh from OFF. 371 | 372 | :param filepath: path to OFF file 373 | :type filepath: str 374 | :return: mesh 375 | :rtype: Mesh 376 | """ 377 | 378 | vertices, faces = read_off(filepath) 379 | 380 | real_faces = [] 381 | for face in faces: 382 | assert len(face) == 4 383 | real_faces.append([face[1], face[2], face[3]]) 384 | 385 | return Mesh(vertices, real_faces) 386 | 387 | def to_off(self, filepath): 388 | """ 389 | Write mesh to OFF. 390 | 391 | :param filepath: path to write file to 392 | :type filepath: str 393 | """ 394 | 395 | faces = np.ones((self.faces.shape[0], 4), dtype = int)*3 396 | faces[:, 1:4] = self.faces[:, :] 397 | 398 | write_off(filepath, self.vertices.tolist(), faces.tolist()) 399 | 400 | @staticmethod 401 | def from_obj(filepath): 402 | """ 403 | Read a mesh from OBJ. 404 | 405 | :param filepath: path to OFF file 406 | :type filepath: str 407 | :return: mesh 408 | :rtype: Mesh 409 | """ 410 | 411 | vertices, faces = read_obj(filepath) 412 | return Mesh(vertices, faces) 413 | 414 | def to_obj(self, filepath): 415 | """ 416 | Write mesh to OBJ file. 417 | 418 | :param filepath: path to OBJ file 419 | :type filepath: str 420 | """ 421 | 422 | write_obj(filepath, self.vertices.tolist(), self.faces.tolist()) -------------------------------------------------------------------------------- /obj_to_off.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from mesh import Mesh 4 | 5 | 6 | def main(): 7 | """ 8 | Convert OBJ to OFF. 9 | """ 10 | 11 | parser = argparse.ArgumentParser(description='Convert OBJ to OFF.') 12 | parser.add_argument('input', type=str, help='OBJ file.') 13 | parser.add_argument('output', type=str, help='OFF file.') 14 | 15 | args = parser.parse_args() 16 | if not os.path.exists(args.input): 17 | print('Input file does not exist.') 18 | exit(1) 19 | 20 | mesh = Mesh.from_obj(args.input) 21 | print('Read %s.' % args.input) 22 | 23 | mesh.to_off(args.output) 24 | print('Wrote %s.' % args.output) 25 | 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /off_to_obj.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from mesh import Mesh 4 | 5 | 6 | def main(): 7 | """ 8 | Convert OFF to OBJ. 9 | """ 10 | 11 | parser = argparse.ArgumentParser(description='Convert OFF to OBJ.') 12 | parser.add_argument('input', type=str, help='OFF file.') 13 | parser.add_argument('output', type=str, help='OBJ file.') 14 | 15 | args = parser.parse_args() 16 | if not os.path.exists(args.input): 17 | print('Input file does not exist.') 18 | exit(1) 19 | 20 | mesh = Mesh.from_off(args.input) 21 | print('Read %s.' % args.input) 22 | 23 | mesh.to_obj(args.output) 24 | print('Wrote %s.' % args.output) 25 | 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /point_cloud.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | 5 | class PointCloud: 6 | """ 7 | Encapsulates a set of points stored as N x 3 matrix. 8 | """ 9 | 10 | def __init__(self, points=None): 11 | """ 12 | Constructor. 13 | 14 | :param points: points as list or np.array 15 | """ 16 | 17 | if points is None: 18 | self.points = np.zeros((0, 3)) 19 | else: 20 | self.points = np.array(points) 21 | 22 | @staticmethod 23 | def from_txt(filepath): 24 | """ 25 | Load from TXT file. 26 | 27 | :param filepath: path to TXT file. 28 | :return: point cloud 29 | """ 30 | 31 | assert os.path.exists(filepath) 32 | 33 | with open(filepath, 'r') as f: 34 | lines = f.readlines() 35 | lines = [line.strip() for line in lines if line.strip() != ''] 36 | 37 | num_points = int(lines[0]) 38 | points = np.zeros((num_points, 3)) 39 | assert num_points == len(lines) - 1 40 | 41 | for i in range(0, num_points): 42 | line = lines[i + 1] 43 | 44 | parts = line.split(' ') 45 | assert len(parts) == 3, "invalid line: %s" % line 46 | 47 | for j in range(3): 48 | points[i, j] = float(parts[j]) 49 | 50 | return PointCloud(points) 51 | 52 | def to_txt(self, filepath): 53 | """ 54 | Write TXT. 55 | 56 | :param filepath: path to output file 57 | """ 58 | with open(filepath, 'w') as f: 59 | f.write(str(self.points.shape[0]) + '\n') 60 | 61 | for n in range(self.points.shape[0]): 62 | f.write(str(self.points[n, 0]) + ' ' + str(self.points[n, 1]) + ' ' + str(self.points[n, 2]) + '\n') 63 | 64 | def to_ply(self, filepath): 65 | """ 66 | To PLY file. 67 | 68 | :param filepath: path to output file 69 | """ 70 | 71 | with open(filepath, 'w') as f: 72 | f.write('ply\n') 73 | f.write('format ascii 1.0\n') 74 | #f.write('format binary_little_endian 1.0\n') 75 | #f.write('format binary_big_endian 1.0\n') 76 | f.write('element vertex ' + str(self.points.shape[0]) + '\n') 77 | f.write('property float x\n') 78 | f.write('property float y\n') 79 | f.write('property float z\n') 80 | f.write('property uchar red\n') 81 | f.write('property uchar green\n') 82 | f.write('property uchar blue\n') 83 | f.write('end_header\n') 84 | 85 | for n in range(self.points.shape[0]): 86 | f.write(str(self.points[n, 0]) + ' ' + str(self.points[n, 1]) + ' ' + str(self.points[n, 2])) 87 | f.write(' 0 0 0\n') -------------------------------------------------------------------------------- /render_binvox.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from blender_utils import * 4 | 5 | 6 | def main(): 7 | """ 8 | Main function for rendering a specific binvox file. 9 | """ 10 | 11 | parser = argparse.ArgumentParser(description='Renders an occupancy grid (BINVOX file).') 12 | parser.add_argument('--binvox', type=str, help='Path to OFF file.') 13 | parser.add_argument('--output', type=str, default='output.png', help='Path to output PNG image.') 14 | 15 | try: 16 | argv = sys.argv[sys.argv.index("--") + 1:] 17 | except ValueError: 18 | argv = "" 19 | args = parser.parse_args(argv) 20 | 21 | if not os.path.exists(args.binvox): 22 | log('BINVOX file not found.', LogLevel.ERROR) 23 | exit() 24 | 25 | camera_target = initialize() 26 | binvox_material = make_material('BRC_Material_Occupancy', (0.66, 0.45, 0.23), 0.8, True) 27 | 28 | load_binvox(args.binvox, 0.0125, binvox_material, (0, 0, 0), (1, 1, 1), 'zxy') 29 | 30 | rotation = (5, 0, -55) 31 | distance = 0.5 32 | render(camera_target, args.output, rotation, distance) 33 | 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /render_off.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from blender_utils import * 4 | 5 | 6 | def main(): 7 | """ 8 | Render a mesh from an OFF file. 9 | """ 10 | 11 | parser = argparse.ArgumentParser(description='Renders a mesh (OFF file).') 12 | parser.add_argument('--off', type=str, help='Path to OFF file.') 13 | parser.add_argument('--output', type=str, default='output.png', help='Path to output PNG image.') 14 | 15 | try: 16 | argv = sys.argv[sys.argv.index("--") + 1:] 17 | except ValueError: 18 | argv = "" 19 | args = parser.parse_args(argv) 20 | 21 | if not os.path.exists(args.off): 22 | log('OFF file not found.', LogLevel.ERROR) 23 | exit() 24 | 25 | camera_target = initialize() 26 | off_material = make_material('BRC_Material_Mesh', (0.66, 0.45, 0.23), 0.8, True) 27 | 28 | load_off(args.off, off_material, (-0.5, -0.5, -0.5), 0.03125, 'xzy') 29 | 30 | rotation = (5, 0, -55) 31 | distance = 0.5 32 | render(camera_target, args.output, rotation, distance) 33 | 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /render_off_txt.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from blender_utils import * 4 | 5 | 6 | def main(): 7 | """ 8 | Render a mesh and a point cloud together. 9 | """ 10 | 11 | parser = argparse.ArgumentParser(description='Renders a mesh (OFF file) and a point cloud (TXT file) together.') 12 | parser.add_argument('--off', type=str, help='Path to OFF file.') 13 | parser.add_argument('--txt', type=str, help='Path to TXT file.') 14 | parser.add_argument('--output', type=str, default='output.png', help='Path to output PNG image.') 15 | 16 | try: 17 | argv = sys.argv[sys.argv.index("--") + 1:] 18 | except ValueError: 19 | argv = "" 20 | args = parser.parse_args(argv) 21 | 22 | if not os.path.exists(args.off): 23 | log('OFF file not found.', LogLevel.ERROR) 24 | exit() 25 | if not os.path.exists(args.txt): 26 | log('TXT file not found.', LogLevel.ERROR) 27 | exit() 28 | 29 | camera_target = initialize() 30 | off_material = make_material('BRC_Material_Mesh', (0.66, 0.45, 0.23), 0.8, True) 31 | txt_material = make_material('BRC_Material_Point_Cloud', (0.65, 0.23, 0.25), 1, True) 32 | 33 | load_off(args.off, off_material, (-0.5, -0.5, -0.5), 0.03125, 'xzy') 34 | load_txt(args.txt, 0.0075, txt_material, (-0.5, -0.5, -0.5), 0.03125, 'xzy') 35 | 36 | rotation = (5, 0, -55) 37 | distance = 0.5 38 | render(camera_target, args.output, rotation, distance) 39 | 40 | 41 | if __name__ == '__main__': 42 | main() 43 | -------------------------------------------------------------------------------- /render_txt.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from blender_utils import * 4 | 5 | 6 | def main(): 7 | """ 8 | Render a point cloud from a TXT file. 9 | """ 10 | 11 | parser = argparse.ArgumentParser(description='Renders a mesh (OFF file) and a point cloud (TXT file) together.') 12 | parser.add_argument('--txt', type=str, help='Path to TXT file.') 13 | parser.add_argument('--output', type=str, default='output.png', help='Path to output PNG image.') 14 | 15 | try: 16 | argv = sys.argv[sys.argv.index("--") + 1:] 17 | except ValueError: 18 | argv = "" 19 | args = parser.parse_args(argv) 20 | 21 | if not os.path.exists(args.txt): 22 | log('TXT file not found.', LogLevel.ERROR) 23 | exit() 24 | 25 | camera_target = initialize() 26 | txt_material = make_material('BRC_Material_Point_Cloud', (0.65, 0.23, 0.25), 1, True) 27 | load_txt(args.txt, 0.0075, txt_material, (-0.5, -0.5, -0.5), 0.03125, 'xzy') 28 | 29 | rotation = (5, 0, -55) 30 | distance = 0.5 31 | render(camera_target, args.output, rotation, distance) 32 | 33 | 34 | if __name__ == '__main__': 35 | main() 36 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidstutz/bpy-visualization-utils/9aec34f4b58d83992c3c3ce7eb6369d9055760bc/screenshot.jpg -------------------------------------------------------------------------------- /txt_to_ply.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from point_cloud import PointCloud 4 | 5 | 6 | def main(): 7 | """ 8 | Convert TXT to PLY. 9 | """ 10 | 11 | parser = argparse.ArgumentParser(description='Convert TXT to PLY.') 12 | parser.add_argument('input', type=str, help='TXT file.') 13 | parser.add_argument('output', type=str, help='PLY file.') 14 | 15 | args = parser.parse_args() 16 | if not os.path.exists(args.input): 17 | print('Input file does not exist.') 18 | exit(1) 19 | 20 | point_cloud = PointCloud.from_txt(args.input) 21 | print('Read %s.' % args.input) 22 | 23 | point_cloud.to_ply(args.output) 24 | print('Wrote %s.' % args.output) 25 | 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /write_binvox.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import argparse 3 | import binvox_rw 4 | 5 | if __name__ == '__main__': 6 | parser = argparse.ArgumentParser(description='Write an example BINVOX file.') 7 | parser.add_argument('output', type=str, help='BINVOX file.') 8 | 9 | args = parser.parse_args() 10 | 11 | volume = np.zeros((32, 32, 32)) 12 | volume[10:22, 10:22, 10:22] = 1 13 | 14 | model = binvox_rw.Voxels(volume > 0.5, volume.shape, (0, 0, 0), 1) 15 | with open(args.output, 'w') as fp: 16 | model.write(fp) 17 | print('Wote %s.' % args.output) -------------------------------------------------------------------------------- /write_txt.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import argparse 3 | from point_cloud import PointCloud 4 | 5 | 6 | if __name__ == '__main__': 7 | 8 | parser = argparse.ArgumentParser(description='Write an example point cloud as TXT.') 9 | parser.add_argument('output', type=str, help='TXT file for example point cloud.') 10 | 11 | args = parser.parse_args() 12 | point_cloud = PointCloud(np.random.random((100, 3))) 13 | point_cloud.to_txt(args.output) 14 | print('Wrote %s.' % args.output) 15 | --------------------------------------------------------------------------------