BPython:
[blender-staging.git] / release / scripts / blender2cal3d.py
1 #!BPY
2
3 """
4 Name: 'Cal3D v0.5'
5 Blender: 232
6 Group: 'Export'
7 Tip: 'Export armature/bone data to the Cal3D library.'
8 """
9
10 # blender2cal3D.py version 0.5
11 # Copyright (C) 2003 Jean-Baptiste LAMY -- jiba@tuxfamily.org
12 #
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
27
28 # This script is a Blender 2.28 => Cal3D 0.7/0.8 converter.
29 # (See http://blender.org and http://cal3d.sourceforge.net)
30 #
31 # Grab the latest version here :
32 # http://oomadness.tuxfamily.org/en/blender2cal3d
33
34 # HOW TO USE :
35 # 1 - load the script in Blender's text editor
36 # 2 - modify the parameters below (e.g. the file name)
37 # 3 - type M-P (meta/alt + P) and wait until script execution is finished
38
39 # ADVICES :
40 # - Use only locrot keys in Blender's action
41 # - Do not put "." in action or bone names, and do not start these names by a figure
42 # - Objects whose names start by "_" are not exported (hidden object)
43 # - All your armature's bones must be connected to another bone (except for the root
44 #   bone). Contrary to Blender, Cal3D doesn't support "floating" bones.
45 # - Only Linux has been tested
46
47 # BUGS / TODO :
48 # - Animation names ARE LOST when exporting (this is due to Blender Python API and cannot
49 #   be fixed until the API change). See parameters for how to rename your animations
50 # - Rotation, translation, or stretch (size changing) of Blender object is still quite
51 #   bugged, so AVOID MOVING / ROTATING / RESIZE OBJECTS (either mesh or armature) !
52 #   Instead, edit the object (with tab), select all points / bones (with "a"),
53 #   and move / rotate / resize them.
54 # - Material color is not supported yet
55 # - Cal3D springs (for clothes and hair) are not supported yet
56 # - Optimization tips : almost all the time is spent on scene.makeCurrent(), called for
57 #   updating the IPO curve's values. Updating a single IPO and not the whole scene
58 #   would speed up a lot.
59
60 # Questions and comments are welcome at jiba@tuxfamily.org
61
62
63 # Parameters :
64
65 # The directory where the data are saved.
66 # blender2cal3d.py will create all files in this directory,
67 # including a .cfg file.
68 # WARNING: As Cal3D stores model in directory and not in a single file,
69 # you MUST avoid putting other file in this directory !
70 # Please give an empty directory (or an unexistant one).
71 # Files may be deleted from this directoty !
72 SAVE_TO_DIR = "cal3d"
73
74 # Use this dictionary to rename animations, as their name is lost at the exportation.
75 RENAME_ANIMATIONS = {
76   # "OldName" : "NewName",
77   
78   }
79
80 # True (=1) to export for the Soya 3D engine (http://oomadness.tuxfamily.org/en/soya).
81 # (=> rotate meshes and skeletons so as X is right, Y is top and -Z is front)
82 EXPORT_FOR_SOYA = 0
83
84 # Enables LODs computation. LODs computation is quite slow, and the algo is surely
85 # not optimal :-(
86 LODS = 0
87
88 # See also BASE_MATRIX below, if you want to rotate/scale/translate the model at
89 # the exportation.
90
91
92 #########################################################################################
93 # Code starts here.
94 # The script should be quite re-useable for writing another Blender animation exporter.
95 # Most of the hell of it is to deal with Blender's head-tail-roll bone's definition.
96
97 import sys, os, os.path, struct, math, string
98 import Blender
99
100 # HACK -- it seems that some Blender versions don't define sys.argv,
101 # which may crash Python if a warning occurs.
102 if not hasattr(sys, "argv"): sys.argv = ["???"]
103
104
105 # Math stuff
106
107 def quaternion2matrix(q):
108   xx = q[0] * q[0]
109   yy = q[1] * q[1]
110   zz = q[2] * q[2]
111   xy = q[0] * q[1]
112   xz = q[0] * q[2]
113   yz = q[1] * q[2]
114   wx = q[3] * q[0]
115   wy = q[3] * q[1]
116   wz = q[3] * q[2]
117   return [[1.0 - 2.0 * (yy + zz),       2.0 * (xy + wz),       2.0 * (xz - wy), 0.0],
118           [      2.0 * (xy - wz), 1.0 - 2.0 * (xx + zz),       2.0 * (yz + wx), 0.0],
119           [      2.0 * (xz + wy),       2.0 * (yz - wx), 1.0 - 2.0 * (xx + yy), 0.0],
120           [0.0                  , 0.0                  , 0.0                  , 1.0]]
121
122 def matrix2quaternion(m):
123   s = math.sqrt(abs(m[0][0] + m[1][1] + m[2][2] + m[3][3]))
124   if s == 0.0:
125     x = abs(m[2][1] - m[1][2])
126     y = abs(m[0][2] - m[2][0])
127     z = abs(m[1][0] - m[0][1])
128     if   (x >= y) and (x >= z): return 1.0, 0.0, 0.0, 0.0
129     elif (y >= x) and (y >= z): return 0.0, 1.0, 0.0, 0.0
130     else:                       return 0.0, 0.0, 1.0, 0.0
131   return quaternion_normalize([
132     -(m[2][1] - m[1][2]) / (2.0 * s),
133     -(m[0][2] - m[2][0]) / (2.0 * s),
134     -(m[1][0] - m[0][1]) / (2.0 * s),
135     0.5 * s,
136     ])
137
138 def quaternion_normalize(q):
139   l = math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3])
140   return q[0] / l, q[1] / l, q[2] / l, q[3] / l
141
142 def quaternion_multiply(q1, q2):
143   r = [
144     q2[3] * q1[0] + q2[0] * q1[3] + q2[1] * q1[2] - q2[2] * q1[1],
145     q2[3] * q1[1] + q2[1] * q1[3] + q2[2] * q1[0] - q2[0] * q1[2],
146     q2[3] * q1[2] + q2[2] * q1[3] + q2[0] * q1[1] - q2[1] * q1[0],
147     q2[3] * q1[3] - q2[0] * q1[0] - q2[1] * q1[1] - q2[2] * q1[2],
148     ]
149   d = math.sqrt(r[0] * r[0] + r[1] * r[1] + r[2] * r[2] + r[3] * r[3])
150   r[0] /= d
151   r[1] /= d
152   r[2] /= d
153   r[3] /= d
154   return r
155
156 def matrix_translate(m, v):
157   m[3][0] += v[0]
158   m[3][1] += v[1]
159   m[3][2] += v[2]
160   return m
161
162 def matrix_multiply(b, a):
163   return [ [
164     a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0],
165     a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1],
166     a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2],
167     0.0,
168     ], [
169     a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0],
170     a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1],
171     a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2],
172     0.0,
173     ], [
174     a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0],
175     a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1],
176     a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2],
177      0.0,
178     ], [
179     a[3][0] * b[0][0] + a[3][1] * b[1][0] + a[3][2] * b[2][0] + b[3][0],
180     a[3][0] * b[0][1] + a[3][1] * b[1][1] + a[3][2] * b[2][1] + b[3][1],
181     a[3][0] * b[0][2] + a[3][1] * b[1][2] + a[3][2] * b[2][2] + b[3][2],
182     1.0,
183     ] ]
184
185 def matrix_invert(m):
186   det = (m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2])
187        - m[1][0] * (m[0][1] * m[2][2] - m[2][1] * m[0][2])
188        + m[2][0] * (m[0][1] * m[1][2] - m[1][1] * m[0][2]))
189   if det == 0.0: return None
190   det = 1.0 / det
191   r = [ [
192       det * (m[1][1] * m[2][2] - m[2][1] * m[1][2]),
193     - det * (m[0][1] * m[2][2] - m[2][1] * m[0][2]),
194       det * (m[0][1] * m[1][2] - m[1][1] * m[0][2]),
195       0.0,
196     ], [
197     - det * (m[1][0] * m[2][2] - m[2][0] * m[1][2]),
198       det * (m[0][0] * m[2][2] - m[2][0] * m[0][2]),
199     - det * (m[0][0] * m[1][2] - m[1][0] * m[0][2]),
200       0.0
201     ], [
202       det * (m[1][0] * m[2][1] - m[2][0] * m[1][1]),
203     - det * (m[0][0] * m[2][1] - m[2][0] * m[0][1]),
204       det * (m[0][0] * m[1][1] - m[1][0] * m[0][1]),
205       0.0,
206     ] ]
207   r.append([
208     -(m[3][0] * r[0][0] + m[3][1] * r[1][0] + m[3][2] * r[2][0]),
209     -(m[3][0] * r[0][1] + m[3][1] * r[1][1] + m[3][2] * r[2][1]),
210     -(m[3][0] * r[0][2] + m[3][1] * r[1][2] + m[3][2] * r[2][2]),
211     1.0,
212     ])
213   return r
214
215 def matrix_rotate_x(angle):
216   cos = math.cos(angle)
217   sin = math.sin(angle)
218   return [
219     [1.0,  0.0, 0.0, 0.0],
220     [0.0,  cos, sin, 0.0],
221     [0.0, -sin, cos, 0.0],
222     [0.0,  0.0, 0.0, 1.0],
223     ]
224
225 def matrix_rotate_y(angle):
226   cos = math.cos(angle)
227   sin = math.sin(angle)
228   return [
229     [cos, 0.0, -sin, 0.0],
230     [0.0, 1.0,  0.0, 0.0],
231     [sin, 0.0,  cos, 0.0],
232     [0.0, 0.0,  0.0, 1.0],
233     ]
234
235 def matrix_rotate_z(angle):
236   cos = math.cos(angle)
237   sin = math.sin(angle)
238   return [
239     [ cos, sin, 0.0, 0.0],
240     [-sin, cos, 0.0, 0.0],
241     [ 0.0, 0.0, 1.0, 0.0],
242     [ 0.0, 0.0, 0.0, 1.0],
243     ]
244
245 def matrix_rotate(axis, angle):
246   vx  = axis[0]
247   vy  = axis[1]
248   vz  = axis[2]
249   vx2 = vx * vx
250   vy2 = vy * vy
251   vz2 = vz * vz
252   cos = math.cos(angle)
253   sin = math.sin(angle)
254   co1 = 1.0 - cos
255   return [
256     [vx2 * co1 + cos,          vx * vy * co1 + vz * sin, vz * vx * co1 - vy * sin, 0.0],
257     [vx * vy * co1 - vz * sin, vy2 * co1 + cos,          vy * vz * co1 + vx * sin, 0.0],
258     [vz * vx * co1 + vy * sin, vy * vz * co1 - vx * sin, vz2 * co1 + cos,          0.0],
259     [0.0, 0.0, 0.0, 1.0],
260     ]
261
262 def matrix_scale(fx, fy, fz):
263   return [
264     [ fx, 0.0, 0.0, 0.0],
265     [0.0,  fy, 0.0, 0.0],
266     [0.0, 0.0,  fz, 0.0],
267     [0.0, 0.0, 0.0, 1.0],
268     ]
269   
270 def point_by_matrix(p, m):
271   return [p[0] * m[0][0] + p[1] * m[1][0] + p[2] * m[2][0] + m[3][0],
272           p[0] * m[0][1] + p[1] * m[1][1] + p[2] * m[2][1] + m[3][1],
273           p[0] * m[0][2] + p[1] * m[1][2] + p[2] * m[2][2] + m[3][2]]
274
275 def point_distance(p1, p2):
276   return math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2)
277
278 def vector_by_matrix(p, m):
279   return [p[0] * m[0][0] + p[1] * m[1][0] + p[2] * m[2][0],
280           p[0] * m[0][1] + p[1] * m[1][1] + p[2] * m[2][1],
281           p[0] * m[0][2] + p[1] * m[1][2] + p[2] * m[2][2]]
282
283 def vector_length(v):
284   return math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
285
286 def vector_normalize(v):
287   l = math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
288   return v[0] / l, v[1] / l, v[2] / l
289
290 def vector_dotproduct(v1, v2):
291   return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]
292
293 def vector_crossproduct(v1, v2):
294   return [
295     v1[1] * v2[2] - v1[2] * v2[1],
296     v1[2] * v2[0] - v1[0] * v2[2],
297     v1[0] * v2[1] - v1[1] * v2[0],
298     ]
299
300 def vector_angle(v1, v2):
301   s = vector_length(v1) * vector_length(v2)
302   f = vector_dotproduct(v1, v2) / s
303   if f >=  1.0: return 0.0
304   if f <= -1.0: return math.pi / 2.0
305   return math.atan(-f / math.sqrt(1.0 - f * f)) + math.pi / 2.0
306
307 def blender_bone2matrix(head, tail, roll):
308   # Convert bone rest state (defined by bone.head, bone.tail and bone.roll)
309   # to a matrix (the more standard notation).
310   # Taken from blenkernel/intern/armature.c in Blender source.
311   # See also DNA_armature_types.h:47.
312   
313   target = [0.0, 1.0, 0.0]
314   delta  = [tail[0] - head[0], tail[1] - head[1], tail[2] - head[2]]
315   nor    = vector_normalize(delta)
316   axis   = vector_crossproduct(target, nor)
317   
318   if vector_dotproduct(axis, axis) > 0.0000000000001:
319     axis    = vector_normalize(axis)
320     theta   = math.acos(vector_dotproduct(target, nor))
321     bMatrix = matrix_rotate(axis, theta)
322     
323   else:
324     if vector_crossproduct(target, nor) > 0.0: updown =  1.0
325     else:                                      updown = -1.0
326     
327     # Quoted from Blender source : "I think this should work ..."
328     bMatrix = [
329       [updown, 0.0, 0.0, 0.0],
330       [0.0, updown, 0.0, 0.0],
331       [0.0, 0.0, 1.0, 0.0],
332       [0.0, 0.0, 0.0, 1.0],
333       ]
334   
335   rMatrix = matrix_rotate(nor, roll)
336   return matrix_multiply(rMatrix, bMatrix)
337
338
339 # Hack for having the model rotated right.
340 # Put in BASE_MATRIX your own rotation if you need some.
341
342 if EXPORT_FOR_SOYA:
343   BASE_MATRIX = matrix_rotate_x(-math.pi / 2.0)
344   
345 else:
346   BASE_MATRIX = None
347
348
349 # Cal3D data structures
350
351 CAL3D_VERSION = 700
352
353 NEXT_MATERIAL_ID = 0
354 class Material:
355   def __init__(self, map_filename = None):
356     self.ambient_r  = 255
357     self.ambient_g  = 255
358     self.ambient_b  = 255
359     self.ambient_a  = 255
360     self.diffuse_r  = 255
361     self.diffuse_g  = 255
362     self.diffuse_b  = 255
363     self.diffuse_a  = 255
364     self.specular_r = 255
365     self.specular_g = 255
366     self.specular_b = 255
367     self.specular_a = 255
368     self.shininess = 1.0
369     if map_filename: self.maps_filenames = [map_filename]
370     else:            self.maps_filenames = []
371     
372     MATERIALS[map_filename] = self
373     
374     global NEXT_MATERIAL_ID
375     self.id = NEXT_MATERIAL_ID
376     NEXT_MATERIAL_ID += 1
377     
378   def to_cal3d(self):
379     s = "CRF\0" + struct.pack("iBBBBBBBBBBBBfi", CAL3D_VERSION, self.ambient_r, self.ambient_g, self.ambient_b, self.ambient_a, self.diffuse_r, self.diffuse_g, self.diffuse_b, self.diffuse_a, self.specular_r, self.specular_g, self.specular_b, self.specular_a, self.shininess, len(self.maps_filenames))
380     for map_filename in self.maps_filenames:
381       s += struct.pack("i", len(map_filename) + 1)
382       s += map_filename + "\0"
383     return s
384   
385 MATERIALS = {}
386
387 class Mesh:
388   def __init__(self, name):
389     self.name      = name
390     self.submeshes = []
391     
392     self.next_submesh_id = 0
393     
394   def to_cal3d(self):
395     s = "CMF\0" + struct.pack("ii", CAL3D_VERSION, len(self.submeshes))
396     s += "".join(map(SubMesh.to_cal3d, self.submeshes))
397     return s
398   
399 class SubMesh:
400   def __init__(self, mesh, material):
401     self.material   = material
402     self.vertices   = []
403     self.faces      = []
404     self.nb_lodsteps = 0
405     self.springs    = []
406     
407     self.next_vertex_id = 0
408     
409     self.mesh = mesh
410     self.id = mesh.next_submesh_id
411     mesh.next_submesh_id += 1
412     mesh.submeshes.append(self)
413     
414   def compute_lods(self):
415     """Computes LODs info for Cal3D (there's no Blender related stuff here)."""
416     
417     print "Start LODs computation..."
418     vertex2faces = {}
419     for face in self.faces:
420       for vertex in (face.vertex1, face.vertex2, face.vertex3):
421         l = vertex2faces.get(vertex)
422         if not l: vertex2faces[vertex] = [face]
423         else: l.append(face)
424         
425     couple_treated         = {}
426     couple_collapse_factor = []
427     for face in self.faces:
428       for a, b in ((face.vertex1, face.vertex2), (face.vertex1, face.vertex3), (face.vertex2, face.vertex3)):
429         a = a.cloned_from or a
430         b = b.cloned_from or b
431         if a.id > b.id: a, b = b, a
432         if not couple_treated.has_key((a, b)):
433           # The collapse factor is simply the distance between the 2 points :-(
434           # This should be improved !!
435           if vector_dotproduct(a.normal, b.normal) < 0.9: continue
436           couple_collapse_factor.append((point_distance(a.loc, b.loc), a, b))
437           couple_treated[a, b] = 1
438       
439     couple_collapse_factor.sort()
440     
441     collapsed    = {}
442     new_vertices = []
443     new_faces    = []
444     for factor, v1, v2 in couple_collapse_factor:
445       # Determines if v1 collapses to v2 or v2 to v1.
446       # We choose to keep the vertex which is on the smaller number of faces, since
447       # this one has more chance of being in an extrimity of the body.
448       # Though heuristic, this rule yields very good results in practice.
449       if   len(vertex2faces[v1]) <  len(vertex2faces[v2]): v2, v1 = v1, v2
450       elif len(vertex2faces[v1]) == len(vertex2faces[v2]):
451         if collapsed.get(v1, 0): v2, v1 = v1, v2 # v1 already collapsed, try v2
452         
453       if (not collapsed.get(v1, 0)) and (not collapsed.get(v2, 0)):
454         collapsed[v1] = 1
455         collapsed[v2] = 1
456         
457         # Check if v2 is already colapsed
458         while v2.collapse_to: v2 = v2.collapse_to
459         
460         common_faces = filter(vertex2faces[v1].__contains__, vertex2faces[v2])
461         
462         v1.collapse_to         = v2
463         v1.face_collapse_count = len(common_faces)
464         
465         for clone in v1.clones:
466           # Find the clone of v2 that correspond to this clone of v1
467           possibles = []
468           for face in vertex2faces[clone]:
469             possibles.append(face.vertex1)
470             possibles.append(face.vertex2)
471             possibles.append(face.vertex3)
472           clone.collapse_to = v2
473           for vertex in v2.clones:
474             if vertex in possibles:
475               clone.collapse_to = vertex
476               break
477             
478           clone.face_collapse_count = 0
479           new_vertices.append(clone)
480
481         # HACK -- all faces get collapsed with v1 (and no faces are collapsed with v1's
482         # clones). This is why we add v1 in new_vertices after v1's clones.
483         # This hack has no other incidence that consuming a little few memory for the
484         # extra faces if some v1's clone are collapsed but v1 is not.
485         new_vertices.append(v1)
486         
487         self.nb_lodsteps += 1 + len(v1.clones)
488         
489         new_faces.extend(common_faces)
490         for face in common_faces:
491           face.can_collapse = 1
492           
493           # Updates vertex2faces
494           vertex2faces[face.vertex1].remove(face)
495           vertex2faces[face.vertex2].remove(face)
496           vertex2faces[face.vertex3].remove(face)
497         vertex2faces[v2].extend(vertex2faces[v1])
498         
499     new_vertices.extend(filter(lambda vertex: not vertex.collapse_to, self.vertices))
500     new_vertices.reverse() # Cal3D want LODed vertices at the end
501     for i in range(len(new_vertices)): new_vertices[i].id = i
502     self.vertices = new_vertices
503     
504     new_faces.extend(filter(lambda face: not face.can_collapse, self.faces))
505     new_faces.reverse() # Cal3D want LODed faces at the end
506     self.faces = new_faces
507     
508     print "LODs computed : %s vertices can be removed (from a total of %s)." % (self.nb_lodsteps, len(self.vertices))
509     
510   def rename_vertices(self, new_vertices):
511     """Rename (change ID) of all vertices, such as self.vertices == new_vertices."""
512     for i in range(len(new_vertices)): new_vertices[i].id = i
513     self.vertices = new_vertices
514     
515   def to_cal3d(self):
516     s =  struct.pack("iiiiii", self.material.id, len(self.vertices), len(self.faces), self.nb_lodsteps, len(self.springs), len(self.material.maps_filenames))
517     s += "".join(map(Vertex.to_cal3d, self.vertices))
518     s += "".join(map(Spring.to_cal3d, self.springs))
519     s += "".join(map(Face  .to_cal3d, self.faces))
520     return s
521
522 class Vertex:
523   def __init__(self, submesh, loc, normal):
524     self.loc    = loc
525     self.normal = normal
526     self.collapse_to         = None
527     self.face_collapse_count = 0
528     self.maps       = []
529     self.influences = []
530     self.weight = None
531     
532     self.cloned_from = None
533     self.clones      = []
534     
535     self.submesh = submesh
536     self.id = submesh.next_vertex_id
537     submesh.next_vertex_id += 1
538     submesh.vertices.append(self)
539     
540   def to_cal3d(self):
541     if self.collapse_to: collapse_id = self.collapse_to.id
542     else:                collapse_id = -1
543     s =  struct.pack("ffffffii", self.loc[0], self.loc[1], self.loc[2], self.normal[0], self.normal[1], self.normal[2], collapse_id, self.face_collapse_count)
544     s += "".join(map(Map.to_cal3d, self.maps))
545     s += struct.pack("i", len(self.influences))
546     s += "".join(map(Influence.to_cal3d, self.influences))
547     if not self.weight is None: s += struct.pack("f", len(self.weight))
548     return s
549   
550 class Map:
551   def __init__(self, u, v):
552     self.u = u
553     self.v = v
554     
555   def to_cal3d(self):
556     return struct.pack("ff", self.u, self.v)
557     
558 class Influence:
559   def __init__(self, bone, weight):
560     self.bone   = bone
561     self.weight = weight
562     
563   def to_cal3d(self):
564     return struct.pack("if", self.bone.id, self.weight)
565     
566 class Spring:
567   def __init__(self, vertex1, vertex2):
568     self.vertex1 = vertex1
569     self.vertex2 = vertex2
570     self.spring_coefficient = 0.0
571     self.idlelength = 0.0
572     
573   def to_cal3d(self):
574     return struct.pack("iiff", self.vertex1.id, self.vertex2.id, self.spring_coefficient, self.idlelength)
575
576 class Face:
577   def __init__(self, submesh, vertex1, vertex2, vertex3):
578     self.vertex1 = vertex1
579     self.vertex2 = vertex2
580     self.vertex3 = vertex3
581     
582     self.can_collapse = 0
583     
584     self.submesh = submesh
585     submesh.faces.append(self)
586     
587   def to_cal3d(self):
588     return struct.pack("iii", self.vertex1.id, self.vertex2.id, self.vertex3.id)
589     
590 class Skeleton:
591   def __init__(self):
592     self.bones = []
593     
594     self.next_bone_id = 0
595     
596   def to_cal3d(self):
597     s = "CSF\0" + struct.pack("ii", CAL3D_VERSION, len(self.bones))
598     s += "".join(map(Bone.to_cal3d, self.bones))
599     return s
600
601 BONES = {}
602
603 class Bone:
604   def __init__(self, skeleton, parent, name, loc, rot):
605     self.parent = parent
606     self.name   = name
607     self.loc = loc
608     self.rot = rot
609     self.children = []
610     
611     self.matrix = matrix_translate(quaternion2matrix(rot), loc)
612     if parent:
613       self.matrix = matrix_multiply(parent.matrix, self.matrix)
614       parent.children.append(self)
615     
616     # lloc and lrot are the bone => model space transformation (translation and rotation).
617     # They are probably specific to Cal3D.
618     m = matrix_invert(self.matrix)
619     self.lloc = m[3][0], m[3][1], m[3][2]
620     self.lrot = matrix2quaternion(m)
621     
622     self.skeleton = skeleton
623     self.id = skeleton.next_bone_id
624     skeleton.next_bone_id += 1
625     skeleton.bones.append(self)
626     
627     BONES[name] = self
628     
629   def to_cal3d(self):
630     s =  struct.pack("i", len(self.name) + 1) + self.name + "\0"
631     
632     # We need to negate quaternion W value, but why ?
633     s += struct.pack("ffffffffffffff", self.loc[0], self.loc[1], self.loc[2], self.rot[0], self.rot[1], self.rot[2], -self.rot[3], self.lloc[0], self.lloc[1], self.lloc[2], self.lrot[0], self.lrot[1], self.lrot[2], -self.lrot[3])
634     if self.parent: s += struct.pack("i", self.parent.id)
635     else:           s += struct.pack("i", -1)
636     s += struct.pack("i", len(self.children))
637     s += "".join(map(lambda bone: struct.pack("i", bone.id), self.children))
638     return s
639   
640 class Animation:
641   def __init__(self, name, duration = 0.0):
642     self.name     = name
643     self.duration = duration
644     self.tracks   = {} # Map bone names to tracks
645     
646   def to_cal3d(self):
647     s = "CAF\0" + struct.pack("ifi", CAL3D_VERSION, self.duration, len(self.tracks))
648     s += "".join(map(Track.to_cal3d, self.tracks.values()))
649     return s
650     
651 class Track:
652   def __init__(self, animation, bone):
653     self.bone      = bone
654     self.keyframes = []
655     
656     self.animation = animation
657     animation.tracks[bone.name] = self
658     
659   def to_cal3d(self):
660     s = struct.pack("ii", self.bone.id, len(self.keyframes))
661     s += "".join(map(KeyFrame.to_cal3d, self.keyframes))
662     return s
663     
664 class KeyFrame:
665   def __init__(self, track, time, loc, rot):
666     self.time = time
667     self.loc  = loc
668     self.rot  = rot
669     
670     self.track = track
671     track.keyframes.append(self)
672     
673   def to_cal3d(self):
674     # We need to negate quaternion W value, but why ?
675     return struct.pack("ffffffff", self.time, self.loc[0], self.loc[1], self.loc[2], self.rot[0], self.rot[1], self.rot[2], -self.rot[3])
676
677
678 def export():
679   # Get the scene
680   
681   scene = Blender.Scene.getCurrent()
682   
683   
684   # Export skeleton (=armature)
685   
686   skeleton = Skeleton()
687   
688   for obj in Blender.Object.Get():
689     data = obj.getData()
690     if type(data) is Blender.Types.ArmatureType:
691       matrix = obj.getMatrix()
692       if BASE_MATRIX: matrix = matrix_multiply(BASE_MATRIX, matrix)
693       
694       def treat_bone(b, parent = None):
695         head = b.getHead()
696         tail = b.getTail()
697         
698         # Turns the Blender's head-tail-roll notation into a quaternion
699         quat = matrix2quaternion(blender_bone2matrix(head, tail, b.getRoll()))
700         
701         if parent:
702           # Compute the translation from the parent bone's head to the child
703           # bone's head, in the parent bone coordinate system.
704           # The translation is parent_tail - parent_head + child_head,
705           # but parent_tail and parent_head must be converted from the parent's parent
706           # system coordinate into the parent system coordinate.
707           
708           parent_invert_transform = matrix_invert(quaternion2matrix(parent.rot))
709           parent_head = vector_by_matrix(parent.head, parent_invert_transform)
710           parent_tail = vector_by_matrix(parent.tail, parent_invert_transform)
711           
712           bone = Bone(skeleton, parent, b.getName(), [parent_tail[0] - parent_head[0] + head[0], parent_tail[1] - parent_head[1] + head[1], parent_tail[2] - parent_head[2] + head[2]], quat)
713         else:
714           # Apply the armature's matrix to the root bones
715           head = point_by_matrix(head, matrix)
716           tail = point_by_matrix(tail, matrix)
717           quat = matrix2quaternion(matrix_multiply(matrix, quaternion2matrix(quat))) # Probably not optimal
718           
719           # Here, the translation is simply the head vector
720           bone = Bone(skeleton, parent, b.getName(), head, quat)
721           
722         bone.head = head
723         bone.tail = tail
724         
725         for child in b.getChildren(): treat_bone(child, bone)
726         
727       for b in data.getBones(): treat_bone(b)
728       
729       # Only one armature / skeleton
730       break
731     
732     
733   # Export Mesh data
734   
735   meshes = []
736   
737   for obj in Blender.Object.Get():
738     data = obj.getData()
739     if (type(data) is Blender.Types.NMeshType) and data.faces:
740       mesh = Mesh(obj.name)
741       meshes.append(mesh)
742       
743       matrix = obj.getMatrix()
744       if BASE_MATRIX: matrix = matrix_multiply(BASE_MATRIX, matrix)
745       
746       faces = data.faces
747       while faces:
748         image          = faces[0].image
749         image_filename = image and image.filename
750         material       = MATERIALS.get(image_filename) or Material(image_filename)
751         
752         # TODO add material color support here
753         
754         submesh  = SubMesh(mesh, material)
755         vertices = {}
756         for face in faces[:]:
757           if (face.image and face.image.filename) == image_filename:
758             faces.remove(face)
759             
760             if not face.smooth:
761               p1 = face.v[0].co
762               p2 = face.v[1].co
763               p3 = face.v[2].co
764               normal = vector_normalize(vector_by_matrix(vector_crossproduct(
765                 [p3[0] - p2[0], p3[1] - p2[1], p3[2] - p2[2]],
766                 [p1[0] - p2[0], p1[1] - p2[1], p1[2] - p2[2]],
767                 ), matrix))
768               
769             face_vertices = []
770             for i in range(len(face.v)):
771               vertex = vertices.get(face.v[i].index)
772               if not vertex:
773                 coord  = point_by_matrix (face.v[i].co, matrix)
774                 if face.smooth: normal = vector_normalize(vector_by_matrix(face.v[i].no, matrix))
775                 vertex  = vertices[face.v[i].index] = Vertex(submesh, coord, normal)
776                 
777                 influences = data.getVertexInfluences(face.v[i].index)
778                 if not influences: print "Warning !  A vertex has no influence !"
779                 
780                 # sum of influences is not always 1.0 in Blender ?!?!
781                 sum = 0.0
782                 for bone_name, weight in influences: sum += weight
783                 
784                 for bone_name, weight in influences:
785                   vertex.influences.append(Influence(BONES[bone_name], weight / sum))
786                   
787               elif not face.smooth:
788                 # We cannot share vertex for non-smooth faces, since Cal3D does not
789                 # support vertex sharing for 2 vertices with different normals.
790                 # => we must clone the vertex.
791                 
792                 old_vertex = vertex
793                 vertex = Vertex(submesh, vertex.loc, normal)
794                 vertex.cloned_from = old_vertex
795                 vertex.influences = old_vertex.influences
796                 old_vertex.clones.append(vertex)
797                 
798               if data.hasFaceUV():
799                 uv = [face.uv[i][0], 1.0 - face.uv[i][1]]
800                 if not vertex.maps: vertex.maps.append(Map(*uv))
801                 elif (vertex.maps[0].u != uv[0]) or (vertex.maps[0].v != uv[1]):
802                   # This vertex can be shared for Blender, but not for Cal3D !!!
803                   # Cal3D does not support vertex sharing for 2 vertices with
804                   # different UV texture coodinates.
805                   # => we must clone the vertex.
806                   
807                   for clone in vertex.clones:
808                     if (clone.maps[0].u == uv[0]) and (clone.maps[0].v == uv[1]):
809                       vertex = clone
810                       break
811                   else: # Not yet cloned...
812                     old_vertex = vertex
813                     vertex = Vertex(submesh, vertex.loc, vertex.normal)
814                     vertex.cloned_from = old_vertex
815                     vertex.influences = old_vertex.influences
816                     vertex.maps.append(Map(*uv))
817                     old_vertex.clones.append(vertex)
818                     
819               face_vertices.append(vertex)
820               
821             # Split faces with more than 3 vertices
822             for i in range(1, len(face.v) - 1):
823               Face(submesh, face_vertices[0], face_vertices[i], face_vertices[i + 1])
824               
825         # Computes LODs info
826         if LODS: submesh.compute_lods()
827         
828   # Export animations
829               
830   ANIMATIONS = {}
831   
832   for ipo in Blender.Ipo.Get():
833     name = ipo.getName()
834     
835     # Try to extract the animation name and the bone name from the IPO name.
836     # THIS MAY NOT WORK !!!
837     # The animation name extracted here is usually NOT the name of the action in Blender
838     
839     splitted = name.split(".")
840     if len(splitted) == 2:
841       animation_name, bone_name = splitted
842       animation_name += ".000"
843     elif len(splitted) == 3:
844       animation_name, a, b = splitted
845       if   a[0] in string.digits:
846         animation_name += "." + a
847         bone_name = b
848       elif b[0] in string.digits:
849         animation_name += "." + b
850         bone_name = a
851       else:
852         print "Un-analysable IPO name :", name
853         continue
854     else:
855       print "Un-analysable IPO name :", name
856       continue
857     
858     animation = ANIMATIONS.get(animation_name)
859     if not animation:
860       animation = ANIMATIONS[animation_name] = Animation(animation_name)
861
862     bone  = BONES[bone_name]
863     track = animation.tracks.get(bone_name)
864     if not track:
865       track = animation.tracks[bone_name] = Track(animation, bone)
866       track.finished = 0
867
868     nb_curve = ipo.getNcurves()
869     has_loc = nb_curve in (3, 7)
870     has_rot = nb_curve in (4, 7)
871     
872     # TODO support size here
873     # Cal3D does not support it yet.
874     
875     try: nb_bez_pts = ipo.getNBezPoints(0)
876     except TypeError:
877       print "No key frame for animation %s, bone %s, skipping..." % (animation_name, bone_name)
878       nb_bez_pts = 0
879       
880     for bez in range(nb_bez_pts): # WARNING ! May not work if not loc !!!
881       time = ipo.getCurveBeztriple(0, bez)[3]
882       scene.currentFrame(int(time))
883
884       # Needed to update IPO's value, but probably not the best way for that...
885       scene.makeCurrent()
886
887       # Convert time units from Blender's frame (starting at 1) to second
888       # (using default FPS of 25)
889       time = (time - 1.0) / 25.0
890
891       if animation.duration < time: animation.duration = time
892       
893       loc = bone.loc
894       rot = bone.rot
895       
896       curves = ipo.getCurves()
897       print curves
898       curve_id = 0
899       while curve_id < len(curves):
900         curve_name = curves[curve_id].getName()
901         if curve_name == "LocX":
902           # Get the translation
903           # We need to blend the translation from the bone rest state (=bone.loc) with
904           # the translation due to IPO.
905           trans = vector_by_matrix((
906             ipo.getCurveCurval(curve_id),
907             ipo.getCurveCurval(curve_id + 1),
908             ipo.getCurveCurval(curve_id + 2),
909             ), bone.matrix)
910           loc = [
911             bone.loc[0] + trans[0],
912             bone.loc[1] + trans[1],
913             bone.loc[2] + trans[2],
914             ]
915           curve_id += 3
916           
917         elif curve_name == "RotX":
918           # Get the rotation of the IPO
919           ipo_rot = [
920             ipo.getCurveCurval(curve_id),
921             ipo.getCurveCurval(curve_id + 1),
922             ipo.getCurveCurval(curve_id + 2),
923             ipo.getCurveCurval(curve_id + 3),
924             ]
925           curve_id += 3 # XXX Strange !!!
926           # We need to blend the rotation from the bone rest state (=bone.rot) with
927           # ipo_rot.
928           rot = quaternion_multiply(ipo_rot, bone.rot)
929           
930         else:
931           print "Unknown IPO curve : %s" % curve_name
932           break #Unknown curves
933       
934       KeyFrame(track, time, loc, rot)
935         
936       
937   # Save all data
938   
939   if not os.path.exists(SAVE_TO_DIR): os.makedirs(SAVE_TO_DIR)
940   else:
941     for file in os.listdir(SAVE_TO_DIR):
942       if file.endswith(".cfg") or file.endswith(".caf") or file.endswith(".cmf") or file.endswith(".csf") or file.endswith(".crf"):
943         os.unlink(os.path.join(SAVE_TO_DIR, file))
944         
945   cfg = open(os.path.join(SAVE_TO_DIR, os.path.basename(SAVE_TO_DIR) + ".cfg"), "wb")
946   print >> cfg, "# Cal3D model exported from Blender with blender2cal3d.py"
947   print >> cfg
948   
949   open(os.path.join(SAVE_TO_DIR, os.path.basename(SAVE_TO_DIR) + ".csf"), "wb").write(skeleton.to_cal3d())
950   print >> cfg, "skeleton=%s.csf" % os.path.basename(SAVE_TO_DIR)
951   print >> cfg
952   
953   for animation in ANIMATIONS.values():
954     if animation.duration: # Cal3D does not support animation with only one state
955       animation.name = RENAME_ANIMATIONS.get(animation.name) or animation.name
956       open(os.path.join(SAVE_TO_DIR, animation.name + ".caf"), "wb").write(animation.to_cal3d())
957       print >> cfg, "animation=%s.caf" % animation.name
958       
959       # Prints animation names and durations, in order to help identifying animation
960       # (since their name are lost).
961       print animation.name, "duration", animation.duration * 25.0 + 1.0
962       
963   print >> cfg
964
965   for mesh in meshes:
966     if not mesh.name.startswith("_"):
967       open(os.path.join(SAVE_TO_DIR, mesh.name + ".cmf"), "wb").write(mesh.to_cal3d())
968       print >> cfg, "mesh=%s.cmf" % mesh.name
969   print >> cfg
970   
971   materials = MATERIALS.values()
972   materials.sort(lambda a, b: cmp(a.id, b.id))
973   for material in materials:
974     if material.maps_filenames: filename = os.path.splitext(os.path.basename(material.maps_filenames[0]))[0]
975     else:                       filename = "plain"
976     open(os.path.join(SAVE_TO_DIR, filename + ".crf"), "wb").write(material.to_cal3d())
977     print >> cfg, "material=%s.crf" % filename
978   print >> cfg
979   
980   print "Saved to", SAVE_TO_DIR
981   print "Done."
982
983 export()