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