Irup Posted March 24, 2018 Share Posted March 24, 2018 I'm developing a Blender import script, but I've not been able to find source code or documentation of any kind for Will Kirby's LibLR2, which is used in the model viewer. I've done my research and reverse-engineered the model files to the point where I can do what the viewer can do, but I wouldn't want to reinvent the wheel any more if the work is already done and available. Edit: Here's the WIP script that imported the above mesh. It works with most models. It's for testing, so you're supposed to use it from the text editor. Change the strings at the bottom of the script to your own file locations. Image tutorial below. from struct import unpack from sys import argv from os.path import split, splitext, join import bpy, bmesh def readnulltermstring(input,skip=False): if type(input) in (str, bytes): #else it's a file object null = '\0' if type(input)==str else b'\0' return input[:input.index(null)] if null in input else input if skip: while 1: c = input.read(1) if not c or c == b'\0': return buildstring = bytearray() while 1: c = input.read(1) if not c or c == b'\0': return buildstring buildstring += c # All models use the .md2 extension, but the actual file format within is discernible by the first 4 bytes. # MDL2 and MDL1 are chunking formats, but MDL0 is not. A chunk's name is a 4-byte int, followed by a length also represented in a 4-byte int. # .bsb files are skeletons, pointing to associated animation .bsa files # .mip textures are .tga files. def import_discern(filepath): with open(filepath,'rb') as f: first4 = f.read(4) if first4 == b'MDL2': return import_mdl2(filepath) elif first4 == b'MDL1': return import_mdl1(filepath) elif first4 == b'MDL0': return import_mdl0(filepath) else: raise AssertionError('Input file is not of type MDL2, MDL1, or MDL0.') def import_mdl2(filepath): f = open(filepath,'rb') open_textures = True try: rootpath = filepath[:filepath.lower().index('game data')] except ValueError: open_textures = False print('Could not trace back to root directory "GAME DATA".') offset = 0 texture_paths = [] objects = [] while 1: chunk_name = f.read(4) if chunk_name == b'\0\0\0\0' or not chunk_name: break chunk_size = unpack('I', f.read(4))[0] chunk_base = f.tell() print('%s: %s' % (chunk_name.decode('ascii'), hex(chunk_size))) if chunk_name == b'MDL2': ints = tuple(f.read(4) for x in range(32)) mdl2_texturecount = unpack('I', f.read(4))[0] mdl2_texturebase = f.tell() print(' Textures:', mdl2_texturecount) for texture_id in range(mdl2_texturecount): texture_path = readnulltermstring(f.read(256)).decode('ascii') texture_type ,\ texture_index = unpack('2I', f.read(8)) print(' ' + texture_path) texture_paths += [texture_path] materials = [bpy.data.materials.new(splitext(split(x)[1])[0]) for x in texture_paths] elif chunk_name == b'P2G0': # weights? pass elif chunk_name == b'GEO1': geo1_unknown0,\ geo1_unknown1,\ geo1_unknown2,\ geo1_meshes = unpack('2IfI', f.read(16)) geo1_unknown4 = unpack('I', f.read(4))[0] geo1_unknown5 = unpack('f', f.read(4))[0] print(' Split models: %i' % geo1_meshes) # meshes are split because textures are defined per mesh for model in range(geo1_meshes): print(' Model %i' % model) md2_bmesh = bmesh.new() geo1_mesh_unknown21 = unpack('H', f.read(2))[0] geo1_mesh_unknown22 = unpack('H', f.read(2))[0] geo1_mesh_unknown0 = unpack('f', f.read(4))[0] geo1_mesh_unknown1 = unpack('f', f.read(4))[0] geo1_mesh_unknown2 = unpack('f', f.read(4))[0] geo1_mesh_unknown3 = unpack('f', f.read(4))[0] geo1_mesh_unknown4 = unpack('I', f.read(4))[0] geo1_mesh_unknown5 = unpack('I', f.read(4))[0] geo1_mesh_unknown6 = unpack('I', f.read(4))[0] geo1_mesh_texture = unpack('H', f.read(2))[0] geo1_mesh_unknown7 = unpack('H', f.read(2))[0] geo1_mesh_unknown8 = unpack('I', f.read(4))[0] geo1_mesh_unknown9 = unpack('I', f.read(4))[0] geo1_mesh_unknown10 = unpack('I', f.read(4))[0] geo1_mesh_unknown11 = unpack('I', f.read(4))[0] geo1_mesh_unknown12 = unpack('I', f.read(4))[0] geo1_mesh_unknown13 = unpack('I', f.read(4))[0] geo1_mesh_unknown14 = unpack('I', f.read(4))[0] geo1_mesh_unknown15 = unpack('I', f.read(4))[0] geo1_mesh_unknown16 = unpack('I', f.read(4))[0] geo1_mesh_unknown17 = unpack('I', f.read(4))[0] geo1_mesh_vertexsize= unpack('I', f.read(4))[0] geo1_mesh_unknown19 = unpack('I', f.read(4))[0] geo1_mesh_unknown20 = unpack('H', f.read(2))[0] print(' Texture: %s' % texture_paths[geo1_mesh_texture]) #print(' Start of vertices:', hex(f.tell())) geo1_mesh_vertices = unpack('H', f.read(2))[0] geo1_mesh_unknownxyz = unpack('3f', f.read(4*3)) #print(' Vertices:', geo1_mesh_vertices) geo1_mesh_vertexbase = f.tell() print(' Vertices: %i' % geo1_mesh_vertices) uvs = [] for vertex in range(geo1_mesh_vertices): md2_bmesh.verts.new() md2_bmesh.verts.ensure_lookup_table() print(' Vertex format: 0x%04x' % geo1_mesh_unknown20) for vertex in range(geo1_mesh_vertices): vertex_xyz = unpack('3f', f.read(4*3)) if geo1_mesh_vertexsize == 0x20: pass elif geo1_mesh_vertexsize == 0x24: geo1_mesh_unknown20_f = unpack('f', f.read(4))[0] elif geo1_mesh_vertexsize == 0x28: geo1_mesh_unknown20_f = unpack('2f', f.read(8)) else: raise AssertionError('Unexpected vertex struct size (%s).' % hex(geo1_mesh_vertexsize)) vertex_normal = unpack('3f', f.read(4*3)) vertex_uv = unpack('2f', f.read(4*2)) md2_bmesh.verts[vertex].co = vertex_xyz md2_bmesh.verts[vertex].normal = vertex_normal uvs += [vertex_uv] print(' End of vertices:', hex(f.tell())) geo1_mesh_unknown19 = unpack('I', f.read(4))[0] geo1_mesh_unknown20 = unpack('I', f.read(4))[0] #print(' Start of polygons:', hex(f.tell())) geo1_mesh_polygons = unpack('I', f.read(4))[0] // 3 print(' Polygons: %i' % geo1_mesh_polygons) for polygon in range(geo1_mesh_polygons): md2_bmesh.faces.new((md2_bmesh.verts[x] for x in unpack('3H', f.read(2*3))))#.material_index = geo1_mesh_texture #print(' End of polygons:', hex(f.tell())) # set uv maps md2_bmesh.verts.ensure_lookup_table() md2_bmesh.faces.ensure_lookup_table() md2_bmesh.verts.index_update() uv_layer = md2_bmesh.loops.layers.uv.new() for face in md2_bmesh.faces: for loop in face.loops: loop[uv_layer].uv = uvs[loop.vert.index] md2_mesh = bpy.data.meshes.new(str(model)) md2_bmesh.to_mesh(md2_mesh) md2_obj = bpy.data.objects.new(str(model), md2_mesh) md2_obj.data.materials.append(materials[geo1_mesh_texture]) bpy.context.scene.objects.link(md2_obj) objects += [md2_obj] elif chunk_name == b'COLD': pass offset += 8 + chunk_size f.seek(offset) f.close() for object in objects: object.select = True bpy.context.scene.objects.active = objects[0] bpy.ops.object.join() objects[0].rotation_euler = (__import__('math').pi / 2, 0, 0) if open_textures: for texturepath in texture_paths: bpy.ops.image.open(filepath = join(rootpath, splitext(texturepath)[0] + '.mip')) def export_mdl2(filepath): pass import_discern( r'C:\Users\Irup\Desktop\lr2\GAMEDATA\GAME DATA\LOM\OBJECTS\MODELS\REJOOV_WORKGODAMNYOU.MD2' ) r'C:\Users\Irup\Desktop\lr2\GAMEDATA\GAME DATA\ADVENTURERS\OBJECTS\MODELS\WATERFALL_01.MD2' r'C:\Users\Irup\Desktop\lr2\GAMEDATA\GAME DATA\CHARACTERS\HEADS\MODELS\PLAYER1.MD2' r'C:\Users\Irup\Desktop\lr2\GAMEDATA\GAME DATA\ANIMATION\ALBATROS\ALBATROS.MD2' r'C:\Users\Irup\Desktop\lr2\GAMEDATA\GAME DATA\CHARACTERS\HEADS\MODELS\SPARKY.MD2' r'C:\Users\Irup\Desktop\lr2\GAMEDATA\GAME DATA\LOM\OBJECTS\MODELS\REJOOV_WORKGODAMNYOU.MD2' r'C:\Users\Irup\Desktop\lr2\GAMEDATA\GAME DATA\OBJECTS\BRICKS\SQUARE\1X1 RED hacked.MD2' r'C:\Users\Irup\Desktop\lr2\GAMEDATA\GAME DATA\OBJECTS\BRICKS\SQUARE\1X1 RED.MD2' pedroj234 and le717 2 Link to comment Share on other sites More sharing options...
lol username Posted March 24, 2018 Share Posted March 24, 2018 Source isn't available; your best bet would be to disassemble it with something like dotPeek. I'm sure that importer will be very useful to folks, keep at it! Link to comment Share on other sites More sharing options...
Fluffy Cupcake Posted March 24, 2018 Share Posted March 24, 2018 I thought it was at one point - what happened with it? Did Will have to pull it when he got a job at TT? Link to comment Share on other sites More sharing options...
Irup Posted March 24, 2018 Author Share Posted March 24, 2018 It did not occur to me that I could just disassemble it, and the results I'm seeing are perfect. Thanks for the replies! Link to comment Share on other sites More sharing options...
Irup Posted March 24, 2018 Author Share Posted March 24, 2018 Oh right, where can I get the most recent version of the library? Link to comment Share on other sites More sharing options...
lol username Posted March 24, 2018 Share Posted March 24, 2018 8 minutes ago, Irup said: Oh right, where can I get the most recent version of the library? I'm not sure anyone (including Will) even knows at this point. I'd just compare whatever DLLs you might have and disassemble the newest. It wasn't ever really complete anyway, AFAIK. Learn what you can from it, but it was fairly old and messy so you'd probably want to re-invent at least some of the wheel anyway, and figure out whatever it didn't have. Link to comment Share on other sites More sharing options...
Fluffy Cupcake Posted March 24, 2018 Share Posted March 24, 2018 Would you be willing to take a stab at the terrain files after you are done with that? That is one thing we've never been able properly rip with model ripper programs that attach themselves to the game. You don't have to but it'd be a neat addition, I am very appreciative that you are making this script for the md2 files already. Link to comment Share on other sites More sharing options...
Irup Posted March 24, 2018 Author Share Posted March 24, 2018 Edited to actually include the WIP script so I won't be disappointing anyone reading the title and getting excited. I see now that the models are much more complex than I previously thought, so a proper importer could take some time. Link to comment Share on other sites More sharing options...
le717 Posted March 24, 2018 Share Posted March 24, 2018 If you want some experience with integrating with the Blender UI, hit me up. I have experience with that. You may also want to consider putting this on GitHub (or the like, e.g., BitBucket or GitLab) for easy distribution and to keep track of changes. Keep up the good work! It looks good! Link to comment Share on other sites More sharing options...
Recommended Posts