소스 검색

Blender Exporter Version 4.2

- Blender Actions support
- Skeleton animation library .blends now supported section
- Inverse Kinematics friendly skeleton exportingsection
- Export time much shorter for .blends with armatures with actions
jeff 9 년 전
부모
커밋
d155e19344
3개의 변경된 파일322개의 추가작업 그리고 189개의 파일을 삭제
  1. 309 184
      Exporters/Blender/io_export_babylon.py
  2. 4 4
      Exporters/Blender/readme.md
  3. 9 1
      dist/preview release/what's new.md

+ 309 - 184
Exporters/Blender/io_export_babylon.py

@@ -1,7 +1,7 @@
 bl_info = {
     'name': 'Babylon.js',
     'author': 'David Catuhe, Jeff Palmer',
-    'version': (4, 1, 0),
+    'version': (4, 2, 0),
     'blender': (2, 75, 0),
     'location': 'File > Export > Babylon.js (.babylon)',
     'description': 'Export Babylon.js scenes (.babylon)',
@@ -22,7 +22,7 @@ import sys, traceback # for writing errors to log file
 #===============================================================================
 # Registration the calling of the INFO_MT_file_export file selector
 def menu_func(self, context):
-    self.layout.operator(Main.bl_idname, text = 'Babylon.js [.babylon]')
+    self.layout.operator(Main.bl_idname, text = 'Babylon.js [.babylon] ver ' + format_version())
 
 def register():
     bpy.utils.register_module(__name__)
@@ -35,6 +35,10 @@ def unregister():
 if __name__ == '__main__':
     register()
 #===============================================================================
+def format_version():
+    version = bl_info['version']
+    return str(version[0]) + '.' + str(version[1]) +  '.' + str(version[2])
+
 # output related constants
 MAX_VERTEX_ELEMENTS = 65535
 MAX_VERTEX_ELEMENTS_32Bit = 16777216
@@ -42,6 +46,7 @@ VERTEX_OUTPUT_PER_LINE = 100
 MAX_FLOAT_PRECISION_INT = 4
 MAX_FLOAT_PRECISION = '%.' + str(MAX_FLOAT_PRECISION_INT) + 'f'
 COMPRESS_MATRIX_INDICES = True # this is True for .babylon exporter & False for TOB
+FRAME_BASED_ANIMATION = True # this is only able to be turned off by the TOB exporter right now
 
 # used in World constructor, defined in BABYLON.Scene
 #FOGMODE_NONE = 0
@@ -127,7 +132,7 @@ ANIMATIONLOOPMODE_CYCLE = 1
 #===============================================================================
 # Panel displayed in Scene Tab of properties, so settings can be saved in a .blend file
 class ExporterSettingsPanel(bpy.types.Panel):
-    bl_label = 'Exporter Settings'
+    bl_label = 'Exporter Settings ' + format_version()
     bl_space_type = 'PROPERTIES'
     bl_region_type = 'WINDOW'
     bl_context = 'scene'
@@ -141,7 +146,7 @@ class ExporterSettingsPanel(bpy.types.Panel):
         name="Flat shade entire scene",
         description="Use face normals on all meshes.  Increases vertices.",
         default = False,
-        )        
+        )
     bpy.types.Scene.attachedSound = bpy.props.StringProperty(
         name='Sound',
         description='',
@@ -162,6 +167,11 @@ class ExporterSettingsPanel(bpy.types.Panel):
         description="turn textures into encoded strings, for direct inclusion into source code",
         default = False,
         )
+    bpy.types.Scene.ignoreIKBones = bpy.props.BoolProperty(
+        name="Ignore IK Bones",
+        description="Do not export bones with '.ik' in the name",
+        default = False,
+        )
 
     def draw(self, context):
         layout = self.layout
@@ -170,6 +180,7 @@ class ExporterSettingsPanel(bpy.types.Panel):
         layout.prop(scene, "export_onlySelectedLayer")
         layout.prop(scene, "export_flatshadeScene")
         layout.prop(scene, "inlineTextures")
+        layout.prop(scene, "ignoreIKBones")
 
         box = layout.box()
         box.prop(scene, 'attachedSound')
