/io_import_hardreset_meta.py
Python | 554 lines | 536 code | 1 blank | 17 comment | 3 complexity | c130abc9c951f3b70308a9b40a7ce2a8 MD5 | raw file
- # ##### BEGIN GPL LICENSE BLOCK #####
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License
- # as published by the Free Software Foundation; either version 2
- # of the License, or (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software Foundation,
- # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- #
- # ##### END GPL LICENSE BLOCK #####
- bl_info= {
- "name": "Import HardReset Models",
- "author": "Mr. Wonko",
- "version": (0, 1),
- "blender": (2, 5, 9),
- "api": 39307,
- "location": "File > Import > HardReset Model (.meta)",
- "description": "Imports Hard Reset .meta/.rhm models",
- "warning": "",
- "category": "Import-Export"}
-
- import bpy, os, struct
- # helper function to read next char from file
- def peek(file):
- c = file.read(1)
- if c != "":
- #file.seek(-1, 1) # 1 back (-1) from current position (1 == SEEK_CUR) - does not work for files -.-
- file.seek(file.tell() - 1) # 1 back from current position - this is more verbose anyway
- return c
- # '[key] = [value]' -> '[key]', '[value]'
- def toKeyValue(line):
- # split at =
- key, value = line.split("=", 1)
- if not value:
- return None, None
- # prettify, return
- return key.strip(), value
- # '"string"' -> True, 'string'
- def toString(s):
- s = s.strip()
- if s[:1] != '"' or s[-1:] != '"':
- return False, None
- return True, s[1:-1]
- def toColor(s):
- s = s.strip()
- if s[:1] != '(' or s[-1:] != ')':
- return False, [0, 0, 0]
- s = s[1:-1]
- values = s.split(",")
- if len(values) < 3:
- return False, [0, 0, 0]
- return True, [float(values[0]), float(values[1]), float(values[2])]
- # halfFloat (16 bit) read as Short to Float
- # from http://forums.devshed.com/python-programming-11/converting-half-precision-floating-point-numbers-from-hexidecimal-to-decimal-576842.html
- def halfToFloat(h):
- sign = int((h >> 15) & 0x00000001)
- exponent = int((h >> 10) & 0x0000001f)
- fraction = int(h & 0x000003ff)
- if exponent == 0:
- if fraction == 0:
- return int(sign << 31)
- else:
- while not (fraction & 0x00000400):
- fraction <<= 1
- exponent -= 1
- exponent += 1
- fraction &= ~0x00000400
- elif exponent == 31:
- if fraction == 0:
- return int((sign << 31) | 0x7f800000)
- else:
- return int((sign << 31) | 0x7f800000 | (fraction << 13))
- exponent = exponent + (127 -15)
- fraction = fraction << 13
- return struct.unpack("f", struct.pack("I", (sign << 31) | (exponent << 23) | fraction))[0]
- UnhandledChunkKeys = []
- # a chunk will translate into a mesh/object pair in Blender.
- class Chunk:
- def __init__(self):
- self.message = ""
- self.startIndex = -1 # first (triangle) index belonging to this chunk - pretty much required, I guess
- self.primCount = -1 # amount of (triangle) indices belonging to this chunk - pretty much required, I guess
- self.baseIndex = 0 # indices must be offset by this much - allows for more than 2^16 vertices to be used, just not /per chunk/
- self.diffuse = "" # diffuse texture
- self.specular = "" # specular texture
- self.normal = "" # normal map texture
- self.vColor = [1, 1, 1] # vertex colour
- self.material = "" # material, for physics (esp. electricity) I guess
- # todo: add more!
-
- def loadFromMeta(self, file):
- #read lines while there are any interesting ones
- while peek(file) not in ["[", ""]: # next definition, EOF
- # read line
- line = file.readline()
-
- if line == "\n": # empty line
- continue
- line = line.strip("\n")
- # split at =
- key, value = toKeyValue(line)
- if not key:
- self.message = "line without ="
- return False
- # use
-
- # mesh information
- if key == "StartIndex":
- self.startIndex = int(value)
- continue
- if key == "PrimCount":
- self.primCount = int(value)
- continue
- if key == "BaseIndex":
- self.baseIndex = int(value)
- continue
-
- # material, basically
- if key == "Diffuse":
- self.diffuse = toString(value)
- continue
- if key == "Specular":
- self.specular = toString(value)
- continue
- if key == "Normal":
- self.normal = toString(value)
- continue
- if key == "vColor":
- self.material = toColor(value)
- continue
-
- # physics
- if key == "Material":
- self.material = toString(value)
- continue
-
- # bounds
- if key == "Bounds":
- # I don't need them bounds
- continue
- # todo: add more
- """
-
- fBaseUVTile = 1.000000
- fLayerUVTile = 1.000000
- fWrapAroundTerm = 1.000000
-
- fSpecularMultiplier = 4.000000
- fSpecularPowerMultiplier = 20.000000
- fEnvMultiplier = 1.000000
- fEmissiveMultiplier = 1.000000
- """
-
- # unhandled key?
- if key not in UnhandledChunkKeys: # only warn once
- print("Info: Unhandled Chunk Key \"%s\"" % (key))
- UnhandledChunkKeys.append(key)
- continue
- if self.startIndex == -1:
- self.message = "No StartIndex defined!"
- return False
- if self.primCount == -1:
- self.message = "No PrimCount defined!"
- return False
- return True
-
- def toBlender(self, geometry, name):
- vertexPositions = [] # not all the vertices are used...
- indices = [] # same for the indices, and they're also different since the vertices are different.
- indexMap = {} # which old vertex index maps to which new one?
- self.mesh = bpy.data.meshes.new(name)
- for triangleIndex in range(self.primCount): # need to build tuples, so I can't really iterate
- triangle = [0, 0, 0]
- # I don't want the mesh to contain unused vertices, so I need to iterate through the indices and only use those vertices.
- for vertexIndex in range(3):
- originalIndex = geometry.indices[self.startIndex + triangleIndex * 3 + vertexIndex] + self.baseIndex
- # that means I need to map the old indices (of the complete vertex list) to new ones (of this mesh's vertex list)
- # if this vertex has not yet been used...
- if originalIndex not in indexMap:
- # ... I need to add it to the list of used vertices and map its index
- indexMap[originalIndex] = len(vertexPositions)
- vertexPositions.append(geometry.vertices[originalIndex].position)
- # now it's just a matter of looking up the correct index.
- triangle[vertexIndex] = indexMap[originalIndex]
- indices.append(triangle)
- self.mesh.from_pydata(vertexPositions, [], indices) # vertices, edges, faces
-
- #UV Data
- assert(len(self.mesh.uv_textures) == 0)
- uv_texture = self.mesh.uv_textures.new() # create UV Texture - it contains UV Mapping data
- assert(len(uv_texture.data) == self.primCount) # the data should already exist, but zeroed.
- for triangleIndex in range(self.primCount):
- uv = [None, None, None]
- for vertexIndex in range(3):
- index = geometry.indices[self.startIndex + triangleIndex * 3 + vertexIndex] + self.baseIndex
- vertex = geometry.vertices[index]
- uv[vertexIndex] = vertex.uv
- uv_texture.data[triangleIndex].uv1 = uv[0]
- uv_texture.data[triangleIndex].uv2 = uv[1]
- uv_texture.data[triangleIndex].uv3 = uv[2]
-
- self.object = bpy.data.objects.new(name, self.mesh) # no data assigned to this object -> empty
- bpy.context.scene.objects.link(self.object) # add to current scene
- return True
- UnhandledMeshKeys = []
- # more like a group of mesh objects, though they may possibly contain only one (or none?)
- class Mesh:
- def __init__(self):
- self.message = ""
-
- self.name = ""
- self.childNum = 0 # not sure what this is
-
- # how many chunks, starting from which index?
- self.numChunks = -1
- self.chunkStart = -1
-
- def loadFromMeta(self, file):
- #read lines while there are any interesting ones
- while peek(file) not in ["[", ""]: # next definition, EOF
- # read line
- line = file.readline()
-
- if line == "\n": # empty line - should not happen?
- continue
- line = line.strip("\n")
- # split at =
- key, value = toKeyValue(line)
- if not key:
- self.message = "line without ="
- return False
- # use
-
- # use
- if key == "ChunkCount":
- self.numChunks = int(value)
- continue
- if key == "ChunkStart":
- self.chunkStart = int(value)
- continue
- if key == "Name":
- success, self.name = toString(value)
- if not success:
- self.message = "Name is no string"
- return False
- continue
- if key == "ChildNum":
- self.childNum = int(value)
- continue
- if key == "Bounds":
- # I don't need to read bounds - I just have to save 'em.
- continue
- # unhandled key?
- if key not in UnhandledMeshKeys: # only warn once
- print("Info: Unhandled Mesh Key \"%s\"" % (key))
- UnhandledMeshKeys.append(key)
- continue
-
- if self.numChunks == -1:
- self.message = "No ChunkCount defined!"
- return False
- if self.chunkStart == -1:
- self.message = "No ChunkStart defined!"
- return False
-
- return True
-
- def toBlender(self, geometry):
- """ Creates a Group (empty) and adds the children """
- self.object = bpy.data.objects.new(self.name, None) # no data assigned to this object -> empty
- bpy.context.scene.objects.link(self.object) # add to current scene
- for index, chunk in enumerate(geometry.chunks[self.chunkStart:self.chunkStart+self.numChunks]):
- if not chunk.toBlender(geometry, "%s_%d" % (self.name, index)):
- self.message = chunk.message
- return False
- chunk.object.parent = self.object
- return True
- class Vertex:
- def __init__(self):
- self.position = [0, 0, 0]
- self.uv = [0, 0]
- # what are the other values saved?
-
- def loadFromRhm(self, file):
- bindata = file.read(32) # a vertex is 32 bytes long.
- if len(bindata) < 32:
- return False, "Unexpected End of File"
- data = struct.unpack("3f12x2H4x", bindata) # 3 floats (position), 12 unknown bytes, 2 unsigned? shorts (UV) and another 4 unknown/unused bytes (always 0)
- for i in range(3):
- self.position[i] = data[i]
- shortSize = pow(2, 16)
- for i in range(2):
- # negative position (offset) so I don't need to change it when I figure out the other 12 bytes (neat, huh?)
- # I normalize the value to [0.0, 1.0] - I guess that's correct?
- self.uv[i] = halfToFloat(data[-2+i])
- self.uv[1] = 1 - self.uv[1] # flip Y
- return True, ""
- UnhandledGeometryKeys = []
- class Geometry:
- def __init__(self):
- # error message, if any
- self.message = ""
-
- self.numVertices = -1
- self.numIndices = -1
- self.numMeshes = -1
- # I wonder why there is no NumChunks?
-
- self.meshes = []
- self.chunks = []
-
- self.vertices = []
- self.indices = () # all the indices, not grouped
-
- def loadFromMeta(self, file):
- while peek(file) not in ["[", ""]:
- line = file.readline()
- if line == "\n":
- continue
- line = line.strip("\n")
-
- # split at =
- key, value = toKeyValue(line)
- if not key:
- self.message = "line without ="
- return False
-
- # use
- if key == "Meshes":
- self.numMeshes = int(value)
- continue
- if key == "Vertices":
- self.numVertices = int(value)
- continue
- if key == "Indices":
- self.numIndices = int(value)
- continue
- # unhandled key?
- if key not in UnhandledGeometryKeys: # only warn once
- print("Info: Unhandled Geometry Key \"%s\"" % (key))
- UnhandledGeometryKeys.append(key)
- continue
- return True
-
- def loadFromRhm(self, file):
- # read vertices
- for i in range(self.numVertices):
- v = Vertex()
- success, message = v.loadFromRhm(file)
- if not success:
- self.message = "Error reading vertex %d: %s" % (i, message)
- return False
- self.vertices.append(v)
- print("Read %d vertices" % self.numVertices)
-
- # read triangles
- bindata = file.read(2*self.numIndices)
- if len(bindata) < 2*self.numIndices:
- self.message = "Error reading indices: Unexpected end of file!"
- return False
- self.indices = struct.unpack("%dH" % self.numIndices, bindata) # 3 unsigned shorts (2 byte - only up to 65536 vertices!)
- print("Read %d indices" % self.numIndices)
-
- # read check sum
- checksumBin = file.read(4)
- if len(checksumBin) < 4:
- self.message = "Error reading checksum: Unexpected end of file!"
- return False
- checksum = struct.unpack("i", checksumBin)
- print("Checksum (?): %d" % checksum)
-
- return True
-
- def toBlender(self):
- for mesh in self.meshes:
- if not mesh.toBlender(self):
- self.message = mesh.message
- return False
- return True
- unhandledBlocks = []
- class HRImporter:
- def __init__(self):
- self.message = ""
- self.geometry = None
-
- def importModel(self, filepath):
- # strip extension
- pathWithoutExtension, extension = os.path.splitext(filepath)
- # is this a .meta file?
- if extension != ".meta":
- # no! we don't like that.
- self.message = "No .meta file!"
- return False
-
- # load .meta file - header
- if not self.loadMeta(filepath):
- return False
-
- # load .rhm file - vertices/triangles
- if not self.loadRhm(pathWithoutExtension + ".rhm"):
- return False
-
- # write gathered information to blender
- if not self.toBlender():
- return False
-
- # if we got here, it can only mean one thing: Success!
- return True
-
- def loadMeta(self, filepath):
- with open(filepath, "r") as file:
- # most common error
- self.message = "Invalid/unsupported file (see console for details)"
-
- while True:
- line = file.readline()
- if line == "":
- break
- if line == "\n": # empty line - might happen?
- continue
- line = line.strip("\n")
-
- if line == "[Geometry]":
- if self.geometry:
- print("Multiple [Geometry] blocks!")
- return False
- self.geometry = Geometry()
- if not self.geometry.loadFromMeta(file):
- print("Error reading [Geometry] block:\n%s" % self.geometry.message)
- return False
- continue
-
- if line == "[Mesh]":
- mesh = Mesh()
- if not mesh.loadFromMeta(file):
- print("Error reading [Mesh] block:\n%s" % (i, mesh.message))
- return False
- self.geometry.meshes.append(mesh)
- continue
-
- if line == "[Chunk]":
- chunk = Chunk()
- if not chunk.loadFromMeta(file):
- print("Error reading [Chunk] block:\n%s" % (i, chunk.message))
- return False
- self.geometry.chunks.append(chunk)
- continue
-
- # warn about unhandled blocks - but only once.
- if line not in unhandledBlocks:
- unhandledBlocks.append(line)
- print("Warning: Unhandled block: %s" % line)
- # skip this
- while peek(file) not in ["[", ""]:
- file.readline()
- continue
-
- # check if the amount of meshes is correct
- if len(self.geometry.meshes) != self.geometry.numMeshes:
- print("Error: Number of [Mesh] blocks does not match Geometry.Meshes! (%d should be %d)" % (len(self.geometry.meshes), self.geometry.numMeshes) )
- return False
-
- return True
- self.message = "Could not open " + filepath
- return False
-
- def loadRhm(self, filepath):
- with open(filepath, "rb") as file:
- if not self.geometry.loadFromRhm(file):
- self.message = "Error reading .rhm file:\n%s" % self.geometry.message
- return False
-
- # file should be over now
- if len(file.read(1)) != 0:
- self.message = "Rhm file longer than expected!"
- return False
-
- return True
- self.message = "Could not open " + filepath
- return False
-
- def toBlender(self):
- # Before adding any meshes or armatures go into Object mode.
- if bpy.ops.object.mode_set.poll():
- bpy.ops.object.mode_set(mode='OBJECT')
- if not self.geometry.toBlender():
- self.message = self.geometry.message
- return False
- return True
- from bpy.props import StringProperty, BoolProperty
- # Operator - automatically registered on creation.
- class IMPORT_HR_META(bpy.types.Operator):
- '''Import Hard Reset Meta Model Operator.'''
- bl_idname = "import_scene_hardreset.meta"
- bl_label = "Import Hard Reset Model (.meta)"
- bl_description = "Imports a Hard Reset .meta/.rhm pair."
- bl_options = {'REGISTER', 'UNDO'}
- filepath = StringProperty(name="File Path", description="Filepath used for importing the Hard Reset file", maxlen=1024, default="")
- def execute(self, context):
- importer = HRImporter()
- if not importer.importModel(self.filepath):
- self.report({'ERROR'}, importer.message)
- return {'FINISHED'}
- def invoke(self, context, event):
- wm= context.window_manager
- wm.fileselect_add(self)
- return {'RUNNING_MODAL'}
- # callback when menu entry is selected
- def menu_callback(self, context):
- # calls the operator
- self.layout.operator(IMPORT_HR_META.bl_idname, text="Hard Reset Model (.meta)")
- # called when module is registered
- def register():
- bpy.utils.register_module(__name__)
- # add menu entry
- bpy.types.INFO_MT_file_import.append(menu_callback)
- def unregister():
- bpy.utils.unregister_module(__name__)
- # remove menu entry
- bpy.types.INFO_MT_file_import.remove(menu_callback)
- # if this is executed as a file: register it
- if __name__ == "__main__":
- register()