SHOW:
|
|
- or go back to the newest paste.
| 1 | # bitcpy | |
| 2 | # CaveTroll | |
| 3 | ||
| 4 | bl_info = {
| |
| 5 | "name": "Legend of Grimrock Mesh Format (.mesh)", | |
| 6 | "author": "", | |
| 7 | "version": (1, 0, 1), | |
| 8 | "blender": (2, 5, 7), | |
| 9 | "api": 36339, | |
| 10 | "location": "File > Import > Legend of Grimrock Mesh (.mesh)", | |
| 11 | "description": "Import Legend of Grimrock Meshes (.mesh)", | |
| 12 | "warning": "", | |
| 13 | "wiki_url": "", | |
| 14 | "tracker_url": "", | |
| 15 | "category": "Import-Export"} | |
| 16 | ||
| 17 | import os | |
| 18 | import struct | |
| 19 | ||
| 20 | import bpy | |
| 21 | from mathutils import * | |
| 22 | ||
| 23 | from bpy.props import * | |
| 24 | from bpy_extras.io_utils import ExportHelper, ImportHelper | |
| 25 | from bpy_extras.image_utils import load_image | |
| 26 | ||
| 27 | class _mesh_material(object): | |
| 28 | __slots__ = ( | |
| 29 | "bl_mat", | |
| 30 | "bl_image", | |
| 31 | "name", | |
| 32 | "index_offset", | |
| 33 | "num_primitives", | |
| 34 | ) | |
| 35 | ||
| 36 | def __init__(self): | |
| 37 | self.bl_mat = None | |
| 38 | self.bl_image = None | |
| 39 | self.name = "<Empty>" | |
| 40 | self.index_offset = 0 | |
| 41 | self.num_primitives = 0 | |
| 42 | ||
| 43 | # Reads file magic from file | |
| 44 | def read_magic(file_object, endian = '<'): | |
| 45 | data = struct.unpack(endian+'4s', file.read(4))[0] | |
| 46 | return data; | |
| 47 | ||
| 48 | # Read signed integer from file | |
| 49 | def read_int(file_object, endian = '<'): | |
| 50 | data = struct.unpack(endian+'i', file_object.read(4))[0] | |
| 51 | return data | |
| 52 | ||
| 53 | def read_int2(file_object, endian = '<'): | |
| 54 | data = struct.unpack(endian+'ii', file_object.read(8)) | |
| 55 | return data | |
| 56 | ||
| 57 | def read_int3(file_object, endian = '<'): | |
| 58 | data = struct.unpack(endian+'iii', file_object.read(12)) | |
| 59 | return data | |
| 60 | ||
| 61 | def read_int4(file_object, endian = '<'): | |
| 62 | data = struct.unpack(endian+'iiii', file_object.read(16)) | |
| 63 | return data | |
| 64 | ||
| 65 | # Read floating point number from file | |
| 66 | def read_float(file_object, endian = '<'): | |
| 67 | data = struct.unpack(endian+'f', file_object.read(4))[0] | |
| 68 | return data | |
| 69 | ||
| 70 | def read_float2(file_object, endian = '<'): | |
| 71 | data = struct.unpack(endian+'ff', file_object.read(8)) | |
| 72 | return data | |
| 73 | ||
| 74 | def read_float3(file_object, endian = '<'): | |
| 75 | data = struct.unpack(endian+'fff', file_object.read(12)) | |
| 76 | return data | |
| 77 | ||
| 78 | def read_float4(file_object, endian = '<'): | |
| 79 | data = struct.unpack(endian+'ffff', file_object.read(16)) | |
| 80 | return data | |
| 81 | ||
| 82 | # Read unsigned bytes from file | |
| 83 | def read_byte(file_object, endian = '<'): | |
| 84 | data = struct.unpack(endian+'B', file_object.read(1))[0] | |
| 85 | return data | |
| 86 | ||
| 87 | def read_byte2(file_object, endian = '<'): | |
| 88 | data = struct.unpack(endian+'BB', file_object.read(2)) | |
| 89 | return data | |
| 90 | ||
| 91 | def read_byte3(file_object, endian = '<'): | |
| 92 | data = struct.unpack(endian+'BBB', file_object.read(3)) | |
| 93 | return data | |
| 94 | ||
| 95 | def read_byte4(file_object, endian = '<'): | |
| 96 | data = struct.unpack(endian+'BBBB', file_object.read(4)) | |
| 97 | return data | |
| 98 | ||
| 99 | # Read string from file | |
| 100 | def read_string(file_object, num, endian = '<'): | |
| 101 | raw_string = struct.unpack(endian+str(num)+'s', file_object.read(num))[0] | |
| 102 | data = raw_string.decode("utf-8", "ignore")
| |
| 103 | return data | |
| 104 | ||
| 105 | ||
| 106 | # Build content base path | |
| 107 | def build_assets_path(filename): | |
| 108 | #filename = "C:/grimrock/assets/models/wall_sets/dungeon/floor_01.mesh" | |
| 109 | pathname = os.path.dirname(filename) | |
| 110 | ||
| 111 | index = pathname.rfind("assets")
| |
| 112 | if index < 0: | |
| 113 | return pathname | |
| 114 | ||
| 115 | return pathname[0:index] | |
| 116 | ||
| 117 | ||
| 118 | # Parse the '/assets/materials/default.materials' file | |
| 119 | def parse_material_file( path_assets, find_material ): | |
| 120 | ||
| 121 | # diffuse, normal, specular | |
| 122 | texture_names = ["", "", ""] | |
| 123 | ||
| 124 | filename = os.path.join(path_assets, "assets/materials/default.materials") | |
| 125 | if os.path.exists(filename) == False: | |
| 126 | return texture_names | |
| 127 | ||
| 128 | file = open(filename, 'r') | |
| 129 | ||
| 130 | # Parse material (very primitive ...) | |
| 131 | material_scope = False | |
| 132 | material_found = False | |
| 133 | for line in file.readlines(): | |
| 134 | tokens = line.split() | |
| 135 | num = len(tokens) | |
| 136 | if num <= 0: | |
| 137 | continue | |
| 138 | token0 = tokens[0].strip() | |
| 139 | if material_scope == True: | |
| 140 | if token0 == "}": | |
| 141 | if material_found == True: | |
| 142 | break | |
| 143 | material_scope = False | |
| 144 | continue | |
| 145 | if num < 3: | |
| 146 | continue | |
| 147 | token1 = tokens[1].strip() | |
| 148 | token2 = tokens[2].strip() | |
| 149 | ||
| 150 | if material_found == True: | |
| 151 | if (token0 == "DiffuseMap" or token0 == "SpecularMap" or token0 == "NormalMap") and token1 == "=": | |
| 152 | if token2.endswith(','):
| |
| 153 | token2 = token2[0:-1] | |
| 154 | if token2.startswith('"') and token2.endswith('"'):
| |
| 155 | token2 = token2[1:-1] | |
| 156 | if token0 == "DiffuseMap": | |
| 157 | texture_names[0] = token2 | |
| 158 | elif token0 == "NormalMap": | |
| 159 | texture_names[1] = token2 | |
| 160 | elif token0 == "SpecularMap": | |
| 161 | texture_names[2] = token2 | |
| 162 | else: | |
| 163 | if token0 == "Name" and token1 == "=": | |
| 164 | if token2.endswith(','):
| |
| 165 | token2 = token2[0:-1] | |
| 166 | if token2.startswith('"') and token2.endswith('"'):
| |
| 167 | token2 = token2[1:-1] | |
| 168 | if token2 == find_material: | |
| 169 | material_found = True | |
| 170 | else: | |
| 171 | if token0 == "material{":
| |
| 172 | material_scope = True | |
| 173 | elif num > 1 and (token0 == "material" and tokens[1] == "{"):
| |
| 174 | material_scope = True | |
| 175 | file.close() | |
| 176 | ||
| 177 | for i in range(len(texture_names)): | |
| 178 | texture_name = texture_names[i] | |
| 179 | if len(texture_name) <= 0: | |
| 180 | continue | |
| 181 | ||
| 182 | # Check if source exists (.tga) or whatever was given in material file | |
| 183 | source_name = os.path.join(path_assets, texture_name) | |
| 184 | if os.path.exists(source_name): | |
| 185 | texture_name[i] = source_name | |
| 186 | continue | |
| 187 | ||
| 188 | # Check for .d3d9_texture instead | |
| 189 | base_name, ext = os.path.splitext(source_name) | |
| 190 | dxt_name = base_name + ".d3d9_texture" | |
| 191 | ||
| 192 | if os.path.exists(dxt_name): | |
| 193 | texture_names[i] = dxt_name | |
| 194 | continue | |
| 195 | ||
| 196 | # Didn't find file, just remove it | |
| 197 | texture_names[i] = "" | |
| 198 | ||
| 199 | return texture_names | |
| 200 | ||
| 201 | ||
| 202 | def load_mesh(filename, context): | |
| 203 | # Dig out file base name and extension | |
| 204 | name, ext = os.path.splitext(os.path.basename(filename)) | |
| 205 | ||
| 206 | path_assets = build_assets_path(filename) | |
| 207 | ||
| 208 | print("Opening file: " + filename)
| |
| 209 | file = open(filename, 'rb') | |
| 210 | try: | |
| 211 | magic = struct.unpack("<4s", file.read(4))[0]
| |
| 212 | except: | |
| 213 | print("Error parsing file header!")
| |
| 214 | file.close() | |
| 215 | return | |
| 216 | ||
| 217 | # Figure out if it's a valid mesh file | |
| 218 | if magic != b'MESH': | |
| 219 | print("Not a valid mesh model!")
| |
| 220 | file.close() | |
| 221 | return | |
| 222 | ||
| 223 | mesh_unknown = read_int(file) | |
| 224 | num_vertices = read_int(file) | |
| 225 | ||
| 226 | # This will store all vertices and index info | |
| 227 | vertex_set = [] | |
| 228 | indices = [] | |
| 229 | materials = [] | |
| 230 | ||
| 231 | for i in range(15): | |
| 232 | # Assumption that this is what it actually means ... | |
| 233 | data_type = read_int(file) | |
| 234 | num_comp = read_int(file) | |
| 235 | byte_width = read_int(file) | |
| 236 | ||
| 237 | print( "Vertex Attrib %d" % i ) | |
| 238 | print( "\tData Type: %d" % data_type ) | |
| 239 | print( "\tNum Components: %d" % num_comp ) | |
| 240 | print( "\tByte Width: %d" % byte_width ) | |
| 241 | ||
| 242 | # Skip empty | |
| 243 | if data_type == 0 and num_comp == 0 and byte_width == 0: | |
| 244 | # Add empty set | |
| 245 | vertex_set.append([]) | |
| 246 | continue | |
| 247 | ||
| 248 | # Report unknown data types (?) | |
| 249 | type_size = 0 | |
| 250 | if data_type == 0: | |
| 251 | type_size = 1 # byte ? | |
| 252 | elif data_type == 2: | |
| 253 | type_size = 4 # int32 ? | |
| 254 | elif data_type == 3: | |
| 255 | type_size = 4 # float | |
| 256 | else: | |
| 257 | print("Valid mesh, but not supported vertex data: data_type = %d" % data_type)
| |
| 258 | file.close() | |
| 259 | return | |
| 260 | ||
| 261 | # byte_width should be type_size*num_comp | |
| 262 | if byte_width != (num_comp*type_size): | |
| 263 | print("Valid mesh, but not supported vertex data: data_size = %d" % data_size)
| |
| 264 | file.close() | |
| 265 | return | |
| 266 | ||
| 267 | vertex_data = [] | |
| 268 | for j in range(num_vertices): | |
| 269 | data = [] | |
| 270 | if num_comp == 2: | |
| 271 | if data_type == 0: | |
| 272 | data = read_byte2(file) | |
| 273 | elif data_type == 2: | |
| 274 | data = read_int2(file) | |
| 275 | elif data_type == 3: | |
| 276 | data = read_float2(file) | |
| 277 | elif num_comp == 3: | |
| 278 | if data_type == 0: | |
| 279 | data = read_byte3(file) | |
| 280 | elif data_type == 2: | |
| 281 | data = read_int3(file) | |
| 282 | elif data_type == 3: | |
| 283 | data = read_float3(file) | |
| 284 | elif num_comp == 4: | |
| 285 | if data_type == 0: | |
| 286 | data = read_byte4(file) | |
| 287 | elif data_type == 2: | |
| 288 | data = read_int4(file) | |
| 289 | elif data_type == 3: | |
| 290 | data = read_float4(file) | |
| 291 | vertex_data.append(data) | |
| 292 | ||
| 293 | vertex_set.append(vertex_data) | |
| 294 | ||
| 295 | num_indices = read_int(file) | |
| 296 | for i in range(num_indices): | |
| 297 | index = read_int(file) | |
| 298 | indices.append(index) | |
| 299 | ||
| 300 | num_materials = read_int(file) | |
| 301 | for i in range(num_materials): | |
| 302 | name_length = read_int(file) | |
| 303 | material_name = read_string(file, name_length) | |
| 304 | print("Material Name: " + material_name)
| |
| 305 | ||
| 306 | material_unknown = read_int(file) | |
| 307 | #if pre_material != 2: | |
| 308 | # print("Valid mesh, but not supported material data: pre_material = " + str(pre_material))
| |
| 309 | # file.close() | |
| 310 | # return | |
| 311 | ||
| 312 | index_offset = read_int(file) | |
| 313 | num_primitives = read_int(file) | |
| 314 | ||
| 315 | material = _mesh_material() | |
| 316 | material.name = material_name | |
| 317 | material.index_offset = index_offset | |
| 318 | material.num_primitives = num_primitives | |
| 319 | ||
| 320 | materials.append(material) | |
| 321 | ||
| 322 | # Origin x,y,z (?) | |
| 323 | origin = read_float3(file) | |
| 324 | ||
| 325 | # ? | |
| 326 | read_float(file) | |
| 327 | ||
| 328 | # Bounds min x,y,z (?) | |
| 329 | bounds_min = read_float3( file ) | |
| 330 | ||
| 331 | # Bounds max x,y,z (?) | |
| 332 | bounds_max = read_float3( file ) | |
| 333 | ||
| 334 | file.close() | |
| 335 | ||
| 336 | # Build the blender object | |
| 337 | build_objects(name, vertex_set, indices, materials, path_assets) | |
| 338 | ||
| 339 | ||
| 340 | def add_material_texture( bl_material, type, filename ): | |
| 341 | # Create texture | |
| 342 | bl_texture = bpy.data.textures.new(name=type, type="IMAGE") | |
| 343 | ||
| 344 | # Try to load image | |
| 345 | image = load_image(filename) | |
| 346 | if image: | |
| 347 | bl_texture.image = image | |
| 348 | ||
| 349 | # Create texture slot and link to 'uvset1' | |
| 350 | mtex = bl_material.texture_slots.add() | |
| 351 | mtex.texture = bl_texture | |
| 352 | mtex.texture_coords = "UV" | |
| 353 | ||
| 354 | # Ensure diffuse/specular/normal uses proper types | |
| 355 | mtex.use_map_color_diffuse = (type == "diffuse") | |
| 356 | mtex.use_map_color_spec = (type == "specular") | |
| 357 | mtex.use_map_normal = (type == "normal") | |
| 358 | ||
| 359 | return image | |
| 360 | ||
| 361 | ||
| 362 | def create_material( path_assets, material ): | |
| 363 | # Parse material file for the actual texture images | |
| 364 | texture_names = parse_material_file(path_assets, material.name) | |
| 365 | ||
| 366 | # Create blender material | |
| 367 | bl_material = bpy.data.materials.new(material.name) | |
| 368 | ||
| 369 | # Create textures and texture slots on material | |
| 370 | image_diffuse = add_material_texture(bl_material, "diffuse", texture_names[0]) | |
| 371 | image_normal = add_material_texture(bl_material, "normal", texture_names[1]) | |
| 372 | image_specular = add_material_texture(bl_material, "specular", texture_names[2]) | |
| 373 | ||
| 374 | # Assign material | |
| 375 | material.bl_mat = bl_material | |
| 376 | ||
| 377 | # Assign preview display image for material | |
| 378 | material.bl_image = image_diffuse | |
| 379 | ||
| 380 | ||
| 381 | def build_objects(name, vertex_set, indices, materials, path_assets): | |
| 382 | print("Building Blender data")
| |
| 383 | - | print("Vertex sets: %d" % len(vertex_set))
|
| 383 | + | |
| 384 | vertices = vertex_set[0] | |
| 385 | normals = vertex_set[1] | |
| 386 | tangents = vertex_set[2] | |
| 387 | bitangents = vertex_set[3] | |
| 388 | byte_colors = vertex_set[4] | |
| 389 | texcoords = vertex_set[5] | |
| 390 | ||
| 391 | num_vertices = len(vertices) | |
| 392 | num_indices = len(indices) | |
| 393 | num_primitives = int(num_indices / 3) | |
| 394 | num_materials = len(materials) | |
| 395 | ||
| 396 | print("Num vertices: " + str(num_vertices))
| |
| 397 | print("Num indices: " + str(num_indices))
| |
| 398 | print("Num primitives: " + str(num_primitives))
| |
| 399 | print("Num materials: " + str(num_materials))
| |
| 400 | ||
| 401 | # Convert color attribute from [0, 255] to [0, 1] range | |
| 402 | float_colors = [] | |
| 403 | if len(byte_colors) > 0: | |
| 404 | for i in range(num_vertices): | |
| 405 | r = byte_colors[i][0] / 255.0 | |
| 406 | g = byte_colors[i][1] / 255.0 | |
| 407 | b = byte_colors[i][2] / 255.0 | |
| 408 | a = byte_colors[i][3] / 255.0 | |
| 409 | float_colors.append([r, g, b, a]) | |
| 410 | ||
| 411 | - | print(float_colors) |
| 411 | + | |
| 412 | if bpy.ops.object.mode_set.poll(): | |
| 413 | bpy.ops.object.mode_set(mode='OBJECT') | |
| 414 | ||
| 415 | # Create mesh | |
| 416 | me = bpy.data.meshes.new(name) | |
| 417 | ||
| 418 | # Add vertices | |
| 419 | me.vertices.add(num_vertices) | |
| 420 | for i in range(num_vertices): | |
| 421 | # Flip y-z component so it looks more natural in blender | |
| 422 | me.vertices[i].co[0] = vertices[i][0] | |
| 423 | me.vertices[i].co[1] = vertices[i][2] | |
| 424 | me.vertices[i].co[2] = vertices[i][1] | |
| 425 | ||
| 426 | # Add faces | |
| 427 | me.faces.add(num_primitives) | |
| 428 | for fi in range(num_primitives): | |
| 429 | idx = fi * 3 | |
| 430 | for i in range(3): | |
| 431 | # Flip indices because of vertex flip | |
| 432 | me.faces[fi].vertices_raw[2-i] = indices[idx+i] | |
| 433 | ||
| 434 | # Add vertex colors | |
| 435 | if len(float_colors) > 0: | |
| 436 | clrmap = me.vertex_colors.new("clrset1")
| |
| 437 | for fi in range(num_primitives): | |
| 438 | clrf = clrmap.data[fi] | |
| 439 | clrf.color1 = float_colors[indices[(fi*3)+2]][0:3] | |
| 440 | clrf.color2 = float_colors[indices[(fi*3)+1]][0:3] | |
| 441 | - | clrf.color1 = float_colors[indices[(fi*3)+0]][0:3] |
| 441 | + | |
| 442 | - | clrf.color2 = float_colors[indices[(fi*3)+0]][0:3] |
| 442 | + | |
| 443 | # Add uv map | |
| 444 | uvmap = me.uv_textures.new("uvset1")
| |
| 445 | for fi in range(num_primitives): | |
| 446 | uvf = uvmap.data[fi] | |
| 447 | uvf.uv1 = texcoords[indices[(fi*3)+2]] | |
| 448 | uvf.uv2 = texcoords[indices[(fi*3)+1]] | |
| 449 | - | uvf.uv1 = texcoords[indices[(fi*3)+0]] |
| 449 | + | uvf.uv3 = texcoords[indices[(fi*3)+0]] |
| 450 | ||
| 451 | - | uvf.uv3 = texcoords[indices[(fi*3)+2]] |
| 451 | + | |
| 452 | uvf.uv1.y = 1.0 - uvf.uv1.y | |
| 453 | uvf.uv2.y = 1.0 - uvf.uv2.y | |
| 454 | uvf.uv3.y = 1.0 - uvf.uv3.y | |
| 455 | ||
| 456 | # Create object and link with scene | |
| 457 | ob = bpy.data.objects.new(name, me) | |
| 458 | bpy.context.scene.objects.link(ob) | |
| 459 | ||
| 460 | # Add/create all our materials | |
| 461 | for material in materials: | |
| 462 | create_material(path_assets, material) | |
| 463 | ||
| 464 | # Assign materials to mesh | |
| 465 | for mi in range(num_materials): | |
| 466 | material = materials[mi] | |
| 467 | me.materials.append(material.bl_mat) | |
| 468 | ||
| 469 | face_offset = int(material.index_offset / 3) | |
| 470 | for fi in range(material.num_primitives): | |
| 471 | me.faces[face_offset+fi].material_index = mi | |
| 472 | ||
| 473 | uvf = uvmap.data[face_offset+fi] | |
| 474 | uvf.image = material.bl_image | |
| 475 | ||
| 476 | # Update mesh and scene | |
| 477 | me.update() | |
| 478 | bpy.context.scene.update() | |
| 479 | ||
| 480 | ||
| 481 | class IMPORT_OT_mesh(bpy.types.Operator, ImportHelper): | |
| 482 | # Import Mesh Operator. | |
| 483 | bl_idname = "import_scene.mesh" | |
| 484 | bl_label = "Import Mesh" | |
| 485 | bl_description = "Import a Legend of Grimrock mesh" | |
| 486 | bl_options = { 'REGISTER', 'UNDO' }
| |
| 487 | ||
| 488 | filepath = StringProperty(name="File Path", description="Filepath used for importing the mesh file.", maxlen=1024, default="") | |
| 489 | ||
| 490 | def execute(self, context): | |
| 491 | load_mesh(self.filepath, context) | |
| 492 | return {'FINISHED'}
| |
| 493 | ||
| 494 | def invoke(self, context, event): | |
| 495 | wm = context.window_manager | |
| 496 | wm.fileselect_add(self) | |
| 497 | return {'RUNNING_MODAL'}
| |
| 498 | ||
| 499 | ||
| 500 | def menu_func(self, context): | |
| 501 | self.layout.operator(IMPORT_OT_mesh.bl_idname, text="Legend of Grimrock Mesh (.mesh)") | |
| 502 | ||
| 503 | ||
| 504 | def register(): | |
| 505 | bpy.utils.register_module(__name__) | |
| 506 | bpy.types.INFO_MT_file_import.append(menu_func) | |
| 507 | ||
| 508 | ||
| 509 | def unregister(): | |
| 510 | bpy.utils.unregister_module(__name__) | |
| 511 | bpy.types.INFO_MT_file_import.remove(menu_func) | |
| 512 | ||
| 513 | ||
| 514 | if __name__ == "__main__": | |
| 515 | register() |