Hi,

Here is a modified version of Blender3Cal3D.py, which have been proposed by a 
contributor. In particular, it fixes some bug/limitations for moved, rotated or 
scaled blender objects. However, this version is about 100 times slower than 
the current Blender3Cal3D.py, thus i refused it.

You might find it useful, or not.

Jiba
#!BPY

"""
Name: 'Cal3D v0.9'
Blender: 241
Group: 'Export'
Tip: 'Export armature/bone/mesh/action data to the Cal3D format.'
"""

# blender2cal3D.py version 0.10
# Copyright (C) 2003-2004 Jean-Baptiste LAMY -- [EMAIL PROTECTED]
# Copyright (C) 2004 Matthias Braun -- [EMAIL PROTECTED]
# Copyright (C) 2006 Loic Dachary -- [EMAIL PROTECTED]
#
# 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.13"
__author__  = "Jean-Baptiste 'Jiba' Lamy"
__email__   = "[EMAIL PROTECTED]"
__url__     = "Soya3d's homepage http://home.gna.org/oomadness/en/soya/";
__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.41).

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)

It can be run in batch mode, as following :
    blender model.blend -P blender2cal3d.py --blender2cal3d FILENAME=model.cfg EXPORT_FOR_GL=1
You can pass as many parameters as you want at the end, "EXPORT_FOR_GL=1" is just an
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.
"""

#
# Summary of changes between 0.12 and 0.13
#
# The changes were motivated by the API change that occured
# between blender 2.37 and blender 2.40.
#
# - Use the Mathutils module instead of locally implemented vector/matrix/quat
#   operations.
# - Complete rewrite of the tracks export based on PoseBone and curframe
#   instead of complex calculation based on the IPO curves
# - Take advantage of the pre-calculated bone matrices to build the cal3d
#   bones
# - Rewrite EXPORT_FOR_SOYA into EXPORT_FOR_GL that transforms each matrix
#   and vector to the GL convention instead of adding a transform node at
#   the root of the mesh. Although a transform node is not really an issue,
#   the undesirable side effect is that when the character is immersed in
#   a scene using the GL convention, the developper must keep in mind that
#   it is using a different convention. Mixing conventions within a scene
#   is very, very error prone and confusing in the long run.
#
# Loic Dachary Mon Feb 27 19:11:19 CET 2006
#

# Parameters :

# Filename to export to (if "", display a file selector dialog).
FILENAME = ""

# 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_GL = 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 = 1.0

# Set to 1 if you want to prefix all filename with the model name
# (e.g. knight_walk.xaf instead of walk.xaf)
PREFIX_FILE_WITH_MODEL_NAME = 0

# Set to 0 to use Cal3D binary format
XML = 1


MESSAGES = ""

#########################################################################################
# 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

try:
  import psyco
  psyco.full()
except:
  print "* Blender2Cal3D * (Psyco not found)"
  
import Blender
from Blender import Registry
from Blender.Window import DrawProgressBar
from Blender import Draw, BGL
from Blender.Mathutils import *

import logging 
reload(logging)

import types

import textwrap

# 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 = ["???"]

# 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.setLevel(logging.DEBUG)

log.info("Starting...")

#
# Change vector/matrix from blender convetion to OpenGL convention
#
ROT90X = RotationMatrix(90, 4, 'x')
INVERT_ROT90X = RotationMatrix(90, 4, 'x').invert()

def Vector2GL(vector):
  if EXPORT_FOR_GL:
    return ROT90X * vector
  else:
    return vector

def Matrix2GL(matrix):
  if EXPORT_FOR_GL:
    return ROT90X * matrix * INVERT_ROT90X
  else:
    return matrix
  
# Cal3D data structures

CAL3D_VERSION = 1000

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 and len(map_filename) > 2 and map_filename[:2] == "//": 
      map_filename = map_filename[2:]
    log.warning("Material with name %s",map_filename)
    
    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
    
  # old cal3d format
  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
 
  # new xml format
  def to_cal3d_xml(self):
    s = "<?xml version=\"1.0\"?>\n"
    s += "<HEADER MAGIC=\"XRF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
    s += "<MATERIAL NUMMAPS=\"" + str(len(self.maps_filenames)) + "\">\n"
    s += "  <AMBIENT>" + str(self.ambient_r) + " " + str(self.ambient_g) + " " + str(self.ambient_b) + " " + str(self.ambient_a) + "</AMBIENT>\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 += "  <SHININESS>" + str(self.shininess) + "</SHININESS>\n";

    for map_filename in self.maps_filenames:
      s += "  <MAP>" + map_filename + "</MAP>\n";
      
    s += "</MATERIAL>\n";
        
    return s
  
MATERIALS = {}

class Mesh:
  def __init__(self, name):
    name=string.replace(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

  def to_cal3d_xml(self):
    s = "<?xml version=\"1.0\"?>\n"
    s += "<HEADER MAGIC=\"XMF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
    s += "<MESH NUMSUBMESH=\"%i\">\n" % len(self.submeshes)
    s += "".join(map(SubMesh.to_cal3d_xml, self.submeshes))
    s += "</MESH>\n"                                                  
    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)."""
    
    log.info("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 DotVecs(a.normal, b.normal) < 0.9: continue
          couple_collapse_factor.append(((a.loc - b.loc).length, 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
    
    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):
    """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

  def to_cal3d_xml(self):
    s = "  <SUBMESH NUMVERTICES=\"%i\" NUMFACES=\"%i\" MATERIAL=\"%i\" " % \
        (len(self.vertices), len(self.faces), self.material.id)
    s += "NUMLODSTEPS=\"%i\" NUMSPRINGS=\"%i\" NUMTEXCOORDS=\"%i\">\n" % \
         (self.nb_lodsteps, len(self.springs),
         len(self.material.maps_filenames))
    s += "".join(map(Vertex.to_cal3d_xml, self.vertices))
    s += "".join(map(Spring.to_cal3d_xml, self.springs))
    s += "".join(map(Face.to_cal3d_xml, self.faces))
    s += "  </SUBMESH>\n"
    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):
    loc = Vector2GL(self.loc)
    normal = Vector2GL(self.normal)
    if self.collapse_to: collapse_id = self.collapse_to.id
    else:                collapse_id = -1
    s =  struct.pack("ffffffii", loc[0], loc[1], loc[2], normal[0], normal[1], 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
  
  def to_cal3d_xml(self):
    loc = Vector2GL(self.loc)
    normal = Vector2GL(self.normal)
    if self.collapse_to:
      collapse_id = self.collapse_to.id
    else:
      collapse_id = -1
    s = "    <VERTEX ID=\"%i\" NUMINFLUENCES=\"%i\">\n" % \
        (self.id, len(self.influences))
    s += "      <POS>%f %f %f</POS>\n" % (loc[0], loc[1], loc[2])
    s += "      <NORM>%f %f %f</NORM>\n" % \
         (normal[0], normal[1], normal[2])
    if collapse_id != -1:
      s += "      <COLLAPSEID>%i</COLLAPSEID>\n" % collapse_id
      s += "      <COLLAPSECOUNT>%i</COLLAPSECOUNT>\n" % \
           self.face_collapse_count
    s += "".join(map(Map.to_cal3d_xml, self.maps))
    s += "".join(map(Influence.to_cal3d_xml, self.influences))
    if not self.weight is None:
      s += "      <PHYSIQUE>%f</PHYSIQUE>\n" % len(self.weight)
    s += "    </VERTEX>\n"
    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)

  def to_cal3d_xml(self):
    return "      <TEXCOORD>%f %f</TEXCOORD>\n" % (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)

  def to_cal3d_xml(self):
    return "      <INFLUENCE ID=\"%i\">%f</INFLUENCE>\n" % \
           (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)

  def to_cal3d_xml(self):
    return "    <SPRING VERTEXID=\"%i %i\" COEF=\"%f\" LENGTH=\"%f\"/>\n" % \
           (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)

  def to_cal3d_xml(self):
    return "    <FACE VERTEXID=\"%i %i %i\"/>\n" % \
           (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

  def to_cal3d_xml(self):
    s = "<?xml version=\"1.0\"?>\n"
    s += "<HEADER MAGIC=\"XSF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
    s += "<SKELETON NUMBONES=\"%i\">\n" % len(self.bones)
    s += "".join(map(Bone.to_cal3d_xml, self.bones))
    s += "</SKELETON>\n"
    return s

BONES = {}

class Bone:
  def __init__(self, skeleton, parent, bone, armature_matrix):
    self.parent = parent
    self.name   = string.replace(bone.name,'.','_')
    absolute_matrix = bone.matrix['ARMATURESPACE'] * armature_matrix
    self.invert_matrix = Matrix(absolute_matrix).invert()
    if parent:
      matrix = absolute_matrix * self.parent.invert_matrix
    else:
      matrix = armature_matrix
    self.local_matrix = matrix

    self.children = []
    self.skeleton = skeleton
    self.id = skeleton.next_bone_id
    if self.parent:
      self.parent.children.append(self)
    skeleton.next_bone_id += 1
    skeleton.bones.append(self)
    BONES[self.name] = self
    
  def to_cal3d(self):
    s =  struct.pack("i", len(self.name) + 1) + self.name + "\0"
    
    matrix = Matrix2GL(self.local_matrix)
    lloc = matrix.translationPart()
    lrot = matrix.toQuat()

    matrix = Matrix2GL(self.invert_matrix)
    loc = matrix.translationPart()
    rot = matrix.toQuat()

    #
    # Negate the rotation because blender rotations are clockwise
    # and cal3d rotations are counterclockwise
    # 
    s += struct.pack("ffffffffffffff", loc[0], loc[1], loc[2], rot[0], rot[1], rot[2], -rot[3], lloc[0], lloc[1], lloc[2], lrot[0], lrot[1], lrot[2], -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

  def to_cal3d_xml(self):
    s = "  <BONE ID=\"%i\" NAME=\"%s\" NUMCHILD=\"%i\">\n" % \
        (self.id, self.name, len(self.children))

    #
    # TRANSLATION and ROTATION are relative to the parent bone.
    # They are virtually useless since the animations (.XAF .CAF)
    # will always override them.
    #
    matrix = Matrix2GL(self.local_matrix)
    translation = matrix.translationPart()
    rotation = matrix.toQuat()
    s += "    <TRANSLATION>%f %f %f</TRANSLATION>\n" % \
         (translation.x, translation.y, translation.z)
    #
    # Negate the rotation because blender rotations are clockwise
    # and cal3d rotations are counterclockwise
    # 
    s += "    <ROTATION>%f %f %f -%f</ROTATION>\n" % \
         (rotation.x, rotation.y, rotation.z, rotation.w)

    #
    # LOCALTRANSLATION and LOCALROTATION are the invert of the cumulated
    # TRANSLATION and ROTATION (see above). It is used to calculate the
    # delta between an animated bone and the original non animated bone.
    # This delta will be applied to the influenced vertexes. 
    #
    matrix = Matrix2GL(self.invert_matrix)
    translation = matrix.translationPart()
    rotation = matrix.toQuat()
    s += "    <LOCALTRANSLATION>%f %f %f</LOCALTRANSLATION>\n" % \
         (translation.x, translation.y, translation.z)
    #
    # Negate the rotation because blender rotations are clockwise
    # and cal3d rotations are counterclockwise
    # 
    s += "    <LOCALROTATION>%f %f %f -%f</LOCALROTATION>\n" % \
         (rotation.x, rotation.y, rotation.z, rotation.w)

    if self.parent:
      s += "    <PARENTID>%i</PARENTID>\n" % self.parent.id
    else:
      s += "    <PARENTID>%i</PARENTID>\n" % -1
    s += "".join(map(lambda bone: "    <CHILDID>%i</CHILDID>\n" % bone.id,
         self.children))
    s += "  </BONE>\n"
    return s

class Animation:
  def __init__(self, name, duration = 0.0):
    name=string.replace(name,'.','_')
    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

  def to_cal3d_xml(self):
    s = "<?xml version=\"1.0\"?>\n"
    s += "<HEADER MAGIC=\"XAF\" VERSION=\"%i\"/>\n" % CAL3D_VERSION
    s += "<ANIMATION DURATION=\"%f\" NUMTRACKS=\"%i\">\n" % \
         (self.duration, len(self.tracks))                            
    s += "".join(map(Track.to_cal3d_xml, self.tracks.values()))
    s += "</ANIMATION>\n"
    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

  def to_cal3d_xml(self):
    s = "  <TRACK BONEID=\"%i\" NUMKEYFRAMES=\"%i\">\n" % \
        (self.bone.id, len(self.keyframes))
    s += "".join(map(KeyFrame.to_cal3d_xml, self.keyframes))
    s += "  </TRACK>\n"
    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):
    #
    # Negate the rotation because blender rotations are clockwise
    # and cal3d rotations are counterclockwise
    # 
    return struct.pack("ffffffff", self.time, self.loc.x, self.loc.y, self.loc.z, self.rot.x, self.rot.y, self.rot.z, -self.rot.w)
  
  def to_cal3d_xml(self):
    s = "    <KEYFRAME TIME=\"%f\">\n" % self.time
    s += "      <TRANSLATION>%f %f %f</TRANSLATION>\n" % \
         (self.loc.x, self.loc.y, self.loc.z)
    #
    # Negate the rotation because blender rotations are clockwise
    # and cal3d rotations are counterclockwise
    # 
    s += "      <ROTATION>%f %f %f -%f</ROTATION>\n" % \
         (self.rot.x, self.rot.y, self.rot.z, self.rot.w)
    s += "    </KEYFRAME>\n"
    return s                                                      
  
def export(filename):
  global MESSAGES

  # Get the scene
  scene = Blender.Scene.getCurrent()
  
  # ---- Export skeleton (=armature) ----------------------------------------
  
  if Blender.mode == 'interactive': DrawProgressBar(0.0,'Exporting skeleton...')

  skeleton = Skeleton()

  foundarmature = False
  armature = None
  for obj in Blender.Object.Get():
    data = obj.getData()
    if type(data) is not Blender.Types.ArmatureType:
      continue

    armature = obj

    if foundarmature == True:
      log.error("Found multiple armatures! '" + obj.getName() + "' ignored.\n")
      continue

    foundarmature = True
    matrix = obj.getMatrix()
    
    def treat_bone(b, parent = None):
      if parent:
        log.debug("Parented Bone: %s",b.name)

        bone = Bone(skeleton, parent, b, matrix)
      else:
        bone = Bone(skeleton, None, b, matrix)

      if b.hasChildren():
        for child in b.children:
          treat_bone(child, bone)
     
    foundroot = False
    for b in data.bones.values():
      # child bones are handled in treat_bone
      if b.parent != None:
        continue
      if foundroot == True:
        log.warning("Warning: Found multiple root-bones, this may not be supported in cal3d.")
        #print "Ignoring bone '" + b.getName() + "' and it's childs."
        #continue
        
      treat_bone(b)
      foundroot = True

  # ---- Export Mesh data ---------------------------------------------------

  if Blender.mode == 'interactive': DrawProgressBar(0.3,'Exporting meshes...')
  
  meshes = []
  
  for obj in Blender.Object.Get():
    data = obj.getData()
    if (type(data) is Blender.Types.NMeshType) and data.faces:
      mesh_name = obj.getName()
      if mesh_name[0]=='_': continue
      
      log.debug("Mesh: %s",mesh_name)
      
      mesh = Mesh(mesh_name)
      meshes.append(mesh)
      
      matrix = obj.getMatrix()
        
      faces = data.faces
      while faces:
        image          = faces[0].image
        image_filename = image and image.filename
        material       = MATERIALS.get(image_filename) or Material(image_filename)
        outputuv       = len(material.maps_filenames) > 0
        
        # 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:
              try:
                p1 = face.v[0].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 = (CrossVecs(
                Vector(p3[0] - p2[0], p3[1] - p2[1], p3[2] - p2[2]),
                Vector(p1[0] - p2[0], p1[1] - p2[1], p1[2] - p2[2]),
                ).resize4D() * matrix).normalize()

            face_vertices = []
            for i in range(len(face.v)):
              vertex = vertices.get(face.v[i].index)
              if not vertex:
                coord    = Vector(face.v[i].co).resize4D() * matrix
                normal   = face.v[i].no
                vertex   = vertices[face.v[i].index] = Vertex(submesh, coord, normal)
                
                influences = data.getVertexInfluences(face.v[i].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:
                  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())
                  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 = 0.0
                for bone_name, weight in influences:
                  if bone_name in BONES:
                    sum += weight
                
                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:
                    log.error("Couldn't find bone '%s' which influences object '%s'.\n" % (bone_name, obj.getName()))
                    continue
                  if sum:
                    normalized_weight = weight / sum
                  else:
                    normalized_weight = 1.0
                  vertex.influences.append(Influence(BONES[bone_name], normalized_weight))
                  
              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:
                  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 = Vertex(submesh, vertex.loc, vertex.normal)
                    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 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 --------------------------------------------------

  if Blender.mode == 'interactive': DrawProgressBar(0.7,'Exporting animations...')

  ANIMATIONS = {}

  for (action_name, action) in Blender.Armature.NLA.GetActions().iteritems():
    animation = Animation(action_name)

    action.setActive(armature)

    name2ipo = {}
    frames = []
    for (ipo_name, ipo) in action.getAllChannelIpos().iteritems():
      has_frames = False
      for curve in ipo.getCurves():
        for point in curve.getPoints():
          has_frames = True
          frame = point.pt[0]
          if frame not in frames:
            frames.append(int(frame))
      if has_frames:
        name2ipo[ipo_name] = ipo
    frames.sort()

    if not frames:
      continue

    animation.duration = frames[-1]

    for (ipo_name, ipo) in name2ipo.iteritems():

      bone_name = string.replace(ipo_name,'.','_')
      bone = BONES[bone_name]
      track = Track(animation, bone)
      track.finished = 0
      animation.tracks[bone_name] = track

      blender_bone = armature.getPose().bones[ipo_name]
      for curframe in frames:
        Blender.Set('curframe', curframe)
        matrix = blender_bone.quat.toMatrix()
        matrix.resize4x4()
        matrix[3] = Vector(*blender_bone.loc).resize4D()
        if blender_bone.size != Vector(1, 1, 1):
          log.error("Action " + action_name + ": Bone " + ipo_name + " resized " + str(blender_bone.size) + " at frame " + str(curframe) + " not supported and ignored")
#        scaleMatrix = ScaleMatrix(blender_bone.size[0], 4, Vector(1, 0, 0)) * ScaleMatrix(blender_bone.size[1], 4, Vector(0, 1, 0)) * ScaleMatrix(blender_bone.size[2], 4, Vector(0, 0, 1))
#        print bone_name + "\nscale Matrix\n" + str(scaleMatrix)
#        matrix *= scaleMatrix
 #       print bone_name + "\ndelta Matrix\n" + str(matrix)
        matrix = Matrix2GL(matrix * bone.local_matrix)
#        print bone_name + "\ncal3d Matrix\n" + str(matrix)
        #
        # Assume 25 fps since there is currently (blender 2.41) no way to
        # query the fps of the timeline
        #
        KeyFrame(track, (curframe-1) / 25.0, matrix.translationPart(), matrix.toQuat())

    if animation.duration > 0:
      animation.duration /= 25.0
      ANIMATIONS[action_name] = animation
      
  # Save all data
  if filename.endswith(".cfg"):
    filename = os.path.splitext(filename)[0]
  BASENAME = os.path.basename(filename)         
  DIRNAME  = os.path.dirname(filename) or "."

  try: os.makedirs(DIRNAME)
  except: pass
  
  if PREFIX_FILE_WITH_MODEL_NAME: PREFIX = BASENAME + "_"
  else:                           PREFIX = ""
  if XML: FORMAT_PREFIX = "x"; encode = lambda x: x.to_cal3d_xml()
  else:   FORMAT_PREFIX = "c"; encode = lambda x: x.to_cal3d()
  #print DIRNAME + " - " + BASENAME
  
  cfg = open(os.path.join(DIRNAME, BASENAME + ".cfg"), "wb")
  print >> cfg, "# Cal3D model exported from Blender with blender2cal3d.py"
  print >> cfg

  if SCALE != 1.0:
    print >> cfg, "scale=%s" % SCALE
    print >> cfg
    
  filename = BASENAME + "." + FORMAT_PREFIX + "sf"
  log.debug("FILENAME %s" % filename)
  open(os.path.join(DIRNAME, filename), "wb").write(encode(skeleton))
  print >> cfg, "skeleton=%s" % filename
  print >> cfg
  
  for animation in ANIMATIONS.values():
    if not animation.name.startswith("_"):
      if animation.duration: # Cal3D does not support animation with only one state
        filename = PREFIX + animation.name + "." + FORMAT_PREFIX + "af"
        open(os.path.join(DIRNAME, filename), "wb").write(encode(animation))
        print >> cfg, "animation=%s" % filename
        
  print >> cfg
  
  for mesh in meshes:
    if not mesh.name.startswith("_"):
      filename = PREFIX + mesh.name + "." + FORMAT_PREFIX + "mf"
      open(os.path.join(DIRNAME, filename), "wb").write(encode(mesh))
      print >> cfg, "mesh=%s" % filename
  print >> cfg
  
  materials = MATERIALS.values()
  materials.sort(lambda a, b: cmp(a.id, b.id))
  for material in materials:
    if material.maps_filenames:
      filename = PREFIX + os.path.splitext(os.path.basename(material.maps_filenames[0]))[0] + "." + FORMAT_PREFIX + "rf"
    else:
      filename = PREFIX + "plain." + FORMAT_PREFIX + "rf"
    open(os.path.join(DIRNAME, filename), "wb").write(encode(material))
    print >> cfg, "material=%s" % filename
  print >> cfg
  
  try:
    glob_params = Blender.Text.get("soya_params").asLines()
    for glob_param in glob_params:
      print >> cfg, glob_param
  except: pass
  
  # 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."""

    text=textwrap.wrap(text,40)

    text+=['']
    
    if log.has_critical:
      text+=['There were critical errors!!!!']

    elif log.has_errors:
      text+=['There were errors!']

    elif log.has_warnings:
      text+=['There were warnings']
    
    # 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
def fs_callback(filename):
  save_to_registry(filename)
  #hotshot_export(filename)
  export(filename)
  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
if "--blender2cal3d" in sys.argv:
  args = sys.argv[sys.argv.index("--blender2cal3d") + 1:]
  for arg in args:
    attr, val = arg.split("=")
    try: val = int(val)
    except:
      try: val = float(val)
      except: pass
    globals()[attr] = val
  export(FILENAME)
  Blender.Quit()
  
else:
  if FILENAME: fs_callback(FILENAME)
  else:
    defaultname = Blender.Get("filename")
    
    if defaultname.endswith(".blend"):
      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)


_______________________________________________
Soya-user mailing list
Soya-user@gna.org
https://mail.gna.org/listinfo/soya-user

Reply via email to