├── ImportCSV.py ├── README.md └── images └── Preview.png /ImportCSV.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import csv 4 | import mathutils 5 | from bpy_extras.io_utils import axis_conversion, orientation_helper 6 | from bpy.props import ( 7 | BoolProperty, 8 | IntProperty, 9 | IntVectorProperty, 10 | StringProperty, 11 | ) 12 | 13 | 14 | bl_info = { 15 | "name": "CSV Importer", 16 | "author": "Puxtril", 17 | "version": (1, 3, 0), 18 | "blender": (2, 80, 0), 19 | "location": "File > Import-Export", 20 | "description": "Import CSV mesh dump", 21 | "category": "Import-Export", 22 | } 23 | 24 | 25 | @orientation_helper(axis_forward="Z", axis_up="Y") 26 | class Import_CSV(bpy.types.Operator): 27 | """Imports .csv meshes""" 28 | bl_idname = "object.import_csv" 29 | bl_label = "Import csv" 30 | bl_options = {"PRESET", "UNDO"} 31 | filepath: bpy.props.StringProperty(subtype="FILE_PATH") 32 | filter_glob: StringProperty(default="*.csv", options={"HIDDEN"}) 33 | 34 | ######################################################################## 35 | # General Properties 36 | 37 | mirrorVertX: bpy.props.BoolProperty( 38 | name="Mirror X", 39 | description="Mirror all the vertices across X axis", 40 | default=True, 41 | ) 42 | vertexOrder: bpy.props.BoolProperty( 43 | name="Flip Winding", 44 | description="Reorder vertices in counter-clockwise order", 45 | default=False, 46 | ) 47 | mirrorUV: bpy.props.BoolProperty( 48 | name="Mirror V", 49 | description="Flip UV Maps vertically", 50 | default=True, 51 | ) 52 | cleanMesh: bpy.props.BoolProperty( 53 | name="Clean Mesh", 54 | description="Remove doubles and enable smooth shading", 55 | default=True, 56 | ) 57 | showNormalize: bpy.props.BoolProperty( 58 | name="Show Normalize", 59 | description="Show options to normalize input values", 60 | default=False, 61 | ) 62 | skipFirstRow: bpy.props.BoolProperty( 63 | name="Skip Title", 64 | description="Skip first row of the .csv file", 65 | default=True, 66 | ) 67 | positionIndex: bpy.props.IntVectorProperty( 68 | name="Positions", 69 | description="Column numbers (0 indexed) of vertex positions", 70 | size=3, 71 | min=0, 72 | soft_max=20, 73 | default=(2, 3, 4), 74 | ) 75 | 76 | ######################################################################## 77 | # UV properties 78 | 79 | uvCount: bpy.props.IntProperty( 80 | name="UV Map Count", 81 | description="Number of UV maps to import", 82 | min=0, 83 | max=5, 84 | default=1, 85 | ) 86 | uvIndex0: bpy.props.IntVectorProperty( 87 | name="UV 1", 88 | description="Column numbers (0 indexed) of UV map", 89 | size=2, 90 | min=0, 91 | soft_max=20, 92 | default=(14, 15), 93 | ) 94 | uvNormalize0: bpy.props.IntProperty( 95 | name="Normalize UV 1", 96 | description="Divide inputs by this number", 97 | default=1 98 | ) 99 | uvIndex1: bpy.props.IntVectorProperty( 100 | name="UV 2", 101 | description="Column numbers (0 indexed) of UV map", 102 | size=2, 103 | min=0, 104 | soft_max=20, 105 | default=(0, 0), 106 | ) 107 | uvNormalize1: bpy.props.IntProperty( 108 | name="Normalize UV 2", 109 | description="Divide inputs by this number", 110 | default=1 111 | ) 112 | uvIndex2: bpy.props.IntVectorProperty( 113 | name="UV 3", 114 | description="Column numbers (0 indexed) of UV map", 115 | size=2, 116 | min=0, 117 | soft_max=20, 118 | default=(0, 0), 119 | ) 120 | uvNormalize2: bpy.props.IntProperty( 121 | name="Normalize UV 3", 122 | description="Divide inputs by this number", 123 | default=1 124 | ) 125 | uvIndex3: bpy.props.IntVectorProperty( 126 | name="UV 4", 127 | description="Column numbers (0 indexed) of UV map", 128 | size=2, 129 | min=0, 130 | soft_max=20, 131 | default=(0, 0), 132 | ) 133 | uvNormalize3: bpy.props.IntProperty( 134 | name="Normalize UV 4", 135 | description="Divide inputs by this number", 136 | default=1 137 | ) 138 | uvIndex4: bpy.props.IntVectorProperty( 139 | name="UV 5", 140 | description="Column numbers (0 indexed) of UV map", 141 | size=2, 142 | min=0, 143 | soft_max=20, 144 | default=(0, 0), 145 | ) 146 | uvNormalize4: bpy.props.IntProperty( 147 | name="Normalize UV 5", 148 | description="Divide inputs by this number", 149 | default=1 150 | ) 151 | 152 | ######################################################################## 153 | # Vertex Color3 Properties 154 | 155 | color3Count: bpy.props.IntProperty( 156 | name="Vertex Color RGB Count", 157 | description="Number of Vertex Colors (RGB) to import", 158 | min=0, 159 | max=5, 160 | default=0, 161 | ) 162 | color3Index0: bpy.props.IntVectorProperty( 163 | name="Vertex Color RGB 1", 164 | description="Column numbers (0 indexed) of Vertex Colors (RGB)", 165 | size=3, 166 | min=0, 167 | soft_max=20, 168 | default=(10, 11, 12), 169 | ) 170 | color3Normalize0: bpy.props.IntProperty( 171 | name="Normalize Color RGB 1", 172 | description="Divide inputs by this number", 173 | default=1 174 | ) 175 | color3Index1: bpy.props.IntVectorProperty( 176 | name="Vertex Color RGB 2", 177 | description="Column numbers (0 indexed) of Vertex Colors (RGB)", 178 | size=3, 179 | min=0, 180 | soft_max=20, 181 | default=(0, 0, 0), 182 | ) 183 | color3Normalize1: bpy.props.IntProperty( 184 | name="Normalize Color RGB 2", 185 | description="Divide inputs by this number", 186 | default=1 187 | ) 188 | color3Index2: bpy.props.IntVectorProperty( 189 | name="Vertex Color RGB 3", 190 | description="Column numbers (0 indexed) of Vertex Colors (RGB)", 191 | size=3, 192 | min=0, 193 | soft_max=20, 194 | default=(0, 0, 0), 195 | ) 196 | color3Normalize2: bpy.props.IntProperty( 197 | name="Normalize Colors RGB 3", 198 | description="Divide inputs by this number", 199 | default=1 200 | ) 201 | color3Index3: bpy.props.IntVectorProperty( 202 | name="Vertex Color RGB 4", 203 | description="Column numbers (0 indexed) of Vertex Colors (RGB)", 204 | size=3, 205 | min=0, 206 | soft_max=20, 207 | default=(0, 0, 0), 208 | ) 209 | color3Normalize3: bpy.props.IntProperty( 210 | name="Normalize Colors RGB 4", 211 | description="Divide inputs by this number", 212 | default=1 213 | ) 214 | color3Index4: bpy.props.IntVectorProperty( 215 | name="Vertex Color RGB 5", 216 | description="Column numbers (0 indexed) of Vertex Colors (RGB)", 217 | size=3, 218 | min=0, 219 | soft_max=20, 220 | default=(0, 0, 0), 221 | ) 222 | color3Normalize4: bpy.props.IntProperty( 223 | name="Normalize Colors RGB 5", 224 | description="Divide inputs by this number", 225 | default=1 226 | ) 227 | 228 | ######################################################################## 229 | # Vertex Color Properties 230 | 231 | colorCount: bpy.props.IntProperty( 232 | name="Vertex Color Alpha Count", 233 | description="Number of Vertex Colors (Alpha) to import", 234 | min=0, 235 | max=5, 236 | default=0, 237 | ) 238 | colorIndex0: bpy.props.IntProperty( 239 | name="Vertex Color Alpha 1", 240 | description="Column number (0 indexed) of Vertex Color (Alpha)", 241 | min=0, 242 | soft_max=20, 243 | default=0, 244 | ) 245 | colorNormalize0: bpy.props.IntProperty( 246 | name="Normalize Color Alpha 1", 247 | description="Divide input by this number", 248 | default=1 249 | ) 250 | colorIndex1: bpy.props.IntProperty( 251 | name="Vertex Color Alpha 2", 252 | description="Column number (0 indexed) of Vertex Color (Alpha)", 253 | min=0, 254 | soft_max=20, 255 | default=0, 256 | ) 257 | colorNormalize1: bpy.props.IntProperty( 258 | name="Normalize Color Alpha 2", 259 | description="Divide input by this number", 260 | default=1 261 | ) 262 | colorIndex2: bpy.props.IntProperty( 263 | name="Vertex Color Alpha 3", 264 | description="Column number (0 indexed) of Vertex Color (Alpha)", 265 | min=0, 266 | soft_max=20, 267 | default=0, 268 | ) 269 | colorNormalize2: bpy.props.IntProperty( 270 | name="Normalize Color Alpha 3", 271 | description="Divide input by this number", 272 | default=1 273 | ) 274 | colorIndex3: bpy.props.IntProperty( 275 | name="Vertex Color Alpha 4", 276 | description="Column number (0 indexed) of Vertex Color (Alpha)", 277 | min=0, 278 | soft_max=20, 279 | default=0, 280 | ) 281 | colorNormalize3: bpy.props.IntProperty( 282 | name="Normalize Color Alpha 4", 283 | description="Divide input by this number", 284 | default=1 285 | ) 286 | colorIndex4: bpy.props.IntProperty( 287 | name="Vertex Color Alpha 5", 288 | description="Column number (0 indexed) of Vertex Color (Alpha)", 289 | min=0, 290 | soft_max=20, 291 | default=0, 292 | ) 293 | colorNormalize4: bpy.props.IntProperty( 294 | name="Normalize Color Alpha 5", 295 | description="Divide input by this number", 296 | default=1 297 | ) 298 | 299 | ######################################################################## 300 | # Operator Functions 301 | 302 | def execute(self, context): 303 | transformMatrix = axis_conversion( 304 | from_forward=self.axis_forward, 305 | from_up=self.axis_up, 306 | ).to_4x4() 307 | 308 | # Only parse what it shown in the importer UI 309 | uvArgs = [self.uvIndex0, self.uvIndex1, self.uvIndex2, self.uvIndex3, self.uvIndex4] 310 | color3Args = [self.color3Index0, self.color3Index1, self.color3Index2, self.color3Index3, self.color3Index4] 311 | colorArgs = [self.colorIndex0, self.colorIndex1, self.colorIndex2, self.colorIndex3, self.colorIndex4] 312 | 313 | verts, faces, uvs, color3s, colors = importCSV( 314 | self.filepath, 315 | self.positionIndex, 316 | uvArgs[: self.uvCount], 317 | color3Args[: self.color3Count], 318 | colorArgs[: self.colorCount], 319 | self.mirrorVertX, 320 | self.mirrorUV, 321 | self.vertexOrder, 322 | self.skipFirstRow, 323 | ) 324 | 325 | # Don't do anything if not shown 326 | if self.showNormalize: 327 | uvsNormalizeArgs = [self.uvNormalize0, self.uvNormalize1, self.uvNormalize2, self.uvNormalize3, self.uvNormalize4] 328 | color3sNormalizeArgs = [self.color3Normalize0, self.color3Normalize1, self.color3Normalize2, self.color3Normalize3, self.color3Normalize4] 329 | colorsNormalizeArgs = [self.colorNormalize0, self.colorNormalize1, self.colorNormalize2, self.colorNormalize3, self.colorNormalize4] 330 | else: 331 | uvsNormalizeArgs = [1, 1, 1, 1, 1] 332 | color3sNormalizeArgs = [1, 1, 1, 1, 1] 333 | colorsNormalizeArgs = [1, 1, 1, 1, 1] 334 | 335 | meshObj = createMesh( 336 | verts, 337 | faces, 338 | uvs, 339 | uvsNormalizeArgs[: self.uvCount], 340 | color3s, 341 | color3sNormalizeArgs[: self.color3Count], 342 | colors, 343 | colorsNormalizeArgs[: self.colorCount], 344 | transformMatrix 345 | ) 346 | 347 | if self.cleanMesh: 348 | tempBmesh = bmesh.new() 349 | tempBmesh.from_mesh(meshObj.data) 350 | bmesh.ops.remove_doubles(tempBmesh, verts=tempBmesh.verts, dist=0.0001) 351 | for face in tempBmesh.faces: 352 | face.smooth = True 353 | tempBmesh.to_mesh(meshObj.data) 354 | tempBmesh.clear() 355 | meshObj.data.update() 356 | 357 | return {"FINISHED"} 358 | 359 | def invoke(self, context, event): 360 | context.window_manager.fileselect_add(self) 361 | return {"RUNNING_MODAL"} 362 | 363 | def draw(self, context): 364 | generalBox = self.layout.box() 365 | generalBox.prop(self, "axis_forward") 366 | generalBox.prop(self, "axis_up") 367 | row1 = generalBox.row() 368 | row1.prop(self, "mirrorVertX") 369 | row1.prop(self, "mirrorUV") 370 | row2 = generalBox.row() 371 | row2.prop(self, "cleanMesh") 372 | row2.prop(self, "vertexOrder") 373 | row3 = generalBox.row() 374 | row3.prop(self, "showNormalize") 375 | row3.prop(self, "skipFirstRow") 376 | 377 | indexBox = self.layout.box() 378 | indexBoxRow = indexBox.row() 379 | indexBoxRow.column().prop(self, "positionIndex") 380 | 381 | uvBox = self.layout.box() 382 | uvBox.prop(self, "uvCount") 383 | for i in range(self.uvCount): 384 | uvBox.prop(self, f"uvIndex{i}") 385 | if self.showNormalize: 386 | uvBox.prop(self, f"uvNormalize{i}") 387 | 388 | color3Box = self.layout.box() 389 | color3Box.prop(self, "color3Count") 390 | for i in range(self.color3Count): 391 | color3Box.prop(self, f"color3Index{i}") 392 | if self.showNormalize: 393 | color3Box.prop(self, f"color3Normalize{i}") 394 | 395 | colorBox = self.layout.box() 396 | colorBox.prop(self, "colorCount") 397 | for i in range(self.colorCount): 398 | colorBox.prop(self, f"colorIndex{i}") 399 | if self.showNormalize: 400 | colorBox.prop(self, f"colorNormalize{i}") 401 | 402 | 403 | def importCSV( 404 | filepath: str, 405 | posIndicies: tuple, 406 | uvMapsIndicies: list, 407 | color3sIndicies: list, 408 | colorsIndicies: list, 409 | mirrorVertX: bool, 410 | mirrorUV: bool, 411 | flipVertOrder: bool, 412 | skipFirstRow: bool, 413 | ): 414 | # list> 415 | vertices = [] 416 | # list> 417 | faces = [] 418 | # list>> 419 | uvs = [] 420 | for _ in range(len(uvMapsIndicies)): 421 | uvs.append([]) 422 | # list>> 423 | color3s = [] 424 | for _ in range(len(color3sIndicies)): 425 | color3s.append([]) 426 | # list> 427 | colors = [] 428 | for _ in range(len(colorsIndicies)): 429 | colors.append([]) 430 | 431 | x_mod = -1 if mirrorVertX else 1 432 | 433 | with open(filepath) as f: 434 | reader = csv.reader(f) 435 | 436 | if skipFirstRow: 437 | next(reader) 438 | 439 | curFace = [] 440 | for rowIndex, row in enumerate(reader): 441 | curVertexIndex = rowIndex 442 | 443 | # Position 444 | curPos = ( 445 | readFloatFromArray(row, posIndicies[0]) * x_mod, 446 | readFloatFromArray(row, posIndicies[1]), 447 | readFloatFromArray(row, posIndicies[2]) 448 | ) 449 | vertices.append(curPos) 450 | 451 | # UV Maps 452 | for i in range(len(uvMapsIndicies)): 453 | curUV = ( 454 | readFloatFromArray(row, uvMapsIndicies[i][0]), 455 | readFloatFromArray(row, uvMapsIndicies[i][1]) 456 | ) 457 | if mirrorUV: 458 | curUV = (curUV[0], 1 - curUV[1]) 459 | uvs[i].append(curUV) 460 | 461 | # Vertex Colors3 462 | for i in range(len(color3sIndicies)): 463 | curColor3 = ( 464 | readFloatFromArray(row, color3sIndicies[i][0]), 465 | readFloatFromArray(row, color3sIndicies[i][1]), 466 | readFloatFromArray(row, color3sIndicies[i][2]) 467 | ) 468 | color3s[i].append(curColor3) 469 | 470 | # Vertex Colors 471 | for i in range(len(colorsIndicies)): 472 | curColor = readFloatFromArray(row, colorsIndicies[i]) 473 | colors[i].append(curColor) 474 | 475 | # Append Faces 476 | curFace.append(curVertexIndex) 477 | if len(curFace) > 2: 478 | if flipVertOrder: 479 | faces.append((curFace[2], curFace[1], curFace[0])) 480 | else: 481 | faces.append(curFace) 482 | curFace = [] 483 | 484 | return vertices, faces, uvs, color3s, colors 485 | 486 | 487 | def createMesh( 488 | vertices: list, 489 | faces: list, 490 | uvs: list, 491 | uvsNormalize: list, 492 | color3s: list, 493 | color3sNormalize: list, 494 | colors: list, 495 | colorsNormalize: list, 496 | transformMatrix 497 | ): 498 | mesh = bpy.data.meshes.new("name") 499 | mesh.from_pydata(vertices, [], faces) 500 | 501 | # UV Maps 502 | for uvIndex in range(len(uvs)): 503 | uvLayer = mesh.uv_layers.new(name=f"UV{uvIndex}") 504 | for vertexIndex in range(len(uvLayer.data)): 505 | curUVs = uvs[uvIndex][vertexIndex] 506 | curUVsNorm = list(map(lambda x: x / uvsNormalize[uvIndex], curUVs)) 507 | uvLayer.data[vertexIndex].uv = curUVsNorm 508 | 509 | # Vertex Colors3 510 | for color3Index in range(len(color3s)): 511 | color3Layer = mesh.vertex_colors.new(name=f"rgb{color3Index}") 512 | for vertexIndex in range(len(color3Layer.data)): 513 | curCol3 = color3s[color3Index][vertexIndex] 514 | curCol3Norm = list(map(lambda x: x / color3sNormalize[color3Index], curCol3)) 515 | color3Layer.data[vertexIndex].color = [curCol3Norm[0], curCol3Norm[1], curCol3Norm[2], 0] 516 | 517 | # Vertex Colors 518 | for colorIndex in range(len(colors)): 519 | colorLayer = mesh.vertex_colors.new(name=f"alpha{colorIndex}") 520 | for vertexIndex in range(len(colorLayer.data)): 521 | curCol = colors[colorIndex][vertexIndex] 522 | curColNorm = curCol / colorsNormalize[colorIndex] 523 | colorLayer.data[vertexIndex].color = [curColNorm, curColNorm, curColNorm, 0] 524 | 525 | obj = bpy.data.objects.new("name", mesh) 526 | obj.data.transform(transformMatrix) 527 | obj.matrix_world = mathutils.Matrix() 528 | bpy.context.scene.collection.objects.link(obj) 529 | return obj 530 | 531 | 532 | # IndexError and ValueError can be thrown here 533 | # But wrap it in a generic `Exception` because... why not 534 | def readFloatFromArray(arr, index, default = 0.0): 535 | try: 536 | return float(arr[index]) 537 | except Exception: 538 | return default 539 | 540 | 541 | def menuItem(self, context): 542 | self.layout.operator(Import_CSV.bl_idname, text="Mesh CSV (.csv)") 543 | 544 | 545 | def register(): 546 | bpy.utils.register_class(Import_CSV) 547 | bpy.types.TOPBAR_MT_file_import.append(menuItem) 548 | 549 | 550 | def unregister(): 551 | bpy.utils.unregister_class(Import_CSV) 552 | bpy.types.TOPBAR_MT_file_import.remove(menuItem) 553 | 554 | 555 | if __name__ == "__main__": 556 | register() 557 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSV Importer 2 | 3 | While created for Renderdoc .csv files, this is intended to be a general-purpose .csv mesh importer. It will assume vertex indicies as the row number, and crate faces based on that. All options on the import panel have descriptive tooltips. 4 | 5 | To install, download ImportCSV.py and install as a blender addon. *Do not clone the repository as a .zip file* 6 | 7 | # Supported Mesh Data 8 | 9 | * Position 10 | * Normal 11 | * UV Maps (up to 5) 12 | * Vertex Colors (up to 5) 13 | 14 | # Import Options Screenshot 15 | 16 | ![Import Options](images/Preview.png) 17 | 18 | In each category, specify the index (0-based) in the .csv for each column of data. 19 | 20 | Not pictured: the extra options when the "Show Normalize" is checked. This will add an extra field to each category that will divide input values by a number; "normalizing" the data. -------------------------------------------------------------------------------- /images/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Puxtril/CSV-Import/5b55077b804e08041aae7a48c2147a80599f0215/images/Preview.png --------------------------------------------------------------------------------