Jump to content

Is LibLR2 source available anywhere? (WIP Blender import script)

Recommended Posts


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.





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?
		elif chunk_name == b'GEO1':
			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()
				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
				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_obj = bpy.data.objects.new(str(model), md2_mesh)
				objects += [md2_obj]
		elif chunk_name == b'COLD':
		offset += 8 + chunk_size
	for object in objects: object.select = True
	bpy.context.scene.objects.active = objects[0]
	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):


Share this post

Link to post
Share on other sites

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!

Share this post

Link to post
Share on other sites

I thought it was at one point - what happened with it? Did Will have to pull it when he got a job at TT

Share this post

Link to post
Share on other sites

It did not occur to me that I could just disassemble it, and the results I'm seeing are perfect. Thanks for the replies!

Share this post

Link to post
Share on other sites
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.

Share this post

Link to post
Share on other sites

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.

Share this post

Link to post
Share on other sites

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.

Share this post

Link to post
Share on other sites

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! :D

Share this post

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.