[python] cal3d export

Date : Thu, 25 Oct 2007 15:15:26 +0300
To : <XSI(at)Softimage.COM>
From : "Alexander Hemery" <vortex(at)fhw.gr>
Subject : [python] cal3d export
Hi everyone,
 
I've attached a python script from blender and I was wondering if it would be relatively easy for any python gurus here to modify it for XSI.
 
We NEED a cal3d exporter ...but these are very deep waters for me ...and the programmers here don't know the xsi api, so we're stuck...
 
Alex.
 
 
 
#!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(at)free.fr
# Copyright (C) 2004 Matthias Braun -- matze(at)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:
				if BONES.get(bone_name):
					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')

Search the XSI List archives here or use the advanced search form to search across mailing lists. Searching help is available.
This site supposedly brought to you by Benjamin Grosser and the Imaging Technology Group.