#!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.
- no support for exporting springs yet
- 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;
- 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:
- Use only a single armature;
- Use only a single rootbone (Cal3D doesn't support floating bones);
- Use only locrot keys (Cal3D doesn't support bone's size change);
- Don't try to create child/parent constructs in blender object, that gets exported
incorrectly at the moment;
- 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('\n')
file.write('\n' % CAL3D_VERSION)
file.write('\n' % len(self.maps_filenames))
file.write('\t%i %i %i %i\n' % self.amb)
file.write('\t%i %i %i %i\n' % self.diff)
file.write('\t%i %i %i %i\n' % self.spec)
file.write('\t%i\n' % self.shininess)
for map_filename in self.maps_filenames:
file.write('\t\n' % map_filename)
file.write('\n')
class Cal3DMesh:
def __init__(self, ob, blend_mesh):
self.name = ob.name
self.submeshes = []
matrix = ob.matrixWorld
#if BASE_MATRIX:
# matrix = matrix_multiply(BASE_MATRIX, matrix)
faces = list(blend_mesh.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(self, material, len(self.submeshes))
self.submeshes.append(submesh)
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, blend_vert in enumerate(face_v):
vertex = vertices.get(blend_vert.index)
if not vertex:
#coord = blend_vert.co * matrix
coord = blend_vert.co
if face.smooth:
#normal = blend_vert.no * matrix
normal = blend_vert.no
#normal.normalize()
vertex = vertices[blend_vert.index] = Cal3DVertex(coord, normal, len(submesh.vertices))
submesh.vertices.append(vertex)
influences = blend_mesh.getVertexInfluences(blend_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 blend_mesh.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()
def writeCal3D(self, file):
file.write('\n')
file.write('\n' % CAL3D_VERSION)
file.write('\n' % len(self.submeshes))
for submesh in self.submeshes:
submesh.writeCal3D(file)
file.write('\n')
class Cal3DSubMesh:
def __init__(self, mesh, material, id):
self.material = material
self.vertices = []
self.faces = []
self.nb_lodsteps = 0
self.springs = []
self.id = id
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\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\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\n' % \
(self.id, len(self.influences)))
file.write('\t\t\t%.6f %.6f %.6f\n' % (self.loc[0], self.loc[1], self.loc[2]))
file.write('\t\t\t%.6f %.6f %.6f\n' % \
(self.normal[0], self.normal[1], self.normal[2]))
if collapse_id != -1:
file.write('\t\t\t%i\n' % collapse_id)
file.write('\t\t\t%i\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%.6f\n' % len(self.weight))
file.write('\t\t\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%.6f %.6f\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%.6f\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\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\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('\n')
file.write('\n' % CAL3D_VERSION)
file.write('\n' % len(self.bones))
for item in self.bones:
item.writeCal3D(file)
file.write('\n')
BONES = {}
POSEBONES= {}
class Cal3DBone(object):
__slots__ = 'head', 'tail', 'name', 'cal3d_parent', 'loc', 'rot', 'children', 'matrix', 'lloc', 'lrot', '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.rot
# 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.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, 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 = vector_add(ploc, parentheadtotail)
#rot = quaternion_multiply(blender2cal3dquat(blend_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, blend_bone.getLoc())
# rot = quaternion_multiply(blender2cal3dquat(blend_bone.getQuat()), quat)
loc = head
rot = quat
self.head = head
self.tail = tail
self.cal3d_parent = cal3d_parent
self.name = blend_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 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\n' % \
(self.id, self.name, len(self.children)))
# We need to negate quaternion W value, but why ?
file.write('\t\t%.6f %.6f %.6f\n' % \
(self.loc[0], self.loc[1], self.loc[2]))
file.write('\t\t%.6f %.6f %.6f %.6f\n' % \
(self.rot[0], self.rot[1], self.rot[2], -self.rot[3]))
file.write('\t\t%.6f %.6f %.6f\n' % \
(self.lloc[0], self.lloc[1], self.lloc[2]))
file.write('\t\t%.6f %.6f %.6f %.6f\n' % \
(self.lrot[0], self.lrot[1], self.lrot[2], -self.lrot[3]))
if self.cal3d_parent:
file.write('\t\t%i\n' % self.cal3d_parent.id)
else:
file.write('\t\t%i\n' % -1)
for item in self.children:
file.write('\t\t%i\n' % item.id)
file.write('\t\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('\n')
file.write('\n' % CAL3D_VERSION)
file.write('\n' % \
(self.duration, len(self.tracks)))
for item in self.tracks.itervalues():
item.writeCal3D(file)
file.write('\n')
class Cal3DTrack:
def __init__(self, bone):
self.bone = bone
self.keyframes = []
def writeCal3D(self, file):
file.write('\t\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\n' % self.time)
file.write('\t\t\t%.6f %.6f %.6f\n' % \
(self.loc[0], self.loc[1], self.loc[2]))
# We need to negate quaternion W value, but why ?
file.write('\t\t\t%.6f %.6f %.6f %.6f\n' % \
(self.rot[0], self.rot[1], self.rot[2], -self.rot[3]))
file.write('\t\t\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
blend_mesh = ob.getData(mesh=1)
BPyMesh.meshCalcNormals(blend_mesh)
if not blend_mesh.faces: continue
meshes.append( Cal3DMesh(ob, blend_mesh) )
# ---- Export animations --------------------------------------------------
ANIMATIONS = {}
SUPPORTED_IPOS = "QuatW", "QuatX", "QuatY", "QuatZ", "LocX", "LocY", "LocZ"
for animation_name, blend_action in Blender.Armature.NLA.GetActions().iteritems():
#for blend_action in [blender_armature.action]:
#animation_name = a[0]
#animation_name = blend_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.
blend_action_ipos = blend_action.getAllChannelIpos()
start_frame = 300000 # largest frame
for bone_name, ipo in blend_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
blend_action_ipos_items = blend_action_ipos.items()
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, []) )
for bone_name, ipo in blend_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)
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')