diff options
Diffstat (limited to 'release/scripts/export_cal3d.py')
-rw-r--r-- | release/scripts/export_cal3d.py | 1112 |
1 files changed, 0 insertions, 1112 deletions
diff --git a/release/scripts/export_cal3d.py b/release/scripts/export_cal3d.py deleted file mode 100644 index 990ac480e3d..00000000000 --- a/release/scripts/export_cal3d.py +++ /dev/null @@ -1,1112 +0,0 @@ -#!BPY -""" -Name: 'Cal3D (.cfg .xaf .xsf .xmf .xrf)...' -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.9f' -__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). - -# 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 math -import Blender -import BPyMesh -import BPySys -import BPyArmature -import BPyObject -import bpy - -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 = {} # keys are (mat.name, img.name) - -class Cal3DMaterial(object): - __slots__ = 'amb', 'diff', 'spec', 'shininess', 'maps_filenames', 'id' - def __init__(self, blend_world, blend_material, blend_images): - - # Material Settings - if blend_world: amb = [ int(c*255) for c in blend_world.amb ] - else: amb = [0,0,0] # Default value - - if blend_material: - self.amb = tuple([int(c*blend_material.amb) for c in amb] + [255]) - self.diff = tuple([int(c*255) for c in blend_material.rgbCol] + [int(blend_material.alpha*255)]) - self.spec = tuple([int(c*255) for c in blend_material.rgbCol] + [int(blend_material.alpha*255)]) - self.shininess = (float(blend_material.hard)-1)/5.10 - else: - self.amb = tuple(amb + [255]) - self.diff = (255,255,255,255) - self.spec = (255,255,255,255) - self.shininess = 1.0 - - self.maps_filenames = [] - for image in blend_images: - if image: - self.maps_filenames.append( image.filename.split('\\')[-1].split('/')[-1] ) - - self.id = len(MATERIALS) - MATERIALS[blend_material, blend_images] = 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>%.6f</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(object): - __slots__ = 'name', 'submeshes', 'matrix', 'matrix_normal' - def __init__(self, ob, blend_mesh, blend_world): - self.name = ob.name - self.submeshes = [] - - BPyMesh.meshCalcNormals(blend_mesh) - - self.matrix = ob.matrixWorld - self.matrix_normal = self.matrix.copy().rotationPart() - - #if BASE_MATRIX: - # matrix = matrix_multiply(BASE_MATRIX, matrix) - - face_groups = {} - blend_materials = blend_mesh.materials - uvlayers = () - mat = None # incase we have no materials - if blend_mesh.faceUV: - uvlayers = blend_mesh.getUVLayerNames() - if len(uvlayers) == 1: - for f in blend_mesh.faces: - image = (f.image,) # bit in a tuple so we can match multi UV code - if blend_materials: mat = blend_materials[f.mat] # if no materials, mat will always be None - face_groups.setdefault( (mat,image), (mat,image,[]) )[2].append( f ) - else: - # Multi UV's - face_multi_images = [[] for i in xrange(len(blend_mesh.faces))] - face_multi_uvs = [[[] for i in xrange(len(f)) ] for f in blend_mesh.faces] - for uvlayer in uvlayers: - blend_mesh.activeUVLayer = uvlayer - for i, f in enumerate(blend_mesh.faces): - face_multi_images[i].append(f.image) - if f.image: - for j, uv in enumerate(f.uv): - face_multi_uvs[i][j].append( tuple(uv) ) - - # Convert UV's to tuples so they can be compared with eachother - # when creating new verts - for fuv in face_multi_uvs: - for i, uv in enumerate(fuv): - fuv[i] = tuple(uv) - - for i, f in enumerate(blend_mesh.faces): - image = tuple(face_multi_images[i]) - if blend_materials: mat = blend_materials[f.mat] - face_groups.setdefault( (mat,image), (mat,image,[]) )[2].append( f ) - else: - # No UV's - for f in blend_mesh.faces: - if blend_materials: mat = blend_materials[f.mat] - face_groups.setdefault( (mat,()), (mat,(),[]) )[2].append( f ) - - for blend_material, blend_images, faces in face_groups.itervalues(): - - try: material = MATERIALS[blend_material, blend_images] - except: material = MATERIALS[blend_material, blend_images] = Cal3DMaterial(blend_world, blend_material, blend_images) - - submesh = Cal3DSubMesh(self, material, len(self.submeshes)) - self.submeshes.append(submesh) - - # Check weather we need to write UVs, dont do it if theres no image - # Multilayer UV's have alredy checked that they have images when - # building face_multi_uvs - if len(uvlayers) == 1: - if blend_images == (None,): - write_single_layer_uvs = False - else: - write_single_layer_uvs = True - - - for face in faces: - - if not face.smooth: - normal = face.no - - face_vertices = [] - face_v = face.v - - - if len(uvlayers)>1: - for i, blend_vert in enumerate(face_v): - if face.smooth: normal = blend_vert.no - vertex = submesh.getVertex(blend_mesh, blend_vert, normal, face_multi_uvs[face.index][i]) - face_vertices.append(vertex) - - elif len(uvlayers)==1: - if write_single_layer_uvs: - face_uv = face.uv - - for i, blend_vert in enumerate(face_v): - if face.smooth: normal = blend_vert.no - if write_single_layer_uvs: uvs = (tuple(face_uv[i]),) - else: uvs = () - - vertex = submesh.getVertex(blend_mesh, blend_vert, normal, uvs ) - face_vertices.append(vertex) - else: - # No UVs - for i, blend_vert in enumerate(face_v): - if face.smooth: normal = blend_vert.no - vertex = submesh.getVertex(blend_mesh, blend_vert, normal, () ) - face_vertices.append(vertex) - - - # Split faces with more than 3 vertices - for i in xrange(1, len(face) - 1): - submesh.faces.append(Cal3DFace(face_vertices[0], face_vertices[i], face_vertices[i + 1])) - - 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, self.matrix, self.matrix_normal) - file.write('</MESH>\n') - - -class Cal3DSubMesh(object): - __slots__ = 'material', 'vertices', 'vert_mapping', 'vert_count', 'faces', 'nb_lodsteps', 'springs', 'id' - def __init__(self, mesh, material, id): - self.material = material - self.vertices = [] - self.vert_mapping = {} # map original indicies to local - self.vert_count = 0 - self.faces = [] - self.nb_lodsteps = 0 - self.springs = [] - self.id = id - - def getVertex(self, blend_mesh, blend_vert, normal, maps): - ''' - Request a vertex, and create a new one or return a matching vertex - ''' - blend_index = blend_vert.index - index_map = self.vert_mapping.get(blend_index) - - if index_map == None: - vertex = Cal3DVertex(blend_vert.co, normal, maps, blend_mesh.getVertexInfluences(blend_index)) - self.vertices.append([vertex]) - self.vert_mapping[blend_index] = len(self.vert_mapping) - self.vert_count +=1 - return vertex - else: - vertex_list = self.vertices[index_map] - - for v in vertex_list: - if v.normal == normal and\ - v.maps == maps: - return v # reusing - - # No match, add a new vert - # Use the first verts influences - vertex = Cal3DVertex(blend_vert.co, normal, maps, vertex_list[0].influences) - vertex_list.append(vertex) - # self.vert_mapping[blend_index] = len(self.vert_mapping) - self.vert_count +=1 - return vertex - - - 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 writeCal3D(self, file, matrix, matrix_normal): - - file.write('\t<SUBMESH NUMVERTICES="%i" NUMFACES="%i" MATERIAL="%i" ' % \ - (self.vert_count, 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))) - - i = 0 - for v in self.vertices: - for item in v: - item.id = i - item.writeCal3D(file, matrix, matrix_normal) - i += 1 - - for item in self.springs: - item.writeCal3D(file) - for item in self.faces: - item.writeCal3D(file) - - file.write('\t</SUBMESH>\n') - -class Cal3DVertex(object): - __slots__ = 'loc','normal','collapse_to','face_collapse_count','maps','influences','weight','cloned_from','clones','id' - def __init__(self, loc, normal, maps, blend_influences): - self.loc = loc - self.normal = normal - self.collapse_to = None - self.face_collapse_count = 0 - self.maps = maps - self.weight = None - - self.cloned_from = None - self.clones = [] - - self.id = -1 - - if len(blend_influences) == 0 or isinstance(blend_influences[0], Cal3DInfluence): - # This is a copy from another vert - self.influences = blend_influences - else: - # Pass the blender influences - - self.influences = [] - # 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 blend_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 blend_influences: - sum += weight - - for bone_name, weight in blend_influences: - bone = BONES.get(bone_name) - if not bone: # keys - # print 'Couldnt find bone "%s" which influences object "%s"' % (bone_name, ob.name) - continue - - if weight: - self.influences.append(Cal3DInfluence(bone, weight / sum)) - - - def writeCal3D(self, file, matrix, matrix_normal): - 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' % tuple(self.loc*matrix)) - file.write('\t\t\t<NORM>%.6f %.6f %.6f</NORM>\n' % tuple( (self.normal*matrix_normal).normalize() )) - 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 uv in self.maps: - # we cant have more UV's then our materials image maps - # check for this - file.write('\t\t\t<TEXCOORD>%.6f %.6f</TEXCOORD>\n' % uv) - - 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 Cal3DInfluence(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 Cal3DSpring(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 Cal3DFace(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', 'quat', 'children', 'matrix', 'lloc', 'lquat', 'id' - def __init__(self, skeleton, blend_bone, arm_matrix, cal3d_parent=None): - - # def treat_bone(b, parent = None): - head = blend_bone.head['BONESPACE'] - tail = blend_bone.tail['BONESPACE'] - #print parent.quat - # Turns the Blender's head-tail-roll notation into a quaternion - #quat = matrix2quaternion(blender_bone2matrix(head, tail, blend_bone.roll['BONESPACE'])) - quat = matrix2quaternion(blend_bone.matrix['BONESPACE'].copy().resize4x4()) - - # Pose location - ploc = POSEBONES[blend_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.quat)) - 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, blend_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 = parentheadtotail - - else: - # Apply the armature's matrix to the root bones - head = point_by_matrix(head, arm_matrix) - tail = point_by_matrix(tail, arm_matrix) - - loc = head - quat = matrix2quaternion(matrix_multiply(arm_matrix, quaternion2matrix(quat))) # Probably not optimal - - self.head = head - self.tail = tail - - self.cal3d_parent = cal3d_parent - self.name = blend_bone.name - self.loc = loc - self.quat = quat - self.children = [] - - self.matrix = matrix_translate(quaternion2matrix(quat), loc) - if cal3d_parent: - self.matrix = matrix_multiply(cal3d_parent.matrix, self.matrix) - - # lloc and lquat 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.lquat = matrix2quaternion(m) - - self.id = len(skeleton.bones) - skeleton.bones.append(self) - BONES[self.name] = self - - if not blend_bone.hasChildren(): return - for blend_child in blend_bone.children: - self.children.append(Cal3DBone(skeleton, blend_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.quat[0], self.quat[1], self.quat[2], -self.quat[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.lquat[0], self.lquat[1], self.lquat[2], -self.lquat[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(object): - __slots__ = 'bone', 'keyframes' - 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(object): - __slots__ = 'time', 'loc', 'quat' - def __init__(self, time, loc, quat): - self.time = time - self.loc = loc - self.quat = quat - - 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.quat[0], self.quat[1], self.quat[2], -self.quat[3])) - file.write('\t\t</KEYFRAME>\n') - -def export_cal3d(filename, PREF_SCALE=0.1, PREF_BAKE_MOTION = True, PREF_ACT_ACTION_ONLY=True, PREF_SCENE_FRAMES=False): - 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 sce - - sce = bpy.data.scenes.active - blend_world = sce.world - # ---- Export skeleton (armature) ---------------------------------------- - - skeleton = Cal3DSkeleton() - blender_armature = [ob for ob in sce.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: - # Try find a meshes armature - for ob in sce.objects.context: - blender_armature = BPyObject.getObjectArmature(ob) - if blender_armature: - break - - if not blender_armature: - 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 sce.objects.context: - if ob.type != 'Mesh': continue - blend_mesh = ob.getData(mesh=1) - - if not blend_mesh.faces: continue - meshes.append( Cal3DMesh(ob, blend_mesh, blend_world) ) - - # ---- Export animations -------------------------------------------------- - backup_action = blender_armature.action - - ANIMATIONS = [] - SUPPORTED_IPOS = 'QuatW', 'QuatX', 'QuatY', 'QuatZ', 'LocX', 'LocY', 'LocZ' - - if PREF_ACT_ACTION_ONLY: action_items = [(blender_armature.action.name, blender_armature.action)] - else: action_items = Blender.Armature.NLA.GetActions().items() - - print len(action_items), 'action_items' - - for animation_name, blend_action in action_items: - - # get frame range - if PREF_SCENE_FRAMES: - action_start= Blender.Get('staframe') - action_end= Blender.Get('endframe') - else: - _frames = blend_action.getFrameNumbers() - action_start= min(_frames); - action_end= max(_frames); - del _frames - - blender_armature.action = blend_action - - if PREF_BAKE_MOTION: - # We need to set the action active if we are getting baked data - pose_data = BPyArmature.getBakedPoseData(blender_armature, action_start, action_end) - - # Fake, all we need is bone names - blend_action_ipos_items = [(pbone, True) for pbone in POSEBONES.iterkeys()] - else: - # real (bone_name, ipo) pairs - blend_action_ipos_items = blend_action.getAllChannelIpos().items() - - # 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 - action_bone_names = [name for name, ipo in blend_action_ipos_items] - for bone_name in BONES: # iterkeys - if bone_name not in action_bone_names: - blend_action_ipos_items.append( (bone_name, []) ) - - animation = Cal3DAnimation(animation_name) - # ---------------------------- - ANIMATIONS.append(animation) - animation.duration = 0.0 - - for bone_name, ipo in blend_action_ipos_items: - # Baked bones may have no IPO's width motion still - 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) - - if PREF_BAKE_MOTION: - for i in xrange(action_end - action_start): - cal3dtime = i / 25.0 # assume 25FPS by default - - if cal3dtime > animation.duration: - animation.duration = cal3dtime - - #print pose_data[i][bone_name], i - loc, quat = pose_data[i][bone_name] - - loc = vector_by_matrix_3x3(loc, bone.matrix) - loc = vector_add(bone.loc, loc) - quat = quaternion_multiply(quat, bone.quat) - quat = Quaternion(quat) - - quat.normalize() - quat = tuple(quat) - - track.keyframes.append( Cal3DKeyFrame(cal3dtime, loc, quat) ) - - else: - #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(action_start) - - # 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() - 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) - quat = quaternion_multiply(quat, bone.quat) - quat = Quaternion(quat) - - quat.normalize() - quat = tuple(quat) - - track.keyframes.append( Cal3DKeyFrame(cal3dtime, loc, quat) ) - - - if animation.duration <= 0: - print 'Ignoring Animation "' + animation_name + '": duration is 0.\n' - continue - - # Restore the original armature - blender_armature.action = backup_action - # ------------------------------------- End Animation - - - - cfg = open((filename), 'wb') - cfg.write('# Cal3D model exported from Blender with export_cal3d.py\n# from %s\n' % Blender.Get('filename')) - - if PREF_SCALE != 1.0: cfg.write('scale=%.6f\n' % PREF_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: - 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: - # Just number materials, its less trouble - fname = new_name(str(material.id), '.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') - - -def export_cal3d_ui(filename): - - PREF_SCALE= Blender.Draw.Create(1.0) - PREF_BAKE_MOTION = Blender.Draw.Create(1) - PREF_ACT_ACTION_ONLY= Blender.Draw.Create(1) - PREF_SCENE_FRAMES= Blender.Draw.Create(0) - - block = [\ - ('Scale: ', PREF_SCALE, 0.01, 100, 'The scale to set in the Cal3d .cfg file (unsupported by soya)'),\ - ('Baked Motion', PREF_BAKE_MOTION, 'use final pose position instead of ipo keyframes (IK and constraint support)'),\ - ('Active Action', PREF_ACT_ACTION_ONLY, 'Only export action applied to this armature, else export all actions.'),\ - ('Scene Frames', PREF_SCENE_FRAMES, 'Use scene frame range, else the actions start/end'),\ - ] - - if not Blender.Draw.PupBlock('Cal3D Options', block): - return - - Blender.Window.WaitCursor(1) - export_cal3d(filename, 1.0/PREF_SCALE.val, PREF_BAKE_MOTION.val, PREF_ACT_ACTION_ONLY.val, PREF_SCENE_FRAMES.val) - Blender.Window.WaitCursor(0) - - -#import os -if __name__ == '__main__': - Blender.Window.FileSelector(export_cal3d_ui, 'Cal3D Export', Blender.Get('filename').replace('.blend', '.cfg')) - #export_cal3d('/cally/data/skeleton/skeleton' + '.cfg', 1.0, True, False, False) - #export_cal3d('/test' + '.cfg') - #export_cal3d_ui('/test' + '.cfg') - #os.system('cd /; wine /cal3d_miniviewer.exe /skeleton.cfg') - #os.system('cd /cally/;wine cally') |