diff options
author | Campbell Barton <ideasman42@gmail.com> | 2007-04-24 06:05:40 +0400 |
---|---|---|
committer | Campbell Barton <ideasman42@gmail.com> | 2007-04-24 06:05:40 +0400 |
commit | 54b494540b3ac2c381755a90b35bc9d08a7dac02 (patch) | |
tree | 55dfdbcc071c5a6078d73d28206c1da89cff4f3f /release | |
parent | 94ad8c810c1699188f4c54a9307af31fc5fd9e7a (diff) |
Cal3d exporter
Updated this script to work with the new python API, Mesh from NMesh and lots of other improvements + some bug fixes.
Diffstat (limited to 'release')
-rw-r--r-- | release/scripts/export_cal3d.py | 1011 |
1 files changed, 1011 insertions, 0 deletions
diff --git a/release/scripts/export_cal3d.py b/release/scripts/export_cal3d.py new file mode 100644 index 00000000000..9f273093092 --- /dev/null +++ b/release/scripts/export_cal3d.py @@ -0,0 +1,1011 @@ +#!BPY +""" +Name: 'Cal3D XML' +Blender: 243 +Group: 'Export' +Tip: 'Export armature/bone/mesh/action data to the Cal3D format.' +""" + +# export_cal3d.py +# Copyright (C) 2003-2004 Jean-Baptiste LAMY -- jibalamy@free.fr +# Copyright (C) 2004 Matthias Braun -- matze@braunis.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +__version__ = '0.12' +__author__ = 'Jean-Baptiste, Jiba, Lamy, Campbell Barton (Ideasman42)' +__email__ = ['Authors email, jibalamy:free*fr'] +__url__ = ['Soya3ds homepage, http://home.gna.org/oomadness/en/soya/', 'Cal3d, http://cal3d.sourceforge.net'] +__bpydoc__ =\ +'''This script is a Blender => Cal3D converter. +(See http://blender.org and http://cal3d.sourceforge.net) + +USAGE: + +To install it, place the script in your $HOME/.blender/scripts directory. + +Then open the File->Export->Cal3d v0.9 menu. And select the filename of the .cfg file. +The exporter will create a set of other files with same prefix (ie. bla.cfg, bla.xsf, +bla_Action1.xaf, bla_Action2.xaf, ...). + +You should be able to open the .cfg file in cal3d_miniviewer. + + +NOT (YET) SUPPORTED: + + - Rotation, translation, or stretching Blender objects is still quite +buggy, so AVOID MOVING / ROTATING / RESIZE OBJECTS (either mesh or armature) ! +Instead, edit the object (with tab), select all points / bones (with "a"), +and move / rotate / resize them.<br> + - no support for exporting springs yet<br> + - no support for exporting material colors (most games should only use images +I think...) + + +KNOWN ISSUES: + + - Cal3D versions <=0.9.1 have a bug where animations aren't played when the root bone +is not animated;<br> + - Cal3D versions <=0.9.1 have a bug where objects that aren't influenced by any bones +are not drawn (fixed in Cal3D CVS). + + +NOTES: + +It requires a very recent version of Blender (>= 2.44). + +Build a model following a few rules:<br> + - Use only a single armature;<br> + - Use only a single rootbone (Cal3D doesn't support floating bones);<br> + - Use only locrot keys (Cal3D doesn't support bone's size change);<br> + - Don't try to create child/parent constructs in blender object, that gets exported +incorrectly at the moment;<br> + - Objects or animations whose names start by "_" are not exported (hidden object). + +You can pass as many parameters as you want at the end, "EXPORT_FOR_SOYA=1" is just an +example. The parameters are the same as below. +''' + +# True (=1) to export for the Soya 3D engine +# (http://oomadness.tuxfamily.org/en/soya). +# (=> rotate meshes and skeletons so as X is right, Y is top and -Z is front) +# EXPORT_FOR_SOYA = 0 + +# Enables LODs computation. LODs computation is quite slow, and the algo is +# surely not optimal :-( +LODS = 0 + +# Scale the model (not supported by Soya). +SCALE = 0.04 + +# See also BASE_MATRIX below, if you want to rotate/scale/translate the model at +# the exportation. + +######################################################################################### +# Code starts here. +# The script should be quite re-useable for writing another Blender animation exporter. +# Most of the hell of it is to deal with Blender's head-tail-roll bone's definition. + +import sys, os, os.path, struct, math, string +import Blender +import BPyMesh +import BPySys + + + +def best_armature_root(armature): + ''' + Find the armature root bone with the most children, return that bone + ''' + + bones = [bone for bone in armature.bones.values() if bone.hasChildren() == True] + if len(bones) == 1: + return bones[0] + + # Get the best root since we have more then 1 + bones = [(len(bone.getAllChildren()), bone) for bone in bones] + bones.sort() + return bones[-1][1] # bone with most children + + +Vector = Blender.Mathutils.Vector +Quaternion = Blender.Mathutils.Quaternion +Matrix = Blender.Mathutils.Matrix + +# HACK -- it seems that some Blender versions don't define sys.argv, +# which may crash Python if a warning occurs. +# if not hasattr(sys, 'argv'): sys.argv = ['???'] + +def matrix_multiply(b, a): + return [ [ + a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0], + a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1], + a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2], + 0.0, + ], [ + a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0], + a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1], + a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2], + 0.0, + ], [ + a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0], + a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1], + a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2], + 0.0, + ], [ + a[3][0] * b[0][0] + a[3][1] * b[1][0] + a[3][2] * b[2][0] + b[3][0], + a[3][0] * b[0][1] + a[3][1] * b[1][1] + a[3][2] * b[2][1] + b[3][1], + a[3][0] * b[0][2] + a[3][1] * b[1][2] + a[3][2] * b[2][2] + b[3][2], + 1.0, + ] ] + +# multiplies 2 quaternions in x,y,z,w notation +def quaternion_multiply(q1, q2): + return Quaternion(\ + q2[3] * q1[0] + q2[0] * q1[3] + q2[1] * q1[2] - q2[2] * q1[1], + q2[3] * q1[1] + q2[1] * q1[3] + q2[2] * q1[0] - q2[0] * q1[2], + q2[3] * q1[2] + q2[2] * q1[3] + q2[0] * q1[1] - q2[1] * q1[0], + q2[3] * q1[3] - q2[0] * q1[0] - q2[1] * q1[1] - q2[2] * q1[2],\ + ) + +def matrix_translate(m, v): + m[3][0] += v[0] + m[3][1] += v[1] + m[3][2] += v[2] + return m + +def matrix2quaternion(m): + s = math.sqrt(abs(m[0][0] + m[1][1] + m[2][2] + m[3][3])) + if s == 0.0: + x = abs(m[2][1] - m[1][2]) + y = abs(m[0][2] - m[2][0]) + z = abs(m[1][0] - m[0][1]) + if (x >= y) and (x >= z): return Quaternion(1.0, 0.0, 0.0, 0.0) + elif (y >= x) and (y >= z): return Quaternion(0.0, 1.0, 0.0, 0.0) + else: return Quaternion(0.0, 0.0, 1.0, 0.0) + + q = Quaternion([ + -(m[2][1] - m[1][2]) / (2.0 * s), + -(m[0][2] - m[2][0]) / (2.0 * s), + -(m[1][0] - m[0][1]) / (2.0 * s), + 0.5 * s, + ]) + q.normalize() + print q + return q + +def vector_by_matrix_3x3(p, m): + return [p[0] * m[0][0] + p[1] * m[1][0] + p[2] * m[2][0], + p[0] * m[0][1] + p[1] * m[1][1] + p[2] * m[2][1], + p[0] * m[0][2] + p[1] * m[1][2] + p[2] * m[2][2]] + +def vector_add(v1, v2): + return [v1[0]+v2[0], v1[1]+v2[1], v1[2]+v2[2]] + +def vector_sub(v1, v2): + return [v1[0]-v2[0], v1[1]-v2[1], v1[2]-v2[2]] + +def quaternion2matrix(q): + xx = q[0] * q[0] + yy = q[1] * q[1] + zz = q[2] * q[2] + xy = q[0] * q[1] + xz = q[0] * q[2] + yz = q[1] * q[2] + wx = q[3] * q[0] + wy = q[3] * q[1] + wz = q[3] * q[2] + return Matrix([1.0 - 2.0 * (yy + zz), 2.0 * (xy + wz), 2.0 * (xz - wy), 0.0], + [ 2.0 * (xy - wz), 1.0 - 2.0 * (xx + zz), 2.0 * (yz + wx), 0.0], + [ 2.0 * (xz + wy), 2.0 * (yz - wx), 1.0 - 2.0 * (xx + yy), 0.0], + [0.0 , 0.0 , 0.0 , 1.0]) + +def matrix_invert(m): + det = (m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2]) + - m[1][0] * (m[0][1] * m[2][2] - m[2][1] * m[0][2]) + + m[2][0] * (m[0][1] * m[1][2] - m[1][1] * m[0][2])) + if det == 0.0: return None + det = 1.0 / det + r = [ [ + det * (m[1][1] * m[2][2] - m[2][1] * m[1][2]), + - det * (m[0][1] * m[2][2] - m[2][1] * m[0][2]), + det * (m[0][1] * m[1][2] - m[1][1] * m[0][2]), + 0.0, + ], [ + - det * (m[1][0] * m[2][2] - m[2][0] * m[1][2]), + det * (m[0][0] * m[2][2] - m[2][0] * m[0][2]), + - det * (m[0][0] * m[1][2] - m[1][0] * m[0][2]), + 0.0 + ], [ + det * (m[1][0] * m[2][1] - m[2][0] * m[1][1]), + - det * (m[0][0] * m[2][1] - m[2][0] * m[0][1]), + det * (m[0][0] * m[1][1] - m[1][0] * m[0][1]), + 0.0, + ] ] + r.append([ + -(m[3][0] * r[0][0] + m[3][1] * r[1][0] + m[3][2] * r[2][0]), + -(m[3][0] * r[0][1] + m[3][1] * r[1][1] + m[3][2] * r[2][1]), + -(m[3][0] * r[0][2] + m[3][1] * r[1][2] + m[3][2] * r[2][2]), + 1.0, + ]) + return r + + +def point_by_matrix(p, m): + return [p[0] * m[0][0] + p[1] * m[1][0] + p[2] * m[2][0] + m[3][0], + p[0] * m[0][1] + p[1] * m[1][1] + p[2] * m[2][1] + m[3][1], + p[0] * m[0][2] + p[1] * m[1][2] + p[2] * m[2][2] + m[3][2]] + +# Hack for having the model rotated right. +# Put in BASE_MATRIX your own rotation if you need some. + +BASE_MATRIX = None + + +# Cal3D data structures + +CAL3D_VERSION = 910 +MATERIALS = {} + +class Cal3DMaterial: + def __init__(self, map_filename = None): + self.amb = (255,255,255,255) + self.diff = (255,255,255,255) + self.spec = (255,255,255,255) + self.shininess = 1.0 + + if map_filename: + map_filename = map_filename.split('\\')[-1].split('/')[-1] + self.maps_filenames = [map_filename] + else: + self.maps_filenames = [] + + self.id = len(MATERIALS) + MATERIALS[map_filename] = self + + # new xml format + def writeCal3D(self, file): + file.write('<?xml version="1.0"?>\n') + file.write('<HEADER MAGIC="XRF" VERSION="%i"/>\n' % CAL3D_VERSION) + file.write('<MATERIAL NUMMAPS="%s">\n' % len(self.maps_filenames)) + file.write('\t<AMBIENT>%i %i %i %i</AMBIENT>\n' % self.amb) + file.write('\t<DIFFUSE>%i %i %i %i</DIFFUSE>\n' % self.diff) + file.write('\t<SPECULAR>%i %i %i %i</SPECULAR>\n' % self.spec) + file.write('\t<SHININESS>%i</SHININESS>\n' % self.shininess) + + for map_filename in self.maps_filenames: + file.write('\t<MAP>%s</MAP>\n' % map_filename) + + file.write('</MATERIAL>\n') + + +class Cal3DMesh: + def __init__(self, name): + self.name = name + self.submeshes = [] + self.next_submesh_id = 0 + + def writeCal3D(self, file): + file.write('<?xml version="1.0"?>\n') + file.write('<HEADER MAGIC="XMF" VERSION="%i"/>\n' % CAL3D_VERSION) + file.write('<MESH NUMSUBMESH="%i">\n' % len(self.submeshes)) + for submesh in self.submeshes: + submesh.writeCal3D(file) + file.write('</MESH>\n') + +class Cal3DSubMesh: + def __init__(self, mesh, material): + self.material = material + self.vertices = [] + self.faces = [] + self.nb_lodsteps = 0 + self.springs = [] + + self.next_vertex_id = 0 + + self.mesh = mesh + self.id = mesh.next_submesh_id + mesh.next_submesh_id += 1 + mesh.submeshes.append(self) + + def compute_lods(self): + """Computes LODs info for Cal3D (there's no Blender related stuff here).""" + + print "Start LODs computation..." + vertex2faces = {} + for face in self.faces: + for vertex in (face.vertex1, face.vertex2, face.vertex3): + l = vertex2faces.get(vertex) + if not l: vertex2faces[vertex] = [face] + else: l.append(face) + + couple_treated = {} + couple_collapse_factor = [] + for face in self.faces: + for a, b in ((face.vertex1, face.vertex2), (face.vertex1, face.vertex3), (face.vertex2, face.vertex3)): + a = a.cloned_from or a + b = b.cloned_from or b + if a.id > b.id: a, b = b, a + if not couple_treated.has_key((a, b)): + # The collapse factor is simply the distance between the 2 points :-( + # This should be improved !! + if vector_dotproduct(a.normal, b.normal) < 0.9: continue + couple_collapse_factor.append((point_distance(a.loc, b.loc), a, b)) + couple_treated[a, b] = 1 + + couple_collapse_factor.sort() + + collapsed = {} + new_vertices = [] + new_faces = [] + for factor, v1, v2 in couple_collapse_factor: + # Determines if v1 collapses to v2 or v2 to v1. + # We choose to keep the vertex which is on the smaller number of faces, since + # this one has more chance of being in an extrimity of the body. + # Though heuristic, this rule yields very good results in practice. + if len(vertex2faces[v1]) < len(vertex2faces[v2]): v2, v1 = v1, v2 + elif len(vertex2faces[v1]) == len(vertex2faces[v2]): + if collapsed.get(v1, 0): v2, v1 = v1, v2 # v1 already collapsed, try v2 + + if (not collapsed.get(v1, 0)) and (not collapsed.get(v2, 0)): + collapsed[v1] = 1 + collapsed[v2] = 1 + + # Check if v2 is already colapsed + while v2.collapse_to: v2 = v2.collapse_to + + common_faces = filter(vertex2faces[v1].__contains__, vertex2faces[v2]) + + v1.collapse_to = v2 + v1.face_collapse_count = len(common_faces) + + for clone in v1.clones: + # Find the clone of v2 that correspond to this clone of v1 + possibles = [] + for face in vertex2faces[clone]: + possibles.append(face.vertex1) + possibles.append(face.vertex2) + possibles.append(face.vertex3) + clone.collapse_to = v2 + for vertex in v2.clones: + if vertex in possibles: + clone.collapse_to = vertex + break + + clone.face_collapse_count = 0 + new_vertices.append(clone) + + # HACK -- all faces get collapsed with v1 (and no faces are collapsed with v1's + # clones). This is why we add v1 in new_vertices after v1's clones. + # This hack has no other incidence that consuming a little few memory for the + # extra faces if some v1's clone are collapsed but v1 is not. + new_vertices.append(v1) + + self.nb_lodsteps += 1 + len(v1.clones) + + new_faces.extend(common_faces) + for face in common_faces: + face.can_collapse = 1 + + # Updates vertex2faces + vertex2faces[face.vertex1].remove(face) + vertex2faces[face.vertex2].remove(face) + vertex2faces[face.vertex3].remove(face) + vertex2faces[v2].extend(vertex2faces[v1]) + + new_vertices.extend(filter(lambda vertex: not vertex.collapse_to, self.vertices)) + new_vertices.reverse() # Cal3D want LODed vertices at the end + for i in xrange(len(new_vertices)): new_vertices[i].id = i + self.vertices = new_vertices + + new_faces.extend(filter(lambda face: not face.can_collapse, self.faces)) + new_faces.reverse() # Cal3D want LODed faces at the end + self.faces = new_faces + + print "LODs computed : %s vertices can be removed (from a total of %s)." % (self.nb_lodsteps, len(self.vertices)) + + def rename_vertices(self, new_vertices): + """Rename (change ID) of all vertices, such as self.vertices == new_vertices.""" + for i in xrange(len(new_vertices)): new_vertices[i].id = i + self.vertices = new_vertices + + def writeCal3D(self, file): + file.write('\t<SUBMESH NUMVERTICES="%i" NUMFACES="%i" MATERIAL="%i" ' % \ + (len(self.vertices), len(self.faces), self.material.id)) + file.write('NUMLODSTEPS="%i" NUMSPRINGS="%i" NUMTEXCOORDS="%i">\n' % \ + (self.nb_lodsteps, len(self.springs), + len(self.material.maps_filenames))) + + for item in self.vertices: item.writeCal3D(file) + for item in self.springs: item.writeCal3D(file) + for item in self.faces: item.writeCal3D(file) + + file.write('\t</SUBMESH>\n') + +class Cal3DVertex: + """ + __slots__ =\ + 'loc',# vertex location, worldspace + 'normal',# vertex normal, worldspace + 'collapse_to',# ? + 'face_collapse_count',# ? + 'maps',# uv coords, must support Multi UV's eventually + 'influences',# Bone influences + 'weight',# ? + 'cloned_from',# ? + 'clones',# ? + 'id'# index + """ + def __init__(self, loc, normal, id): + self.loc = loc + self.normal = normal + self.collapse_to = None + self.face_collapse_count = 0 + self.maps = [] + self.influences = [] + self.weight = None + + self.cloned_from = None + self.clones = [] + + self.id = id + + def writeCal3D(self, file): + if self.collapse_to: + collapse_id = self.collapse_to.id + else: + collapse_id = -1 + file.write('\t\t<VERTEX ID="%i" NUMINFLUENCES="%i">\n' % \ + (self.id, len(self.influences))) + file.write('\t\t\t<POS>%.6f %.6f %.6f</POS>\n' % (self.loc[0], self.loc[1], self.loc[2])) + file.write('\t\t\t<NORM>%.6f %.6f %.6f</NORM>\n' % \ + (self.normal[0], self.normal[1], self.normal[2])) + if collapse_id != -1: + file.write('\t\t\t<COLLAPSEID>%i</COLLAPSEID>\n' % collapse_id) + file.write('\t\t\t<COLLAPSECOUNT>%i</COLLAPSECOUNT>\n' % \ + self.face_collapse_count) + + for item in self.maps: + item.writeCal3D(file) + + for item in self.influences: + item.writeCal3D(file) + + if self.weight != None: + file.write('\t\t\t<PHYSIQUE>%.6f</PHYSIQUE>\n' % len(self.weight)) + file.write('\t\t</VERTEX>\n') + + +class Map(object): + __slots__ = 'u', 'v' + def __init__(self, u, v): + self.u = u + self.v = v + + def writeCal3D(self, file): + file.write('\t\t\t<TEXCOORD>%.6f %.6f</TEXCOORD>\n' % (self.u, self.v)) + +class Influence(object): + __slots__ = 'bone', 'weight' + def __init__(self, bone, weight): + self.bone = bone + self.weight = weight + + def writeCal3D(self, file): + file.write('\t\t\t<INFLUENCE ID="%i">%.6f</INFLUENCE>\n' % \ + (self.bone.id, self.weight)) + +class Spring(object): + __slots__ = 'vertex1', 'vertex2', 'spring_coefficient', 'idlelength' + def __init__(self, vertex1, vertex2): + self.vertex1 = vertex1 + self.vertex2 = vertex2 + self.spring_coefficient = 0.0 + self.idlelength = 0.0 + + def writeCal3D(self, file): + file.write('\t\t<SPRING VERTEXID="%i %i" COEF="%.6f" LENGTH="%.6f"/>\n' % \ + (self.vertex1.id, self.vertex2.id, self.spring_coefficient, self.idlelength)) + +class Face(object): + __slots__ = 'vertex1', 'vertex2', 'vertex3', 'can_collapse', + def __init__(self, vertex1, vertex2, vertex3): + self.vertex1 = vertex1 + self.vertex2 = vertex2 + self.vertex3 = vertex3 + self.can_collapse = 0 + + def writeCal3D(self, file): + file.write('\t\t<FACE VERTEXID="%i %i %i"/>\n' % \ + (self.vertex1.id, self.vertex2.id, self.vertex3.id)) + +class Cal3DSkeleton(object): + __slots__ = 'bones' + def __init__(self): + self.bones = [] + + def writeCal3D(self, file): + file.write('<?xml version="1.0"?>\n') + file.write('<HEADER MAGIC="XSF" VERSION="%i"/>\n' % CAL3D_VERSION) + file.write('<SKELETON NUMBONES="%i">\n' % len(self.bones)) + for item in self.bones: + item.writeCal3D(file) + + file.write('</SKELETON>\n') + +BONES = {} +POSEBONES= {} +class Cal3DBone(object): + __slots__ = 'head', 'tail', 'name', 'cal3d_parent', 'loc', 'rot', 'children', 'matrix', 'lloc', 'lrot', 'id' + def __init__(self, skeleton, blen_bone, arm_matrix, cal3d_parent=None): + + # def treat_bone(b, parent = None): + head = blen_bone.head['BONESPACE'] + tail = blen_bone.tail['BONESPACE'] + #print parent.rot + # Turns the Blender's head-tail-roll notation into a quaternion + #quat = matrix2quaternion(blender_bone2matrix(head, tail, blen_bone.roll['BONESPACE'])) + quat = matrix2quaternion(blen_bone.matrix['BONESPACE'].copy().resize4x4()) + + # Pose location + ploc = POSEBONES[blen_bone.name].loc + + if cal3d_parent: + # Compute the translation from the parent bone's head to the child + # bone's head, in the parent bone coordinate system. + # The translation is parent_tail - parent_head + child_head, + # but parent_tail and parent_head must be converted from the parent's parent + # system coordinate into the parent system coordinate. + + parent_invert_transform = matrix_invert(quaternion2matrix(cal3d_parent.rot)) + parent_head = vector_by_matrix_3x3(cal3d_parent.head, parent_invert_transform) + parent_tail = vector_by_matrix_3x3(cal3d_parent.tail, parent_invert_transform) + ploc = vector_add(ploc, blen_bone.head['BONESPACE']) + + # EDIT!!! FIX BONE OFFSET BE CAREFULL OF THIS PART!!! ?? + #diff = vector_by_matrix_3x3(head, parent_invert_transform) + parent_tail= vector_add(parent_tail, head) + # DONE!!! + + parentheadtotail = vector_sub(parent_tail, parent_head) + # hmm this should be handled by the IPos, but isn't for non-animated + # bones which are transformed in the pose mode... + #loc = vector_add(ploc, parentheadtotail) + #rot = quaternion_multiply(blender2cal3dquat(blen_bone.getQuat()), quat) + loc = parentheadtotail + rot = quat + + else: + # Apply the armature's matrix to the root bones + head = point_by_matrix(head, arm_matrix) + tail = point_by_matrix(tail, arm_matrix) + quat = matrix2quaternion(matrix_multiply(arm_matrix, quaternion2matrix(quat))) # Probably not optimal + + # loc = vector_add(head, blen_bone.getLoc()) + # rot = quaternion_multiply(blender2cal3dquat(blen_bone.getQuat()), quat) + loc = head + rot = quat + + self.head = head + self.tail = tail + + self.cal3d_parent = cal3d_parent + self.name = blen_bone.name + self.loc = loc + self.rot = rot + self.children = [] + + self.matrix = matrix_translate(quaternion2matrix(rot), loc) + if cal3d_parent: + self.matrix = matrix_multiply(cal3d_parent.matrix, self.matrix) + + # lloc and lrot are the bone => model space transformation (translation and rotation). + # They are probably specific to Cal3D. + m = matrix_invert(self.matrix) + self.lloc = m[3][0], m[3][1], m[3][2] + self.lrot = matrix2quaternion(m) + + self.id = len(skeleton.bones) + skeleton.bones.append(self) + BONES[self.name] = self + + if not blen_bone.hasChildren(): return + for blen_child in blen_bone.children: + self.children.append(Cal3DBone(skeleton, blen_child, arm_matrix, self)) + + + def writeCal3D(self, file): + file.write('\t<BONE ID="%i" NAME="%s" NUMCHILD="%i">\n' % \ + (self.id, self.name, len(self.children))) + # We need to negate quaternion W value, but why ? + file.write('\t\t<TRANSLATION>%.6f %.6f %.6f</TRANSLATION>\n' % \ + (self.loc[0], self.loc[1], self.loc[2])) + file.write('\t\t<ROTATION>%.6f %.6f %.6f %.6f</ROTATION>\n' % \ + (self.rot[0], self.rot[1], self.rot[2], -self.rot[3])) + file.write('\t\t<LOCALTRANSLATION>%.6f %.6f %.6f</LOCALTRANSLATION>\n' % \ + (self.lloc[0], self.lloc[1], self.lloc[2])) + file.write('\t\t<LOCALROTATION>%.6f %.6f %.6f %.6f</LOCALROTATION>\n' % \ + (self.lrot[0], self.lrot[1], self.lrot[2], -self.lrot[3])) + if self.cal3d_parent: + file.write('\t\t<PARENTID>%i</PARENTID>\n' % self.cal3d_parent.id) + else: + file.write('\t\t<PARENTID>%i</PARENTID>\n' % -1) + + for item in self.children: + file.write('\t\t<CHILDID>%i</CHILDID>\n' % item.id) + + file.write('\t</BONE>\n') + +class Cal3DAnimation: + def __init__(self, name, duration = 0.0): + self.name = name + self.duration = duration + self.tracks = {} # Map bone names to tracks + + def writeCal3D(self, file): + file.write('<?xml version="1.0"?>\n') + file.write('<HEADER MAGIC="XAF" VERSION="%i"/>\n' % CAL3D_VERSION) + file.write('<ANIMATION DURATION="%.6f" NUMTRACKS="%i">\n' % \ + (self.duration, len(self.tracks))) + + for item in self.tracks.itervalues(): + item.writeCal3D(file) + + file.write('</ANIMATION>\n') + +class Cal3DTrack: + def __init__(self, bone): + self.bone = bone + self.keyframes = [] + + def writeCal3D(self, file): + file.write('\t<TRACK BONEID="%i" NUMKEYFRAMES="%i">\n' % \ + (self.bone.id, len(self.keyframes))) + + for item in self.keyframes: + item.writeCal3D(file) + + file.write('\t</TRACK>\n') + +class Cal3DKeyFrame: + def __init__(self, track, time, loc, rot): + self.time = time + self.loc = loc + self.rot = rot + + self.track = track + track.keyframes.append(self) + + def writeCal3D(self, file): + file.write('\t\t<KEYFRAME TIME="%.6f">\n' % self.time) + file.write('\t\t\t<TRANSLATION>%.6f %.6f %.6f</TRANSLATION>\n' % \ + (self.loc[0], self.loc[1], self.loc[2])) + # We need to negate quaternion W value, but why ? + file.write('\t\t\t<ROTATION>%.6f %.6f %.6f %.6f</ROTATION>\n' % \ + (self.rot[0], self.rot[1], self.rot[2], -self.rot[3])) + file.write('\t\t</KEYFRAME>\n') + +def export_cal3d(filename): + if not filename.endswith('.cfg'): + filename += '.cfg' + + file_only = filename.split('/')[-1].split('\\')[-1] + file_only_noext = file_only.split('.')[0] + base_only = filename[:-len(file_only)] + + def new_name(dataname, ext): + return file_only_noext + '_' + BPySys.cleanName(dataname) + ext + + #if EXPORT_FOR_SOYA: + # global BASE_MATRIX + # BASE_MATRIX = matrix_rotate_x(-math.pi / 2.0) + + # Get the scene + scene = Blender.Scene.GetCurrent() + + # ---- Export skeleton (=armature) ---------------------------------------- + + skeleton = Cal3DSkeleton() + blender_armature = [ob for ob in scene.objects.context if ob.type == 'Armature'] + if len(blender_armature) > 1: print "Found multiple armatures! using ",armatures[0].name + if blender_armature: blender_armature = blender_armature[0] + else: + Blender.Draw.PupMenu('Aborting%t|No Armature in selection') + return + + # we need pose bone locations + for pbone in blender_armature.getPose().bones.values(): + POSEBONES[pbone.name] = pbone + + Cal3DBone(skeleton, best_armature_root(blender_armature.getData()), blender_armature.matrixWorld) + + + + # ---- Export Mesh data --------------------------------------------------- + + meshes = [] + + for ob in scene.objects.context: + if ob.type != 'Mesh': continue + bmesh = ob.getData(mesh=1) + BPyMesh.meshCalcNormals(bmesh) + + if not bmesh.faces: continue + mesh = Cal3DMesh(bmesh.name) + mesh_name = ob.name + meshes.append(mesh) + + matrix = ob.matrixWorld + #if BASE_MATRIX: + # matrix = matrix_multiply(BASE_MATRIX, matrix) + + faces = list(bmesh.faces) + while faces: + image = faces[0].image + image_filename = image and image.filename + material = MATERIALS.get(image_filename) or Cal3DMaterial(image_filename) + outputuv = len(material.maps_filenames) > 0 + + # TODO add material color support here + submesh = Cal3DSubMesh(mesh, material) + vertices = {} + for face in faces[:]: + if (face.image and face.image.filename) == image_filename: + faces.remove(face) + + if not face.smooth: + normal = face.no * matrix + normal.normalize() + + face_vertices = [] + face_v = face.v + for i, blen_vert in enumerate(face_v): + vertex = vertices.get(blen_vert.index) + if not vertex: + #coord = blen_vert.co * matrix + coord = blen_vert.co + if face.smooth: + #normal = blen_vert.no * matrix + normal = blen_vert.no + #normal.normalize() + + vertex = vertices[blen_vert.index] = Cal3DVertex(coord, normal, len(submesh.vertices)) + submesh.vertices.append(vertex) + + influences = bmesh.getVertexInfluences(blen_vert.index) + # should this really be a warning? (well currently enabled, + # because blender has some bugs where it doesn't return + # influences in python api though they are set, and because + # cal3d<=0.9.1 had bugs where objects without influences + # aren't drawn. + if not influences: + print '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)' % ob.name + + # sum of influences is not always 1.0 in Blender ?!?! + sum = 0.0 + for bone_name, weight in influences: + sum += weight + + for bone_name, weight in influences: + if bone_name not in BONES: + print 'Couldnt find bone "%s" which influences object "%s"' % (bone_name, ob.name) + continue + if weight: + vertex.influences.append(Influence(BONES[bone_name], weight / sum)) + + elif not face.smooth: + # We cannot share vertex for non-smooth faces, since Cal3D does not + # support vertex sharing for 2 vertices with different normals. + # => we must clone the vertex. + + old_vertex = vertex + vertex = Cal3DVertex(vertex.loc, normal, len(submesh.vertices)) + submesh.vertices.append(vertex) + + vertex.cloned_from = old_vertex + vertex.influences = old_vertex.influences + old_vertex.clones.append(vertex) + + if bmesh.faceUV: + uv = [face.uv[i][0], 1.0 - face.uv[i][1]] + if not vertex.maps: + if outputuv: vertex.maps.append(Map(*uv)) + elif (vertex.maps[0].u != uv[0]) or (vertex.maps[0].v != uv[1]): + # This vertex can be shared for Blender, but not for Cal3D !!! + # Cal3D does not support vertex sharing for 2 vertices with + # different UV texture coodinates. + # => we must clone the vertex. + + for clone in vertex.clones: + if (clone.maps[0].u == uv[0]) and (clone.maps[0].v == uv[1]): + vertex = clone + break + else: # Not yet cloned... + old_vertex = vertex + vertex = Cal3DVertex(vertex.loc, vertex.normal, len(submesh.vertices)) + submesh.vertices.append(vertex) + + vertex.cloned_from = old_vertex + vertex.influences = old_vertex.influences + if outputuv: vertex.maps.append(Map(*uv)) + old_vertex.clones.append(vertex) + + face_vertices.append(vertex) + + # Split faces with more than 3 vertices + for i in xrange(1, len(face.v) - 1): + submesh.faces.append(Face(face_vertices[0], face_vertices[i], face_vertices[i + 1])) + + # Computes LODs info + if LODS: + submesh.compute_lods() + + # ---- Export animations -------------------------------------------------- + ANIMATIONS = {} + SUPPORTED_IPOS = "QuatW", "QuatX", "QuatY", "QuatZ", "LocX", "LocY", "LocZ" + for a in Blender.Armature.NLA.GetActions().iteritems(): + #for blen_action in [blender_armature.action]: + #animation_name = a[0] + animation_name = blen_action.name + animation = Cal3DAnimation(animation_name) + animation.duration = 0.0 + + + # All tracks need to have at least 1 keyframe. + # bones without any keys crash the viewer so we need to find the location for a dummy keyframe. + blen_action_ipos = blen_action.getAllChannelIpos() + start_frame = 300000 # largest frame + for bone_name, ipo in blen_action_ipos.iteritems(): + if ipo: + for curve in ipo: + if curve.name in SUPPORTED_IPOS: + for p in curve.bezierPoints: + start_frame = min(start_frame, p.pt[0]) + + # Write all dummy keyframes, find bones with no actions + if start_frame == 300000: + pass # BAD STUFF NO IPOS + + + # Now we mau have some bones with no channels, easiest to add their names and an empty list here + # this way they are exported with dummy keyfraames at teh first used frame + blen_action_ipos_items = blen_action_ipos.items() + action_bone_names = [name for name, ipo in blen_action_ipos_items] + for bone_name in BONES: # iterkeys + if bone_name not in action_bone_names: + blen_action_ipos_items.append( (bone_name, []) ) + + + for bone_name, ipo in blen_action_ipos_items: + if bone_name not in BONES: + print "\tNo Bone '" + bone_name + "' in (from Animation '" + animation_name + "') ?!?" + continue + + # So we can loop without errors + if ipo==None: ipo = [] + + bone = BONES[bone_name] + track = animation.tracks[bone_name] = Cal3DTrack(bone) + + #run 1: we need to find all time values where we need to produce keyframes + times = set() + for curve in ipo: + curve_name = curve.name + if curve_name in SUPPORTED_IPOS: + for p in curve.bezierPoints: + times.add( p.pt[0] ) + + times = list(times) + times.sort() + + # Incase we have no keys here or ipo==None + if not times: + times.append(start_frame) + + # run2: now create keyframes + for time in times: + cal3dtime = (time-1) / 25.0 # assume 25FPS by default + if cal3dtime > animation.duration: + animation.duration = cal3dtime + trans = Vector(0,0,0) + quat = Quaternion() + + for curve in ipo: + val = curve.evaluate(time) + # val = 0.0 + curve_name= curve.name + if curve_name == "LocX": trans[0] = val + elif curve_name == "LocY": trans[1] = val + elif curve_name == "LocZ": trans[2] = val + elif curve_name == "QuatW": quat[3] = val + elif curve_name == "QuatX": quat[0] = val + elif curve_name == "QuatY": quat[1] = val + elif curve_name == "QuatZ": quat[2] = val + + transt = vector_by_matrix_3x3(trans, bone.matrix) + loc = vector_add(bone.loc, transt) + rot = quaternion_multiply(quat, bone.rot) + rot = Quaternion(rot) + rot.normalize() + rot = tuple(rot) + Cal3DKeyFrame(track, cal3dtime, loc, rot) + + + + Cal3DKeyFrame(track, cal3dtime, loc, rot) + #Cal3DKeyFrame(track, cal3dtime, (0,0,0), (0,0,0,0)) + + if animation.duration <= 0: + print "Ignoring Animation '" + animation_name + "': duration is 0.\n" + continue + ANIMATIONS[animation_name] = animation + + + cfg = open((filename), "wb") + cfg.write('# Cal3D model exported from Blender with export_cal3d.py\n') + + if SCALE != 1.0: cfg.write('scale=%.6f\n' % SCALE) + + fname = file_only_noext + '.xsf' + file = open( base_only + fname, "wb") + skeleton.writeCal3D(file) + file.close() + + cfg.write('skeleton=%s\n' % fname) + + for animation in ANIMATIONS.itervalues(): + if not animation.name.startswith('_'): + if animation.duration > 0.1: # Cal3D does not support animation with only one state + fname = new_name(animation.name, '.xaf') + file = open(base_only + fname, "wb") + animation.writeCal3D(file) + file.close() + cfg.write('animation=%s\n' % fname) + + for mesh in meshes: + if not mesh.name.startswith('_'): + fname = new_name(mesh.name, '.xmf') + file = open(base_only + fname, "wb") + mesh.writeCal3D(file) + file.close() + + cfg.write('mesh=%s\n' % fname) + + materials = MATERIALS.values() + materials.sort(key = lambda a: a.id) + for material in materials: + if material.maps_filenames: + fname = new_name(material.maps_filenames[0].split('\\')[-1].split('/')[-1], '.xrf') + else: + fname = new_name('plain', '.xrf') + + file = open(base_only + fname, "wb") + material.writeCal3D(file) + file.close() + + cfg.write('material=%s\n' % fname) + + print 'Cal3D Saved to "%s.cfg"' % file_only_noext + + # Warnings + if len(animation.tracks) < 2: + Blender.Draw.PupMenu('Warning, the armature has less then 2 tracks, file may not load in Cal3d') + +import os +if __name__ == '__main__': + #Blender.Window.FileSelector(export_cal3d, "Cal3D Export", Blender.Get('filename').replace('.blend', '.cfg')) + export_cal3d('/test' + '.cfg') + os.system('cd /; wine /cal3d_miniviewer.exe /test.cfg') |