Scripts:
[blender.git] / release / scripts / blender2cal3d.py
1 #!BPY
2
3 """
4 Name: 'Cal3D v0.9'
5 Blender: 235
6 Group: 'Export'
7 Tip: 'Export armature/bone/mesh/action data to the Cal3D format.'
8 """
9
10 # blender2cal3D.py
11 # Copyright (C) 2003-2004 Jean-Baptiste LAMY -- jibalamy@free.fr
12 # Copyright (C) 2004 Matthias Braun -- matze@braunis.de
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 __version__ = "0.11"
30 __author__  = "Jean-Baptiste 'Jiba' Lamy"
31 __email__   = ["Author's email, jibalamy:free*fr"]
32 __url__     = ["Soya3d's homepage, http://home.gna.org/oomadness/en/soya/",
33         "Cal3d, http://cal3d.sourceforge.net"]
34 __bpydoc__  = """\
35 This script is a Blender => Cal3D converter.
36 (See http://blender.org and http://cal3d.sourceforge.net)
37
38 USAGE:
39
40 To install it, place the script in your $HOME/.blender/scripts directory.
41
42 Then open the File->Export->Cal3d v0.9 menu. And select the filename of the .cfg file.
43 The exporter will create a set of other files with same prefix (ie. bla.cfg, bla.xsf,
44 bla_Action1.xaf, bla_Action2.xaf, ...).
45
46 You should be able to open the .cfg file in cal3d_miniviewer.
47
48
49 NOT (YET) SUPPORTED:
50
51   - Rotation, translation, or stretching Blender objects is still quite
52 buggy, 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.<br>
55   - no support for exporting springs yet<br>
56   - no support for exporting material colors (most games should only use images
57 I think...)
58
59
60 KNOWN ISSUES:
61
62   - Cal3D versions <=0.9.1 have a bug where animations aren't played when the root bone
63 is not animated;<br>
64   - Cal3D versions <=0.9.1 have a bug where objects that aren't influenced by any bones
65 are not drawn (fixed in Cal3D CVS).
66
67
68 NOTES:
69
70 It requires a very recent version of Blender (>= 2.35).
71
72 Build a model following a few rules:<br>
73   - Use only a single armature;<br>
74   - Use only a single rootbone (Cal3D doesn't support floating bones);<br>
75   - Use only locrot keys (Cal3D doesn't support bone's size change);<br>
76   - Don't try to create child/parent constructs in blender object, that gets exported
77 incorrectly at the moment;<br>
78   - Don't put "." in action or bone names, and do not start these names by a figure;<br>
79   - Objects or animations whose names start by "_" are not exported (hidden object).
80
81 It can be run in batch mode, as following :<br>
82     blender model.blend -P blender2cal3d.py --blender2cal3d FILENAME=model.cfg EXPORT_FOR_SOYA=1
83
84 You can pass as many parameters as you want at the end, "EXPORT_FOR_SOYA=1" is just an
85 example. The parameters are the same as below.
86 """
87
88 # Parameters :
89
90 # Filename to export to (if "", display a file selector dialog).
91 FILENAME = ""
92
93 # True (=1) to export for the Soya 3D engine
94 #     (http://oomadness.tuxfamily.org/en/soya).
95 # (=> rotate meshes and skeletons so as X is right, Y is top and -Z is front)
96 EXPORT_FOR_SOYA = 0
97
98 # Enables LODs computation. LODs computation is quite slow, and the algo is
99 # surely not optimal :-(
100 LODS = 0
101
102 # Scale the model (not supported by Soya).
103 SCALE = 1.0
104
105 # Set to 1 if you want to prefix all filename with the model name
106 # (e.g. knight_walk.xaf instead of walk.xaf)
107 PREFIX_FILE_WITH_MODEL_NAME = 0
108
109 # Set to 0 to use Cal3D binary format
110 XML = 1
111
112
113 MESSAGES = ""
114
115 # See also BASE_MATRIX below, if you want to rotate/scale/translate the model at
116 # the exportation.
117
118 #########################################################################################
119 # Code starts here.
120 # The script should be quite re-useable for writing another Blender animation exporter.
121 # Most of the hell of it is to deal with Blender's head-tail-roll bone's definition.
122
123 import sys, os, os.path, struct, math, string
124 import Blender
125
126 # HACK -- it seems that some Blender versions don't define sys.argv,
127 # which may crash Python if a warning occurs.
128 if not hasattr(sys, "argv"): sys.argv = ["???"]
129
130
131 # transforms a blender to a cal3d quaternion notation (x,y,z,w)
132 def blender2cal3dquat(q):
133   return [q.x, q.y, q.z, q.w]
134
135 def quaternion2matrix(q):
136   xx = q[0] * q[0]
137   yy = q[1] * q[1]
138   zz = q[2] * q[2]
139   xy = q[0] * q[1]
140   xz = q[0] * q[2]
141   yz = q[1] * q[2]
142   wx = q[3] * q[0]
143   wy = q[3] * q[1]
144   wz = q[3] * q[2]
145   return [[1.0 - 2.0 * (yy + zz),       2.0 * (xy + wz),       2.0 * (xz - wy), 0.0],
146           [      2.0 * (xy - wz), 1.0 - 2.0 * (xx + zz),       2.0 * (yz + wx), 0.0],
147           [      2.0 * (xz + wy),       2.0 * (yz - wx), 1.0 - 2.0 * (xx + yy), 0.0],
148           [0.0                  , 0.0                  , 0.0                  , 1.0]]
149
150 def matrix2quaternion(m):
151   s = math.sqrt(abs(m[0][0] + m[1][1] + m[2][2] + m[3][3]))
152   if s == 0.0:
153     x = abs(m[2][1] - m[1][2])
154     y = abs(m[0][2] - m[2][0])
155     z = abs(m[1][0] - m[0][1])
156     if   (x >= y) and (x >= z): return 1.0, 0.0, 0.0, 0.0
157     elif (y >= x) and (y >= z): return 0.0, 1.0, 0.0, 0.0
158     else:                       return 0.0, 0.0, 1.0, 0.0
159   return quaternion_normalize([
160     -(m[2][1] - m[1][2]) / (2.0 * s),
161     -(m[0][2] - m[2][0]) / (2.0 * s),
162     -(m[1][0] - m[0][1]) / (2.0 * s),
163     0.5 * s,
164     ])
165
166 def quaternion_normalize(q):
167   l = math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3])
168   return q[0] / l, q[1] / l, q[2] / l, q[3] / l
169
170 # multiplies 2 quaternions in x,y,z,w notation
171 def quaternion_multiply(q1, q2):
172   return [
173     q2[3] * q1[0] + q2[0] * q1[3] + q2[1] * q1[2] - q2[2] * q1[1],
174     q2[3] * q1[1] + q2[1] * q1[3] + q2[2] * q1[0] - q2[0] * q1[2],
175     q2[3] * q1[2] + q2[2] * q1[3] + q2[0] * q1[1] - q2[1] * q1[0],
176     q2[3] * q1[3] - q2[0] * q1[0] - q2[1] * q1[1] - q2[2] * q1[2],
177     ]
178
179 def matrix_translate(m, v):
180   m[3][0] += v[0]
181   m[3][1] += v[1]
182   m[3][2] += v[2]
183   return m
184
185 def matrix_multiply(b, a):
186   return [ [
187     a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0],
188     a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1],
189     a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2],
190     0.0,
191     ], [
192     a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0],
193     a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1],
194     a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2],
195     0.0,
196     ], [
197     a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0],
198     a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1],
199     a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2],
200      0.0,
201     ], [
202     a[3][0] * b[0][0] + a[3][1] * b[1][0] + a[3][2] * b[2][0] + b[3][0],
203     a[3][0] * b[0][1] + a[3][1] * b[1][1] + a[3][2] * b[2][1] + b[3][1],
204     a[3][0] * b[0][2] + a[3][1] * b[1][2] + a[3][2] * b[2][2] + b[3][2],
205     1.0,
206     ] ]
207
208 def matrix_invert(m):
209   det = (m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2])
210        - m[1][0] * (m[0][1] * m[2][2] - m[2][1] * m[0][2])
211        + m[2][0] * (m[0][1] * m[1][2] - m[1][1] * m[0][2]))
212   if det == 0.0: return None
213   det = 1.0 / det
214   r = [ [
215       det * (m[1][1] * m[2][2] - m[2][1] * m[1][2]),
216     - det * (m[0][1] * m[2][2] - m[2][1] * m[0][2]),
217       det * (m[0][1] * m[1][2] - m[1][1] * m[0][2]),
218       0.0,
219     ], [
220     - det * (m[1][0] * m[2][2] - m[2][0] * m[1][2]),
221       det * (m[0][0] * m[2][2] - m[2][0] * m[0][2]),
222     - det * (m[0][0] * m[1][2] - m[1][0] * m[0][2]),
223       0.0
224     ], [
225       det * (m[1][0] * m[2][1] - m[2][0] * m[1][1]),
226     - det * (m[0][0] * m[2][1] - m[2][0] * m[0][1]),
227       det * (m[0][0] * m[1][1] - m[1][0] * m[0][1]),
228       0.0,
229     ] ]
230   r.append([
231     -(m[3][0] * r[0][0] + m[3][1] * r[1][0] + m[3][2] * r[2][0]),
232     -(m[3][0] * r[0][1] + m[3][1] * r[1][1] + m[3][2] * r[2][1]),
233     -(m[3][0] * r[0][2] + m[3][1] * r[1][2] + m[3][2] * r[2][2]),
234     1.0,
235     ])
236   return r
237
238 def matrix_rotate_x(angle):
239   cos = math.cos(angle)
240   sin = math.sin(angle)
241   return [
242     [1.0,  0.0, 0.0, 0.0],
243     [0.0,  cos, sin, 0.0],
244     [0.0, -sin, cos, 0.0],
245     [0.0,  0.0, 0.0, 1.0],
246     ]
247
248 def matrix_rotate_y(angle):
249   cos = math.cos(angle)
250   sin = math.sin(angle)
251   return [
252     [cos, 0.0, -sin, 0.0],
253     [0.0, 1.0,  0.0, 0.0],
254     [sin, 0.0,  cos, 0.0],
255     [0.0, 0.0,  0.0, 1.0],
256     ]
257
258 def matrix_rotate_z(angle):
259   cos = math.cos(angle)
260   sin = math.sin(angle)
261   return [
262     [ cos, sin, 0.0, 0.0],
263     [-sin, cos, 0.0, 0.0],
264     [ 0.0, 0.0, 1.0, 0.0],
265     [ 0.0, 0.0, 0.0, 1.0],
266     ]
267
268 def matrix_rotate(axis, angle):
269   vx  = axis[0]
270   vy  = axis[1]
271   vz  = axis[2]
272   vx2 = vx * vx
273   vy2 = vy * vy
274   vz2 = vz * vz
275   cos = math.cos(angle)
276   sin = math.sin(angle)
277   co1 = 1.0 - cos
278   return [
279     [vx2 * co1 + cos,          vx * vy * co1 + vz * sin, vz * vx * co1 - vy * sin, 0.0],
280     [vx * vy * co1 - vz * sin, vy2 * co1 + cos,          vy * vz * co1 + vx * sin, 0.0],
281     [vz * vx * co1 + vy * sin, vy * vz * co1 - vx * sin, vz2 * co1 + cos,          0.0],
282     [0.0, 0.0, 0.0, 1.0],
283     ]
284
285 def matrix_scale(fx, fy, fz):
286   return [
287     [ fx, 0.0, 0.0, 0.0],
288     [0.0,  fy, 0.0, 0.0],
289     [0.0, 0.0,  fz, 0.0],
290     [0.0, 0.0, 0.0, 1.0],
291     ]
292   
293 def point_by_matrix(p, m):
294   return [p[0] * m[0][0] + p[1] * m[1][0] + p[2] * m[2][0] + m[3][0],
295           p[0] * m[0][1] + p[1] * m[1][1] + p[2] * m[2][1] + m[3][1],
296           p[0] * m[0][2] + p[1] * m[1][2] + p[2] * m[2][2] + m[3][2]]
297
298 def point_distance(p1, p2):
299   return math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2)
300
301 def vector_add(v1, v2):
302   return [v1[0]+v2[0], v1[1]+v2[1], v1[2]+v2[2]]
303
304 def vector_sub(v1, v2):
305   return [v1[0]-v2[0], v1[1]-v2[1], v1[2]-v2[2]]
306     
307 def vector_by_matrix(p, m):
308   return [p[0] * m[0][0] + p[1] * m[1][0] + p[2] * m[2][0],
309           p[0] * m[0][1] + p[1] * m[1][1] + p[2] * m[2][1],
310           p[0] * m[0][2] + p[1] * m[1][2] + p[2] * m[2][2]]
311
312 def vector_length(v):
313   return math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
314
315 def vector_normalize(v):
316   l = math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
317   return v[0] / l, v[1] / l, v[2] / l
318
319 def vector_dotproduct(v1, v2):
320   return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]
321
322 def vector_crossproduct(v1, v2):
323   return [
324     v1[1] * v2[2] - v1[2] * v2[1],
325     v1[2] * v2[0] - v1[0] * v2[2],
326     v1[0] * v2[1] - v1[1] * v2[0],
327     ]
328
329 def vector_angle(v1, v2):
330   s = vector_length(v1) * vector_length(v2)
331   f = vector_dotproduct(v1, v2) / s
332   if f >=  1.0: return 0.0
333   if f <= -1.0: return math.pi / 2.0
334   return math.atan(-f / math.sqrt(1.0 - f * f)) + math.pi / 2.0
335
336 def blender_bone2matrix(head, tail, roll):
337   # Convert bone rest state (defined by bone.head, bone.tail and bone.roll)
338   # to a matrix (the more standard notation).
339   # Taken from blenkernel/intern/armature.c in Blender source.
340   # See also DNA_armature_types.h:47.
341   
342   target = [0.0, 1.0, 0.0]
343   delta  = [tail[0] - head[0], tail[1] - head[1], tail[2] - head[2]]
344   nor    = vector_normalize(delta)
345   axis   = vector_crossproduct(target, nor)
346   
347   if vector_dotproduct(axis, axis) > 0.0000000000001:
348     axis    = vector_normalize(axis)
349     theta   = math.acos(vector_dotproduct(target, nor))
350     bMatrix = matrix_rotate(axis, theta)
351     
352   else:
353     if vector_dotproduct(target, nor) > 0.0: updown =  1.0
354     else:                                    updown = -1.0
355     
356     # Quoted from Blender source : "I think this should work ..."
357     bMatrix = [
358       [updown, 0.0,    0.0, 0.0],
359       [0.0,    updown, 0.0, 0.0],
360       [0.0,    0.0,    1.0, 0.0],
361       [0.0,    0.0,    0.0, 1.0],
362       ]
363   
364   rMatrix = matrix_rotate(nor, roll)
365   return matrix_multiply(rMatrix, bMatrix)
366
367
368 # Hack for having the model rotated right.
369 # Put in BASE_MATRIX your own rotation if you need some.
370
371 BASE_MATRIX = None
372
373
374 # Cal3D data structures
375
376 CAL3D_VERSION = 910
377
378 NEXT_MATERIAL_ID = 0
379 class Material:
380   def __init__(self, map_filename = None):
381     self.ambient_r  = 255
382     self.ambient_g  = 255
383     self.ambient_b  = 255
384     self.ambient_a  = 255
385     self.diffuse_r  = 255
386     self.diffuse_g  = 255
387     self.diffuse_b  = 255
388     self.diffuse_a  = 255
389     self.specular_r = 255
390     self.specular_g = 255
391     self.specular_b = 255
392     self.specular_a = 255
393     self.shininess = 1.0
394     if map_filename: self.maps_filenames = [map_filename]
395     else:            self.maps_filenames = []
396     
397     MATERIALS[map_filename] = self
398     
399     global NEXT_MATERIAL_ID
400     self.id = NEXT_MATERIAL_ID
401     NEXT_MATERIAL_ID += 1
402     
403   # old cal3d format
404   def to_cal3d(self):
405     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))
406     for map_filename in self.maps_filenames:
407       s += struct.pack("i", len(map_filename) + 1)
408       s += map_filename + "\0"
409     return s
410  
411   # new xml format
412   def to_cal3d_xml(self):
413     s = "<?xml version=\"1.0\"?>\n"
414     s += "<HEADER MAGIC=\"XRF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
415     s += "<MATERIAL NUMMAPS=\"" + str(len(self.maps_filenames)) + "\">\n"
416     s += "  <AMBIENT>" + str(self.ambient_r) + " " + str(self.ambient_g) + " " + str(self.ambient_b) + " " + str(self.ambient_a) + "</AMBIENT>\n";
417     s += "  <DIFFUSE>" + str(self.diffuse_r) + " " + str(self.diffuse_g) + " " + str(self.diffuse_b) + " " + str(self.diffuse_a) + "</DIFFUSE>\n";
418     s += "  <SPECULAR>" + str(self.specular_r) + " " + str(self.specular_g) + " " + str(self.specular_b) + " " + str(self.specular_a) + "</SPECULAR>\n";
419     s += "  <SHININESS>" + str(self.shininess) + "</SHININESS>\n";
420     for map_filename in self.maps_filenames:
421       s += "  <MAP>" + map_filename + "</MAP>\n";
422       
423     s += "</MATERIAL>\n";
424         
425     return s
426   
427 MATERIALS = {}
428
429 class Mesh:
430   def __init__(self, name):
431     self.name      = name
432     self.submeshes = []
433     
434     self.next_submesh_id = 0
435     
436   def to_cal3d(self):
437     s = "CMF\0" + struct.pack("ii", CAL3D_VERSION, len(self.submeshes))
438     s += "".join(map(SubMesh.to_cal3d, self.submeshes))
439     return s
440
441   def to_cal3d_xml(self):
442     s = "<?xml version=\"1.0\"?>\n"
443     s += "<HEADER MAGIC=\"XMF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
444     s += "<MESH NUMSUBMESH=\"%i\">\n" % len(self.submeshes)
445     s += "".join(map(SubMesh.to_cal3d_xml, self.submeshes))
446     s += "</MESH>\n"                                                  
447     return s
448
449 class SubMesh:
450   def __init__(self, mesh, material):
451     self.material   = material
452     self.vertices   = []
453     self.faces      = []
454     self.nb_lodsteps = 0
455     self.springs    = []
456     
457     self.next_vertex_id = 0
458     
459     self.mesh = mesh
460     self.id = mesh.next_submesh_id
461     mesh.next_submesh_id += 1
462     mesh.submeshes.append(self)
463     
464   def compute_lods(self):
465     """Computes LODs info for Cal3D (there's no Blender related stuff here)."""
466     
467     print "Start LODs computation..."
468     vertex2faces = {}
469     for face in self.faces:
470       for vertex in (face.vertex1, face.vertex2, face.vertex3):
471         l = vertex2faces.get(vertex)
472         if not l: vertex2faces[vertex] = [face]
473         else: l.append(face)
474         
475     couple_treated         = {}
476     couple_collapse_factor = []
477     for face in self.faces:
478       for a, b in ((face.vertex1, face.vertex2), (face.vertex1, face.vertex3), (face.vertex2, face.vertex3)):
479         a = a.cloned_from or a
480         b = b.cloned_from or b
481         if a.id > b.id: a, b = b, a
482         if not couple_treated.has_key((a, b)):
483           # The collapse factor is simply the distance between the 2 points :-(
484           # This should be improved !!
485           if vector_dotproduct(a.normal, b.normal) < 0.9: continue
486           couple_collapse_factor.append((point_distance(a.loc, b.loc), a, b))
487           couple_treated[a, b] = 1
488       
489     couple_collapse_factor.sort()
490     
491     collapsed    = {}
492     new_vertices = []
493     new_faces    = []
494     for factor, v1, v2 in couple_collapse_factor:
495       # Determines if v1 collapses to v2 or v2 to v1.
496       # We choose to keep the vertex which is on the smaller number of faces, since
497       # this one has more chance of being in an extrimity of the body.
498       # Though heuristic, this rule yields very good results in practice.
499       if   len(vertex2faces[v1]) <  len(vertex2faces[v2]): v2, v1 = v1, v2
500       elif len(vertex2faces[v1]) == len(vertex2faces[v2]):
501         if collapsed.get(v1, 0): v2, v1 = v1, v2 # v1 already collapsed, try v2
502         
503       if (not collapsed.get(v1, 0)) and (not collapsed.get(v2, 0)):
504         collapsed[v1] = 1
505         collapsed[v2] = 1
506         
507         # Check if v2 is already colapsed
508         while v2.collapse_to: v2 = v2.collapse_to
509         
510         common_faces = filter(vertex2faces[v1].__contains__, vertex2faces[v2])
511         
512         v1.collapse_to         = v2
513         v1.face_collapse_count = len(common_faces)
514         
515         for clone in v1.clones:
516           # Find the clone of v2 that correspond to this clone of v1
517           possibles = []
518           for face in vertex2faces[clone]:
519             possibles.append(face.vertex1)
520             possibles.append(face.vertex2)
521             possibles.append(face.vertex3)
522           clone.collapse_to = v2
523           for vertex in v2.clones:
524             if vertex in possibles:
525               clone.collapse_to = vertex
526               break
527             
528           clone.face_collapse_count = 0
529           new_vertices.append(clone)
530
531         # HACK -- all faces get collapsed with v1 (and no faces are collapsed with v1's
532         # clones). This is why we add v1 in new_vertices after v1's clones.
533         # This hack has no other incidence that consuming a little few memory for the
534         # extra faces if some v1's clone are collapsed but v1 is not.
535         new_vertices.append(v1)
536         
537         self.nb_lodsteps += 1 + len(v1.clones)
538         
539         new_faces.extend(common_faces)
540         for face in common_faces:
541           face.can_collapse = 1
542           
543           # Updates vertex2faces
544           vertex2faces[face.vertex1].remove(face)
545           vertex2faces[face.vertex2].remove(face)
546           vertex2faces[face.vertex3].remove(face)
547         vertex2faces[v2].extend(vertex2faces[v1])
548         
549     new_vertices.extend(filter(lambda vertex: not vertex.collapse_to, self.vertices))
550     new_vertices.reverse() # Cal3D want LODed vertices at the end
551     for i in range(len(new_vertices)): new_vertices[i].id = i
552     self.vertices = new_vertices
553     
554     new_faces.extend(filter(lambda face: not face.can_collapse, self.faces))
555     new_faces.reverse() # Cal3D want LODed faces at the end
556     self.faces = new_faces
557     
558     print "LODs computed : %s vertices can be removed (from a total of %s)." % (self.nb_lodsteps, len(self.vertices))
559     
560   def rename_vertices(self, new_vertices):
561     """Rename (change ID) of all vertices, such as self.vertices == new_vertices."""
562     for i in range(len(new_vertices)): new_vertices[i].id = i
563     self.vertices = new_vertices
564     
565   def to_cal3d(self):
566     s =  struct.pack("iiiiii", self.material.id, len(self.vertices), len(self.faces), self.nb_lodsteps, len(self.springs), len(self.material.maps_filenames))
567     s += "".join(map(Vertex.to_cal3d, self.vertices))
568     s += "".join(map(Spring.to_cal3d, self.springs))
569     s += "".join(map(Face  .to_cal3d, self.faces))
570     return s
571
572   def to_cal3d_xml(self):
573     s = "  <SUBMESH NUMVERTICES=\"%i\" NUMFACES=\"%i\" MATERIAL=\"%i\" " % \
574         (len(self.vertices), len(self.faces), self.material.id)
575     s += "NUMLODSTEPS=\"%i\" NUMSPRINGS=\"%i\" NUMTEXCOORDS=\"%i\">\n" % \
576          (self.nb_lodsteps, len(self.springs),
577          len(self.material.maps_filenames))
578     s += "".join(map(Vertex.to_cal3d_xml, self.vertices))
579     s += "".join(map(Spring.to_cal3d_xml, self.springs))
580     s += "".join(map(Face.to_cal3d_xml, self.faces))
581     s += "  </SUBMESH>\n"
582     return s
583
584 class Vertex:
585   def __init__(self, submesh, loc, normal):
586     self.loc    = loc
587     self.normal = normal
588     self.collapse_to         = None
589     self.face_collapse_count = 0
590     self.maps       = []
591     self.influences = []
592     self.weight = None
593     
594     self.cloned_from = None
595     self.clones      = []
596     
597     self.submesh = submesh
598     self.id = submesh.next_vertex_id
599     submesh.next_vertex_id += 1
600     submesh.vertices.append(self)
601     
602   def to_cal3d(self):
603     if self.collapse_to: collapse_id = self.collapse_to.id
604     else:                collapse_id = -1
605     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)
606     s += "".join(map(Map.to_cal3d, self.maps))
607     s += struct.pack("i", len(self.influences))
608     s += "".join(map(Influence.to_cal3d, self.influences))
609     if not self.weight is None: s += struct.pack("f", len(self.weight))
610     return s
611
612   def to_cal3d_xml(self):
613     if self.collapse_to:
614       collapse_id = self.collapse_to.id
615     else:
616       collapse_id = -1
617     s = "    <VERTEX ID=\"%i\" NUMINFLUENCES=\"%i\">\n" % \
618         (self.id, len(self.influences))
619     s += "      <POS>%f %f %f</POS>\n" % (self.loc[0], self.loc[1], self.loc[2])
620     s += "      <NORM>%f %f %f</NORM>\n" % \
621          (self.normal[0], self.normal[1], self.normal[2])
622     if collapse_id != -1:
623       s += "      <COLLAPSEID>%i</COLLAPSEID>\n" % collapse_id
624       s += "      <COLLAPSECOUNT>%i</COLLAPSECOUNT>\n" % \
625            self.face_collapse_count
626     s += "".join(map(Map.to_cal3d_xml, self.maps))
627     s += "".join(map(Influence.to_cal3d_xml, self.influences))
628     if not self.weight is None:
629       s += "      <PHYSIQUE>%f</PHYSIQUE>\n" % len(self.weight)
630     s += "    </VERTEX>\n"
631     return s
632  
633 class Map:
634   def __init__(self, u, v):
635     self.u = u
636     self.v = v
637     
638   def to_cal3d(self):
639     return struct.pack("ff", self.u, self.v)
640
641   def to_cal3d_xml(self):
642     return "      <TEXCOORD>%f %f</TEXCOORD>\n" % (self.u, self.v)    
643
644 class Influence:
645   def __init__(self, bone, weight):
646     self.bone   = bone
647     self.weight = weight
648     
649   def to_cal3d(self):
650     return struct.pack("if", self.bone.id, self.weight)
651
652   def to_cal3d_xml(self):
653     return "      <INFLUENCE ID=\"%i\">%f</INFLUENCE>\n" % \
654            (self.bone.id, self.weight)
655  
656 class Spring:
657   def __init__(self, vertex1, vertex2):
658     self.vertex1 = vertex1
659     self.vertex2 = vertex2
660     self.spring_coefficient = 0.0
661     self.idlelength = 0.0
662     
663   def to_cal3d(self):
664     return struct.pack("iiff", self.vertex1.id, self.vertex2.id, self.spring_coefficient, self.idlelength)
665
666   def to_cal3d_xml(self):
667     return "    <SPRING VERTEXID=\"%i %i\" COEF=\"%f\" LENGTH=\"%f\"/>\n" % \
668            (self.vertex1.id, self.vertex2.id, self.spring_coefficient,
669            self.idlelength)
670
671 class Face:
672   def __init__(self, submesh, vertex1, vertex2, vertex3):
673     self.vertex1 = vertex1
674     self.vertex2 = vertex2
675     self.vertex3 = vertex3
676     
677     self.can_collapse = 0
678     
679     self.submesh = submesh
680     submesh.faces.append(self)
681     
682   def to_cal3d(self):
683     return struct.pack("iii", self.vertex1.id, self.vertex2.id, self.vertex3.id)
684
685   def to_cal3d_xml(self):
686     return "    <FACE VERTEXID=\"%i %i %i\"/>\n" % \
687            (self.vertex1.id, self.vertex2.id, self.vertex3.id)
688  
689 class Skeleton:
690   def __init__(self):
691     self.bones = []
692     
693     self.next_bone_id = 0
694     
695   def to_cal3d(self):
696     s = "CSF\0" + struct.pack("ii", CAL3D_VERSION, len(self.bones))
697     s += "".join(map(Bone.to_cal3d, self.bones))
698     return s
699
700   def to_cal3d_xml(self):
701     s = "<?xml version=\"1.0\"?>\n"
702     s += "<HEADER MAGIC=\"XSF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
703     s += "<SKELETON NUMBONES=\"%i\">\n" % len(self.bones)
704     s += "".join(map(Bone.to_cal3d_xml, self.bones))
705     s += "</SKELETON>\n"
706     return s
707
708 BONES = {}
709
710 class Bone:
711   def __init__(self, skeleton, parent, name, loc, rot):
712     self.parent = parent
713     self.name   = name
714     self.loc = loc
715     self.rot = rot
716     self.children = []
717     
718     self.matrix = matrix_translate(quaternion2matrix(rot), loc)
719     if parent:
720       self.matrix = matrix_multiply(parent.matrix, self.matrix)
721       parent.children.append(self)
722     
723     # lloc and lrot are the bone => model space transformation (translation and rotation).
724     # They are probably specific to Cal3D.
725     m = matrix_invert(self.matrix)
726     self.lloc = m[3][0], m[3][1], m[3][2]
727     self.lrot = matrix2quaternion(m)
728     
729     self.skeleton = skeleton
730     self.id = skeleton.next_bone_id
731     skeleton.next_bone_id += 1
732     skeleton.bones.append(self)
733     
734     BONES[name] = self
735     
736   def to_cal3d(self):
737     s =  struct.pack("i", len(self.name) + 1) + self.name + "\0"
738     
739     # We need to negate quaternion W value, but why ?
740     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])
741     if self.parent: s += struct.pack("i", self.parent.id)
742     else:           s += struct.pack("i", -1)
743     s += struct.pack("i", len(self.children))
744     s += "".join(map(lambda bone: struct.pack("i", bone.id), self.children))
745     return s
746
747   def to_cal3d_xml(self):
748     s = "  <BONE ID=\"%i\" NAME=\"%s\" NUMCHILD=\"%i\">\n" % \
749         (self.id, self.name, len(self.children))
750     # We need to negate quaternion W value, but why ?
751     s += "    <TRANSLATION>%f %f %f</TRANSLATION>\n" % \
752          (self.loc[0], self.loc[1], self.loc[2])
753     s += "    <ROTATION>%f %f %f %f</ROTATION>\n" % \
754          (self.rot[0], self.rot[1], self.rot[2], -self.rot[3])
755     s += "    <LOCALTRANSLATION>%f %f %f</LOCALTRANSLATION>\n" % \
756          (self.lloc[0], self.lloc[1], self.lloc[2])
757     s += "    <LOCALROTATION>%f %f %f %f</LOCALROTATION>\n" % \
758          (self.lrot[0], self.lrot[1], self.lrot[2], -self.lrot[3])
759     if self.parent:
760       s += "    <PARENTID>%i</PARENTID>\n" % self.parent.id
761     else:
762       s += "    <PARENTID>%i</PARENTID>\n" % -1
763     s += "".join(map(lambda bone: "    <CHILDID>%i</CHILDID>\n" % bone.id,
764          self.children))
765     s += "  </BONE>\n"
766     return s
767
768 class Animation:
769   def __init__(self, name, duration = 0.0):
770     self.name     = name
771     self.duration = duration
772     self.tracks   = {} # Map bone names to tracks
773     
774   def to_cal3d(self):
775     s = "CAF\0" + struct.pack("ifi", CAL3D_VERSION, self.duration, len(self.tracks))
776     s += "".join(map(Track.to_cal3d, self.tracks.values()))
777     return s
778
779   def to_cal3d_xml(self):
780     s = "<?xml version=\"1.0\"?>\n"
781     s += "<HEADER MAGIC=\"XAF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
782     s += "<ANIMATION DURATION=\"%f\" NUMTRACKS=\"%i\">\n" % \
783          (self.duration, len(self.tracks))                            
784     s += "".join(map(Track.to_cal3d_xml, self.tracks.values()))
785     s += "</ANIMATION>\n"
786     return s                                                          
787  
788 class Track:
789   def __init__(self, animation, bone):
790     self.bone      = bone
791     self.keyframes = []
792     
793     self.animation = animation
794     animation.tracks[bone.name] = self
795     
796   def to_cal3d(self):
797     s = struct.pack("ii", self.bone.id, len(self.keyframes))
798     s += "".join(map(KeyFrame.to_cal3d, self.keyframes))
799     return s
800
801   def to_cal3d_xml(self):
802     s = "  <TRACK BONEID=\"%i\" NUMKEYFRAMES=\"%i\">\n" % \
803         (self.bone.id, len(self.keyframes))
804     s += "".join(map(KeyFrame.to_cal3d_xml, self.keyframes))
805     s += "  </TRACK>\n"
806     return s
807     
808 class KeyFrame:
809   def __init__(self, track, time, loc, rot):
810     self.time = time
811     self.loc  = loc
812     self.rot  = rot
813     
814     self.track = track
815     track.keyframes.append(self)
816     
817   def to_cal3d(self):
818     # We need to negate quaternion W value, but why ?
819     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])
820
821   def to_cal3d_xml(self):
822     s = "    <KEYFRAME TIME=\"%f\">\n" % self.time
823     s += "      <TRANSLATION>%f %f %f</TRANSLATION>\n" % \
824          (self.loc[0], self.loc[1], self.loc[2])
825     # We need to negate quaternion W value, but why ?
826     s += "      <ROTATION>%f %f %f %f</ROTATION>\n" % \
827          (self.rot[0], self.rot[1], self.rot[2], -self.rot[3])
828     s += "    </KEYFRAME>\n"
829     return s                                                      
830
831 def export(filename):
832   global MESSAGES
833   
834   if EXPORT_FOR_SOYA:
835     global BASE_MATRIX
836     BASE_MATRIX = matrix_rotate_x(-math.pi / 2.0)
837     
838   # Get the scene
839   scene = Blender.Scene.getCurrent()
840   
841   # ---- Export skeleton (=armature) ----------------------------------------
842
843   skeleton = Skeleton()
844   
845   foundarmature = False
846   for obj in Blender.Object.Get():
847     data = obj.getData()
848     if type(data) is not Blender.Types.ArmatureType:
849       continue
850     
851     if foundarmature == True:
852       MESSAGES += "Found multiple armatures! '" + obj.getName() + "' ignored.\n"
853       continue
854
855     foundarmature = True
856     matrix = obj.getMatrix()
857     if BASE_MATRIX:
858       matrix = matrix_multiply(BASE_MATRIX, matrix)
859     
860     def treat_bone(b, parent = None):
861       head = b.getHead()
862       tail = b.getTail()
863       
864       # Turns the Blender's head-tail-roll notation into a quaternion
865       quat = matrix2quaternion(blender_bone2matrix(head, tail, b.getRoll()))
866       
867       if parent:
868         # Compute the translation from the parent bone's head to the child
869         # bone's head, in the parent bone coordinate system.
870         # The translation is parent_tail - parent_head + child_head,
871         # but parent_tail and parent_head must be converted from the parent's parent
872         # system coordinate into the parent system coordinate.
873         
874         parent_invert_transform = matrix_invert(quaternion2matrix(parent.rot))
875         parent_head = vector_by_matrix(parent.head, parent_invert_transform)
876         parent_tail = vector_by_matrix(parent.tail, parent_invert_transform)
877
878         ploc = vector_add(head, b.getLoc())
879         parentheadtotail = vector_sub(parent_tail, parent_head)
880         # hmm this should be handled by the IPos, but isn't for non-animated
881         # bones which are transformed in the pose mode...
882         #loc = vector_add(ploc, parentheadtotail)
883         #rot = quaternion_multiply(blender2cal3dquat(b.getQuat()), quat)
884         loc = parentheadtotail
885         rot = quat
886         
887         bone = Bone(skeleton, parent, b.getName(), loc, rot)
888       else:
889         # Apply the armature's matrix to the root bones
890         head = point_by_matrix(head, matrix)
891         tail = point_by_matrix(tail, matrix)
892         quat = matrix2quaternion(matrix_multiply(matrix, quaternion2matrix(quat))) # Probably not optimal
893         
894         # loc = vector_add(head, b.getLoc())
895         # rot = quaternion_multiply(blender2cal3dquat(b.getQuat()), quat)
896         loc = head
897         rot = quat
898         
899         # Here, the translation is simply the head vector
900         bone = Bone(skeleton, None, b.getName(), loc, rot)
901         
902       bone.head = head
903       bone.tail = tail
904       
905       for child in b.getChildren():
906         treat_bone(child, bone)
907      
908     foundroot = False
909     for b in data.getBones():
910       # child bones are handled in treat_bone
911       if b.getParent() != None:
912         continue
913       if foundroot == True:
914         print "Warning: Found multiple root-bones, this may not be supported in cal3d."
915         #print "Ignoring bone '" + b.getName() + "' and it's childs."
916         #continue
917         
918       treat_bone(b)
919       foundroot = True
920
921   # ---- Export Mesh data ---------------------------------------------------
922   
923   meshes = []
924   
925   for obj in Blender.Object.Get():
926     data = obj.getData()
927     if (type(data) is Blender.Types.NMeshType) and data.faces:
928       mesh_name = obj.getName()
929       mesh = Mesh(mesh_name)
930       meshes.append(mesh)
931       
932       matrix = obj.getMatrix()
933       if BASE_MATRIX:
934         matrix = matrix_multiply(BASE_MATRIX, matrix)
935         
936       faces = data.faces
937       while faces:
938         image          = faces[0].image
939         image_filename = image and image.filename
940         material       = MATERIALS.get(image_filename) or Material(image_filename)
941         outputuv       = len(material.maps_filenames) > 0
942         
943         # TODO add material color support here
944         
945         submesh  = SubMesh(mesh, material)
946         vertices = {}
947         for face in faces[:]:
948           if (face.image and face.image.filename) == image_filename:
949             faces.remove(face)
950             
951             if not face.smooth:
952               p1 = face.v[0].co
953               p2 = face.v[1].co
954               p3 = face.v[2].co
955               normal = vector_normalize(vector_by_matrix(vector_crossproduct(
956                 [p3[0] - p2[0], p3[1] - p2[1], p3[2] - p2[2]],
957                 [p1[0] - p2[0], p1[1] - p2[1], p1[2] - p2[2]],
958                 ), matrix))
959               
960             face_vertices = []
961             for i in range(len(face.v)):
962               vertex = vertices.get(face.v[i].index)
963               if not vertex:
964                 coord  = point_by_matrix (face.v[i].co, matrix)
965                 if face.smooth:
966                   normal = vector_normalize(vector_by_matrix(face.v[i].no, matrix))
967                 vertex  = vertices[face.v[i].index] = Vertex(submesh, coord, normal)
968
969                 influences = data.getVertexInfluences(face.v[i].index)
970                 # should this really be a warning? (well currently enabled,
971                 # because blender has some bugs where it doesn't return
972                 # influences in python api though they are set, and because
973                 # cal3d<=0.9.1 had bugs where objects without influences
974                 # aren't drawn.
975                 if not influences:
976                   MESSAGES += "A vertex of object '%s' has no influences.\n(This occurs on objects placed in an invisible layer, you can fix it by using a single layer)\n" \
977                               % obj.getName()
978                 
979                 # sum of influences is not always 1.0 in Blender ?!?!
980                 sum = 0.0
981                 for bone_name, weight in influences:
982                   sum += weight
983                 
984                 for bone_name, weight in influences:
985                   if bone_name not in BONES:
986                     MESSAGES += "Couldn't find bone '%s' which influences" \
987                                 "object '%s'.\n" % (bone_name, obj.getName())
988                     continue
989                   vertex.influences.append(Influence(BONES[bone_name], weight / sum))
990                   
991               elif not face.smooth:
992                 # We cannot share vertex for non-smooth faces, since Cal3D does not
993                 # support vertex sharing for 2 vertices with different normals.
994                 # => we must clone the vertex.
995                 
996                 old_vertex = vertex
997                 vertex = Vertex(submesh, vertex.loc, normal)
998                 vertex.cloned_from = old_vertex
999                 vertex.influences = old_vertex.influences
1000                 old_vertex.clones.append(vertex)
1001                 
1002               if data.hasFaceUV():
1003                 uv = [face.uv[i][0], 1.0 - face.uv[i][1]]
1004                 if not vertex.maps:
1005                   if outputuv: vertex.maps.append(Map(*uv))
1006                 elif (vertex.maps[0].u != uv[0]) or (vertex.maps[0].v != uv[1]):
1007                   # This vertex can be shared for Blender, but not for Cal3D !!!
1008                   # Cal3D does not support vertex sharing for 2 vertices with
1009                   # different UV texture coodinates.
1010                   # => we must clone the vertex.
1011                   
1012                   for clone in vertex.clones:
1013                     if (clone.maps[0].u == uv[0]) and (clone.maps[0].v == uv[1]):
1014                       vertex = clone
1015                       break
1016                   else: # Not yet cloned...
1017                     old_vertex = vertex
1018                     vertex = Vertex(submesh, vertex.loc, vertex.normal)
1019                     vertex.cloned_from = old_vertex
1020                     vertex.influences = old_vertex.influences
1021                     if outputuv: vertex.maps.append(Map(*uv))
1022                     old_vertex.clones.append(vertex)
1023                     
1024               face_vertices.append(vertex)
1025               
1026             # Split faces with more than 3 vertices
1027             for i in range(1, len(face.v) - 1):
1028               Face(submesh, face_vertices[0], face_vertices[i], face_vertices[i + 1])
1029               
1030         # Computes LODs info
1031         if LODS:
1032           submesh.compute_lods()
1033         
1034   # ---- Export animations --------------------------------------------------
1035   ANIMATIONS = {}
1036
1037   for a in Blender.Armature.NLA.GetActions().iteritems():
1038     animation_name = a[0]
1039     animation = Animation(animation_name)
1040     animation.duration = 0.0
1041
1042     for b in a[1].getAllChannelIpos().iteritems():
1043       bone_name = b[0]
1044       if bone_name not in BONES:
1045         MESSAGES += "No Bone '" + bone_name + "' defined (from Animation '" \
1046             + animation_name + "' ?!?\n"
1047         continue                                            
1048
1049       bone = BONES[bone_name]
1050
1051       track = Track(animation, bone)
1052       track.finished = 0
1053       animation.tracks[bone_name] = track
1054
1055       ipo = b[1]
1056       
1057       times = []
1058       
1059       # SideNote: MatzeB: Ipo.getCurve(curvename) is broken in blender 2.33 and
1060       # below if the Ipo comes from an Action, so only use Ipo.getCurves()!
1061       # also blender upto 2.33a had a bug where IpoCurve.evaluate was not
1062       # exposed to the python interface :-/
1063       
1064       #run 1: we need to find all time values where we need to produce keyframes
1065       for curve in ipo.getCurves():
1066         curve_name = curve.getName()
1067
1068         if curve_name not in ["QuatW", "QuatX", "QuatY", "QuatZ", "LocX", "LocY", "LocZ"]:
1069           MESSAGES += "Curve type %s not supported in Action '%s' Bone '%s'.\n"\
1070                     % (curve_name, animation_name, bone_name)
1071         
1072         for p in curve.getPoints():
1073           time = p.getPoints() [0]
1074           if time not in times:
1075             times.append(time)
1076       
1077       times.sort()
1078
1079       # run2: now create keyframes
1080       for time in times:
1081         cal3dtime = (time-1) / 25.0 # assume 25FPS by default
1082         if cal3dtime > animation.duration:
1083           animation.duration = cal3dtime
1084         trans = [0, 0, 0]
1085         quat  = [0, 0, 0, 0]
1086         
1087         for curve in ipo.getCurves():
1088           val = curve.evaluate(time)
1089           if curve.getName() == "LocX": trans[0] = val
1090           if curve.getName() == "LocY": trans[1] = val
1091           if curve.getName() == "LocZ": trans[2] = val
1092           if curve.getName() == "QuatW": quat[3] = val
1093           if curve.getName() == "QuatX": quat[0] = val
1094           if curve.getName() == "QuatY": quat[1] = val
1095           if curve.getName() == "QuatZ": quat[2] = val
1096           
1097         transt = vector_by_matrix(trans, bone.matrix)
1098         loc = vector_add(bone.loc, transt)
1099         rot = quaternion_multiply(quat, bone.rot)
1100         rot = quaternion_normalize(rot)
1101         
1102         KeyFrame(track, cal3dtime, loc, rot)
1103         
1104     if animation.duration <= 0:
1105       MESSAGES += "Ignoring Animation '" + animation_name + \
1106                   "': duration is 0.\n"
1107       continue
1108     ANIMATIONS[animation_name] = animation
1109     
1110   # Save all data
1111   if filename.endswith(".cfg"):
1112     filename = os.path.splitext(filename)[0]
1113   BASENAME = os.path.basename(filename)         
1114   DIRNAME  = os.path.dirname(filename)
1115   if PREFIX_FILE_WITH_MODEL_NAME: PREFIX = BASENAME + "_"
1116   else:                           PREFIX = ""
1117   if XML: FORMAT_PREFIX = "x"; encode = lambda x: x.to_cal3d_xml()
1118   else:   FORMAT_PREFIX = "c"; encode = lambda x: x.to_cal3d()
1119   print DIRNAME + " - " + BASENAME
1120   
1121   cfg = open(os.path.join(DIRNAME, BASENAME + ".cfg"), "wb")
1122   print >> cfg, "# Cal3D model exported from Blender with blender2cal3d.py"
1123   print >> cfg
1124
1125   if SCALE != 1.0:
1126     print >> cfg, "scale=%s" % SCALE
1127     print >> cfg
1128     
1129   filename = BASENAME + "." + FORMAT_PREFIX + "sf"
1130   open(os.path.join(DIRNAME, filename), "wb").write(encode(skeleton))
1131   print >> cfg, "skeleton=%s" % filename
1132   print >> cfg
1133   
1134   for animation in ANIMATIONS.values():
1135     if not animation.name.startswith("_"):
1136       if animation.duration: # Cal3D does not support animation with only one state
1137         filename = PREFIX + animation.name + "." + FORMAT_PREFIX + "af"
1138         open(os.path.join(DIRNAME, filename), "wb").write(encode(animation))
1139         print >> cfg, "animation=%s" % filename
1140         
1141   print >> cfg
1142   
1143   for mesh in meshes:
1144     if not mesh.name.startswith("_"):
1145       filename = PREFIX + mesh.name + "." + FORMAT_PREFIX + "mf"
1146       open(os.path.join(DIRNAME, filename), "wb").write(encode(mesh))
1147       print >> cfg, "mesh=%s" % filename
1148   print >> cfg
1149   
1150   materials = MATERIALS.values()
1151   materials.sort(lambda a, b: cmp(a.id, b.id))
1152   for material in materials:
1153     if material.maps_filenames:
1154       filename = PREFIX + os.path.splitext(os.path.basename(material.maps_filenames[0]))[0] + "." + FORMAT_PREFIX + "rf"
1155     else:
1156       filename = PREFIX + "plain." + FORMAT_PREFIX + "rf"
1157     open(os.path.join(DIRNAME, filename), "wb").write(encode(material))
1158     print >> cfg, "material=%s" % filename
1159   print >> cfg
1160   
1161   MESSAGES += "Saved to '%s.cfg'\n" % BASENAME
1162   MESSAGES += "Done."
1163   
1164   # show messages
1165   print MESSAGES
1166
1167 # some (ugly) gui to show the error messages - no scrollbar or other luxury,
1168 # please improve this if you know how
1169 def gui():
1170   global MESSAGES
1171   button = Blender.Draw.Button("Ok", 1, 0, 0, 50, 20, "Close Window")
1172     
1173   lines = MESSAGES.split("\n")
1174   if len(lines) > 15:
1175     lines.append("Please also take a look at your console")
1176   pos = len(lines) * 15 + 20
1177   for line in lines:
1178     Blender.BGL.glRasterPos2i(0, pos)
1179     Blender.Draw.Text(line)
1180     pos -= 15
1181
1182 def event(evt, val):
1183   if evt == Blender.Draw.ESCKEY:
1184     Blender.Draw.Exit()
1185     return
1186
1187 def button_event(evt):
1188   if evt == 1:
1189     Blender.Draw.Exit()
1190     return
1191
1192 # Main script
1193 def fs_callback(filename):
1194   export(filename)
1195   Blender.Draw.Register(gui, event, button_event)
1196
1197
1198 # Check for batch mode
1199 if "--blender2cal3d" in sys.argv:
1200   args = sys.argv[sys.argv.index("--blender2cal3d") + 1:]
1201   for arg in args:
1202     attr, val = arg.split("=")
1203     try: val = int(val)
1204     except:
1205       try: val = float(val)
1206       except: pass
1207     globals()[attr] = val
1208   export(FILENAME)
1209   Blender.Quit()
1210   
1211 else:
1212   if FILENAME: fs_callback(FILENAME)
1213   else:
1214     defaultname = Blender.Get("filename")
1215     if defaultname.endswith(".blend"):
1216       defaultname = defaultname[0:len(defaultname)-len(".blend")] + ".cfg"
1217     Blender.Window.FileSelector(fs_callback, "Cal3D Export", defaultname)
1218
1219