iqm_export.py 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140
  1. # This script is licensed as public domain.
  2. # It has been modified by exezin to change the up vector to match exengine
  3. bl_info = {
  4. "name": "Export Inter-Quake Model (.iqm/.iqe)",
  5. "author": "Lee Salzman",
  6. "version": (2016, 2, 9),
  7. "blender": (2, 74, 0),
  8. "location": "File > Export > Inter-Quake Model",
  9. "description": "Export to the Inter-Quake Model format (.iqm/.iqe)",
  10. "warning": "",
  11. "wiki_url": "",
  12. "tracker_url": "",
  13. "category": "Import-Export"}
  14. import os, struct, math
  15. import mathutils
  16. import bpy
  17. import bpy_extras.io_utils
  18. IQM_POSITION = 0
  19. IQM_TEXCOORD = 1
  20. IQM_NORMAL = 2
  21. IQM_TANGENT = 3
  22. IQM_BLENDINDEXES = 4
  23. IQM_BLENDWEIGHTS = 5
  24. IQM_COLOR = 6
  25. IQM_CUSTOM = 0x10
  26. IQM_BYTE = 0
  27. IQM_UBYTE = 1
  28. IQM_SHORT = 2
  29. IQM_USHORT = 3
  30. IQM_INT = 4
  31. IQM_UINT = 5
  32. IQM_HALF = 6
  33. IQM_FLOAT = 7
  34. IQM_DOUBLE = 8
  35. IQM_LOOP = 1
  36. IQM_HEADER = struct.Struct('<16s27I')
  37. IQM_MESH = struct.Struct('<6I')
  38. IQM_TRIANGLE = struct.Struct('<3I')
  39. IQM_JOINT = struct.Struct('<Ii10f')
  40. IQM_POSE = struct.Struct('<iI20f')
  41. IQM_ANIMATION = struct.Struct('<3IfI')
  42. IQM_VERTEXARRAY = struct.Struct('<5I')
  43. IQM_BOUNDS = struct.Struct('<8f')
  44. MAXVCACHE = 32
  45. class Vertex:
  46. def __init__(self, index, coord, normal, uv, weights, color):
  47. self.index = index
  48. self.coord = coord
  49. self.normal = normal
  50. self.uv = uv
  51. self.weights = weights
  52. self.color = color
  53. def normalizeWeights(self):
  54. # renormalizes all weights such that they add up to 255
  55. # the list is chopped/padded to exactly 4 weights if necessary
  56. if not self.weights:
  57. self.weights = [ (0, 0), (0, 0), (0, 0), (0, 0) ]
  58. return
  59. self.weights.sort(key = lambda weight: weight[0], reverse=True)
  60. if len(self.weights) > 4:
  61. del self.weights[4:]
  62. totalweight = sum([ weight for (weight, bone) in self.weights])
  63. if totalweight > 0:
  64. self.weights = [ (int(round(weight * 255.0 / totalweight)), bone) for (weight, bone) in self.weights]
  65. while len(self.weights) > 1 and self.weights[-1][0] <= 0:
  66. self.weights.pop()
  67. else:
  68. totalweight = len(self.weights)
  69. self.weights = [ (int(round(255.0 / totalweight)), bone) for (weight, bone) in self.weights]
  70. totalweight = sum([ weight for (weight, bone) in self.weights])
  71. while totalweight != 255:
  72. for i, (weight, bone) in enumerate(self.weights):
  73. if totalweight > 255 and weight > 0:
  74. self.weights[i] = (weight - 1, bone)
  75. totalweight -= 1
  76. elif totalweight < 255 and weight < 255:
  77. self.weights[i] = (weight + 1, bone)
  78. totalweight += 1
  79. while len(self.weights) < 4:
  80. self.weights.append((0, self.weights[-1][1]))
  81. def calcScore(self):
  82. if self.uses:
  83. self.score = 2.0 * pow(len(self.uses), -0.5)
  84. if self.cacherank >= 3:
  85. self.score += pow(1.0 - float(self.cacherank - 3)/MAXVCACHE, 1.5)
  86. elif self.cacherank >= 0:
  87. self.score += 0.75
  88. else:
  89. self.score = -1.0
  90. def neighborKey(self, other):
  91. if self.coord < other.coord:
  92. return (self.coord.x, self.coord.y, self.coord.z, other.coord.x, other.coord.y, other.coord.z, tuple(self.weights), tuple(other.weights))
  93. else:
  94. return (other.coord.x, other.coord.y, other.coord.z, self.coord.x, self.coord.y, self.coord.z, tuple(other.weights), tuple(self.weights))
  95. def __hash__(self):
  96. return self.index
  97. def __eq__(self, v):
  98. return self.coord == v.coord and self.normal == v.normal and self.uv == v.uv and self.weights == v.weights and self.color == v.color
  99. class Mesh:
  100. def __init__(self, name, material, verts):
  101. self.name = name
  102. self.material = material
  103. self.verts = [ None for v in verts ]
  104. self.vertmap = {}
  105. self.tris = []
  106. def calcTangents(self):
  107. # See "Tangent Space Calculation" at http://www.terathon.com/code/tangent.html
  108. for v in self.verts:
  109. v.tangent = mathutils.Vector((0.0, 0.0, 0.0))
  110. v.bitangent = mathutils.Vector((0.0, 0.0, 0.0))
  111. for (v0, v1, v2) in self.tris:
  112. dco1 = v1.coord - v0.coord
  113. dco2 = v2.coord - v0.coord
  114. duv1 = v1.uv - v0.uv
  115. duv2 = v2.uv - v0.uv
  116. tangent = dco2*duv1.y - dco1*duv2.y
  117. bitangent = dco2*duv1.x - dco1*duv2.x
  118. if dco2.cross(dco1).dot(bitangent.cross(tangent)) < 0:
  119. tangent.negate()
  120. bitangent.negate()
  121. v0.tangent += tangent
  122. v1.tangent += tangent
  123. v2.tangent += tangent
  124. v0.bitangent += bitangent
  125. v1.bitangent += bitangent
  126. v2.bitangent += bitangent
  127. for v in self.verts:
  128. v.tangent = v.tangent - v.normal*v.tangent.dot(v.normal)
  129. v.tangent.normalize()
  130. if v.normal.cross(v.tangent).dot(v.bitangent) < 0:
  131. v.bitangent = -1.0
  132. else:
  133. v.bitangent = 1.0
  134. def optimize(self):
  135. # Linear-speed vertex cache optimization algorithm by Tom Forsyth
  136. for v in self.verts:
  137. if v:
  138. v.index = -1
  139. v.uses = []
  140. v.cacherank = -1
  141. for i, (v0, v1, v2) in enumerate(self.tris):
  142. v0.uses.append(i)
  143. v1.uses.append(i)
  144. v2.uses.append(i)
  145. for v in self.verts:
  146. if v:
  147. v.calcScore()
  148. besttri = -1
  149. bestscore = -42.0
  150. scores = []
  151. for i, (v0, v1, v2) in enumerate(self.tris):
  152. scores.append(v0.score + v1.score + v2.score)
  153. if scores[i] > bestscore:
  154. besttri = i
  155. bestscore = scores[i]
  156. vertloads = 0 # debug info
  157. vertschedule = []
  158. trischedule = []
  159. vcache = []
  160. while besttri >= 0:
  161. tri = self.tris[besttri]
  162. scores[besttri] = -666.0
  163. trischedule.append(tri)
  164. for v in tri:
  165. if v.cacherank < 0: # debug info
  166. vertloads += 1 # debug info
  167. if v.index < 0:
  168. v.index = len(vertschedule)
  169. vertschedule.append(v)
  170. v.uses.remove(besttri)
  171. v.cacherank = -1
  172. v.score = -1.0
  173. vcache = [ v for v in tri if v.uses ] + [ v for v in vcache if v.cacherank >= 0 ]
  174. for i, v in enumerate(vcache):
  175. v.cacherank = i
  176. v.calcScore()
  177. besttri = -1
  178. bestscore = -42.0
  179. for v in vcache:
  180. for i in v.uses:
  181. v0, v1, v2 = self.tris[i]
  182. scores[i] = v0.score + v1.score + v2.score
  183. if scores[i] > bestscore:
  184. besttri = i
  185. bestscore = scores[i]
  186. while len(vcache) > MAXVCACHE:
  187. vcache.pop().cacherank = -1
  188. if besttri < 0:
  189. for i, score in enumerate(scores):
  190. if score > bestscore:
  191. besttri = i
  192. bestscore = score
  193. print('%s: %d verts optimized to %d/%d loads for %d entry LRU cache' % (self.name, len(self.verts), vertloads, len(vertschedule), MAXVCACHE))
  194. #print('%s: %d verts scheduled to %d' % (self.name, len(self.verts), len(vertschedule)))
  195. self.verts = vertschedule
  196. # print('%s: %d tris scheduled to %d' % (self.name, len(self.tris), len(trischedule)))
  197. self.tris = trischedule
  198. def meshData(self, iqm):
  199. return [ iqm.addText(self.name), iqm.addText(self.material), self.firstvert, len(self.verts), self.firsttri, len(self.tris) ]
  200. class Bone:
  201. def __init__(self, name, origname, index, parent, matrix):
  202. self.name = name
  203. self.origname = origname
  204. self.index = index
  205. self.parent = parent
  206. self.matrix = matrix
  207. self.localmatrix = matrix
  208. if self.parent:
  209. self.localmatrix = parent.matrix.inverted() * self.localmatrix
  210. self.numchannels = 0
  211. self.channelmask = 0
  212. self.channeloffsets = [ 1.0e10, 1.0e10, 1.0e10, 1.0e10, 1.0e10, 1.0e10, 1.0e10, 1.0e10, 1.0e10, 1.0e10 ]
  213. self.channelscales = [ -1.0e10, -1.0e10, -1.0e10, -1.0e10, -1.0e10, -1.0e10, -1.0e10, -1.0e10, -1.0e10, -1.0e10 ]
  214. def jointData(self, iqm):
  215. if self.parent:
  216. parent = self.parent.index
  217. else:
  218. parent = -1
  219. pos = self.localmatrix.to_translation()
  220. orient = self.localmatrix.to_quaternion()
  221. orient.normalize()
  222. if orient.w > 0:
  223. orient.negate()
  224. scale = self.localmatrix.to_scale()
  225. scale.x = round(scale.x*0x10000)/0x10000
  226. scale.y = round(scale.y*0x10000)/0x10000
  227. scale.z = round(scale.z*0x10000)/0x10000
  228. return [ iqm.addText(self.name), parent, pos.x, pos.y, pos.z, orient.x, orient.y, orient.z, orient.w, scale.x, scale.y, scale.z ]
  229. def poseData(self, iqm):
  230. if self.parent:
  231. parent = self.parent.index
  232. else:
  233. parent = -1
  234. return [ parent, self.channelmask ] + self.channeloffsets + self.channelscales
  235. def calcChannelMask(self):
  236. for i in range(0, 10):
  237. self.channelscales[i] -= self.channeloffsets[i]
  238. if self.channelscales[i] >= 1.0e-10:
  239. self.numchannels += 1
  240. self.channelmask |= 1 << i
  241. self.channelscales[i] /= 0xFFFF
  242. else:
  243. self.channelscales[i] = 0.0
  244. return self.numchannels
  245. class Animation:
  246. def __init__(self, name, frames, fps = 0.0, flags = 0):
  247. self.name = name
  248. self.frames = frames
  249. self.fps = fps
  250. self.flags = flags
  251. def calcFrameLimits(self, bones):
  252. for frame in self.frames:
  253. for i, bone in enumerate(bones):
  254. loc, quat, scale, mat = frame[i]
  255. bone.channeloffsets[0] = min(bone.channeloffsets[0], loc.x)
  256. bone.channeloffsets[1] = min(bone.channeloffsets[1], loc.y)
  257. bone.channeloffsets[2] = min(bone.channeloffsets[2], loc.z)
  258. bone.channeloffsets[3] = min(bone.channeloffsets[3], quat.x)
  259. bone.channeloffsets[4] = min(bone.channeloffsets[4], quat.y)
  260. bone.channeloffsets[5] = min(bone.channeloffsets[5], quat.z)
  261. bone.channeloffsets[6] = min(bone.channeloffsets[6], quat.w)
  262. bone.channeloffsets[7] = min(bone.channeloffsets[7], scale.x)
  263. bone.channeloffsets[8] = min(bone.channeloffsets[8], scale.y)
  264. bone.channeloffsets[9] = min(bone.channeloffsets[9], scale.z)
  265. bone.channelscales[0] = max(bone.channelscales[0], loc.x)
  266. bone.channelscales[1] = max(bone.channelscales[1], loc.y)
  267. bone.channelscales[2] = max(bone.channelscales[2], loc.z)
  268. bone.channelscales[3] = max(bone.channelscales[3], quat.x)
  269. bone.channelscales[4] = max(bone.channelscales[4], quat.y)
  270. bone.channelscales[5] = max(bone.channelscales[5], quat.z)
  271. bone.channelscales[6] = max(bone.channelscales[6], quat.w)
  272. bone.channelscales[7] = max(bone.channelscales[7], scale.x)
  273. bone.channelscales[8] = max(bone.channelscales[8], scale.y)
  274. bone.channelscales[9] = max(bone.channelscales[9], scale.z)
  275. def animData(self, iqm):
  276. return [ iqm.addText(self.name), self.firstframe, len(self.frames), self.fps, self.flags ]
  277. def frameData(self, bones):
  278. data = b''
  279. for frame in self.frames:
  280. for i, bone in enumerate(bones):
  281. loc, quat, scale, mat = frame[i]
  282. if (bone.channelmask&0x7F) == 0x7F:
  283. lx = int(round((loc.x - bone.channeloffsets[0]) / bone.channelscales[0]))
  284. ly = int(round((loc.y - bone.channeloffsets[1]) / bone.channelscales[1]))
  285. lz = int(round((loc.z - bone.channeloffsets[2]) / bone.channelscales[2]))
  286. qx = int(round((quat.x - bone.channeloffsets[3]) / bone.channelscales[3]))
  287. qy = int(round((quat.y - bone.channeloffsets[4]) / bone.channelscales[4]))
  288. qz = int(round((quat.z - bone.channeloffsets[5]) / bone.channelscales[5]))
  289. qw = int(round((quat.w - bone.channeloffsets[6]) / bone.channelscales[6]))
  290. data += struct.pack('<7H', lx, ly, lz, qx, qy, qz, qw)
  291. else:
  292. if bone.channelmask & 1:
  293. data += struct.pack('<H', int(round((loc.x - bone.channeloffsets[0]) / bone.channelscales[0])))
  294. if bone.channelmask & 2:
  295. data += struct.pack('<H', int(round((loc.y - bone.channeloffsets[1]) / bone.channelscales[1])))
  296. if bone.channelmask & 4:
  297. data += struct.pack('<H', int(round((loc.z - bone.channeloffsets[2]) / bone.channelscales[2])))
  298. if bone.channelmask & 8:
  299. data += struct.pack('<H', int(round((quat.x - bone.channeloffsets[3]) / bone.channelscales[3])))
  300. if bone.channelmask & 16:
  301. data += struct.pack('<H', int(round((quat.y - bone.channeloffsets[4]) / bone.channelscales[4])))
  302. if bone.channelmask & 32:
  303. data += struct.pack('<H', int(round((quat.z - bone.channeloffsets[5]) / bone.channelscales[5])))
  304. if bone.channelmask & 64:
  305. data += struct.pack('<H', int(round((quat.w - bone.channeloffsets[6]) / bone.channelscales[6])))
  306. if bone.channelmask & 128:
  307. data += struct.pack('<H', int(round((scale.x - bone.channeloffsets[7]) / bone.channelscales[7])))
  308. if bone.channelmask & 256:
  309. data += struct.pack('<H', int(round((scale.y - bone.channeloffsets[8]) / bone.channelscales[8])))
  310. if bone.channelmask & 512:
  311. data += struct.pack('<H', int(round((scale.z - bone.channeloffsets[9]) / bone.channelscales[9])))
  312. return data
  313. def frameBoundsData(self, bones, meshes, frame, invbase):
  314. bbmin = bbmax = None
  315. xyradius = 0.0
  316. radius = 0.0
  317. transforms = []
  318. for i, bone in enumerate(bones):
  319. loc, quat, scale, mat = frame[i]
  320. if bone.parent:
  321. mat = transforms[bone.parent.index] * mat
  322. transforms.append(mat)
  323. for i, mat in enumerate(transforms):
  324. transforms[i] = mat * invbase[i]
  325. for mesh in meshes:
  326. for v in mesh.verts:
  327. pos = mathutils.Vector((0.0, 0.0, 0.0))
  328. for (weight, bone) in v.weights:
  329. if weight > 0:
  330. pos += (transforms[bone] * v.coord) * (weight / 255.0)
  331. if bbmin:
  332. bbmin.x = min(bbmin.x, pos.x)
  333. bbmin.y = min(bbmin.y, pos.y)
  334. bbmin.z = min(bbmin.z, pos.z)
  335. bbmax.x = max(bbmax.x, pos.x)
  336. bbmax.y = max(bbmax.y, pos.y)
  337. bbmax.z = max(bbmax.z, pos.z)
  338. else:
  339. bbmin = pos.copy()
  340. bbmax = pos.copy()
  341. pradius = pos.x*pos.x + pos.y*pos.y
  342. if pradius > xyradius:
  343. xyradius = pradius
  344. pradius += pos.z*pos.z
  345. if pradius > radius:
  346. radius = pradius
  347. if bbmin:
  348. xyradius = math.sqrt(xyradius)
  349. radius = math.sqrt(radius)
  350. else:
  351. bbmin = bbmax = mathutils.Vector((0.0, 0.0, 0.0))
  352. return IQM_BOUNDS.pack(bbmin.x, bbmin.y, bbmin.z, bbmax.x, bbmax.y, bbmax.z, xyradius, radius)
  353. def boundsData(self, bones, meshes):
  354. invbase = []
  355. for bone in bones:
  356. invbase.append(bone.matrix.inverted())
  357. data = b''
  358. for i, frame in enumerate(self.frames):
  359. print('Calculating bounding box for %s:%d' % (self.name, i))
  360. data += self.frameBoundsData(bones, meshes, frame, invbase)
  361. return data
  362. class IQMFile:
  363. def __init__(self):
  364. self.textoffsets = {}
  365. self.textdata = b''
  366. self.meshes = []
  367. self.meshdata = []
  368. self.numverts = 0
  369. self.numtris = 0
  370. self.joints = []
  371. self.jointdata = []
  372. self.numframes = 0
  373. self.framesize = 0
  374. self.anims = []
  375. self.posedata = []
  376. self.animdata = []
  377. self.framedata = []
  378. self.vertdata = []
  379. def addText(self, str):
  380. if not self.textdata:
  381. self.textdata += b'\x00'
  382. self.textoffsets[''] = 0
  383. try:
  384. return self.textoffsets[str]
  385. except:
  386. offset = len(self.textdata)
  387. self.textoffsets[str] = offset
  388. self.textdata += bytes(str, encoding="utf8") + b'\x00'
  389. return offset
  390. def addJoints(self, bones):
  391. for bone in bones:
  392. self.joints.append(bone)
  393. if self.meshes:
  394. self.jointdata.append(bone.jointData(self))
  395. def addMeshes(self, meshes):
  396. self.meshes += meshes
  397. for mesh in meshes:
  398. mesh.firstvert = self.numverts
  399. mesh.firsttri = self.numtris
  400. self.meshdata.append(mesh.meshData(self))
  401. self.numverts += len(mesh.verts)
  402. self.numtris += len(mesh.tris)
  403. def addAnims(self, anims):
  404. self.anims += anims
  405. for anim in anims:
  406. anim.firstframe = self.numframes
  407. self.animdata.append(anim.animData(self))
  408. self.numframes += len(anim.frames)
  409. def calcFrameSize(self):
  410. for anim in self.anims:
  411. anim.calcFrameLimits(self.joints)
  412. self.framesize = 0
  413. for joint in self.joints:
  414. self.framesize += joint.calcChannelMask()
  415. for joint in self.joints:
  416. if self.anims:
  417. self.posedata.append(joint.poseData(self))
  418. print('Exporting %d frames of size %d' % (self.numframes, self.framesize))
  419. def writeVerts(self, file, offset):
  420. if self.numverts <= 0:
  421. return
  422. file.write(IQM_VERTEXARRAY.pack(IQM_POSITION, 0, IQM_FLOAT, 3, offset))
  423. offset += self.numverts * struct.calcsize('<3f')
  424. file.write(IQM_VERTEXARRAY.pack(IQM_TEXCOORD, 0, IQM_FLOAT, 2, offset))
  425. offset += self.numverts * struct.calcsize('<2f')
  426. file.write(IQM_VERTEXARRAY.pack(IQM_NORMAL, 0, IQM_FLOAT, 3, offset))
  427. offset += self.numverts * struct.calcsize('<3f')
  428. file.write(IQM_VERTEXARRAY.pack(IQM_TANGENT, 0, IQM_FLOAT, 4, offset))
  429. offset += self.numverts * struct.calcsize('<4f')
  430. if self.joints:
  431. file.write(IQM_VERTEXARRAY.pack(IQM_BLENDINDEXES, 0, IQM_UBYTE, 4, offset))
  432. offset += self.numverts * struct.calcsize('<4B')
  433. file.write(IQM_VERTEXARRAY.pack(IQM_BLENDWEIGHTS, 0, IQM_UBYTE, 4, offset))
  434. offset += self.numverts * struct.calcsize('<4B')
  435. hascolors = any(mesh.verts and mesh.verts[0].color for mesh in self.meshes)
  436. if hascolors:
  437. file.write(IQM_VERTEXARRAY.pack(IQM_COLOR, 0, IQM_UBYTE, 4, offset))
  438. offset += self.numverts * struct.calcsize('<4B')
  439. for mesh in self.meshes:
  440. for v in mesh.verts:
  441. file.write(struct.pack('<3f', *v.coord))
  442. for mesh in self.meshes:
  443. for v in mesh.verts:
  444. file.write(struct.pack('<2f', *v.uv))
  445. for mesh in self.meshes:
  446. for v in mesh.verts:
  447. file.write(struct.pack('<3f', *v.normal))
  448. for mesh in self.meshes:
  449. for v in mesh.verts:
  450. file.write(struct.pack('<4f', v.tangent.x, v.tangent.y, v.tangent.z, v.bitangent))
  451. if self.joints:
  452. for mesh in self.meshes:
  453. for v in mesh.verts:
  454. file.write(struct.pack('<4B', v.weights[0][1], v.weights[1][1], v.weights[2][1], v.weights[3][1]))
  455. for mesh in self.meshes:
  456. for v in mesh.verts:
  457. file.write(struct.pack('<4B', v.weights[0][0], v.weights[1][0], v.weights[2][0], v.weights[3][0]))
  458. if hascolors:
  459. for mesh in self.meshes:
  460. for v in mesh.verts:
  461. if v.color:
  462. file.write(struct.pack('<4B', v.color[0], v.color[1], v.color[2], v.color[3]))
  463. else:
  464. file.write(struct.pack('<4B', 0, 0, 0, 255))
  465. def calcNeighbors(self):
  466. edges = {}
  467. for mesh in self.meshes:
  468. for i, (v0, v1, v2) in enumerate(mesh.tris):
  469. e0 = v0.neighborKey(v1)
  470. e1 = v1.neighborKey(v2)
  471. e2 = v2.neighborKey(v0)
  472. tri = mesh.firsttri + i
  473. try: edges[e0].append(tri)
  474. except: edges[e0] = [tri]
  475. try: edges[e1].append(tri)
  476. except: edges[e1] = [tri]
  477. try: edges[e2].append(tri)
  478. except: edges[e2] = [tri]
  479. neighbors = []
  480. for mesh in self.meshes:
  481. for i, (v0, v1, v2) in enumerate(mesh.tris):
  482. e0 = edges[v0.neighborKey(v1)]
  483. e1 = edges[v1.neighborKey(v2)]
  484. e2 = edges[v2.neighborKey(v0)]
  485. tri = mesh.firsttri + i
  486. match0 = match1 = match2 = -1
  487. if len(e0) == 2: match0 = e0[e0.index(tri)^1]
  488. if len(e1) == 2: match1 = e1[e1.index(tri)^1]
  489. if len(e2) == 2: match2 = e2[e2.index(tri)^1]
  490. neighbors.append((match0, match1, match2))
  491. self.neighbors = neighbors
  492. def writeTris(self, file):
  493. for mesh in self.meshes:
  494. for (v0, v1, v2) in mesh.tris:
  495. file.write(struct.pack('<3I', v0.index + mesh.firstvert, v1.index + mesh.firstvert, v2.index + mesh.firstvert))
  496. for (n0, n1, n2) in self.neighbors:
  497. if n0 < 0: n0 = 0xFFFFFFFF
  498. if n1 < 0: n1 = 0xFFFFFFFF
  499. if n2 < 0: n2 = 0xFFFFFFFF
  500. file.write(struct.pack('<3I', n0, n1, n2))
  501. def export(self, file, usebbox = True):
  502. self.filesize = IQM_HEADER.size
  503. if self.textdata:
  504. while len(self.textdata) % 4:
  505. self.textdata += b'\x00'
  506. ofs_text = self.filesize
  507. self.filesize += len(self.textdata)
  508. else:
  509. ofs_text = 0
  510. if self.meshdata:
  511. ofs_meshes = self.filesize
  512. self.filesize += len(self.meshdata) * IQM_MESH.size
  513. else:
  514. ofs_meshes = 0
  515. if self.numverts > 0:
  516. ofs_vertexarrays = self.filesize
  517. num_vertexarrays = 4
  518. if self.joints:
  519. num_vertexarrays += 2
  520. hascolors = any(mesh.verts and mesh.verts[0].color for mesh in self.meshes)
  521. if hascolors:
  522. num_vertexarrays += 1
  523. self.filesize += num_vertexarrays * IQM_VERTEXARRAY.size
  524. ofs_vdata = self.filesize
  525. self.filesize += self.numverts * struct.calcsize('<3f2f3f4f')
  526. if self.joints:
  527. self.filesize += self.numverts * struct.calcsize('<4B4B')
  528. if hascolors:
  529. self.filesize += self.numverts * struct.calcsize('<4B')
  530. else:
  531. ofs_vertexarrays = 0
  532. num_vertexarrays = 0
  533. ofs_vdata = 0
  534. if self.numtris > 0:
  535. ofs_triangles = self.filesize
  536. self.filesize += self.numtris * IQM_TRIANGLE.size
  537. ofs_neighbors = self.filesize
  538. self.filesize += self.numtris * IQM_TRIANGLE.size
  539. else:
  540. ofs_triangles = 0
  541. ofs_neighbors = 0
  542. if self.jointdata:
  543. ofs_joints = self.filesize
  544. self.filesize += len(self.jointdata) * IQM_JOINT.size
  545. else:
  546. ofs_joints = 0
  547. if self.posedata:
  548. ofs_poses = self.filesize
  549. self.filesize += len(self.posedata) * IQM_POSE.size
  550. else:
  551. ofs_poses = 0
  552. if self.animdata:
  553. ofs_anims = self.filesize
  554. self.filesize += len(self.animdata) * IQM_ANIMATION.size
  555. else:
  556. ofs_anims = 0
  557. falign = 0
  558. if self.framesize * self.numframes > 0:
  559. ofs_frames = self.filesize
  560. self.filesize += self.framesize * self.numframes * struct.calcsize('<H')
  561. falign = (4 - (self.filesize % 4)) % 4
  562. self.filesize += falign
  563. else:
  564. ofs_frames = 0
  565. if usebbox and self.numverts > 0 and self.numframes > 0:
  566. ofs_bounds = self.filesize
  567. self.filesize += self.numframes * IQM_BOUNDS.size
  568. else:
  569. ofs_bounds = 0
  570. file.write(IQM_HEADER.pack('INTERQUAKEMODEL'.encode('ascii'), 2, self.filesize, 0, len(self.textdata), ofs_text, len(self.meshdata), ofs_meshes, num_vertexarrays, self.numverts, ofs_vertexarrays, self.numtris, ofs_triangles, ofs_neighbors, len(self.jointdata), ofs_joints, len(self.posedata), ofs_poses, len(self.animdata), ofs_anims, self.numframes, self.framesize, ofs_frames, ofs_bounds, 0, 0, 0, 0))
  571. file.write(self.textdata)
  572. for mesh in self.meshdata:
  573. file.write(IQM_MESH.pack(*mesh))
  574. self.writeVerts(file, ofs_vdata)
  575. self.writeTris(file)
  576. for joint in self.jointdata:
  577. file.write(IQM_JOINT.pack(*joint))
  578. for pose in self.posedata:
  579. file.write(IQM_POSE.pack(*pose))
  580. for anim in self.animdata:
  581. file.write(IQM_ANIMATION.pack(*anim))
  582. for anim in self.anims:
  583. file.write(anim.frameData(self.joints))
  584. file.write(b'\x00' * falign)
  585. if usebbox and self.numverts > 0 and self.numframes > 0:
  586. for anim in self.anims:
  587. file.write(anim.boundsData(self.joints, self.meshes))
  588. def findArmature(context):
  589. armature = None
  590. for obj in context.selected_objects:
  591. if obj.type == 'ARMATURE':
  592. armature = obj
  593. break
  594. if not armature:
  595. for obj in context.selected_objects:
  596. if obj.type == 'MESH':
  597. armature = obj.find_armature()
  598. if armature:
  599. break
  600. return armature
  601. def derigifyBones(context, armature, scale):
  602. data = armature.data
  603. defnames = []
  604. orgbones = {}
  605. defbones = {}
  606. org2defs = {}
  607. def2org = {}
  608. defparent = {}
  609. defchildren = {}
  610. for bone in data.bones.values():
  611. if bone.name.startswith('ORG-'):
  612. orgbones[bone.name[4:]] = bone
  613. org2defs[bone.name[4:]] = []
  614. elif bone.name.startswith('DEF-'):
  615. defnames.append(bone.name[4:])
  616. defbones[bone.name[4:]] = bone
  617. defchildren[bone.name[4:]] = []
  618. for name, bone in defbones.items():
  619. orgname = name
  620. orgbone = orgbones.get(orgname)
  621. splitname = -1
  622. if not orgbone:
  623. splitname = name.rfind('.')
  624. suffix = ''
  625. if splitname >= 0 and name[splitname+1:] in [ 'l', 'r', 'L', 'R' ]:
  626. suffix = name[splitname:]
  627. splitname = name.rfind('.', 0, splitname)
  628. if splitname >= 0 and name[splitname+1:splitname+2].isdigit():
  629. orgname = name[:splitname] + suffix
  630. orgbone = orgbones.get(orgname)
  631. org2defs[orgname].append(name)
  632. def2org[name] = orgname
  633. for defs in org2defs.values():
  634. defs.sort()
  635. for name in defnames:
  636. bone = defbones[name]
  637. orgname = def2org[name]
  638. orgbone = orgbones.get(orgname)
  639. defs = org2defs[orgname]
  640. if orgbone:
  641. i = defs.index(name)
  642. if i == 0:
  643. orgparent = orgbone.parent
  644. if orgparent and orgparent.name.startswith('ORG-'):
  645. orgpname = orgparent.name[4:]
  646. defparent[name] = org2defs[orgpname][-1]
  647. else:
  648. defparent[name] = defs[i-1]
  649. if name in defparent:
  650. defchildren[defparent[name]].append(name)
  651. bones = {}
  652. worldmatrix = armature.matrix_world
  653. worklist = [ bone for bone in defnames if bone not in defparent ]
  654. for index, bname in enumerate(worklist):
  655. bone = defbones[bname]
  656. bonematrix = worldmatrix * bone.matrix_local
  657. if scale != 1.0:
  658. bonematrix.translation *= scale
  659. bones[bone.name] = Bone(bname, bone.name, index, bname in defparent and bones.get(defbones[defparent[bname]].name), bonematrix)
  660. worklist.extend(defchildren[bname])
  661. print('De-rigified %d bones' % len(worklist))
  662. return bones
  663. def collectBones(context, armature, scale):
  664. data = armature.data
  665. bones = {}
  666. rot = mathutils.Matrix.Rotation(-1.5708, 4, 'X')
  667. worldmatrix = rot * armature.matrix_world
  668. worklist = [ bone for bone in data.bones.values() if not bone.parent ]
  669. for index, bone in enumerate(worklist):
  670. bonematrix = worldmatrix * bone.matrix_local
  671. if scale != 1.0:
  672. bonematrix.translation *= scale
  673. bones[bone.name] = Bone(bone.name, bone.name, index, bone.parent and bones.get(bone.parent.name), bonematrix)
  674. for child in bone.children:
  675. if child not in worklist:
  676. worklist.append(child)
  677. print('Collected %d bones' % len(worklist))
  678. return bones
  679. def collectAnim(context, armature, scale, bones, action, startframe = None, endframe = None):
  680. if not startframe or not endframe:
  681. startframe, endframe = action.frame_range
  682. startframe = int(startframe)
  683. endframe = int(endframe)
  684. print('Exporting action "%s" frames %d-%d' % (action.name, startframe, endframe))
  685. scene = context.scene
  686. rot = mathutils.Matrix.Rotation(-1.5708, 4, 'X')
  687. worldmatrix = rot * armature.matrix_world
  688. armature.animation_data.action = action
  689. outdata = []
  690. for time in range(startframe, endframe+1):
  691. scene.frame_set(time)
  692. pose = armature.pose
  693. outframe = []
  694. for bone in bones:
  695. posematrix = pose.bones[bone.origname].matrix
  696. if bone.parent:
  697. posematrix = pose.bones[bone.parent.origname].matrix.inverted() * posematrix
  698. else:
  699. posematrix = worldmatrix * posematrix
  700. if scale != 1.0:
  701. posematrix.translation *= scale
  702. loc = posematrix.to_translation()
  703. quat = posematrix.to_quaternion()
  704. quat.normalize()
  705. if quat.w > 0:
  706. quat.negate()
  707. pscale = posematrix.to_scale()
  708. pscale.x = round(pscale.x*0x10000)/0x10000
  709. pscale.y = round(pscale.y*0x10000)/0x10000
  710. pscale.z = round(pscale.z*0x10000)/0x10000
  711. outframe.append((loc, quat, pscale, posematrix))
  712. outdata.append(outframe)
  713. return outdata
  714. def collectAnims(context, armature, scale, bones, animspecs):
  715. if not armature.animation_data:
  716. print('Armature has no animation data')
  717. return []
  718. actions = bpy.data.actions
  719. animspecs = [ spec.strip() for spec in animspecs.split(',') ]
  720. anims = []
  721. scene = context.scene
  722. oldaction = armature.animation_data.action
  723. oldframe = scene.frame_current
  724. for animspec in animspecs:
  725. animspec = [ arg.strip() for arg in animspec.split(':') ]
  726. animname = animspec[0]
  727. if animname not in actions:
  728. print('Action "%s" not found in current armature' % animname)
  729. continue
  730. try:
  731. startframe = int(animspec[1])
  732. except:
  733. startframe = None
  734. try:
  735. endframe = int(animspec[2])
  736. except:
  737. endframe = None
  738. try:
  739. fps = float(animspec[3])
  740. except:
  741. fps = float(scene.render.fps)
  742. try:
  743. flags = int(animspec[4])
  744. except:
  745. flags = 0
  746. framedata = collectAnim(context, armature, scale, bones, actions[animname], startframe, endframe)
  747. anims.append(Animation(animname, framedata, fps, flags))
  748. armature.animation_data.action = oldaction
  749. scene.frame_set(oldframe)
  750. return anims
  751. def collectMeshes(context, bones, scale, matfun, useskel = True, usecol = False, filetype = 'IQM'):
  752. vertwarn = []
  753. objs = context.selected_objects #context.scene.objects
  754. meshes = []
  755. for obj in objs:
  756. if obj.type == 'MESH':
  757. data = obj.to_mesh(context.scene, False, 'PREVIEW')
  758. if not data.polygons:
  759. continue
  760. data.calc_normals_split()
  761. # rm = mathutils.Matrix([[1,0,0,0],[0,0,-1,0],[0,1,0,0],[0,0,0,1]])
  762. rot = mathutils.Matrix.Rotation(-1.5708, 4, 'X')
  763. coordmatrix = rot * obj.matrix_world
  764. normalmatrix = coordmatrix.inverted().transposed()
  765. if scale != 1.0:
  766. coordmatrix = mathutils.Matrix.Scale(scale, 4) * coordmatrix
  767. materials = {}
  768. groups = obj.vertex_groups
  769. uvfaces = data.uv_textures.active and data.uv_textures.active.data
  770. uvlayer = data.uv_layers.active and data.uv_layers.active.data
  771. colors = None
  772. alpha = None
  773. if usecol:
  774. if data.vertex_colors.active:
  775. if data.vertex_colors.active.name.startswith('alpha'):
  776. alpha = data.vertex_colors.active.data
  777. else:
  778. colors = data.vertex_colors.active.data
  779. for layer in data.vertex_colors:
  780. if layer.name.startswith('alpha'):
  781. if not alpha:
  782. alpha = layer.data
  783. elif not colors:
  784. colors = layer.data
  785. for face in data.polygons:
  786. if len(face.vertices) < 3:
  787. continue
  788. if all([ data.vertices[i].co == data.vertices[face.vertices[0]].co for i in face.vertices[1:] ]):
  789. continue
  790. uvface = uvfaces and uvfaces[face.index]
  791. material = os.path.basename(uvface.image.filepath) if uvface and uvface.image else ''
  792. matindex = face.material_index
  793. try:
  794. mesh = materials[obj.name, matindex, material]
  795. except:
  796. try:
  797. matprefix = (data.materials and data.materials[matindex].name) or ''
  798. except:
  799. matprefix = ''
  800. mesh = Mesh(obj.name, matfun(matprefix, material), data.vertices)
  801. meshes.append(mesh)
  802. materials[obj.name, matindex, material] = mesh
  803. verts = mesh.verts
  804. vertmap = mesh.vertmap
  805. faceverts = []
  806. for loopidx in face.loop_indices:
  807. loop = data.loops[loopidx]
  808. v = data.vertices[loop.vertex_index]
  809. vertco = coordmatrix * v.co
  810. if not face.use_smooth:
  811. vertno = mathutils.Vector(face.normal)
  812. else:
  813. vertno = mathutils.Vector(loop.normal)
  814. vertno = normalmatrix * vertno
  815. vertno.normalize()
  816. # flip V axis of texture space
  817. if uvlayer:
  818. uv = uvlayer[loopidx].uv
  819. vertuv = mathutils.Vector((uv[0], 1.0 - uv[1]))
  820. else:
  821. vertuv = mathutils.Vector((0.0, 0.0))
  822. if colors:
  823. vertcol = colors[loopidx].color
  824. vertcol = (int(round(vertcol[0] * 255.0)), int(round(vertcol[1] * 255.0)), int(round(vertcol[2] * 255.0)), 255)
  825. else:
  826. vertcol = None
  827. if alpha:
  828. vertalpha = alpha[loopidx].color
  829. if vertcol:
  830. vertcol = (vertcol[0], vertcol[1], vertcol[2], int(round(vertalpha[0] * 255.0)))
  831. else:
  832. vertcol = (255, 255, 255, int(round(vertalpha[0] * 255.0)))
  833. vertweights = []
  834. if useskel:
  835. for g in v.groups:
  836. try:
  837. vertweights.append((g.weight, bones[groups[g.group].name].index))
  838. except:
  839. if (groups[g.group].name, mesh.name) not in vertwarn:
  840. vertwarn.append((groups[g.group].name, mesh.name))
  841. print('Vertex depends on non-existent bone: %s in mesh: %s' % (groups[g.group].name, mesh.name))
  842. if not face.use_smooth:
  843. vertindex = len(verts)
  844. vertkey = Vertex(vertindex, vertco, vertno, vertuv, vertweights, vertcol)
  845. if filetype == 'IQM':
  846. vertkey.normalizeWeights()
  847. mesh.verts.append(vertkey)
  848. faceverts.append(vertkey)
  849. continue
  850. vertkey = Vertex(v.index, vertco, vertno, vertuv, vertweights, vertcol)
  851. if filetype == 'IQM':
  852. vertkey.normalizeWeights()
  853. if not verts[v.index]:
  854. verts[v.index] = vertkey
  855. faceverts.append(vertkey)
  856. elif verts[v.index] == vertkey:
  857. faceverts.append(verts[v.index])
  858. else:
  859. try:
  860. vertindex = vertmap[vertkey]
  861. faceverts.append(verts[vertindex])
  862. except:
  863. vertindex = len(verts)
  864. vertmap[vertkey] = vertindex
  865. verts.append(vertkey)
  866. faceverts.append(vertkey)
  867. # Quake winding is reversed
  868. for i in range(2, len(faceverts)):
  869. mesh.tris.append((faceverts[0], faceverts[i], faceverts[i-1]))
  870. for mesh in meshes:
  871. mesh.optimize()
  872. if filetype == 'IQM':
  873. mesh.calcTangents()
  874. print('%s %s: generated %d triangles' % (mesh.name, mesh.material, len(mesh.tris)))
  875. return meshes
  876. def exportIQE(file, meshes, bones, anims):
  877. file.write('# Inter-Quake Export\n\n')
  878. for bone in bones:
  879. if bone.parent:
  880. parent = bone.parent.index
  881. else:
  882. parent = -1
  883. file.write('joint "%s" %d\n' % (bone.name, parent))
  884. if meshes:
  885. pos = bone.localmatrix.to_translation()
  886. orient = bone.localmatrix.to_quaternion()
  887. orient.normalize()
  888. if orient.w > 0:
  889. orient.negate()
  890. scale = bone.localmatrix.to_scale()
  891. scale.x = round(scale.x*0x10000)/0x10000
  892. scale.y = round(scale.y*0x10000)/0x10000
  893. scale.z = round(scale.z*0x10000)/0x10000
  894. if scale.x == 1.0 and scale.y == 1.0 and scale.z == 1.0:
  895. file.write('\tpq %.8f %.8f %.8f %.8f %.8f %.8f %.8f\n' % (pos.x, pos.y, pos.z, orient.x, orient.y, orient.z, orient.w))
  896. else:
  897. file.write('\tpq %.8f %.8f %.8f %.8f %.8f %.8f %.8f %.8f %.8f %.8f\n' % (pos.x, pos.y, pos.z, orient.x, orient.y, orient.z, orient.w, scale.x, scale.y, scale.z))
  898. hascolors = any(mesh.verts and mesh.verts[0].color for mesh in meshes)
  899. for mesh in meshes:
  900. file.write('\nmesh "%s"\n\tmaterial "%s"\n\n' % (mesh.name, mesh.material))
  901. for v in mesh.verts:
  902. file.write('vp %.8f %.8f %.8f\n\tvt %.8f %.8f\n\tvn %.8f %.8f %.8f\n' % (v.coord.x, v.coord.y, v.coord.z, v.uv.x, v.uv.y, v.normal.x, v.normal.y, v.normal.z))
  903. if bones:
  904. weights = '\tvb'
  905. for weight in v.weights:
  906. weights += ' %d %.8f' % (weight[1], weight[0])
  907. file.write(weights + '\n')
  908. if hascolors:
  909. if v.color:
  910. file.write('\tvc %.8f %.8f %.8f %.8f\n' % (v.color[0] / 255.0, v.color[1] / 255.0, v.color[2] / 255.0, v.color[3] / 255.0))
  911. else:
  912. file.write('\tvc 0 0 0 1\n')
  913. file.write('\n')
  914. for (v0, v1, v2) in mesh.tris:
  915. file.write('fm %d %d %d\n' % (v0.index, v1.index, v2.index))
  916. for anim in anims:
  917. file.write('\nanimation "%s"\n\tframerate %.8f\n' % (anim.name, anim.fps))
  918. if anim.flags&IQM_LOOP:
  919. file.write('\tloop\n')
  920. for frame in anim.frames:
  921. file.write('\nframe\n')
  922. for (pos, orient, scale, mat) in frame:
  923. if scale.x == 1.0 and scale.y == 1.0 and scale.z == 1.0:
  924. file.write('pq %.8f %.8f %.8f %.8f %.8f %.8f %.8f\n' % (pos.x, pos.y, pos.z, orient.x, orient.y, orient.z, orient.w))
  925. else:
  926. file.write('pq %.8f %.8f %.8f %.8f %.8f %.8f %.8f %.8f %.8f %.8f\n' % (pos.x, pos.y, pos.z, orient.x, orient.y, orient.z, orient.w, scale.x, scale.y, scale.z))
  927. file.write('\n')
  928. def exportIQM(context, filename, usemesh = True, useskel = True, usebbox = True, usecol = False, scale = 1.0, animspecs = None, matfun = (lambda prefix, image: image), derigify = False, boneorder = None):
  929. armature = findArmature(context)
  930. if useskel and not armature:
  931. print('No armature selected')
  932. return
  933. if filename.lower().endswith('.iqm'):
  934. filetype = 'IQM'
  935. elif filename.lower().endswith('.iqe'):
  936. filetype = 'IQE'
  937. else:
  938. print('Unknown file type: %s' % filename)
  939. return
  940. if useskel:
  941. if derigify:
  942. bones = derigifyBones(context, armature, scale)
  943. else:
  944. bones = collectBones(context, armature, scale)
  945. else:
  946. bones = {}
  947. if boneorder:
  948. try:
  949. f = open(bpy_extras.io_utils.path_reference(boneorder, os.path.dirname(bpy.data.filepath), os.path.dirname(filename)), "r", encoding = "utf-8")
  950. names = [line.strip() for line in f.readlines()]
  951. f.close()
  952. names = [name for name in names if name in [bone.name for bone in bones.values()]]
  953. if len(names) != len(bones):
  954. print('Bone order (%d) does not match skeleton (%d)' % (len(names), len(bones)))
  955. return
  956. print('Reordering bones')
  957. for bone in bones.values():
  958. bone.index = names.index(bone.name)
  959. except:
  960. print('Failed opening bone order: %s' % boneorder)
  961. return
  962. bonelist = sorted(bones.values(), key = lambda bone: bone.index)
  963. if usemesh:
  964. meshes = collectMeshes(context, bones, scale, matfun, useskel, usecol, filetype)
  965. else:
  966. meshes = []
  967. if useskel and animspecs:
  968. anims = collectAnims(context, armature, scale, bonelist, animspecs)
  969. else:
  970. anims = []
  971. if filetype == 'IQM':
  972. iqm = IQMFile()
  973. iqm.addMeshes(meshes)
  974. iqm.addJoints(bonelist)
  975. iqm.addAnims(anims)
  976. iqm.calcFrameSize()
  977. iqm.calcNeighbors()
  978. if filename:
  979. try:
  980. if filetype == 'IQM':
  981. file = open(filename, 'wb')
  982. else:
  983. file = open(filename, 'w')
  984. except:
  985. print ('Failed writing to %s' % (filename))
  986. return
  987. if filetype == 'IQM':
  988. iqm.export(file, usebbox)
  989. elif filetype == 'IQE':
  990. exportIQE(file, meshes, bonelist, anims)
  991. file.close()
  992. print('Saved %s file to %s' % (filetype, filename))
  993. else:
  994. print('No %s file was generated' % (filetype))
  995. class ExportIQM(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
  996. '''Export an Inter-Quake Model IQM or IQE file'''
  997. bl_idname = "export.iqm"
  998. bl_label = 'Export IQM'
  999. filename_ext = ".iqm"
  1000. animspec = bpy.props.StringProperty(name="Animations", description="Animations to export", maxlen=1024, default="")
  1001. usemesh = bpy.props.BoolProperty(name="Meshes", description="Generate meshes", default=True)
  1002. useskel = bpy.props.BoolProperty(name="Skeleton", description="Generate skeleton", default=True)
  1003. usebbox = bpy.props.BoolProperty(name="Bounding boxes", description="Generate bounding boxes", default=True)
  1004. usecol = bpy.props.BoolProperty(name="Vertex colors", description="Export vertex colors", default=False)
  1005. usescale = bpy.props.FloatProperty(name="Scale", description="Scale of exported model", default=1.0, min=0.0, step=50, precision=2)
  1006. #usetrans = bpy.props.FloatVectorProperty(name="Translate", description="Translate position of exported model", step=50, precision=2, size=3)
  1007. matfmt = bpy.props.EnumProperty(name="Materials", description="Material name format", items=[("m+i-e", "material+image-ext", ""), ("m", "material", ""), ("i", "image", "")], default="m+i-e")
  1008. derigify = bpy.props.BoolProperty(name="De-rigify", description="Export only deformation bones from rigify", default=False)
  1009. boneorder = bpy.props.StringProperty(name="Bone order", description="Override ordering of bones", subtype="FILE_NAME", default="")
  1010. def execute(self, context):
  1011. if self.properties.matfmt == "m+i-e":
  1012. matfun = lambda prefix, image: prefix + os.path.splitext(image)[0]
  1013. elif self.properties.matfmt == "m":
  1014. matfun = lambda prefix, image: prefix
  1015. else:
  1016. matfun = lambda prefix, image: image
  1017. exportIQM(context, self.properties.filepath, self.properties.usemesh, self.properties.useskel, self.properties.usebbox, self.properties.usecol, self.properties.usescale, self.properties.animspec, matfun, self.properties.derigify, self.properties.boneorder)
  1018. return {'FINISHED'}
  1019. def check(self, context):
  1020. filepath = bpy.path.ensure_ext(self.filepath, '.iqm')
  1021. filepathalt = bpy.path.ensure_ext(self.filepath, '.iqe')
  1022. if filepath != self.filepath and filepathalt != self.filepath:
  1023. self.filepath = filepath
  1024. return True
  1025. return False
  1026. def menu_func(self, context):
  1027. self.layout.operator(ExportIQM.bl_idname, text="Inter-Quake Model (.iqm, .iqe)")
  1028. def register():
  1029. bpy.utils.register_module(__name__)
  1030. bpy.types.INFO_MT_file_export.append(menu_func)
  1031. def unregister():
  1032. bpy.utils.unregister_module(__name__)
  1033. bpy.types.INFO_MT_file_export.remove(menu_func)
  1034. if __name__ == "__main__":
  1035. register()