PageRenderTime 54ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/io_import_hardreset_meta.py

https://github.com/mrwonko/Blender-Hard-Reset-Model-Importer
Python | 554 lines | 536 code | 1 blank | 17 comment | 3 complexity | c130abc9c951f3b70308a9b40a7ce2a8 MD5 | raw file
  1. # ##### BEGIN GPL LICENSE BLOCK #####
  2. #
  3. # This program is free software; you can redistribute it and/or
  4. # modify it under the terms of the GNU General Public License
  5. # as published by the Free Software Foundation; either version 2
  6. # of the License, or (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software Foundation,
  15. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. #
  17. # ##### END GPL LICENSE BLOCK #####
  18. bl_info= {
  19. "name": "Import HardReset Models",
  20. "author": "Mr. Wonko",
  21. "version": (0, 1),
  22. "blender": (2, 5, 9),
  23. "api": 39307,
  24. "location": "File > Import > HardReset Model (.meta)",
  25. "description": "Imports Hard Reset .meta/.rhm models",
  26. "warning": "",
  27. "category": "Import-Export"}
  28. import bpy, os, struct
  29. # helper function to read next char from file
  30. def peek(file):
  31. c = file.read(1)
  32. if c != "":
  33. #file.seek(-1, 1) # 1 back (-1) from current position (1 == SEEK_CUR) - does not work for files -.-
  34. file.seek(file.tell() - 1) # 1 back from current position - this is more verbose anyway
  35. return c
  36. # '[key] = [value]' -> '[key]', '[value]'
  37. def toKeyValue(line):
  38. # split at =
  39. key, value = line.split("=", 1)
  40. if not value:
  41. return None, None
  42. # prettify, return
  43. return key.strip(), value
  44. # '"string"' -> True, 'string'
  45. def toString(s):
  46. s = s.strip()
  47. if s[:1] != '"' or s[-1:] != '"':
  48. return False, None
  49. return True, s[1:-1]
  50. def toColor(s):
  51. s = s.strip()
  52. if s[:1] != '(' or s[-1:] != ')':
  53. return False, [0, 0, 0]
  54. s = s[1:-1]
  55. values = s.split(",")
  56. if len(values) < 3:
  57. return False, [0, 0, 0]
  58. return True, [float(values[0]), float(values[1]), float(values[2])]
  59. # halfFloat (16 bit) read as Short to Float
  60. # from http://forums.devshed.com/python-programming-11/converting-half-precision-floating-point-numbers-from-hexidecimal-to-decimal-576842.html
  61. def halfToFloat(h):
  62. sign = int((h >> 15) & 0x00000001)
  63. exponent = int((h >> 10) & 0x0000001f)
  64. fraction = int(h & 0x000003ff)
  65. if exponent == 0:
  66. if fraction == 0:
  67. return int(sign << 31)
  68. else:
  69. while not (fraction & 0x00000400):
  70. fraction <<= 1
  71. exponent -= 1
  72. exponent += 1
  73. fraction &= ~0x00000400
  74. elif exponent == 31:
  75. if fraction == 0:
  76. return int((sign << 31) | 0x7f800000)
  77. else:
  78. return int((sign << 31) | 0x7f800000 | (fraction << 13))
  79. exponent = exponent + (127 -15)
  80. fraction = fraction << 13
  81. return struct.unpack("f", struct.pack("I", (sign << 31) | (exponent << 23) | fraction))[0]
  82. UnhandledChunkKeys = []
  83. # a chunk will translate into a mesh/object pair in Blender.
  84. class Chunk:
  85. def __init__(self):
  86. self.message = ""
  87. self.startIndex = -1 # first (triangle) index belonging to this chunk - pretty much required, I guess
  88. self.primCount = -1 # amount of (triangle) indices belonging to this chunk - pretty much required, I guess
  89. self.baseIndex = 0 # indices must be offset by this much - allows for more than 2^16 vertices to be used, just not /per chunk/
  90. self.diffuse = "" # diffuse texture
  91. self.specular = "" # specular texture
  92. self.normal = "" # normal map texture
  93. self.vColor = [1, 1, 1] # vertex colour
  94. self.material = "" # material, for physics (esp. electricity) I guess
  95. # todo: add more!
  96. def loadFromMeta(self, file):
  97. #read lines while there are any interesting ones
  98. while peek(file) not in ["[", ""]: # next definition, EOF
  99. # read line
  100. line = file.readline()
  101. if line == "\n": # empty line
  102. continue
  103. line = line.strip("\n")
  104. # split at =
  105. key, value = toKeyValue(line)
  106. if not key:
  107. self.message = "line without ="
  108. return False
  109. # use
  110. # mesh information
  111. if key == "StartIndex":
  112. self.startIndex = int(value)
  113. continue
  114. if key == "PrimCount":
  115. self.primCount = int(value)
  116. continue
  117. if key == "BaseIndex":
  118. self.baseIndex = int(value)
  119. continue
  120. # material, basically
  121. if key == "Diffuse":
  122. self.diffuse = toString(value)
  123. continue
  124. if key == "Specular":
  125. self.specular = toString(value)
  126. continue
  127. if key == "Normal":
  128. self.normal = toString(value)
  129. continue
  130. if key == "vColor":
  131. self.material = toColor(value)
  132. continue
  133. # physics
  134. if key == "Material":
  135. self.material = toString(value)
  136. continue
  137. # bounds
  138. if key == "Bounds":
  139. # I don't need them bounds
  140. continue
  141. # todo: add more
  142. """
  143. fBaseUVTile = 1.000000
  144. fLayerUVTile = 1.000000
  145. fWrapAroundTerm = 1.000000
  146. fSpecularMultiplier = 4.000000
  147. fSpecularPowerMultiplier = 20.000000
  148. fEnvMultiplier = 1.000000
  149. fEmissiveMultiplier = 1.000000
  150. """
  151. # unhandled key?
  152. if key not in UnhandledChunkKeys: # only warn once
  153. print("Info: Unhandled Chunk Key \"%s\"" % (key))
  154. UnhandledChunkKeys.append(key)
  155. continue
  156. if self.startIndex == -1:
  157. self.message = "No StartIndex defined!"
  158. return False
  159. if self.primCount == -1:
  160. self.message = "No PrimCount defined!"
  161. return False
  162. return True
  163. def toBlender(self, geometry, name):
  164. vertexPositions = [] # not all the vertices are used...
  165. indices = [] # same for the indices, and they're also different since the vertices are different.
  166. indexMap = {} # which old vertex index maps to which new one?
  167. self.mesh = bpy.data.meshes.new(name)
  168. for triangleIndex in range(self.primCount): # need to build tuples, so I can't really iterate
  169. triangle = [0, 0, 0]
  170. # I don't want the mesh to contain unused vertices, so I need to iterate through the indices and only use those vertices.
  171. for vertexIndex in range(3):
  172. originalIndex = geometry.indices[self.startIndex + triangleIndex * 3 + vertexIndex] + self.baseIndex
  173. # that means I need to map the old indices (of the complete vertex list) to new ones (of this mesh's vertex list)
  174. # if this vertex has not yet been used...
  175. if originalIndex not in indexMap:
  176. # ... I need to add it to the list of used vertices and map its index
  177. indexMap[originalIndex] = len(vertexPositions)
  178. vertexPositions.append(geometry.vertices[originalIndex].position)
  179. # now it's just a matter of looking up the correct index.
  180. triangle[vertexIndex] = indexMap[originalIndex]
  181. indices.append(triangle)
  182. self.mesh.from_pydata(vertexPositions, [], indices) # vertices, edges, faces
  183. #UV Data
  184. assert(len(self.mesh.uv_textures) == 0)
  185. uv_texture = self.mesh.uv_textures.new() # create UV Texture - it contains UV Mapping data
  186. assert(len(uv_texture.data) == self.primCount) # the data should already exist, but zeroed.
  187. for triangleIndex in range(self.primCount):
  188. uv = [None, None, None]
  189. for vertexIndex in range(3):
  190. index = geometry.indices[self.startIndex + triangleIndex * 3 + vertexIndex] + self.baseIndex
  191. vertex = geometry.vertices[index]
  192. uv[vertexIndex] = vertex.uv
  193. uv_texture.data[triangleIndex].uv1 = uv[0]
  194. uv_texture.data[triangleIndex].uv2 = uv[1]
  195. uv_texture.data[triangleIndex].uv3 = uv[2]
  196. self.object = bpy.data.objects.new(name, self.mesh) # no data assigned to this object -> empty
  197. bpy.context.scene.objects.link(self.object) # add to current scene
  198. return True
  199. UnhandledMeshKeys = []
  200. # more like a group of mesh objects, though they may possibly contain only one (or none?)
  201. class Mesh:
  202. def __init__(self):
  203. self.message = ""
  204. self.name = ""
  205. self.childNum = 0 # not sure what this is
  206. # how many chunks, starting from which index?
  207. self.numChunks = -1
  208. self.chunkStart = -1
  209. def loadFromMeta(self, file):
  210. #read lines while there are any interesting ones
  211. while peek(file) not in ["[", ""]: # next definition, EOF
  212. # read line
  213. line = file.readline()
  214. if line == "\n": # empty line - should not happen?
  215. continue
  216. line = line.strip("\n")
  217. # split at =
  218. key, value = toKeyValue(line)
  219. if not key:
  220. self.message = "line without ="
  221. return False
  222. # use
  223. # use
  224. if key == "ChunkCount":
  225. self.numChunks = int(value)
  226. continue
  227. if key == "ChunkStart":
  228. self.chunkStart = int(value)
  229. continue
  230. if key == "Name":
  231. success, self.name = toString(value)
  232. if not success:
  233. self.message = "Name is no string"
  234. return False
  235. continue
  236. if key == "ChildNum":
  237. self.childNum = int(value)
  238. continue
  239. if key == "Bounds":
  240. # I don't need to read bounds - I just have to save 'em.
  241. continue
  242. # unhandled key?
  243. if key not in UnhandledMeshKeys: # only warn once
  244. print("Info: Unhandled Mesh Key \"%s\"" % (key))
  245. UnhandledMeshKeys.append(key)
  246. continue
  247. if self.numChunks == -1:
  248. self.message = "No ChunkCount defined!"
  249. return False
  250. if self.chunkStart == -1:
  251. self.message = "No ChunkStart defined!"
  252. return False
  253. return True
  254. def toBlender(self, geometry):
  255. """ Creates a Group (empty) and adds the children """
  256. self.object = bpy.data.objects.new(self.name, None) # no data assigned to this object -> empty
  257. bpy.context.scene.objects.link(self.object) # add to current scene
  258. for index, chunk in enumerate(geometry.chunks[self.chunkStart:self.chunkStart+self.numChunks]):
  259. if not chunk.toBlender(geometry, "%s_%d" % (self.name, index)):
  260. self.message = chunk.message
  261. return False
  262. chunk.object.parent = self.object
  263. return True
  264. class Vertex:
  265. def __init__(self):
  266. self.position = [0, 0, 0]
  267. self.uv = [0, 0]
  268. # what are the other values saved?
  269. def loadFromRhm(self, file):
  270. bindata = file.read(32) # a vertex is 32 bytes long.
  271. if len(bindata) < 32:
  272. return False, "Unexpected End of File"
  273. data = struct.unpack("3f12x2H4x", bindata) # 3 floats (position), 12 unknown bytes, 2 unsigned? shorts (UV) and another 4 unknown/unused bytes (always 0)
  274. for i in range(3):
  275. self.position[i] = data[i]
  276. shortSize = pow(2, 16)
  277. for i in range(2):
  278. # negative position (offset) so I don't need to change it when I figure out the other 12 bytes (neat, huh?)
  279. # I normalize the value to [0.0, 1.0] - I guess that's correct?
  280. self.uv[i] = halfToFloat(data[-2+i])
  281. self.uv[1] = 1 - self.uv[1] # flip Y
  282. return True, ""
  283. UnhandledGeometryKeys = []
  284. class Geometry:
  285. def __init__(self):
  286. # error message, if any
  287. self.message = ""
  288. self.numVertices = -1
  289. self.numIndices = -1
  290. self.numMeshes = -1
  291. # I wonder why there is no NumChunks?
  292. self.meshes = []
  293. self.chunks = []
  294. self.vertices = []
  295. self.indices = () # all the indices, not grouped
  296. def loadFromMeta(self, file):
  297. while peek(file) not in ["[", ""]:
  298. line = file.readline()
  299. if line == "\n":
  300. continue
  301. line = line.strip("\n")
  302. # split at =
  303. key, value = toKeyValue(line)
  304. if not key:
  305. self.message = "line without ="
  306. return False
  307. # use
  308. if key == "Meshes":
  309. self.numMeshes = int(value)
  310. continue
  311. if key == "Vertices":
  312. self.numVertices = int(value)
  313. continue
  314. if key == "Indices":
  315. self.numIndices = int(value)
  316. continue
  317. # unhandled key?
  318. if key not in UnhandledGeometryKeys: # only warn once
  319. print("Info: Unhandled Geometry Key \"%s\"" % (key))
  320. UnhandledGeometryKeys.append(key)
  321. continue
  322. return True
  323. def loadFromRhm(self, file):
  324. # read vertices
  325. for i in range(self.numVertices):
  326. v = Vertex()
  327. success, message = v.loadFromRhm(file)
  328. if not success:
  329. self.message = "Error reading vertex %d: %s" % (i, message)
  330. return False
  331. self.vertices.append(v)
  332. print("Read %d vertices" % self.numVertices)
  333. # read triangles
  334. bindata = file.read(2*self.numIndices)
  335. if len(bindata) < 2*self.numIndices:
  336. self.message = "Error reading indices: Unexpected end of file!"
  337. return False
  338. self.indices = struct.unpack("%dH" % self.numIndices, bindata) # 3 unsigned shorts (2 byte - only up to 65536 vertices!)
  339. print("Read %d indices" % self.numIndices)
  340. # read check sum
  341. checksumBin = file.read(4)
  342. if len(checksumBin) < 4:
  343. self.message = "Error reading checksum: Unexpected end of file!"
  344. return False
  345. checksum = struct.unpack("i", checksumBin)
  346. print("Checksum (?): %d" % checksum)
  347. return True
  348. def toBlender(self):
  349. for mesh in self.meshes:
  350. if not mesh.toBlender(self):
  351. self.message = mesh.message
  352. return False
  353. return True
  354. unhandledBlocks = []
  355. class HRImporter:
  356. def __init__(self):
  357. self.message = ""
  358. self.geometry = None
  359. def importModel(self, filepath):
  360. # strip extension
  361. pathWithoutExtension, extension = os.path.splitext(filepath)
  362. # is this a .meta file?
  363. if extension != ".meta":
  364. # no! we don't like that.
  365. self.message = "No .meta file!"
  366. return False
  367. # load .meta file - header
  368. if not self.loadMeta(filepath):
  369. return False
  370. # load .rhm file - vertices/triangles
  371. if not self.loadRhm(pathWithoutExtension + ".rhm"):
  372. return False
  373. # write gathered information to blender
  374. if not self.toBlender():
  375. return False
  376. # if we got here, it can only mean one thing: Success!
  377. return True
  378. def loadMeta(self, filepath):
  379. with open(filepath, "r") as file:
  380. # most common error
  381. self.message = "Invalid/unsupported file (see console for details)"
  382. while True:
  383. line = file.readline()
  384. if line == "":
  385. break
  386. if line == "\n": # empty line - might happen?
  387. continue
  388. line = line.strip("\n")
  389. if line == "[Geometry]":
  390. if self.geometry:
  391. print("Multiple [Geometry] blocks!")
  392. return False
  393. self.geometry = Geometry()
  394. if not self.geometry.loadFromMeta(file):
  395. print("Error reading [Geometry] block:\n%s" % self.geometry.message)
  396. return False
  397. continue
  398. if line == "[Mesh]":
  399. mesh = Mesh()
  400. if not mesh.loadFromMeta(file):
  401. print("Error reading [Mesh] block:\n%s" % (i, mesh.message))
  402. return False
  403. self.geometry.meshes.append(mesh)
  404. continue
  405. if line == "[Chunk]":
  406. chunk = Chunk()
  407. if not chunk.loadFromMeta(file):
  408. print("Error reading [Chunk] block:\n%s" % (i, chunk.message))
  409. return False
  410. self.geometry.chunks.append(chunk)
  411. continue
  412. # warn about unhandled blocks - but only once.
  413. if line not in unhandledBlocks:
  414. unhandledBlocks.append(line)
  415. print("Warning: Unhandled block: %s" % line)
  416. # skip this
  417. while peek(file) not in ["[", ""]:
  418. file.readline()
  419. continue
  420. # check if the amount of meshes is correct
  421. if len(self.geometry.meshes) != self.geometry.numMeshes:
  422. print("Error: Number of [Mesh] blocks does not match Geometry.Meshes! (%d should be %d)" % (len(self.geometry.meshes), self.geometry.numMeshes) )
  423. return False
  424. return True
  425. self.message = "Could not open " + filepath
  426. return False
  427. def loadRhm(self, filepath):
  428. with open(filepath, "rb") as file:
  429. if not self.geometry.loadFromRhm(file):
  430. self.message = "Error reading .rhm file:\n%s" % self.geometry.message
  431. return False
  432. # file should be over now
  433. if len(file.read(1)) != 0:
  434. self.message = "Rhm file longer than expected!"
  435. return False
  436. return True
  437. self.message = "Could not open " + filepath
  438. return False
  439. def toBlender(self):
  440. # Before adding any meshes or armatures go into Object mode.
  441. if bpy.ops.object.mode_set.poll():
  442. bpy.ops.object.mode_set(mode='OBJECT')
  443. if not self.geometry.toBlender():
  444. self.message = self.geometry.message
  445. return False
  446. return True
  447. from bpy.props import StringProperty, BoolProperty
  448. # Operator - automatically registered on creation.
  449. class IMPORT_HR_META(bpy.types.Operator):
  450. '''Import Hard Reset Meta Model Operator.'''
  451. bl_idname = "import_scene_hardreset.meta"
  452. bl_label = "Import Hard Reset Model (.meta)"
  453. bl_description = "Imports a Hard Reset .meta/.rhm pair."
  454. bl_options = {'REGISTER', 'UNDO'}
  455. filepath = StringProperty(name="File Path", description="Filepath used for importing the Hard Reset file", maxlen=1024, default="")
  456. def execute(self, context):
  457. importer = HRImporter()
  458. if not importer.importModel(self.filepath):
  459. self.report({'ERROR'}, importer.message)
  460. return {'FINISHED'}
  461. def invoke(self, context, event):
  462. wm= context.window_manager
  463. wm.fileselect_add(self)
  464. return {'RUNNING_MODAL'}
  465. # callback when menu entry is selected
  466. def menu_callback(self, context):
  467. # calls the operator
  468. self.layout.operator(IMPORT_HR_META.bl_idname, text="Hard Reset Model (.meta)")
  469. # called when module is registered
  470. def register():
  471. bpy.utils.register_module(__name__)
  472. # add menu entry
  473. bpy.types.INFO_MT_file_import.append(menu_callback)
  474. def unregister():
  475. bpy.utils.unregister_module(__name__)
  476. # remove menu entry
  477. bpy.types.INFO_MT_file_import.remove(menu_callback)
  478. # if this is executed as a file: register it
  479. if __name__ == "__main__":
  480. register()