blender/release/scripts/blender2cal3d.py
Matt Ebb d34f89b755 * Edited and consistent-ified the File->Import menu item labels and fileselect button labels, as discussed on the forums.
* Added items for importing DXF, VRML, etc. in the File->Import menu, that just call the normal Open function. Most people don't even know that you can open these formats through the normal Open fileselect, so this will make it more obvious.

* Removed the 'Export Selected' menu, and put poor old lonely STL in the Import and Export menus too. Most of the exporters export only the selected object anyway, so it's not really a necessary distinction to make.
2004-06-11 02:12:37 +00:00

985 lines
33 KiB
Python

#!BPY
"""
Name: 'Cal3D v0.5...'
Blender: 232
Group: 'Export'
Tip: 'Export armature/bone data to the Cal3D library.'
"""
# $Id$
#
# blender2cal3D.py version 0.5
# Copyright (C) 2003 Jean-Baptiste LAMY -- jiba@tuxfamily.org
#
# 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
# This script is a Blender 2.28 => Cal3D 0.7/0.8 converter.
# (See http://blender.org and http://cal3d.sourceforge.net)
#
# Grab the latest version here :
# http://oomadness.tuxfamily.org/en/blender2cal3d
# HOW TO USE :
# 1 - load the script in Blender's text editor
# 2 - modify the parameters below (e.g. the file name)
# 3 - type M-P (meta/alt + P) and wait until script execution is finished
# ADVICES :
# - Use only locrot keys in Blender's action
# - Do not put "." in action or bone names, and do not start these names by a figure
# - Objects whose names start by "_" are not exported (hidden object)
# - All your armature's bones must be connected to another bone (except for the root
# bone). Contrary to Blender, Cal3D doesn't support "floating" bones.
# - Only Linux has been tested
# BUGS / TODO :
# - Animation names ARE LOST when exporting (this is due to Blender Python API and cannot
# be fixed until the API change). See parameters for how to rename your animations
# - Rotation, translation, or stretch (size changing) of Blender object is still quite
# bugged, 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.
# - Material color is not supported yet
# - Cal3D springs (for clothes and hair) are not supported yet
# - Optimization tips : almost all the time is spent on scene.makeCurrent(), called for
# updating the IPO curve's values. Updating a single IPO and not the whole scene
# would speed up a lot.
# Questions and comments are welcome at jiba@tuxfamily.org
# Parameters :
# The directory where the data are saved.
# blender2cal3d.py will create all files in this directory,
# including a .cfg file.
# WARNING: As Cal3D stores model in directory and not in a single file,
# you MUST avoid putting other file in this directory !
# Please give an empty directory (or an unexistant one).
# Files may be deleted from this directoty !
SAVE_TO_DIR = "cal3d"
# Use this dictionary to rename animations, as their name is lost at the exportation.
RENAME_ANIMATIONS = {
# "OldName" : "NewName",
}
# 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
# 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
# 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 = ["???"]
# Math stuff
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 [[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 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 1.0, 0.0, 0.0, 0.0
elif (y >= x) and (y >= z): return 0.0, 1.0, 0.0, 0.0
else: return 0.0, 0.0, 1.0, 0.0
return quaternion_normalize([
-(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,
])
def quaternion_normalize(q):
l = math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3])
return q[0] / l, q[1] / l, q[2] / l, q[3] / l
def quaternion_multiply(q1, q2):
r = [
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],
]
d = math.sqrt(r[0] * r[0] + r[1] * r[1] + r[2] * r[2] + r[3] * r[3])
r[0] /= d
r[1] /= d
r[2] /= d
r[3] /= d
return r
def matrix_translate(m, v):
m[3][0] += v[0]
m[3][1] += v[1]
m[3][2] += v[2]
return m
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,
] ]
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 matrix_rotate_x(angle):
cos = math.cos(angle)
sin = math.sin(angle)
return [
[1.0, 0.0, 0.0, 0.0],
[0.0, cos, sin, 0.0],
[0.0, -sin, cos, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
def matrix_rotate_y(angle):
cos = math.cos(angle)
sin = math.sin(angle)
return [
[cos, 0.0, -sin, 0.0],
[0.0, 1.0, 0.0, 0.0],
[sin, 0.0, cos, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
def matrix_rotate_z(angle):
cos = math.cos(angle)
sin = math.sin(angle)
return [
[ cos, sin, 0.0, 0.0],
[-sin, cos, 0.0, 0.0],
[ 0.0, 0.0, 1.0, 0.0],
[ 0.0, 0.0, 0.0, 1.0],
]
def matrix_rotate(axis, angle):
vx = axis[0]
vy = axis[1]
vz = axis[2]
vx2 = vx * vx
vy2 = vy * vy
vz2 = vz * vz
cos = math.cos(angle)
sin = math.sin(angle)
co1 = 1.0 - cos
return [
[vx2 * co1 + cos, vx * vy * co1 + vz * sin, vz * vx * co1 - vy * sin, 0.0],
[vx * vy * co1 - vz * sin, vy2 * co1 + cos, vy * vz * co1 + vx * sin, 0.0],
[vz * vx * co1 + vy * sin, vy * vz * co1 - vx * sin, vz2 * co1 + cos, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
def matrix_scale(fx, fy, fz):
return [
[ fx, 0.0, 0.0, 0.0],
[0.0, fy, 0.0, 0.0],
[0.0, 0.0, fz, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
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]]
def point_distance(p1, p2):
return math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2)
def vector_by_matrix(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_length(v):
return math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
def vector_normalize(v):
l = math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
return v[0] / l, v[1] / l, v[2] / l
def vector_dotproduct(v1, v2):
return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]
def vector_crossproduct(v1, v2):
return [
v1[1] * v2[2] - v1[2] * v2[1],
v1[2] * v2[0] - v1[0] * v2[2],
v1[0] * v2[1] - v1[1] * v2[0],
]
def vector_angle(v1, v2):
s = vector_length(v1) * vector_length(v2)
f = vector_dotproduct(v1, v2) / s
if f >= 1.0: return 0.0
if f <= -1.0: return math.pi / 2.0
return math.atan(-f / math.sqrt(1.0 - f * f)) + math.pi / 2.0
def blender_bone2matrix(head, tail, roll):
# Convert bone rest state (defined by bone.head, bone.tail and bone.roll)
# to a matrix (the more standard notation).
# Taken from blenkernel/intern/armature.c in Blender source.
# See also DNA_armature_types.h:47.
target = [0.0, 1.0, 0.0]
delta = [tail[0] - head[0], tail[1] - head[1], tail[2] - head[2]]
nor = vector_normalize(delta)
axis = vector_crossproduct(target, nor)
if vector_dotproduct(axis, axis) > 0.0000000000001:
axis = vector_normalize(axis)
theta = math.acos(vector_dotproduct(target, nor))
bMatrix = matrix_rotate(axis, theta)
else:
if vector_crossproduct(target, nor) > 0.0: updown = 1.0
else: updown = -1.0
# Quoted from Blender source : "I think this should work ..."
bMatrix = [
[updown, 0.0, 0.0, 0.0],
[0.0, updown, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
rMatrix = matrix_rotate(nor, roll)
return matrix_multiply(rMatrix, bMatrix)
# Hack for having the model rotated right.
# Put in BASE_MATRIX your own rotation if you need some.
if EXPORT_FOR_SOYA:
BASE_MATRIX = matrix_rotate_x(-math.pi / 2.0)
else:
BASE_MATRIX = None
# Cal3D data structures
CAL3D_VERSION = 700
NEXT_MATERIAL_ID = 0
class Material:
def __init__(self, map_filename = None):
self.ambient_r = 255
self.ambient_g = 255
self.ambient_b = 255
self.ambient_a = 255
self.diffuse_r = 255
self.diffuse_g = 255
self.diffuse_b = 255
self.diffuse_a = 255
self.specular_r = 255
self.specular_g = 255
self.specular_b = 255
self.specular_a = 255
self.shininess = 1.0
if map_filename: self.maps_filenames = [map_filename]
else: self.maps_filenames = []
MATERIALS[map_filename] = self
global NEXT_MATERIAL_ID
self.id = NEXT_MATERIAL_ID
NEXT_MATERIAL_ID += 1
def to_cal3d(self):
s = "CRF\0" + struct.pack("iBBBBBBBBBBBBfi", CAL3D_VERSION, self.ambient_r, self.ambient_g, self.ambient_b, self.ambient_a, self.diffuse_r, self.diffuse_g, self.diffuse_b, self.diffuse_a, self.specular_r, self.specular_g, self.specular_b, self.specular_a, self.shininess, len(self.maps_filenames))
for map_filename in self.maps_filenames:
s += struct.pack("i", len(map_filename) + 1)
s += map_filename + "\0"
return s
MATERIALS = {}
class Mesh:
def __init__(self, name):
self.name = name
self.submeshes = []
self.next_submesh_id = 0
def to_cal3d(self):
s = "CMF\0" + struct.pack("ii", CAL3D_VERSION, len(self.submeshes))
s += "".join(map(SubMesh.to_cal3d, self.submeshes))
return s
class SubMesh:
def __init__(self, mesh, material):
self.material = material
self.vertices = []
self.faces = []
self.nb_lodsteps = 0
self.springs = []
self.next_vertex_id = 0
self.mesh = mesh
self.id = mesh.next_submesh_id
mesh.next_submesh_id += 1
mesh.submeshes.append(self)
def compute_lods(self):
"""Computes LODs info for Cal3D (there's no Blender related stuff here)."""
print "Start LODs computation..."
vertex2faces = {}
for face in self.faces:
for vertex in (face.vertex1, face.vertex2, face.vertex3):
l = vertex2faces.get(vertex)
if not l: vertex2faces[vertex] = [face]
else: l.append(face)
couple_treated = {}
couple_collapse_factor = []
for face in self.faces:
for a, b in ((face.vertex1, face.vertex2), (face.vertex1, face.vertex3), (face.vertex2, face.vertex3)):
a = a.cloned_from or a
b = b.cloned_from or b
if a.id > b.id: a, b = b, a
if not couple_treated.has_key((a, b)):
# The collapse factor is simply the distance between the 2 points :-(
# This should be improved !!
if vector_dotproduct(a.normal, b.normal) < 0.9: continue
couple_collapse_factor.append((point_distance(a.loc, b.loc), a, b))
couple_treated[a, b] = 1
couple_collapse_factor.sort()
collapsed = {}
new_vertices = []
new_faces = []
for factor, v1, v2 in couple_collapse_factor:
# Determines if v1 collapses to v2 or v2 to v1.
# We choose to keep the vertex which is on the smaller number of faces, since
# this one has more chance of being in an extrimity of the body.
# Though heuristic, this rule yields very good results in practice.
if len(vertex2faces[v1]) < len(vertex2faces[v2]): v2, v1 = v1, v2
elif len(vertex2faces[v1]) == len(vertex2faces[v2]):
if collapsed.get(v1, 0): v2, v1 = v1, v2 # v1 already collapsed, try v2
if (not collapsed.get(v1, 0)) and (not collapsed.get(v2, 0)):
collapsed[v1] = 1
collapsed[v2] = 1
# Check if v2 is already colapsed
while v2.collapse_to: v2 = v2.collapse_to
common_faces = filter(vertex2faces[v1].__contains__, vertex2faces[v2])
v1.collapse_to = v2
v1.face_collapse_count = len(common_faces)
for clone in v1.clones:
# Find the clone of v2 that correspond to this clone of v1
possibles = []
for face in vertex2faces[clone]:
possibles.append(face.vertex1)
possibles.append(face.vertex2)
possibles.append(face.vertex3)
clone.collapse_to = v2
for vertex in v2.clones:
if vertex in possibles:
clone.collapse_to = vertex
break
clone.face_collapse_count = 0
new_vertices.append(clone)
# HACK -- all faces get collapsed with v1 (and no faces are collapsed with v1's
# clones). This is why we add v1 in new_vertices after v1's clones.
# This hack has no other incidence that consuming a little few memory for the
# extra faces if some v1's clone are collapsed but v1 is not.
new_vertices.append(v1)
self.nb_lodsteps += 1 + len(v1.clones)
new_faces.extend(common_faces)
for face in common_faces:
face.can_collapse = 1
# Updates vertex2faces
vertex2faces[face.vertex1].remove(face)
vertex2faces[face.vertex2].remove(face)
vertex2faces[face.vertex3].remove(face)
vertex2faces[v2].extend(vertex2faces[v1])
new_vertices.extend(filter(lambda vertex: not vertex.collapse_to, self.vertices))
new_vertices.reverse() # Cal3D want LODed vertices at the end
for i in range(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 range(len(new_vertices)): new_vertices[i].id = i
self.vertices = new_vertices
def to_cal3d(self):
s = struct.pack("iiiiii", self.material.id, len(self.vertices), len(self.faces), self.nb_lodsteps, len(self.springs), len(self.material.maps_filenames))
s += "".join(map(Vertex.to_cal3d, self.vertices))
s += "".join(map(Spring.to_cal3d, self.springs))
s += "".join(map(Face .to_cal3d, self.faces))
return s
class Vertex:
def __init__(self, submesh, loc, normal):
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.submesh = submesh
self.id = submesh.next_vertex_id
submesh.next_vertex_id += 1
submesh.vertices.append(self)
def to_cal3d(self):
if self.collapse_to: collapse_id = self.collapse_to.id
else: collapse_id = -1
s = struct.pack("ffffffii", self.loc[0], self.loc[1], self.loc[2], self.normal[0], self.normal[1], self.normal[2], collapse_id, self.face_collapse_count)
s += "".join(map(Map.to_cal3d, self.maps))
s += struct.pack("i", len(self.influences))
s += "".join(map(Influence.to_cal3d, self.influences))
if not self.weight is None: s += struct.pack("f", len(self.weight))
return s
class Map:
def __init__(self, u, v):
self.u = u
self.v = v
def to_cal3d(self):
return struct.pack("ff", self.u, self.v)
class Influence:
def __init__(self, bone, weight):
self.bone = bone
self.weight = weight
def to_cal3d(self):
return struct.pack("if", self.bone.id, self.weight)
class Spring:
def __init__(self, vertex1, vertex2):
self.vertex1 = vertex1
self.vertex2 = vertex2
self.spring_coefficient = 0.0
self.idlelength = 0.0
def to_cal3d(self):
return struct.pack("iiff", self.vertex1.id, self.vertex2.id, self.spring_coefficient, self.idlelength)
class Face:
def __init__(self, submesh, vertex1, vertex2, vertex3):
self.vertex1 = vertex1
self.vertex2 = vertex2
self.vertex3 = vertex3
self.can_collapse = 0
self.submesh = submesh
submesh.faces.append(self)
def to_cal3d(self):
return struct.pack("iii", self.vertex1.id, self.vertex2.id, self.vertex3.id)
class Skeleton:
def __init__(self):
self.bones = []
self.next_bone_id = 0
def to_cal3d(self):
s = "CSF\0" + struct.pack("ii", CAL3D_VERSION, len(self.bones))
s += "".join(map(Bone.to_cal3d, self.bones))
return s
BONES = {}
class Bone:
def __init__(self, skeleton, parent, name, loc, rot):
self.parent = parent
self.name = name
self.loc = loc
self.rot = rot
self.children = []
self.matrix = matrix_translate(quaternion2matrix(rot), loc)
if parent:
self.matrix = matrix_multiply(parent.matrix, self.matrix)
parent.children.append(self)
# 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.skeleton = skeleton
self.id = skeleton.next_bone_id
skeleton.next_bone_id += 1
skeleton.bones.append(self)
BONES[name] = self
def to_cal3d(self):
s = struct.pack("i", len(self.name) + 1) + self.name + "\0"
# We need to negate quaternion W value, but why ?
s += struct.pack("ffffffffffffff", self.loc[0], self.loc[1], self.loc[2], self.rot[0], self.rot[1], self.rot[2], -self.rot[3], self.lloc[0], self.lloc[1], self.lloc[2], self.lrot[0], self.lrot[1], self.lrot[2], -self.lrot[3])
if self.parent: s += struct.pack("i", self.parent.id)
else: s += struct.pack("i", -1)
s += struct.pack("i", len(self.children))
s += "".join(map(lambda bone: struct.pack("i", bone.id), self.children))
return s
class Animation:
def __init__(self, name, duration = 0.0):
self.name = name
self.duration = duration
self.tracks = {} # Map bone names to tracks
def to_cal3d(self):
s = "CAF\0" + struct.pack("ifi", CAL3D_VERSION, self.duration, len(self.tracks))
s += "".join(map(Track.to_cal3d, self.tracks.values()))
return s
class Track:
def __init__(self, animation, bone):
self.bone = bone
self.keyframes = []
self.animation = animation
animation.tracks[bone.name] = self
def to_cal3d(self):
s = struct.pack("ii", self.bone.id, len(self.keyframes))
s += "".join(map(KeyFrame.to_cal3d, self.keyframes))
return s
class KeyFrame:
def __init__(self, track, time, loc, rot):
self.time = time
self.loc = loc
self.rot = rot
self.track = track
track.keyframes.append(self)
def to_cal3d(self):
# We need to negate quaternion W value, but why ?
return struct.pack("ffffffff", self.time, self.loc[0], self.loc[1], self.loc[2], self.rot[0], self.rot[1], self.rot[2], -self.rot[3])
def export():
# Get the scene
scene = Blender.Scene.getCurrent()
# Export skeleton (=armature)
skeleton = Skeleton()
for obj in Blender.Object.Get():
data = obj.getData()
if type(data) is Blender.Types.ArmatureType:
matrix = obj.getMatrix()
if BASE_MATRIX: matrix = matrix_multiply(BASE_MATRIX, matrix)
def treat_bone(b, parent = None):
head = b.getHead()
tail = b.getTail()
# Turns the Blender's head-tail-roll notation into a quaternion
quat = matrix2quaternion(blender_bone2matrix(head, tail, b.getRoll()))
if 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(parent.rot))
parent_head = vector_by_matrix(parent.head, parent_invert_transform)
parent_tail = vector_by_matrix(parent.tail, parent_invert_transform)
bone = Bone(skeleton, parent, b.getName(), [parent_tail[0] - parent_head[0] + head[0], parent_tail[1] - parent_head[1] + head[1], parent_tail[2] - parent_head[2] + head[2]], quat)
else:
# Apply the armature's matrix to the root bones
head = point_by_matrix(head, matrix)
tail = point_by_matrix(tail, matrix)
quat = matrix2quaternion(matrix_multiply(matrix, quaternion2matrix(quat))) # Probably not optimal
# Here, the translation is simply the head vector
bone = Bone(skeleton, parent, b.getName(), head, quat)
bone.head = head
bone.tail = tail
for child in b.getChildren(): treat_bone(child, bone)
for b in data.getBones(): treat_bone(b)
# Only one armature / skeleton
break
# Export Mesh data
meshes = []
for obj in Blender.Object.Get():
data = obj.getData()
if (type(data) is Blender.Types.NMeshType) and data.faces:
mesh = Mesh(obj.name)
meshes.append(mesh)
matrix = obj.getMatrix()
if BASE_MATRIX: matrix = matrix_multiply(BASE_MATRIX, matrix)
faces = data.faces
while faces:
image = faces[0].image
image_filename = image and image.filename
material = MATERIALS.get(image_filename) or Material(image_filename)
# TODO add material color support here
submesh = SubMesh(mesh, material)
vertices = {}
for face in faces[:]:
if (face.image and face.image.filename) == image_filename:
faces.remove(face)
if not face.smooth:
p1 = face.v[0].co
p2 = face.v[1].co
p3 = face.v[2].co
normal = vector_normalize(vector_by_matrix(vector_crossproduct(
[p3[0] - p2[0], p3[1] - p2[1], p3[2] - p2[2]],
[p1[0] - p2[0], p1[1] - p2[1], p1[2] - p2[2]],
), matrix))
face_vertices = []
for i in range(len(face.v)):
vertex = vertices.get(face.v[i].index)
if not vertex:
coord = point_by_matrix (face.v[i].co, matrix)
if face.smooth: normal = vector_normalize(vector_by_matrix(face.v[i].no, matrix))
vertex = vertices[face.v[i].index] = Vertex(submesh, coord, normal)
influences = data.getVertexInfluences(face.v[i].index)
if not influences: print "Warning ! A vertex has no influence !"
# 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:
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 = Vertex(submesh, vertex.loc, normal)
vertex.cloned_from = old_vertex
vertex.influences = old_vertex.influences
old_vertex.clones.append(vertex)
if data.hasFaceUV():
uv = [face.uv[i][0], 1.0 - face.uv[i][1]]
if not vertex.maps: 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 = Vertex(submesh, vertex.loc, vertex.normal)
vertex.cloned_from = old_vertex
vertex.influences = old_vertex.influences
vertex.maps.append(Map(*uv))
old_vertex.clones.append(vertex)
face_vertices.append(vertex)
# Split faces with more than 3 vertices
for i in range(1, len(face.v) - 1):
Face(submesh, face_vertices[0], face_vertices[i], face_vertices[i + 1])
# Computes LODs info
if LODS: submesh.compute_lods()
# Export animations
ANIMATIONS = {}
for ipo in Blender.Ipo.Get():
name = ipo.getName()
# Try to extract the animation name and the bone name from the IPO name.
# THIS MAY NOT WORK !!!
# The animation name extracted here is usually NOT the name of the action in Blender
splitted = name.split(".")
if len(splitted) == 2:
animation_name, bone_name = splitted
animation_name += ".000"
elif len(splitted) == 3:
animation_name, a, b = splitted
if a[0] in string.digits:
animation_name += "." + a
bone_name = b
elif b[0] in string.digits:
animation_name += "." + b
bone_name = a
else:
print "Un-analysable IPO name :", name
continue
else:
print "Un-analysable IPO name :", name
continue
animation = ANIMATIONS.get(animation_name)
if not animation:
animation = ANIMATIONS[animation_name] = Animation(animation_name)
bone = BONES[bone_name]
track = animation.tracks.get(bone_name)
if not track:
track = animation.tracks[bone_name] = Track(animation, bone)
track.finished = 0
nb_curve = ipo.getNcurves()
has_loc = nb_curve in (3, 7)
has_rot = nb_curve in (4, 7)
# TODO support size here
# Cal3D does not support it yet.
try: nb_bez_pts = ipo.getNBezPoints(0)
except TypeError:
print "No key frame for animation %s, bone %s, skipping..." % (animation_name, bone_name)
nb_bez_pts = 0
for bez in range(nb_bez_pts): # WARNING ! May not work if not loc !!!
time = ipo.getCurveBeztriple(0, bez)[3]
scene.currentFrame(int(time))
# Needed to update IPO's value, but probably not the best way for that...
scene.makeCurrent()
# Convert time units from Blender's frame (starting at 1) to second
# (using default FPS of 25)
time = (time - 1.0) / 25.0
if animation.duration < time: animation.duration = time
loc = bone.loc
rot = bone.rot
curves = ipo.getCurves()
print curves
curve_id = 0
while curve_id < len(curves):
curve_name = curves[curve_id].getName()
if curve_name == "LocX":
# Get the translation
# We need to blend the translation from the bone rest state (=bone.loc) with
# the translation due to IPO.
trans = vector_by_matrix((
ipo.getCurveCurval(curve_id),
ipo.getCurveCurval(curve_id + 1),
ipo.getCurveCurval(curve_id + 2),
), bone.matrix)
loc = [
bone.loc[0] + trans[0],
bone.loc[1] + trans[1],
bone.loc[2] + trans[2],
]
curve_id += 3
elif curve_name == "RotX":
# Get the rotation of the IPO
ipo_rot = [
ipo.getCurveCurval(curve_id),
ipo.getCurveCurval(curve_id + 1),
ipo.getCurveCurval(curve_id + 2),
ipo.getCurveCurval(curve_id + 3),
]
curve_id += 3 # XXX Strange !!!
# We need to blend the rotation from the bone rest state (=bone.rot) with
# ipo_rot.
rot = quaternion_multiply(ipo_rot, bone.rot)
else:
print "Unknown IPO curve : %s" % curve_name
break #Unknown curves
KeyFrame(track, time, loc, rot)
# Save all data
if not os.path.exists(SAVE_TO_DIR): os.makedirs(SAVE_TO_DIR)
else:
for file in os.listdir(SAVE_TO_DIR):
if file.endswith(".cfg") or file.endswith(".caf") or file.endswith(".cmf") or file.endswith(".csf") or file.endswith(".crf"):
os.unlink(os.path.join(SAVE_TO_DIR, file))
cfg = open(os.path.join(SAVE_TO_DIR, os.path.basename(SAVE_TO_DIR) + ".cfg"), "wb")
print >> cfg, "# Cal3D model exported from Blender with blender2cal3d.py"
print >> cfg
open(os.path.join(SAVE_TO_DIR, os.path.basename(SAVE_TO_DIR) + ".csf"), "wb").write(skeleton.to_cal3d())
print >> cfg, "skeleton=%s.csf" % os.path.basename(SAVE_TO_DIR)
print >> cfg
for animation in ANIMATIONS.values():
if animation.duration: # Cal3D does not support animation with only one state
animation.name = RENAME_ANIMATIONS.get(animation.name) or animation.name
open(os.path.join(SAVE_TO_DIR, animation.name + ".caf"), "wb").write(animation.to_cal3d())
print >> cfg, "animation=%s.caf" % animation.name
# Prints animation names and durations, in order to help identifying animation
# (since their name are lost).
print animation.name, "duration", animation.duration * 25.0 + 1.0
print >> cfg
for mesh in meshes:
if not mesh.name.startswith("_"):
open(os.path.join(SAVE_TO_DIR, mesh.name + ".cmf"), "wb").write(mesh.to_cal3d())
print >> cfg, "mesh=%s.cmf" % mesh.name
print >> cfg
materials = MATERIALS.values()
materials.sort(lambda a, b: cmp(a.id, b.id))
for material in materials:
if material.maps_filenames: filename = os.path.splitext(os.path.basename(material.maps_filenames[0]))[0]
else: filename = "plain"
open(os.path.join(SAVE_TO_DIR, filename + ".crf"), "wb").write(material.to_cal3d())
print >> cfg, "material=%s.crf" % filename
print >> cfg
print "Saved to", SAVE_TO_DIR
print "Done."
export()