==scripts ==

latest update blender2cal3d by Jean-Baptiste LAMY, fixes script to give correct output with current CVS
This commit is contained in:
2006-04-19 00:54:21 +00:00
parent 9bfd86730d
commit 98b64dc93b

View File

@@ -1,7 +1,7 @@
#!BPY #!BPY
""" """
Name: 'Cal3D v0.9' Name: 'Cal3D'
Blender: 235 Blender: 235
Group: 'Export' Group: 'Export'
Tip: 'Export armature/bone/mesh/action data to the Cal3D format.' Tip: 'Export armature/bone/mesh/action data to the Cal3D format.'
@@ -26,65 +26,78 @@ Tip: 'Export armature/bone/mesh/action data to the Cal3D format.'
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
__version__ = "0.11" __version__ = "0.13"
__author__ = "Jean-Baptiste 'Jiba' Lamy" __author__ = "Jean-Baptiste 'Jiba' Lamy"
__email__ = ["Author's email, jibalamy:free*fr"] __email__ = "jibalamy@free.fr"
__url__ = ["Soya3d's homepage, http://home.gna.org/oomadness/en/soya/", __url__ = "Soya3d's homepage http://home.gna.org/oomadness/en/soya/"
"Cal3d, http://cal3d.sourceforge.net"] __bpydoc__ = """This script is a Blender => Cal3D converter.
__bpydoc__ = """\
This script is a Blender => Cal3D converter.
(See http://blender.org and http://cal3d.sourceforge.net) (See http://blender.org and http://cal3d.sourceforge.net)
USAGE: USAGE
To install it, place the script in your $HOME/.blender/scripts directory. To install it, place the script in your $HOME/.blender/scripts directory.
Then open the File->Export->Cal3d menu. And select the filename of the .cfg file.
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, The exporter will create a set of other files with same prefix (ie. bla.cfg, bla.xsf,
bla_Action1.xaf, bla_Action2.xaf, ...). bla_Action1.xaf, bla_Action2.xaf, ...).
You should be able to open the .cfg file in cal3d_miniviewer. You should be able to open the .cfg file in cal3d_miniviewer.
NOT (YET) SUPPORTED: NOT (YET) SUPPORTED
- Rotation, translation, or stretching Blender objects is still quite - Rotation, translation, or stretching Blender objects is still quite
buggy, so AVOID MOVING / ROTATING / RESIZE OBJECTS (either mesh or armature) ! buggy, so AVOID MOVING / ROTATING / RESIZE OBJECTS (either mesh or armature) !
Instead, edit the object (with tab), select all points / bones (with "a"), Instead, edit the object (with tab), select all points / bones (with "a"),
and move / rotate / resize them.<br> and move / rotate / resize them.
- no support for exporting springs yet<br> - no support for exporting springs yet
- no support for exporting material colors (most games should only use images - no support for exporting material colors (most games should only use images
I think...) I think...)
KNOWN ISSUES: KNOWN ISSUES
- Cal3D versions <=0.9.1 have a bug where animations aren't played when the root bone - Cal3D versions <=0.9.1 have a bug where animations aren't played when the root bone
is not animated;<br> is not animated
- Cal3D versions <=0.9.1 have a bug where objects that aren't influenced by any bones - Cal3D versions <=0.9.1 have a bug where objects that aren't influenced by any bones
are not drawn (fixed in Cal3D CVS). are not drawn (fixed in Cal3D CVS)
NOTES: NOTES
It requires a very recent version of Blender (>= 2.35). It requires a very recent version of Blender (tested with 2.41).
Build a model following a few rules:<br> Build a model follows a few rules:
- Use only a single armature;<br> - Use only a single armature
- Use only a single rootbone (Cal3D doesn't support floating bones);<br> - Use only a single rootbone (Cal3D doesn't support floating bones)
- Use only locrot keys (Cal3D doesn't support bone's size change);<br> - 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 - Don't try to create child/parent constructs in blender object, that gets exported
incorrectly at the moment;<br> incorrectly at the moment
- Don't put "." in action or bone names, and do not start these names by a figure;<br> - Objects or animations whose names start by "_" are not exported (hidden object)
- Objects or animations whose names start by "_" are not exported (hidden object).
It can be run in batch mode, as following :<br> It can be run in batch mode, as following :
blender model.blend -P blender2cal3d.py --blender2cal3d FILENAME=model.cfg EXPORT_FOR_SOYA=1 blender model.blend -P blender2cal3d.py --blender2cal3d FILENAME=model.cfg EXPORT_FOR_SOYA=1
You can pass as many parameters as you want at the end, "EXPORT_FOR_SOYA=1" is just an 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. exemple. The parameters are the same than below.
Logging is sent through python logger instance. you can control verbosity by changing
log.set_level(DEBUG|WARNING|ERROR|CRITICAL). to print to it use log.debug(),
log.warning(), log.error() and log.critical(). the logger breaks normal operation
by printing _all_ info messages. ( log.info() ). Output is sent to stdout and to
a blender text.
Psyco support, turned off by default. can speed up export by 25% on my tests.
Turned off by default.
hotshot_export function to see profile. just replace export with hotshot export
to see.
Any vertices found without influence are placed into a vertex group called
"_no_inf". [FIXME] i need to stop this for batch more
running under gui mode the last export dir is saved into the blender registry
and called back again.
""" """
# Parameters : # Parameters :
# Filename to export to (if "", display a file selector dialog). # Filename to export to (if "", display a file selector dialog).
@@ -109,6 +122,12 @@ PREFIX_FILE_WITH_MODEL_NAME = 0
# Set to 0 to use Cal3D binary format # Set to 0 to use Cal3D binary format
XML = 1 XML = 1
# Configuration text buffer
CONFIG_TEXT = ""
# Allows to replace a material by another
MATERIAL_MAP = {}
MESSAGES = "" MESSAGES = ""
@@ -121,12 +140,112 @@ MESSAGES = ""
# Most of the hell of it is to deal with Blender's head-tail-roll bone's definition. # 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 sys, os, os.path, struct, math, string
try:
import psyco
psyco.full()
except:
print "* Blender2Cal3D * (Psyco not found)"
import Blender import Blender
from Blender import Registry
from Blender.Window import DrawProgressBar
from Blender import Draw, BGL
import logging
reload(logging)
import types
import textwrap
# HACK -- it seems that some Blender versions don't define sys.argv, # HACK -- it seems that some Blender versions don't define sys.argv,
# which may crash Python if a warning occurs. # which may crash Python if a warning occurs.
if not hasattr(sys, "argv"): sys.argv = ["???"] if not hasattr(sys, "argv"): sys.argv = ["???"]
# our own logger class. it works just the same as a normal logger except
# all info messages get show.
class Logger(logging.Logger):
def __init__(self,name,level=logging.NOTSET):
logging.Logger.__init__(self,name,level)
self.has_warnings=False
self.has_errors=False
self.has_critical=False
def info(self,msg,*args,**kwargs):
apply(self._log,(logging.INFO,msg,args),kwargs)
def warning(self,msg,*args,**kwargs):
logging.Logger.warning(self,msg,*args,**kwargs)
self.has_warnings=True
def error(self,msg,*args,**kwargs):
logging.Logger.error(self,msg,*args,**kwargs)
self.has_errors=True
def critical(self,msg,*args,**kwargs):
logging.Logger.critical(self,msg,*args,**kwargs)
self.has_errors=True
# should be able to make this print to stdout in realtime and save MESSAGES
# as well. perhaps also have a log to file option
class LogHandler(logging.StreamHandler):
def __init__(self):
logging.StreamHandler.__init__(self,sys.stdout)
if "blender2cal3d_log" not in Blender.Text.Get():
self.outtext=Blender.Text.New("blender2cal3d_log")
else:
self.outtext=Blender.Text.Get('blender2cal3d_log')
self.outtext.clear()
self.lastmsg=''
def emit(self,record):
# print to stdout and to a new blender text object
msg=self.format(record)
if msg==self.lastmsg:
return
self.lastmsg=msg
self.outtext.write("%s\n" %msg)
logging.StreamHandler.emit(self,record)
"""
try:
msg=self.format(record)
if not hasattr(types,"UnicodeType"):
self.stream.write("%s\n" % msg)
else:
try:
self.stream.write("%s\n" % msg)
except UnicodeError:
self.stream.write("%s\n" % msg.encode("UTF-8"))
self.flush()
except:
self.handleError(record)
"""
logging.setLoggerClass(Logger)
log=logging.getLogger('blender2cal3d')
handler=LogHandler()
formatter=logging.Formatter('%(levelname)s %(message)s')
handler.setFormatter(formatter)
log.addHandler(handler)
# set this to minimum output level. eg. logging.DEBUG, logging.WARNING, logging.ERROR
# logging.CRITICAL. logging.INFO will make little difference as these always get
# output'd
log.setLevel(logging.WARNING)
log.info("Starting...")
# transforms a blender to a cal3d quaternion notation (x,y,z,w) # transforms a blender to a cal3d quaternion notation (x,y,z,w)
def blender2cal3dquat(q): def blender2cal3dquat(q):
@@ -147,21 +266,57 @@ def quaternion2matrix(q):
[ 2.0 * (xz + wy), 2.0 * (yz - wx), 1.0 - 2.0 * (xx + yy), 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]] [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 matrix2quaternion(m): def matrix2quaternion(m):
s = math.sqrt(abs(m[0][0] + m[1][1] + m[2][2] + m[3][3])) t = m[0][0] + m[1][1] + m[2][2] + m[3][3]
if s == 0.0: if t > 0.00000001:
x = abs(m[2][1] - m[1][2]) s = math.sqrt(t) * 2
y = abs(m[0][2] - m[2][0]) return quaternion_normalize([
z = abs(m[1][0] - m[0][1]) -(m[2][1] - m[1][2]) / s,
if (x >= y) and (x >= z): return 1.0, 0.0, 0.0, 0.0 -(m[0][2] - m[2][0]) / s,
elif (y >= x) and (y >= z): return 0.0, 1.0, 0.0, 0.0 -(m[1][0] - m[0][1]) / s,
else: return 0.0, 0.0, 1.0, 0.0 0.25 * s,
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,
]) ])
else:
if (m[0][0] > m[1][1]) and (m[0][0] > m[2][2]):
s = math.sqrt(1.0 + m[0][0] - m[1][1] - m[2][2]) * 2
return quaternion_normalize([
0.25 * s,
-(m[1][0] + m[0][1]) / s,
-(m[0][2] + m[2][0]) / s,
-(m[2][1] - m[1][2]) / s,
])
elif m[1][1] > m[2][2]:
s = math.sqrt(1.0 + m[1][1] - m[0][0] - m[2][2]) * 2
return quaternion_normalize([
-(m[1][0] + m[0][1]) / s,
0.25 * s,
-(m[2][1] + m[1][2]) / s,
-(m[0][2] - m[2][0]) / s,
])
else:
s = math.sqrt(1.0 + m[2][2] - m[0][0] - m[1][1]) * 2
return quaternion_normalize([
-(m[0][2] + m[2][0]) / s,
-(m[2][1] + m[1][2]) / s,
0.25 * s,
-(m[1][0] - m[0][1]) / s,
])
def quaternion_normalize(q): def quaternion_normalize(q):
l = math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]) l = math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3])
@@ -314,7 +469,8 @@ def vector_length(v):
def vector_normalize(v): def vector_normalize(v):
l = math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) 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 if l: return v[0] / l, v[1] / l, v[2] / l
else: return v
def vector_dotproduct(v1, v2): def vector_dotproduct(v1, v2):
return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2] return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]
@@ -391,6 +547,9 @@ class Material:
self.specular_b = 255 self.specular_b = 255
self.specular_a = 255 self.specular_a = 255
self.shininess = 1.0 self.shininess = 1.0
log.debug("Material with name %s",map_filename)
if map_filename: self.maps_filenames = [map_filename] if map_filename: self.maps_filenames = [map_filename]
else: self.maps_filenames = [] else: self.maps_filenames = []
@@ -417,6 +576,7 @@ class Material:
s += " <DIFFUSE>" + str(self.diffuse_r) + " " + str(self.diffuse_g) + " " + str(self.diffuse_b) + " " + str(self.diffuse_a) + "</DIFFUSE>\n"; s += " <DIFFUSE>" + str(self.diffuse_r) + " " + str(self.diffuse_g) + " " + str(self.diffuse_b) + " " + str(self.diffuse_a) + "</DIFFUSE>\n";
s += " <SPECULAR>" + str(self.specular_r) + " " + str(self.specular_g) + " " + str(self.specular_b) + " " + str(self.specular_a) + "</SPECULAR>\n"; s += " <SPECULAR>" + str(self.specular_r) + " " + str(self.specular_g) + " " + str(self.specular_b) + " " + str(self.specular_a) + "</SPECULAR>\n";
s += " <SHININESS>" + str(self.shininess) + "</SHININESS>\n"; s += " <SHININESS>" + str(self.shininess) + "</SHININESS>\n";
for map_filename in self.maps_filenames: for map_filename in self.maps_filenames:
s += " <MAP>" + map_filename + "</MAP>\n"; s += " <MAP>" + map_filename + "</MAP>\n";
@@ -428,6 +588,7 @@ MATERIALS = {}
class Mesh: class Mesh:
def __init__(self, name): def __init__(self, name):
name=string.replace(name,'.','_')
self.name = name self.name = name
self.submeshes = [] self.submeshes = []
@@ -464,7 +625,8 @@ class SubMesh:
def compute_lods(self): def compute_lods(self):
"""Computes LODs info for Cal3D (there's no Blender related stuff here).""" """Computes LODs info for Cal3D (there's no Blender related stuff here)."""
print "Start LODs computation..." log.info("Start LODs computation...")
vertex2faces = {} vertex2faces = {}
for face in self.faces: for face in self.faces:
for vertex in (face.vertex1, face.vertex2, face.vertex3): for vertex in (face.vertex1, face.vertex2, face.vertex3):
@@ -555,7 +717,7 @@ class SubMesh:
new_faces.reverse() # Cal3D want LODed faces at the end new_faces.reverse() # Cal3D want LODed faces at the end
self.faces = new_faces self.faces = new_faces
print "LODs computed : %s vertices can be removed (from a total of %s)." % (self.nb_lodsteps, len(self.vertices)) log.info("LODs computed : %s vertices can be removed (from a total of %s)." % (self.nb_lodsteps, len(self.vertices)))
def rename_vertices(self, new_vertices): def rename_vertices(self, new_vertices):
"""Rename (change ID) of all vertices, such as self.vertices == new_vertices.""" """Rename (change ID) of all vertices, such as self.vertices == new_vertices."""
@@ -608,7 +770,7 @@ class Vertex:
s += "".join(map(Influence.to_cal3d, self.influences)) s += "".join(map(Influence.to_cal3d, self.influences))
if not self.weight is None: s += struct.pack("f", len(self.weight)) if not self.weight is None: s += struct.pack("f", len(self.weight))
return s return s
def to_cal3d_xml(self): def to_cal3d_xml(self):
if self.collapse_to: if self.collapse_to:
collapse_id = self.collapse_to.id collapse_id = self.collapse_to.id
@@ -710,12 +872,14 @@ BONES = {}
class Bone: class Bone:
def __init__(self, skeleton, parent, name, loc, rot): def __init__(self, skeleton, parent, name, loc, rot):
self.parent = parent self.parent = parent
name=string.replace(name,'.','_')
self.name = name self.name = name
self.loc = loc self.loc = loc
self.rot = rot self.rot = rot
self.children = [] self.children = []
self.matrix = matrix_translate(quaternion2matrix(rot), loc) self.matrix = self.local_matrix = matrix_translate(quaternion2matrix(rot), loc)
if parent: if parent:
self.matrix = matrix_multiply(parent.matrix, self.matrix) self.matrix = matrix_multiply(parent.matrix, self.matrix)
parent.children.append(self) parent.children.append(self)
@@ -767,6 +931,7 @@ class Bone:
class Animation: class Animation:
def __init__(self, name, duration = 0.0): def __init__(self, name, duration = 0.0):
name=string.replace(name,'.','_')
self.name = name self.name = name
self.duration = duration self.duration = duration
self.tracks = {} # Map bone names to tracks self.tracks = {} # Map bone names to tracks
@@ -817,7 +982,7 @@ class KeyFrame:
def to_cal3d(self): def to_cal3d(self):
# We need to negate quaternion W value, but why ? # 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]) 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 to_cal3d_xml(self): def to_cal3d_xml(self):
s = " <KEYFRAME TIME=\"%f\">\n" % self.time s = " <KEYFRAME TIME=\"%f\">\n" % self.time
s += " <TRANSLATION>%f %f %f</TRANSLATION>\n" % \ s += " <TRANSLATION>%f %f %f</TRANSLATION>\n" % \
@@ -827,7 +992,7 @@ class KeyFrame:
(self.rot[0], self.rot[1], self.rot[2], -self.rot[3]) (self.rot[0], self.rot[1], self.rot[2], -self.rot[3])
s += " </KEYFRAME>\n" s += " </KEYFRAME>\n"
return s return s
def export(filename): def export(filename):
global MESSAGES global MESSAGES
@@ -839,9 +1004,11 @@ def export(filename):
scene = Blender.Scene.getCurrent() scene = Blender.Scene.getCurrent()
# ---- Export skeleton (=armature) ---------------------------------------- # ---- Export skeleton (=armature) ----------------------------------------
if Blender.mode == 'interactive': DrawProgressBar(0.0,'Exporting skeleton...')
skeleton = Skeleton() skeleton = Skeleton()
foundarmature = False foundarmature = False
for obj in Blender.Object.Get(): for obj in Blender.Object.Get():
data = obj.getData() data = obj.getData()
@@ -849,7 +1016,7 @@ def export(filename):
continue continue
if foundarmature == True: if foundarmature == True:
MESSAGES += "Found multiple armatures! '" + obj.getName() + "' ignored.\n" log.error("Found multiple armatures! '" + obj.getName() + "' ignored.\n")
continue continue
foundarmature = True foundarmature = True
@@ -858,11 +1025,11 @@ def export(filename):
matrix = matrix_multiply(BASE_MATRIX, matrix) matrix = matrix_multiply(BASE_MATRIX, matrix)
def treat_bone(b, parent = None): def treat_bone(b, parent = None):
head = b.getHead() head = b.head["BONESPACE"]
tail = b.getTail() tail = b.tail["BONESPACE"]
# Turns the Blender's head-tail-roll notation into a quaternion # Turns the Blender's head-tail-roll notation into a quaternion
quat = matrix2quaternion(blender_bone2matrix(head, tail, b.getRoll())) quat = matrix2quaternion(blender_bone2matrix(head, tail, b.roll["BONESPACE"]))
if parent: if parent:
# Compute the translation from the parent bone's head to the child # Compute the translation from the parent bone's head to the child
@@ -874,8 +1041,10 @@ def export(filename):
parent_invert_transform = matrix_invert(quaternion2matrix(parent.rot)) parent_invert_transform = matrix_invert(quaternion2matrix(parent.rot))
parent_head = vector_by_matrix(parent.head, parent_invert_transform) parent_head = vector_by_matrix(parent.head, parent_invert_transform)
parent_tail = vector_by_matrix(parent.tail, parent_invert_transform) parent_tail = vector_by_matrix(parent.tail, parent_invert_transform)
ploc = vector_add(head, b.getLoc())
#ploc = vector_add(head, b.getLoc())
parentheadtotail = vector_sub(parent_tail, parent_head) parentheadtotail = vector_sub(parent_tail, parent_head)
# hmm this should be handled by the IPos, but isn't for non-animated # hmm this should be handled by the IPos, but isn't for non-animated
# bones which are transformed in the pose mode... # bones which are transformed in the pose mode...
@@ -884,7 +1053,9 @@ def export(filename):
loc = parentheadtotail loc = parentheadtotail
rot = quat rot = quat
bone = Bone(skeleton, parent, b.getName(), loc, rot) log.debug("Parented Bone: %s",b.name)
bone = Bone(skeleton, parent, b.name, loc, rot)
else: else:
# Apply the armature's matrix to the root bones # Apply the armature's matrix to the root bones
head = point_by_matrix(head, matrix) head = point_by_matrix(head, matrix)
@@ -896,29 +1067,34 @@ def export(filename):
loc = head loc = head
rot = quat rot = quat
log.debug("Non Parented Bone: %s",b.name)
# Here, the translation is simply the head vector # Here, the translation is simply the head vector
bone = Bone(skeleton, None, b.getName(), loc, rot) bone = Bone(skeleton, None, b.name, loc, rot)
bone.head = head bone.head = head
bone.tail = tail bone.tail = tail
for child in b.getChildren(): if b.hasChildren():
treat_bone(child, bone) for child in b.children:
treat_bone(child, bone)
foundroot = False foundroot = False
for b in data.getBones(): for b in data.bones.values():
# child bones are handled in treat_bone # child bones are handled in treat_bone
if b.getParent() != None: if b.parent != None:
continue continue
if foundroot == True: if foundroot == True:
print "Warning: Found multiple root-bones, this may not be supported in cal3d." log.warning("Warning: Found multiple root-bones, this may not be supported in cal3d.")
#print "Ignoring bone '" + b.getName() + "' and it's childs." #print "Ignoring bone '" + b.name + "' and it's childs."
#continue #continue
treat_bone(b) treat_bone(b)
foundroot = True foundroot = True
# ---- Export Mesh data --------------------------------------------------- # ---- Export Mesh data ---------------------------------------------------
if Blender.mode == 'interactive': DrawProgressBar(0.3,'Exporting meshes...')
meshes = [] meshes = []
@@ -926,6 +1102,10 @@ def export(filename):
data = obj.getData() data = obj.getData()
if (type(data) is Blender.Types.NMeshType) and data.faces: if (type(data) is Blender.Types.NMeshType) and data.faces:
mesh_name = obj.getName() mesh_name = obj.getName()
if mesh_name[0]=='_': continue
log.debug("Mesh: %s",mesh_name)
mesh = Mesh(mesh_name) mesh = Mesh(mesh_name)
meshes.append(mesh) meshes.append(mesh)
@@ -937,7 +1117,13 @@ def export(filename):
while faces: while faces:
image = faces[0].image image = faces[0].image
image_filename = image and image.filename image_filename = image and image.filename
material = MATERIALS.get(image_filename) or Material(image_filename) image_name = os.path.splitext(os.path.basename(image_filename))[0]
#print "MATERIAL", image_filename, image_name
if MATERIAL_MAP.has_key(image_name):
image_filename2 = os.path.join(os.path.dirname(image_filename), MATERIAL_MAP[image_name] + os.path.splitext(image_filename)[1])
#print "=>", image_filename
else: image_filename2 = image_filename
material = MATERIALS.get(image_filename2) or Material(image_filename2)
outputuv = len(material.maps_filenames) > 0 outputuv = len(material.maps_filenames) > 0
# TODO add material color support here # TODO add material color support here
@@ -949,9 +1135,14 @@ def export(filename):
faces.remove(face) faces.remove(face)
if not face.smooth: if not face.smooth:
p1 = face.v[0].co try:
p2 = face.v[1].co p1 = face.v[0].co
p3 = face.v[2].co p2 = face.v[1].co
p3 = face.v[2].co
except IndexError:
log.error("You have faces with less that three verticies!")
continue
normal = vector_normalize(vector_by_matrix(vector_crossproduct( normal = vector_normalize(vector_by_matrix(vector_crossproduct(
[p3[0] - p2[0], p3[1] - p2[1], p3[2] - p2[2]], [p3[0] - p2[0], p3[1] - p2[1], p3[2] - p2[2]],
[p1[0] - p2[0], p1[1] - p2[1], p1[2] - p2[2]], [p1[0] - p2[0], p1[1] - p2[1], p1[2] - p2[2]],
@@ -961,11 +1152,11 @@ def export(filename):
for i in range(len(face.v)): for i in range(len(face.v)):
vertex = vertices.get(face.v[i].index) vertex = vertices.get(face.v[i].index)
if not vertex: if not vertex:
coord = point_by_matrix (face.v[i].co, matrix) coord = point_by_matrix (face.v[i].co, matrix)
if face.smooth: if face.smooth:
normal = vector_normalize(vector_by_matrix(face.v[i].no, matrix)) normal = vector_normalize(vector_by_matrix(face.v[i].no, matrix))
vertex = vertices[face.v[i].index] = Vertex(submesh, coord, normal) vertex = vertices[face.v[i].index] = Vertex(submesh, coord, normal)
influences = data.getVertexInfluences(face.v[i].index) influences = data.getVertexInfluences(face.v[i].index)
# should this really be a warning? (well currently enabled, # should this really be a warning? (well currently enabled,
# because blender has some bugs where it doesn't return # because blender has some bugs where it doesn't return
@@ -973,8 +1164,11 @@ def export(filename):
# cal3d<=0.9.1 had bugs where objects without influences # cal3d<=0.9.1 had bugs where objects without influences
# aren't drawn. # aren't drawn.
if not influences: if not influences:
MESSAGES += "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)\n" \ log.error("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)\n. The vertex has been added to a vertex group called _no_inf" % obj.getName())
% obj.getName() if '_no_inf' not in data.getVertGroupNames():
data.addVertGroup('_no_inf')
data.assignVertsToGroup('_no_inf',[face.v[i].index],0.5,'add')
# sum of influences is not always 1.0 in Blender ?!?! # sum of influences is not always 1.0 in Blender ?!?!
sum = 0.0 sum = 0.0
@@ -982,9 +1176,12 @@ def export(filename):
sum += weight sum += weight
for bone_name, weight in influences: for bone_name, weight in influences:
bone_name=string.replace(bone_name,'.','_')
if bone_name=='':
log.critical('Found bone with no name which influences %s' % obj.getName())
continue
if bone_name not in BONES: if bone_name not in BONES:
MESSAGES += "Couldn't find bone '%s' which influences" \ log.error("Couldn't find bone '%s' which influences object '%s'.\n" % (bone_name, obj.getName()))
"object '%s'.\n" % (bone_name, obj.getName())
continue continue
vertex.influences.append(Influence(BONES[bone_name], weight / sum)) vertex.influences.append(Influence(BONES[bone_name], weight / sum))
@@ -1032,18 +1229,23 @@ def export(filename):
submesh.compute_lods() submesh.compute_lods()
# ---- Export animations -------------------------------------------------- # ---- Export animations --------------------------------------------------
if Blender.mode == 'interactive': DrawProgressBar(0.7,'Exporting animations...')
ANIMATIONS = {} ANIMATIONS = {}
for a in Blender.Armature.NLA.GetActions().iteritems(): for a in Blender.Armature.NLA.GetActions().iteritems():
animation_name = a[0] animation_name = a[0]
log.debug( "Animation: %s",animation_name)
animation = Animation(animation_name) animation = Animation(animation_name)
animation.duration = 0.0 animation.duration = 0.0
for b in a[1].getAllChannelIpos().iteritems(): for b in a[1].getAllChannelIpos().iteritems():
bone_name = b[0] bone_name = string.replace(b[0],'.','_')
if bone_name not in BONES: if bone_name not in BONES:
MESSAGES += "No Bone '" + bone_name + "' defined (from Animation '" \ log.error("No Bone '" + bone_name + "' defined (from Animation '" + animation_name + "' ?!?\n")
+ animation_name + "' ?!?\n"
continue continue
bone = BONES[bone_name] bone = BONES[bone_name]
@@ -1066,8 +1268,8 @@ def export(filename):
curve_name = curve.getName() curve_name = curve.getName()
if curve_name not in ["QuatW", "QuatX", "QuatY", "QuatZ", "LocX", "LocY", "LocZ"]: if curve_name not in ["QuatW", "QuatX", "QuatY", "QuatZ", "LocX", "LocY", "LocZ"]:
MESSAGES += "Curve type %s not supported in Action '%s' Bone '%s'.\n"\ log.error("Curve type %s not supported in Action '%s' Bone '%s'.\n"\
% (curve_name, animation_name, bone_name) % (curve_name, animation_name, bone_name))
for p in curve.getPoints(): for p in curve.getPoints():
time = p.getPoints() [0] time = p.getPoints() [0]
@@ -1093,17 +1295,28 @@ def export(filename):
if curve.getName() == "QuatX": quat[0] = val if curve.getName() == "QuatX": quat[0] = val
if curve.getName() == "QuatY": quat[1] = val if curve.getName() == "QuatY": quat[1] = val
if curve.getName() == "QuatZ": quat[2] = val if curve.getName() == "QuatZ": quat[2] = val
log.debug('Curve: %s' % curve.getName())
if quat==[0,0,0,0]:
log.critical('You are using just Loc keys. You must use LocRot keys instead.')
continue
transt = vector_by_matrix(trans, bone.local_matrix)
transt = vector_by_matrix(trans, bone.matrix)
loc = vector_add(bone.loc, transt) loc = vector_add(bone.loc, transt)
rot = quaternion_multiply(quat, bone.rot) rot = quaternion_multiply(quat, bone.rot)
rot = quaternion_normalize(rot)
try:
rot = quaternion_normalize(rot)
except:
import traceback
log.error('quaternion_normalize failed')
KeyFrame(track, cal3dtime, loc, rot) KeyFrame(track, cal3dtime, loc, rot)
if animation.duration <= 0: if animation.duration <= 0:
MESSAGES += "Ignoring Animation '" + animation_name + \ log.warning("Ignoring Animation '" + animation_name + \
"': duration is 0.\n" "': duration is 0.")
continue continue
ANIMATIONS[animation_name] = animation ANIMATIONS[animation_name] = animation
@@ -1112,11 +1325,15 @@ def export(filename):
filename = os.path.splitext(filename)[0] filename = os.path.splitext(filename)[0]
BASENAME = os.path.basename(filename) BASENAME = os.path.basename(filename)
DIRNAME = os.path.dirname(filename) DIRNAME = os.path.dirname(filename)
try: os.makedirs(DIRNAME)
except: pass
if PREFIX_FILE_WITH_MODEL_NAME: PREFIX = BASENAME + "_" if PREFIX_FILE_WITH_MODEL_NAME: PREFIX = BASENAME + "_"
else: PREFIX = "" else: PREFIX = ""
if XML: FORMAT_PREFIX = "x"; encode = lambda x: x.to_cal3d_xml() if XML: FORMAT_PREFIX = "x"; encode = lambda x: x.to_cal3d_xml()
else: FORMAT_PREFIX = "c"; encode = lambda x: x.to_cal3d() else: FORMAT_PREFIX = "c"; encode = lambda x: x.to_cal3d()
print DIRNAME + " - " + BASENAME #print DIRNAME + " - " + BASENAME
cfg = open(os.path.join(DIRNAME, BASENAME + ".cfg"), "wb") cfg = open(os.path.join(DIRNAME, BASENAME + ".cfg"), "wb")
print >> cfg, "# Cal3D model exported from Blender with blender2cal3d.py" print >> cfg, "# Cal3D model exported from Blender with blender2cal3d.py"
@@ -1158,42 +1375,106 @@ def export(filename):
print >> cfg, "material=%s" % filename print >> cfg, "material=%s" % filename
print >> cfg print >> cfg
MESSAGES += "Saved to '%s.cfg'\n" % BASENAME try:
MESSAGES += "Done." glob_params = Blender.Text.get("soya_params").asLines()
for glob_param in glob_params:
print >> cfg, glob_param
except: pass
# show messages if CONFIG_TEXT:
print MESSAGES for line in Blender.Text.get(CONFIG_TEXT).asLines():
if not line.startswith("material_"):
print >> cfg, line
# Remove soya cached data -- they need to be re-computed, since the model have changed
for filename in os.listdir(DIRNAME):
if filename.startswith("neighbors"):
os.remove(os.path.join(DIRNAME, filename))
log.info("Saved to '%s.cfg'" % BASENAME)
log.info("Done.")
if Blender.mode == 'interactive': DrawProgressBar(1.0,'Done!')
class BlenderGui:
def __init__(self):
text="""A log has been written to a blender text window. Change this window type to
a text window and you will be able to select the file."""
# some (ugly) gui to show the error messages - no scrollbar or other luxury, text=textwrap.wrap(text,40)
# please improve this if you know how
def gui(): text+=['']
global MESSAGES
button = Blender.Draw.Button("Ok", 1, 0, 0, 50, 20, "Close Window")
lines = MESSAGES.split("\n") if log.has_critical:
if len(lines) > 15: text+=['There were critical errors!!!!']
lines.append("Please also take a look at your console")
pos = len(lines) * 15 + 20
for line in lines:
Blender.BGL.glRasterPos2i(0, pos)
Blender.Draw.Text(line)
pos -= 15
def event(evt, val): elif log.has_errors:
if evt == Blender.Draw.ESCKEY: text+=['There were errors!']
Blender.Draw.Exit()
return
def button_event(evt): elif log.has_warnings:
if evt == 1: text+=['There were warnings']
Blender.Draw.Exit()
return # add any more text before here
text.reverse()
self.msg=text
Blender.Draw.Register(self.gui, self.event, self.button_event)
def gui(self,):
quitbutton = Blender.Draw.Button("Exit", 1, 0, 0, 100, 20, "Close Window")
y=35
for line in self.msg:
BGL.glRasterPos2i(10,y)
Blender.Draw.Text(line)
y+=15
def event(self,evt, val):
if evt == Blender.Draw.ESCKEY:
Blender.Draw.Exit()
return
def button_event(self,evt):
if evt == 1:
Blender.Draw.Exit()
return
def hotshot_export(filename):
import hotshot,hotshot.stats
prof=hotshot.Profile('blender2cal3d.prof')
print prof.runcall(export,filename)
prof.close()
stats=hotshot.stats.load('blender2cal3d.prof')
stats.strip_dirs()
stats.sort_stats('time','calls')
stats.print_stats()
# Main script # Main script
def fs_callback(filename): def fs_callback(filename):
save_to_registry(filename)
#hotshot_export(filename)
export(filename) export(filename)
Blender.Draw.Register(gui, event, button_event) BlenderGui()
def save_to_registry(filename):
dir,name=os.path.split(filename)
d={'default_path':dir,
}
log.info('storing %s to registry' % str(d))
Registry.SetKey('blender2cal3d',d)
def get_from_registry():
d=Registry.GetKey('blender2cal3d')
if d:
log.info('got %s from registry' % str(d))
return d['default_path']
else:
return ''
# Check for batch mode # Check for batch mode
if "--blender2cal3d" in sys.argv: if "--blender2cal3d" in sys.argv:
@@ -1205,6 +1486,14 @@ if "--blender2cal3d" in sys.argv:
try: val = float(val) try: val = float(val)
except: pass except: pass
globals()[attr] = val globals()[attr] = val
if CONFIG_TEXT:
for line in Blender.Text.get(CONFIG_TEXT).asLines():
if line.startswith("material_"):
old, new = line[9:].split("=")
MATERIAL_MAP[old] = new
log.info("Maps material %s to %s" % (old, new))
export(FILENAME) export(FILENAME)
Blender.Quit() Blender.Quit()
@@ -1212,8 +1501,15 @@ else:
if FILENAME: fs_callback(FILENAME) if FILENAME: fs_callback(FILENAME)
else: else:
defaultname = Blender.Get("filename") defaultname = Blender.Get("filename")
if defaultname.endswith(".blend"): if defaultname.endswith(".blend"):
defaultname = defaultname[0:len(defaultname)-len(".blend")] + ".cfg" defaultname = defaultname[0:len(defaultname)-len(".blend")] + ".cfg"
dir,name=os.path.split(defaultname)
lastpath=get_from_registry()
defaultname=os.path.join(lastpath,name)
Blender.Window.FileSelector(fs_callback, "Cal3D Export", defaultname) Blender.Window.FileSelector(fs_callback, "Cal3D Export", defaultname)