home *** CD-ROM | disk | FTP | other *** search
/ Windows News 2005 November / WNnov2005.iso / Windows / Equipement / Blender / blender-2.37a-windows.exe / $_5_ / .blender / scripts / blender2cal3d.py < prev    next >
Text File  |  2004-12-05  |  44KB  |  1,220 lines

  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.  
  1220.