@@ -234,8 +245,7 @@ class Main(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
 
             Main.log_handler = io.open(self.filepathMinusExtension + '.log', 'w', encoding='utf8')
             version = bl_info['version']
-            Main.log('Exporter version: ' + str(version[0]) + '.' + str(version[1]) +  '.' + str(version[2]) +
-                             ', Blender version: ' + bpy.app.version_string)
+            Main.log('Exporter version: ' + format_version() + ', Blender version: ' + bpy.app.version_string)
 
             if bpy.ops.object.mode_set.poll():
                 bpy.ops.object.mode_set(mode = 'OBJECT')
@@ -271,6 +281,15 @@ class Main(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
             if scene.attachedSound != '':
                 self.sounds.append(Sound(scene.attachedSound, scene.autoPlaySound, scene.loopSound))
 
+            # separate loop doing all skeletons, so available in Mesh to make skipping IK bones possible
+            for object in [object for object in scene.objects]:
+                if object.type == 'ARMATURE':  #skeleton.pose.bones
+                    if object.is_visible(scene):
+                        self.skeletons.append(Skeleton(object, scene, skeletonId))
+                        skeletonId += 1
+                    else:
+                        Main.warn('The following armature not visible in scene thus ignored: ' + object.name)
+
             # exclude lamps in this pass, so ShadowGenerator constructor can be passed meshesAnNodes
             for object in [object for object in scene.objects]:
                 if object.type == 'CAMERA':
@@ -279,13 +298,6 @@ class Main(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
                     else:
                         Main.warn('The following camera not visible in scene thus ignored: ' + object.name)
 
-                elif object.type == 'ARMATURE':  #skeleton.pose.bones
-                    if object.is_visible(scene):
-                        self.skeletons.append(Skeleton(object, scene, skeletonId))
-                        skeletonId += 1
-                    else:
-                        Main.warn('The following armature not visible in scene thus ignored: ' + object.name)
-
                 elif object.type == 'MESH':
                     forcedParent = None
                     nameID = ''
@@ -315,7 +327,7 @@ class Main(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
                 elif object.type == 'EMPTY':
                     self.meshesAndNodes.append(Node(object))
 
-                elif object.type != 'LAMP':
+                elif object.type != 'LAMP' and object.type != 'ARMATURE':
                     Main.warn('The following object (type - ' +  object.type + ') is not currently exportable thus ignored: ' + object.name)
 
             # Lamp / shadow Generator pass; meshesAnNodes complete & forceParents included
@@ -477,6 +489,13 @@ class Main(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
             if obj.layers[l] and scene.layers[l]:
                 return True
         return False
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    def get_skeleton(self, name):
+        for skeleton in self.skeletons:
+            if skeleton.name == name:
+                return skeleton
+        #really cannot happen, will cause exception in caller
+        return None
 #===============================================================================
 class World:
     def __init__(self, scene):
@@ -535,33 +554,63 @@ class Sound:
         file_handler.write('}')
 #===============================================================================
 class FCurveAnimatable:
-    def __init__(self, object, supportsRotation, supportsPosition, supportsScaling, xOffsetForRotation = 0):
+    def define_animations(self, object, supportsRotation, supportsPosition, supportsScaling, xOffsetForRotation = 0):
 
         # just because a sub-class can be animatable does not mean it is
         self.animationsPresent = object.animation_data and object.animation_data.action
 
-        rotAnim = False
-        locAnim = False
-        scaAnim = False
-        useQuat = object.rotation_mode=='QUATERNION'
-
         if (self.animationsPresent):
-            Main.log('FCurve animation processing begun for:  ' + object.name, 1)
-            self.animations = []
-            for fcurve in object.animation_data.action.fcurves:
-                if supportsRotation and fcurve.data_path == 'rotation_euler' and rotAnim == False and useQuat == False:
-                    self.animations.append(VectorAnimation(object, 'rotation_euler', 'rotation', -1, xOffsetForRotation))
-                    rotAnim = True
-                elif supportsRotation and fcurve.data_path == 'rotation_quaternion' and rotAnim == False and useQuat == True:
-                    self.animations.append(QuaternionAnimation(object, 'rotation_quaternion', 'rotationQuaternion', 1, xOffsetForRotation))
-                    rotAnim = True
-                elif supportsPosition and fcurve.data_path == 'location' and locAnim == False:
-                    self.animations.append(VectorAnimation(object, 'location', 'position', 1))
-                    locAnim = True
-                elif supportsScaling and fcurve.data_path == 'scale' and scaAnim == False:
-                    self.animations.append(VectorAnimation(object, 'scale', 'scaling', 1))
-                    scaAnim = True
+            Main.log('animation processing begun', 2)
+            # instance each type of animation support regardless of whether there is any data for it
+            if supportsRotation:
+                if object.rotation_mode == 'QUATERNION':
+                    if object.type == 'CAMERA':
+                        # if it's a camera, convert quaternions to euler XYZ
+                        rotAnimation = QuaternionToEulerAnimation(object, 'rotation', 'rotation_quaternion', -1, xOffsetForRotation)
+                    else:
+                        rotAnimation = QuaternionAnimation(object, 'rotationQuaternion', 'rotation_quaternion', 1, xOffsetForRotation)
+                else:
+                    rotAnimation = VectorAnimation(object, 'rotation', 'rotation_euler', -1, xOffsetForRotation)
+
+            if supportsPosition:
+                posAnimation = VectorAnimation(object, 'position', 'location')
+
+            if supportsScaling:
+                scaleAnimation = VectorAnimation(object, 'scaling', 'scale')
+
+            self.ranges = []
+            frameOffset = bpy.context.scene.frame_start
+
+            for action in bpy.data.actions:
+                # get the range / assigning the action to the object
+                animationRange = AnimationRange.actionPrep(object, action, False, frameOffset)
+                if animationRange is None:
+                    continue
+
+                if supportsRotation:
+                    hasData = rotAnimation.append_range(object, frameOffset)
+
+                if supportsPosition:
+                    hasData |= posAnimation.append_range(object, frameOffset)
+
+                if supportsScaling:
+                    hasData |= scaleAnimation.append_range(object, frameOffset)
+
+                if hasData:
+                    Main.log('processing action ' + action.name, 3)
+                    self.ranges.append(animationRange)
+                    frameOffset = animationRange.frame_end + 1
+
             #Set Animations
+            self.animations = []
+            if supportsRotation and len(rotAnimation.frames) > 0:
+                 self.animations.append(rotAnimation)
+
+            if supportsPosition and len(posAnimation.frames) > 0:
+                 self.animations.append(posAnimation)
+
+            if supportsScaling and len(scaleAnimation.frames) > 0:
+                 self.animations.append(scaleAnimation)
 
             if (hasattr(object.data, "autoAnimate") and object.data.autoAnimate):
                 self.autoAnimate = True
@@ -585,6 +634,17 @@ class FCurveAnimatable:
                 first = False
             file_handler.write(']')
 
+            file_handler.write(',"ranges":[')
+            first = True
+            for range in self.ranges:
+                if first != True:
+                    file_handler.write(',')
+                first = False
+
+                range.to_scene_file(file_handler)
+
+            file_handler.write(']')
+
             if (hasattr(self, "autoAnimate") and self.autoAnimate):
                 write_bool(file_handler, 'autoAnimate', self.autoAnimate)
                 write_int(file_handler, 'autoAnimateFrom', self.autoAnimateFrom)
@@ -593,10 +653,10 @@ class FCurveAnimatable:
 #===============================================================================
 class Mesh(FCurveAnimatable):
     def __init__(self, object, scene, startFace, forcedParent, nameID, exporter):
-        super().__init__(object, True, True, True)  #Should animations be done when forcedParent
-
         self.name = object.name + str(nameID)
         Main.log('processing begun of mesh:  ' + self.name)
+        self.define_animations(object, True, True, True)  #Should animations be done when forcedParent
+
         self.isVisible = not object.hide_render
         self.isEnabled = not object.data.loadDisabled
         useFlatShading = scene.export_flatshadeScene or object.data.useFlatShading
@@ -612,6 +672,8 @@ class Mesh(FCurveAnimatable):
             objArmature = object.find_armature()
             if objArmature != None:
                 hasSkeleton = True
+                # used to get bone index, since could be skipping IK bones
+                skeleton = exporter.get_skeleton(objArmature.name)
                 i = 0
                 for obj in scene.objects:
                     if obj.type == "ARMATURE":
@@ -810,7 +872,7 @@ class Mesh(FCurveAnimatable):
                     vertex = mesh.vertices[vertex_index]
                     position = vertex.co
                     normal = face.normal if useFlatShading else vertex.normal
-                    
+
                     #skeletons
                     if hasSkeleton:
                         matricesWeights = []
@@ -821,10 +883,11 @@ class Mesh(FCurveAnimatable):
                             index = group.group
                             weight = group.weight
 
+                            # do not need boneIndex; using skeleton.get_index_of_bone()
                             for boneIndex, bone in enumerate(objArmature.pose.bones):
                                 if object.vertex_groups[index].name == bone.name:
                                     matricesWeights.append(weight)
-                                    matricesIndices.append(boneIndex)
+                                    matricesIndices.append(skeleton.get_index_of_bone(bone.name))
 
                     # Texture coordinates
                     if hasUV:
@@ -853,7 +916,7 @@ class Mesh(FCurveAnimatable):
                             vNormal = vertices_Normals[vertex_index][index_UV]
                             if (normal.x != vNormal.x or normal.y != vNormal.y or normal.z != vNormal.z):
                                 continue;
-                            
+
                             if hasUV:
                                 vUV = vertices_UVs[vertex_index][index_UV]
                                 if (vUV[0] != vertex_UV[0] or vUV[1] != vertex_UV[1]):
@@ -873,7 +936,7 @@ class Mesh(FCurveAnimatable):
                                 vSkWeight = vertices_sk_weights[vertex_index]
                                 vSkIndices = vertices_sk_indices[vertex_index]
                                 if not same_array(vSkWeight[index_UV], matricesWeights) or not same_array(vSkIndices[index_UV], matricesIndices):
-                                    continue 
+                                    continue
 
                             if vertices_indices[vertex_index][index_UV] >= subMeshVerticesStart:
                                 alreadySaved = True
@@ -888,10 +951,10 @@ class Mesh(FCurveAnimatable):
                         # Export new one
                         index = verticesCount
                         alreadySavedVertices[vertex_index] = True
-                        
-                        vertices_Normals[vertex_index].append(normal)                        
+
+                        vertices_Normals[vertex_index].append(normal)
                         self.normals.append(normal)
-                        
+
                         if hasUV:
                             vertices_UVs[vertex_index].append(vertex_UV)
                             self.uvs.append(vertex_UV[0])
@@ -948,12 +1011,12 @@ class Mesh(FCurveAnimatable):
                 self.skeletonIndices = Mesh.packSkeletonIndices(self.skeletonIndices)
                 if (self.numBoneInfluencers > 4):
                     self.skeletonIndicesExtra = Mesh.packSkeletonIndices(self.skeletonIndicesExtra)
-                
+
             Main.log('Total Influencers:  ' + format_f(totalInfluencers), 3)
             Main.log('Avg # of influencers per vertex:  ' + format_f(totalInfluencers / len(self.positions)), 3)
             Main.log('Highest # of influencers observed:  ' + str(highestInfluenceObserved) + ', num vertices with this:  ' + format_int(influenceCounts[highestInfluenceObserved if highestInfluenceObserved < 9 else 0]), 3)
             Main.log('exported as ' + str(self.numBoneInfluencers) + ' influencers', 3)
-            nWeights = len(self.skeletonWeights) + len(self.skeletonWeightsExtra) if hasattr(self, 'skeletonWeightsExtra') else 0
+            nWeights = len(self.skeletonWeights) + (len(self.skeletonWeightsExtra) if hasattr(self, 'skeletonWeightsExtra') else 0)
             Main.log('num skeletonWeights and skeletonIndices:  ' + str(nWeights), 3)
 
         numZeroAreaFaces = self.find_zero_area_faces()
@@ -990,43 +1053,43 @@ class Mesh(FCurveAnimatable):
         if (maxInfluencers > 8 or maxInfluencers < 1):
             maxInfluencers = 8
             Main.warn('Maximum # of influencers invalid, set to 8', 3)
-            
+
         self.numBoneInfluencers = maxInfluencers if maxInfluencers < highestObserved else highestObserved
         needExtras = self.numBoneInfluencers > 4
-        
+
         maxInfluencersExceeded = 0
-        
+
         fixedWeights = []
         fixedIndices = []
-        
+
         fixedWeightsExtra = []
         fixedIndicesExtra = []
-        
+
         for i in range(len(weightsPerVertex)):
             weights = weightsPerVertex[i]
             indices = indicesPerVertex[i]
             nInfluencers = len(weights)
-            
+
             if (nInfluencers > self.numBoneInfluencers):
                 maxInfluencersExceeded += 1
                 Mesh.sortByDescendingInfluence(weights, indices)
-                
+
             for j in range(4):
                 fixedWeights.append(weights[j] if nInfluencers > j else 0.0)
                 fixedIndices.append(indices[j] if nInfluencers > j else 0  )
-                
+
             if needExtras:
                 for j in range(4, 8):
                     fixedWeightsExtra.append(weights[j] if nInfluencers > j else 0.0)
                     fixedIndicesExtra.append(indices[j] if nInfluencers > j else 0  )
-                            
+
         self.skeletonWeights = fixedWeights
         self.skeletonIndices = fixedIndices
-        
+
         if needExtras:
             self.skeletonWeightsExtra = fixedWeightsExtra
             self.skeletonIndicesExtra = fixedIndicesExtra
-            
+
         if maxInfluencersExceeded > 0:
             Main.warn('Maximum # of influencers exceeded for ' + format_int(maxInfluencersExceeded) + ' vertices, extras ignored', 3)
 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -1042,12 +1105,12 @@ class Mesh(FCurveAnimatable):
                     tmp = weights[idx]
                     weights[idx    ] = weights[idx - 1]
                     weights[idx - 1] = tmp
-                    
+
                     tmp = indices[idx]
                     indices[idx    ] = indices[idx - 1]
                     indices[idx - 1] = tmp
-                    
-                    notSorted = True    
+
+                    notSorted = True
 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     # assume that toFixedInfluencers has already run, which ensures indices length is a multiple of 4
     @staticmethod
@@ -1060,9 +1123,9 @@ class Mesh(FCurveAnimatable):
             matricesIndicesCompressed += indices[idx + 1] <<  8
             matricesIndicesCompressed += indices[idx + 2] << 16
             matricesIndicesCompressed += indices[idx + 3] << 24
-            
+
             compressedIndices.append(matricesIndicesCompressed)
-            
+
         return compressedIndices
 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     def to_scene_file(self, file_handler):
@@ -1094,7 +1157,7 @@ class Mesh(FCurveAnimatable):
             write_float(file_handler, 'physicsRestitution', self.physicsRestitution)
 
         # Geometry
-        if hasattr(self, 'skeletonId'): 
+        if hasattr(self, 'skeletonId'):
             write_int(file_handler, 'skeletonId', self.skeletonId)
             write_int(file_handler, 'numBoneInfluencers', self.numBoneInfluencers)
 
@@ -1175,8 +1238,8 @@ class MeshInstance:
 #===============================================================================
 class Node(FCurveAnimatable):
     def __init__(self, node):
-        super().__init__(node, True, True, True)  #Should animations be done when forcedParent
         Main.log('processing begun of node:  ' + node.name)
+        self.define_animations(object, True, True, True)  #Should animations be done when forcedParent
         self.name = node.name
 
         if node.parent and node.parent.type != 'ARMATURE':
@@ -1241,9 +1304,11 @@ class Bone:
         self.name = bone.name
         self.length = bone.length
         self.index = index
+        self.blenderBoneObj = bone # record so can be used by get_matrix, called by append_animation_pose
+        self.parentBone = bone.parent
 
-        matrix_world = skeleton.matrix_world
-        self.matrix = Bone.get_matrix(bone, matrix_world)
+        self.matrix_world = skeleton.matrix_world
+        self.matrix = self.get_matrix()
 
         parentId = -1
         if (bone.parent):
@@ -1256,33 +1321,24 @@ class Bone:
 
         #animation
         if (skeleton.animation_data):
-            Main.log('animation begun of bone:  ' + self.name, 3)
-            self.animation = Animation(ANIMATIONTYPE_MATRIX, scene.render.fps, ANIMATIONLOOPMODE_CYCLE, 'anim', '_matrix')
-
-            start_frame = scene.frame_start
-            end_frame = scene.frame_end
-            previousBoneMatrix = None
-            for frame in range(start_frame, end_frame + 1):
-                bpy.context.scene.frame_set(frame)
-                currentBoneMatrix = Bone.get_matrix(bone, matrix_world)
-
-                if (frame != end_frame and same_matrix4(currentBoneMatrix, previousBoneMatrix)):
-                    continue
-
-                self.animation.frames.append(frame)
-                self.animation.values.append(Bone.get_matrix(bone, matrix_world))
-                previousBoneMatrix = currentBoneMatrix
+            self.animation = Animation(ANIMATIONTYPE_MATRIX, ANIMATIONLOOPMODE_CYCLE, 'anim', '_matrix')
+            self.previousBoneMatrix = None
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    def append_animation_pose(self, frame, force = False):
+        currentBoneMatrix = self.get_matrix()
 
-            bpy.context.scene.frame_set(start_frame)
+        if (force or not same_matrix4(currentBoneMatrix, self.previousBoneMatrix)):
+            self.animation.frames.append(frame)
+            self.animation.values.append(currentBoneMatrix)
+            self.previousBoneMatrix = currentBoneMatrix
 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    @staticmethod
-    def get_matrix(bone, matrix_world):
+    def get_matrix(self):
         SystemMatrix = mathutils.Matrix.Scale(-1, 4, mathutils.Vector((0, 0, 1))) * mathutils.Matrix.Rotation(math.radians(-90), 4, 'X')
 
-        if (bone.parent):
-            return (SystemMatrix * matrix_world * bone.parent.matrix).inverted() * (SystemMatrix * matrix_world * bone.matrix)
+        if (self.parentBone):
+            return (SystemMatrix * self.matrix_world * self.parentBone.matrix).inverted() * (SystemMatrix * self.matrix_world * self.blenderBoneObj.matrix)
         else:
-            return SystemMatrix * matrix_world * bone.matrix
+            return  SystemMatrix * self.matrix_world * self.blenderBoneObj.matrix
 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     def to_scene_file(self, file_handler):
         file_handler.write('\n{')
@@ -1306,13 +1362,41 @@ class Skeleton:
         self.id = id
         self.bones = []
 
-        bones = skeleton.pose.bones
-        j = 0
-        for bone in bones:
-            self.bones.append(Bone(bone, skeleton, scene, j))
-            j = j + 1
+        for bone in skeleton.pose.bones:
+            if scene.ignoreIKBones and '.ik' in bone.name:
+                Main.log('Ignoring IK bone:  ' + bone.name, 2)
+                continue
+
+            self.bones.append(Bone(bone, skeleton, scene, len(self.bones)))
 
-        Main.log('processing complete of skeleton:  ' + skeleton.name)
+        if (skeleton.animation_data):
+            self.ranges = []
+            frameOffset = scene.frame_start
+            for action in bpy.data.actions:
+                # get the range / assigning the action to the object
+                animationRange = AnimationRange.actionPrep(skeleton, action, FRAME_BASED_ANIMATION, frameOffset)
+                if animationRange is None:
+                    continue
+
+                Main.log('processing action ' + action.name, 2)
+                self.ranges.append(animationRange)
+
+                for frame in animationRange.frames:
+                    bpy.context.scene.frame_set(frame)
+
+                    for bone in self.bones:
+                        bone.append_animation_pose(frame + frameOffset, frame == animationRange.highest_frame)
+
+                frameOffset = animationRange.frame_end + 1
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # Since IK bones could be being skipped, looking up index of bone in second pass of mesh required
+    def get_index_of_bone(self, boneName):
+        for bone in self.bones:
+            if boneName == bone.name:
+                return bone.index
+
+        # should not happen, but if it does clearly a bug, so terminate
+        raise 'bone name "' + boneName + '" not found in skeleton'
 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     def to_scene_file(self, file_handler):
         file_handler.write('{')
@@ -1329,18 +1413,30 @@ class Skeleton:
             bone.to_scene_file(file_handler)
 
         file_handler.write(']')
+
+        if hasattr(self, 'ranges'):
+            file_handler.write(',"ranges":[')
+            first = True
+            for range in self.ranges:
+                if first != True:
+                    file_handler.write(',')
+                first = False
+
+                range.to_scene_file(file_handler)
+
+            file_handler.write(']')
+
         file_handler.write('}')
 #===============================================================================
 class Camera(FCurveAnimatable):
     def __init__(self, camera):
-        super().__init__(camera, True, True, False, math.pi / 2)
-
         if camera.parent and camera.parent.type != 'ARMATURE':
             self.parentId = camera.parent.name
 
         self.CameraType = camera.data.CameraType
         self.name = camera.name
         Main.log('processing begun of camera (' + self.CameraType + '):  ' + self.name)
+        self.define_animations(camera, True, True, False, math.pi / 2)
         self.position = camera.location
 
         # for quaternions, convert to euler XYZ, otherwise, use the default rotation_euler
@@ -1441,13 +1537,13 @@ class Camera(FCurveAnimatable):
 #===============================================================================
 class Light(FCurveAnimatable):
     def __init__(self, light):
-        super().__init__(light, False, True, False)
-
         if light.parent and light.parent.type != 'ARMATURE':
             self.parentId = light.parent.name
 
         self.name = light.name
         Main.log('processing begun of light (' + light.data.type + '):  ' + self.name)
+        self.define_animations(light, False, True, False)
+
         light_type_items = {'POINT': POINT_LIGHT, 'SUN': DIRECTIONAL_LIGHT, 'SPOT': SPOT_LIGHT, 'HEMI': HEMI_LIGHT, 'AREA': POINT_LIGHT}
         self.light_type = light_type_items[light.data.type]
 
@@ -1595,7 +1691,7 @@ class Texture:
             usingMap = texture.uv_layer
             if len(usingMap) == 0:
                 usingMap = mesh.data.uv_textures[0].name
-                
+
             Main.log('Image texture found, type:  ' + slot + ', mapped using: "' + usingMap + '"', 3)
             if mesh.data.uv_textures[0].name == usingMap:
                 self.coordinatesIndex = 0
@@ -1770,7 +1866,7 @@ class BakingRecipe:
                     # ignore empty slots
                     if mtex.texture.type == 'NONE':
                         continue
-                    
+
                     # for images, just need to make sure there is only 1 per type
                     if mtex.texture.type == 'IMAGE' and not forceBaking:
                         if mtex.use_map_diffuse or mtex.use_map_color_diffuse:
@@ -1778,46 +1874,46 @@ class BakingRecipe:
                                 nReflectionImages += 1
                             else:
                                 nDiffuseImages += 1
-    
+
                         if mtex.use_map_ambient:
                             nAmbientImages += 1
-    
+
                         if mtex.use_map_alpha:
                             nOpacityImages += 1
-    
+
                         if mtex.use_map_emit:
                             nEmissiveImages += 1
-    
+
                         if mtex.use_map_normal:
                             nBumpImages += 1
-    
+
                         if mtex.use_map_color_spec:
                             nSpecularImages += 1
 
                     else:
                         self.needsBaking = True
-    
+
                         if mtex.use_map_diffuse or mtex.use_map_color_diffuse:
                             if mtex.texture_coords == 'REFLECTION':
                                 self.reflectionBaking = True
                             else:
                                 self.diffuseBaking = True
-    
+
                         if mtex.use_map_ambient:
                             self.ambientBaking = True
-    
+
                         if mtex.use_map_alpha:
                             self.opacityBaking = True
-    
+
                         if mtex.use_map_emit:
                             self.emissiveBaking = True
-    
+
                         if mtex.use_map_normal:
                             self.bumpBaking = True
-    
+
                         if mtex.use_map_color_spec:
                             self.specularBaking = True
-                            
+
                 # 2nd pass 2 check for multiples of a given image type
                 if nDiffuseImages > 1:
                     self.needsBaking = self.diffuseBaking = True
@@ -1833,13 +1929,13 @@ class BakingRecipe:
                     self.needsBaking = self.bumpBaking = True
                 if nSpecularImages > 1:
                     self.needsBaking = self.specularBaking = True
-                        
+
         self.multipleRenders = blenderRender and self.cyclesRender
-        
+
         # check for really old .blend file, eg. 2.49, to ensure that everything requires exists
         if self.needsBaking and bpy.data.screens.find('UV Editing') == -1:
             Main.warn('Contains material requiring baking, but resources not available.  Probably .blend very old', 2)
-            self.needsBaking = False     
+            self.needsBaking = False
 #===============================================================================
 # Not intended to be instanced directly
 class Material:
@@ -1868,9 +1964,9 @@ class Material:
 class StdMaterial(Material):
     def __init__(self, material_slot, exporter, mesh):
         super().__init__(mesh.data.checkReadyOnlyOnce)
-        nameSpace = Main.nameSpace if mesh.data.materialNameSpace == DEFAULT_MATERIAL_NAMESPACE else mesh.data.materialNameSpace 
+        nameSpace = Main.nameSpace if mesh.data.materialNameSpace == DEFAULT_MATERIAL_NAMESPACE else mesh.data.materialNameSpace
         self.name = nameSpace + '.' + material_slot.name
-                
+
         Main.log('processing begun of Standard material:  ' +  material_slot.name, 2)
 
         # a material slot is not a reference to an actual material; need to look up
@@ -1932,7 +2028,7 @@ class StdMaterial(Material):
 class BakedMaterial(Material):
     def __init__(self, exporter, mesh, recipe):
         super().__init__(mesh.data.checkReadyOnlyOnce)
-        nameSpace = Main.nameSpace if mesh.data.materialNameSpace == DEFAULT_MATERIAL_NAMESPACE else mesh.data.materialNameSpace 
+        nameSpace = Main.nameSpace if mesh.data.materialNameSpace == DEFAULT_MATERIAL_NAMESPACE else mesh.data.materialNameSpace
         self.name = nameSpace + '.' + mesh.name
         Main.log('processing begun of baked material:  ' +  mesh.name, 2)
 
@@ -1960,8 +2056,8 @@ class BakedMaterial(Material):
          # UV unwrap operates on mesh in only edit mode, procedurals can also give error of 'no images to be found' when not done
          # select all verticies of mesh, since smart_project works only with selected verticies
         bpy.ops.object.mode_set(mode='EDIT')
-        bpy.ops.mesh.select_all(action='SELECT') 
-        
+        bpy.ops.mesh.select_all(action='SELECT')
+
         # you need UV on a mesh in order to bake image.  This is not reqd for procedural textures, so may not exist
         # need to look if it might already be created, if so use the first one
         uv = mesh.data.uv_textures[0] if len(mesh.data.uv_textures) > 0 else None
@@ -2088,18 +2184,80 @@ class BakedMaterial(Material):
                 bpy.data.images.remove(image)
                 break
 #===============================================================================
+class AnimationRange:
+    # constructor called by the static actionPrep method
+    def __init__(self, name, frames, frameOffset):
+        self.name = name
+        self.highest_frame = frames[len(frames) - 1]
+        self.frame_start = frameOffset
+        self.frame_end   = frameOffset + self.highest_frame
+        self.frames = frames
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    def to_scene_file(self, file_handler):
+        file_handler.write('{')
+        write_string(file_handler, 'name', self.name, True)
+        write_int(file_handler, 'from', self.frame_start)
+        write_int(file_handler, 'to', self.frame_end)
+        file_handler.write('}')
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    @staticmethod
+    def actionPrep(object, action, includeAllFrames, frameOffset):
+        # assign the action & test if there is any data for that action for this object
+#        bpy.context.scene.objects.active = object
+        object.animation_data.action = action
+        if len(object.animation_data.action.fcurves) == 0:
+            return None
+
+        if includeAllFrames:
+            frame_start, frame_end = [int(x) for x in action.frame_range]
+            frames = range(frame_start, frame_end)
+
+        else:
+            # capture built up from fcurves
+            frames = dict()
+            for fcurve in object.animation_data.action.fcurves:
+                for key in fcurve.keyframe_points:
+                    frame = key.co.x
+                    frames[frame] = True
+
+            frames = sorted(frames)
+
+        return AnimationRange(action.name, frames, frameOffset)
+#===============================================================================
 class Animation:
-    def __init__(self, dataType, framePerSecond, loopBehavior, name, propertyInBabylon):
+    def __init__(self, dataType, loopBehavior, name, propertyInBabylon, attrInBlender = None, mult = 1, xOffset = 0):
         self.dataType = dataType
-        self.framePerSecond = framePerSecond
+        self.framePerSecond = bpy.context.scene.render.fps
         self.loopBehavior = loopBehavior
         self.name = name
         self.propertyInBabylon = propertyInBabylon
 
+        # these never get used by Bones, so optional in contructor args
+        self.attrInBlender = attrInBlender
+        self.mult = mult
+        self.xOffset = xOffset
+
         #keys
         self.frames = []
         self.values = [] # vector3 for ANIMATIONTYPE_VECTOR3 & matrices for ANIMATIONTYPE_MATRIX
 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # a separate method outside of constructor, so can be called once for each Blender Action object participates in
+    def append_range(self, object, frameOffset):
+        # action already assigned, always using poses, not every frame, build up again filtering by attrInBlender
+        frames = dict()
+        for fcurve in object.animation_data.action.fcurves:
+            if fcurve.data_path == self.attrInBlender:
+                for key in fcurve.keyframe_points:
+                    frame = key.co.x
+                    frames[frame] = 1
+
+        for Frame in sorted(frames):
+            self.frames.append(Frame + frameOffset)
+            bpy.context.scene.frame_set(int(Frame))
+            self.values.append(self.get_attr(object))
+
+        return len(frames) > 0
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     # for auto animate
     def get_first_frame(self):
         return self.frames[0] if len(self.frames) > 0 else -1
@@ -2140,60 +2298,27 @@ class Animation:
         file_handler.write('}')
 #===============================================================================
 class VectorAnimation(Animation):
-    def __init__(self, object, attrInBlender, propertyInBabylon, mult, xOffset = 0):
-        super().__init__(ANIMATIONTYPE_VECTOR3, 30, ANIMATIONLOOPMODE_CYCLE, propertyInBabylon + ' animation', propertyInBabylon)
-
-        # capture  built up from fcurves
-        frames = dict()
-        for fcurve in object.animation_data.action.fcurves:
-            if fcurve.data_path == attrInBlender:
-                for key in fcurve.keyframe_points:
-                    frame = key.co.x
-                    frames[frame] = 1
-
-        #for each frame (next step ==> set for key frames)
-        for Frame in sorted(frames):
-            self.frames.append(Frame)
-            bpy.context.scene.frame_set(int(Frame + bpy.context.scene.frame_start))
-            self.values.append(scale_vector(getattr(object, attrInBlender), mult, xOffset))
+    def __init__(self, object, propertyInBabylon, attrInBlender, mult = 1, xOffset = 0):
+        super().__init__(ANIMATIONTYPE_VECTOR3, ANIMATIONLOOPMODE_CYCLE, propertyInBabylon + ' animation', propertyInBabylon, attrInBlender, mult, xOffset)
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    def get_attr(self, object):
+        return scale_vector(getattr(object, self.attrInBlender), self.mult, self.xOffset)
 #===============================================================================
 class QuaternionAnimation(Animation):
-    def __init__(self, object, attrInBlender, propertyInBabylon, mult, xOffset = 0):
-        super().__init__(ANIMATIONTYPE_QUATERNION, 30, ANIMATIONLOOPMODE_CYCLE, propertyInBabylon + ' animation', propertyInBabylon)
-
-        # capture  built up from fcurves
-        frames = dict()
-        for fcurve in object.animation_data.action.fcurves:
-            if fcurve.data_path == attrInBlender:
-                for key in fcurve.keyframe_points:
-                    frame = key.co.x
-                    frames[frame] = 1
-
-        #for each frame (next step ==> set for key frames)
-        for Frame in sorted(frames):
-            self.frames.append(Frame)
-            bpy.context.scene.frame_set(int(Frame + bpy.context.scene.frame_start))
-            self.values.append(post_rotate_quaternion(getattr(object, attrInBlender), xOffset))
+    def __init__(self, object, propertyInBabylon, attrInBlender, mult = 1, xOffset = 0):
+        super().__init__(ANIMATIONTYPE_QUATERNION, ANIMATIONLOOPMODE_CYCLE, propertyInBabylon + ' animation', propertyInBabylon, attrInBlender, mult, xOffset)
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    def get_attr(self, object):
+        return post_rotate_quaternion(getattr(object, self.attrInBlender), self.xOffset)
 #===============================================================================
 class QuaternionToEulerAnimation(Animation):
-    def __init__(self, object, attrInBlender, propertyInBabylon, mult, xOffset = 0):
-        super().__init__(ANIMATIONTYPE_VECTOR3, 30, ANIMATIONLOOPMODE_CYCLE, propertyInBabylon + ' animation', propertyInBabylon)
-
-        # capture  built up from fcurves
-        frames = dict()
-        for fcurve in object.animation_data.action.fcurves:
-            if fcurve.data_path == attrInBlender:
-                for key in fcurve.keyframe_points:
-                    frame = key.co.x
-                    frames[frame] = 1
-
-        #for each frame (next step ==> set for key frames)
-        for Frame in sorted(frames):
-            self.frames.append(Frame)
-            bpy.context.scene.frame_set(int(Frame + bpy.context.scene.frame_start))
-            quat = getattr(object, attrInBlender)
-            eul  = quat.to_euler("XYZ")
-            self.values.append(scale_vector(eul, mult, xOffset))
+    def __init__(self, propertyInBabylon, attrInBlender, mult = 1, xOffset = 0):
+        super().__init__(ANIMATIONTYPE_VECTOR3, ANIMATIONLOOPMODE_CYCLE, propertyInBabylon + ' animation', propertyInBabylon, attrInBlender, mult, Offset)
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    def get_attr(self, object):
+        quat = getattr(object, self.attrInBlender)
+        eul  = quat.to_euler("XYZ")
+        return scale_vector(eul, self.mult, self.xOffset)
 #===============================================================================
 #  module level formatting methods, called from multiple classes
 #===============================================================================
@@ -2310,12 +2435,12 @@ def same_matrix4(matA, matB):
     if(matA is None or matB is None): return False
     if (len(matA) != len(matB)): return False
     for i in range(len(matA)):
-        if (round(matA[i][0], MAX_FLOAT_PRECISION_INT) != round(matB[i][0], MAX_FLOAT_PRECISION_INT) or 
-            round(matA[i][1], MAX_FLOAT_PRECISION_INT) != round(matB[i][1], MAX_FLOAT_PRECISION_INT) or 
-            round(matA[i][2], MAX_FLOAT_PRECISION_INT) != round(matB[i][2], MAX_FLOAT_PRECISION_INT) or 
-            round(matA[i][3], MAX_FLOAT_PRECISION_INT) != round(matB[i][3], MAX_FLOAT_PRECISION_INT)): 
+        if (round(matA[i][0], MAX_FLOAT_PRECISION_INT) != round(matB[i][0], MAX_FLOAT_PRECISION_INT) or
+            round(matA[i][1], MAX_FLOAT_PRECISION_INT) != round(matB[i][1], MAX_FLOAT_PRECISION_INT) or
+            round(matA[i][2], MAX_FLOAT_PRECISION_INT) != round(matB[i][2], MAX_FLOAT_PRECISION_INT) or
+            round(matA[i][3], MAX_FLOAT_PRECISION_INT) != round(matB[i][3], MAX_FLOAT_PRECISION_INT)):
             return False
-        
+
     return True
 
 def same_vertex(vertA, vertB):
@@ -2327,7 +2452,7 @@ def same_array(arrayA, arrayB):
     if len(arrayA) != len(arrayB): return False
     for i in range(len(arrayA)):
         if arrayA[i] != arrayB[i] : return False
-        
+
     return True
 #===============================================================================
 # module level methods for writing JSON (.babylon) files
@@ -2551,7 +2676,7 @@ bpy.types.Lamp.shadowBlurBoxOffset = bpy.props.IntProperty(
 )
 
 class ObjectPanel(bpy.types.Panel):
-    bl_label = 'Babylon.js'
+    bl_label = 'Babylon.js ' + format_version()
     bl_space_type = 'PROPERTIES'
     bl_region_type = 'WINDOW'
     bl_context = 'data'
@@ -2570,23 +2695,23 @@ class ObjectPanel(bpy.types.Panel):
             row = layout.row()
             row.prop(ob.data, 'useFlatShading')
             row.prop(ob.data, 'checkCollisions')
-            
+
             row = layout.row()
             row.prop(ob.data, 'castShadows')
             row.prop(ob.data, 'receiveShadows')
-            
+
             row = layout.row()
             row.prop(ob.data, 'freezeWorldMatrix')
             row.prop(ob.data, 'loadDisabled')
-            
-            layout.prop(ob.data, 'autoAnimate')            
+
+            layout.prop(ob.data, 'autoAnimate')
             layout.prop(ob.data, 'maxInfluencers')
 
             box = layout.box()
             box.label('Materials')
             box.prop(ob.data, 'materialNameSpace')
             box.prop(ob.data, 'checkReadyOnlyOnce')
-            
+
             box = layout.box()
             box.label(text='Procedural Texture / Cycles Baking')
             box.prop(ob.data, 'bakeSize')

+ 4 - 4
Exporters/Blender/readme.md

@@ -14,7 +14,7 @@ For a discussion of Tower of Babel exporter, along with the difference this expo
  * Check collisions
  * Gravity
  * Ellipsoid
- * Animations
+ * Actions exported as AnimationRanges
  * 3D Camera Rigs
  * All kind of Babylon.js cameras can be chosen from a custom dropdown list
 * **Lights**
@@ -28,7 +28,7 @@ For a discussion of Tower of Babel exporter, along with the difference this expo
  * Diffuse color
  * Specular color
  * Shadow maps, all types (For directional lights)
- * Animations
+ * Actions exported as AnimationRanges
 * **Materials**
  * Name
  * Name space
@@ -77,9 +77,9 @@ For a discussion of Tower of Babel exporter, along with the difference this expo
  * Check collisions
  * Billboard
  * Receive and cast shadows
- * Bones (armatures) and bones' animations
+ * Bones (armatures) and bones' actions
  	* Variable Max Bone Influencers / vertex
- * Animations
+ * Actions exported as AnimationRanges
 
 
 

+ 9 - 1
dist/preview release/what's new.md

@@ -7,7 +7,7 @@
       - Normal material: http://doc.babylonjs.com/extensions/normal ([temechon](https://github.com/temechon))
       - Lava Material: http://doc.babylonjs.com/extensions/lava ([temechon](https://github.com/temechon))
       - PBR Material: http://doc.babylonjs.com/extensions/pbr ([deltakosh](https://github.com/deltakosh))
-    - New cache mecanism for StandardMaterial ([deltakosh](https://github.com/deltakosh))
+    - New cache mechanism for StandardMaterial ([deltakosh](https://github.com/deltakosh))
     - New Solid Particle System ([jerome](https://github.com/jbousquie))
     - New `StandardMaterial.lightmapTexture` which can be controlled with `StandardMaterial.useLightmapAsShadowMap` ([deltakosh](https://github.com/deltakosh))
     - Support for reflection probes. [See documentation here](http://doc.babylonjs.com/tutorials/How_to_use_Reflection_probes) ([deltakosh](https://github.com/deltakosh))
@@ -17,6 +17,10 @@
     - New `Mesh.CreatePolyhedron()` method ([jerome](https://github.com/jbousquie))
     - New `Mesh.CreateIcoSphere()` method. [Demo here](http://www.babylonjs-playground.com/#24DUYD) (G'kar)
     - Introducing [babylon.core.js](http://doc.babylonjs.com/generals/Framework_versions) ([deltakosh](https://github.com/deltakosh))
+    - Introducing AnimationRanges for Skeletons and Animations ([deltakosh](https://github.com/deltakosh))
+      - Added parsing / serialization, copying between similar skeletons, &amp; better deletion ([Palmer-JC](https://github.com/Palmer-JC))
+      - Expanded AnimationRanges for Nodes (Mesh, Lights, &amp; Cameras) ([Palmer-JC](https://github.com/Palmer-JC))
+      - Support for added to Blender Exporter ([Palmer-JC](https://github.com/Palmer-JC))
   - **Updates**
     - Added ability to skip current prepared Action to next active Action (chained by Action.then(Action)) ([vouskprod](http://www.github.com/vousk)) 
     - Added new event triggers `ActionManager.OnLongPressTrigger` and `ActionManager.OnPickDownTrigger` ([vouskprod](http://www.github.com/vousk)) 
@@ -56,6 +60,10 @@
     - Added scene onPointerMove public callback. [PR](https://github.com/BabylonJS/Babylon.js/pull/810) ([RaananW](https://github.com/RaananW))
     - Added streaming support for BABYLON.Sound ([davrous](https://github.com/davrous))
     - Added collisionsEnabled and workerCollisions for serialization [PR](https://github.com/BabylonJS/Babylon.js/pull/830) ([Dad72](https://github.com/dad72))
+    - Changed from a fixed maximum of bone influencers, 4, to variable, 1-8 ([Palmer-JC](https://github.com/Palmer-JC))
+      - Support for added to Blender Exporter ([Palmer-JC](https://github.com/Palmer-JC))
+    - Float32Arrays can now directly be specified for vertex data attributes, `Mesh.updateVerticesDataDirectly` deprecated ([Palmer-JC](https://github.com/Palmer-JC))
+    
   - **Bug fixes**
     - Fixed a bug with spherical mapping ([deltakosh](https://github.com/deltakosh)) 
     - Fixed a bug with clone and createInstance which was forcing the recomputation of bounding boxes ([deltakosh](https://github.com/deltakosh))