Browse Source

Updated bl_info author, version, and blender metadata.
Fixed menu label to read "Three.js".
Reimplemented the "Frame index as time" option.
Animation data is an array of all actions in the scene.
Reimplemented the latest skeletal animation logic that had previously been missed.
When copying textures destination files will be removed first.
Return values of several mush functions were not consistent and causing errors.

repsac 10 years ago
parent
commit
348a3c3c53

+ 1 - 0
utils/exporters/blender/.gitignore

@@ -1,2 +1,3 @@
 tests/review
 __pycache__/
+tmp/

+ 16 - 4
utils/exporters/blender/addons/io_three/__init__.py

@@ -35,9 +35,9 @@ SETTINGS_FILE_EXPORT = 'three_settings_export.js'
 
 bl_info = {
     'name': 'Three.js Format',
-    'author': 'Ed Caspersen (repsac)',
-    'version': (1, 0, 0),
-    'blender': (2, 7, 2),
+    'author': 'repsac, mrdoob, yomotsu, mpk, jpweeks',
+    'version': (1, 1, 0),
+    'blender': (2, 7, 3),
     'location': 'File > Import-Export',
     'description': 'Export Three.js formatted JSON files.',
     'warning': '',
@@ -232,6 +232,7 @@ def save_settings_export(properties):
         constants.MORPH_TARGETS: properties.option_animation_morph,
         constants.ANIMATION: properties.option_animation_skeletal,
         constants.FRAME_STEP: properties.option_frame_step,
+        constants.FRAME_INDEX_AS_TIME: properties.option_frame_index_as_time,
         constants.INFLUENCES_PER_VERTEX: properties.option_influences
     }
 
@@ -366,6 +367,10 @@ def restore_settings_export(properties):
     properties.option_frame_step = settings.get(
         constants.FRAME_STEP, 
         constants.EXPORT_OPTIONS[constants.FRAME_STEP])
+
+    properties.option_frame_index_as_time = settings.get(
+        constants.FRAME_INDEX_AS_TIME,
+        constants.EXPORT_OPTIONS[constants.FRAME_INDEX_AS_TIME])
     ## }
 
 def compression_types():
@@ -522,6 +527,11 @@ class ExportThree(bpy.types.Operator, ExportHelper):
         description='Export animation (skeletal)', 
         default=constants.EXPORT_OPTIONS[constants.ANIMATION])
 
+    option_frame_index_as_time = BoolProperty(
+        name='Frame index as time',
+        description='Use (original) frame index as frame time',
+        default=constants.EXPORT_OPTIONS[constants.FRAME_INDEX_AS_TIME])
+
     option_frame_step = IntProperty(
         name='Frame step', 
         description='Animation frame step', 
@@ -669,11 +679,13 @@ class ExportThree(bpy.types.Operator, ExportHelper):
         row.prop(self.properties, 'option_animation_skeletal')
         row = layout.row()
         row.prop(self.properties, 'option_frame_step')
+        row = layout.row()
+        row.prop(self.properties, 'option_frame_index_as_time')
         ## }
 
 def menu_func_export(self, context):
     default_path = bpy.data.filepath.replace('.blend', constants.EXTENSION)
-    text = 'Three (%s)' % constants.EXTENSION
+    text = 'Three.js (%s)' % constants.EXTENSION
     operator = self.layout.operator(ExportThree.bl_idname, text=text)
     operator.filepath = default_path
 

+ 10 - 3
utils/exporters/blender/addons/io_three/constants.py

@@ -49,7 +49,8 @@ SCALE = 'scale'
 COMPRESSION = 'compression'
 MAPS = 'maps'
 FRAME_STEP = 'frameStep'
-ANIMATION = 'animation'
+FRAME_INDEX_AS_TIME = 'frameIndexAsTime'
+ANIMATION = 'animations'
 MORPH_TARGETS = 'morphTargets'
 SKIN_INDICES = 'skinIndices'
 SKIN_WEIGHTS = 'skinWeights'
@@ -93,6 +94,7 @@ EXPORT_OPTIONS = {
     FACE_MATERIALS: False,
     SCALE: 1,
     FRAME_STEP: 1,
+    FRAME_INDEX_AS_TIME: False,
     SCENE: True,
     MIX_COLORS: False,
     COMPRESSION: None,
@@ -210,10 +212,15 @@ IMAGE = 'image'
 
 NAME = 'name'
 PARENT = 'parent'
-
-#@TODO move to api.constants?
+LENGTH = 'length'
+FPS = 'fps'
+HIERARCHY = 'hierarchy'
 POS = 'pos'
 ROTQ = 'rotq'
+ROT = 'rot'
+SCL = 'scl'
+TIME = 'time'
+KEYS = 'keys'
 
 AMBIENT = 'ambient'
 COLOR = 'color'

+ 206 - 127
utils/exporters/blender/addons/io_three/exporter/api/mesh.py

@@ -24,57 +24,61 @@ def _mesh(func):
 def animation(mesh, options):
     logger.debug('mesh.animation(%s, %s)', mesh, options)
     armature = _armature(mesh)
-    if armature and armature.animation_data:
-        return _skeletal_animations(armature, options)
+    animations = []
+    if armature:
+        for action in data.actions:
+            logger.info('Processing action %s', action.name)
+            animations.append(
+                _skeletal_animations(action, armature, options))
+    else:
+        logger.warning('No armature found')
+        
+    return animations
 
 
 @_mesh
 def bones(mesh):
     logger.debug('mesh.bones(%s)', mesh)
     armature = _armature(mesh)
-    if not armature: return
-
     bones = []
     bone_map = {}
+
+    if not armature: 
+        return bones, bone_map
+
     bone_count = 0
-    bone_index_rel = 0
 
-    for bone in armature.data.bones:
-        logger.info('Parsing bone %s', bone.name)
+    armature_matrix = armature.matrix_world
+    for bone_count, pose_bone in enumerate(armature.pose.bones):
+        armature_bone = pose_bone.bone
+        bone_index = None
 
-        if bone.parent is None or bone.parent.use_deform is False:
-            bone_pos = bone.head_local
+        if armature_bone.parent is None:
+            bone_matrix = armature_matrix * armature_bone.matrix_local
             bone_index = -1
         else:
-            bone_pos = bone.head_local - bone.parent.head_local
-            bone_index = 0
-            index = 0
-            for parent in armature.data.bones:
-                if parent.name == bone.parent.name:
-                    bone_index = bone_map.get(index)
+            parent_bone = armature_bone.parent
+            parent_matrix = armature_matrix * parent_bone.matrix_local
+            bone_matrix = armature_matrix * armature_bone.matrix_local
+            bone_matrix = parent_matrix.inverted() * bone_matrix
+            bone_index = index = 0
+
+            for pose_parent in armature.pose.bones:
+                armature_parent = pose_parent.bone.name
+                if armature_parent == parent_bone.name:
+                    bone_index = index
                 index += 1
 
-        bone_world_pos = armature.matrix_world * bone_pos
-        x_axis = bone_world_pos.x
-        y_axis = bone_world_pos.z
-        z_axis = -bone_world_pos.y
-
-        if bone.use_deform:
-            logger.debug('Adding bone %s at: %s, %s', 
-                bone.name, bone_index, bone_index_rel)
-            bone_map[bone_count] = bone_index_rel
-            bone_index_rel += 1
-            bones.append({
-                constants.PARENT: bone_index,
-                constants.NAME: bone.name,
-                constants.POS: (x_axis, y_axis, z_axis),
-                constants.ROTQ: (0,0,0,1)
-            })
-        else:
-            logger.debug('Ignoring bone %s at: %s, %s', 
-                bone.name, bone_index, bone_index_rel)
+        bone_map[bone_count] = bone_count
 
-        bone_count += 1
+        pos, rot, scl = bone_matrix.decompose()
+        bones.append({
+            constants.PARENT: bone_index,
+            constants.NAME: armature_bone.name,
+            constants.POS: (pos.x, pos.z, -pos.y),
+            constants.ROTQ: (rot.x, rot.z, -rot.y, rot.w),
+            'scl': (scl.x, scl.z, scl.y)
+        })
 
     return (bones, bone_map)
 
@@ -101,8 +105,6 @@ def buffer_normal(mesh, options):
     return normals_
 
 
-
-
 @_mesh
 def buffer_position(mesh, options):
     position = []
@@ -367,12 +369,12 @@ def materials(mesh, options):
 @_mesh
 def normals(mesh, options):
     logger.debug('mesh.normals(%s, %s)', mesh, options)
-    flattened = []
+    normal_vectors = []
 
     for vector in _normals(mesh, options):
-        flattened.extend(vector)
+        normal_vectors.extend(vector)
 
-    return flattened
+    return normal_vectors
 
 
 @_mesh
@@ -631,12 +633,13 @@ def _armature(mesh):
 
 def _skinning_data(mesh, bone_map, influences, array_index):
     armature = _armature(mesh)
-    if not armature: return
+    manifest = []
+    if not armature: 
+        return manifest
 
     obj = object_.objects_using_mesh(mesh)[0]
     logger.debug('Skinned object found %s', obj.name)
 
-    manifest = []
     for vertex in mesh.vertices:
         bone_array = []
         for group in vertex.groups:
@@ -648,9 +651,9 @@ def _skinning_data(mesh, bone_map, influences, array_index):
             if index >= len(bone_array):
                 manifest.append(0)
                 continue
-
-            for bone_index, bone in enumerate(armature.data.bones):
-                if bone.name != obj.vertex_groups[bone_array[index][0]].name:
+            name = obj.vertex_groups[bone_array[index][0]].name
+            for bone_index, bone in enumerate(armature.pose.bones):
+                if bone.name != name:
                     continue
                 if array_index is 0:
                     entry = bone_map.get(bone_index, -1)
@@ -665,101 +668,177 @@ def _skinning_data(mesh, bone_map, influences, array_index):
     return manifest
 
 
-def _skeletal_animations(armature, options):
-    action = armature.animation_data.action
+def _find_channels(action, bone, channel_type):
+    result = []
+
+    if len(action.groups):
+        
+        group_index = -1
+        for index, group in enumerate(action.groups):
+            if group.name == bone.name:
+                group_index = index
+                #@TODO: break?
+
+        if group_index > -1:
+            for channel in action.groups[group_index].channels:
+                if channel_type in channel.data_path:
+                    result.append(channel)
+
+    else:
+        bone_label = '"%s"' % bone.name
+        for channel in action.fcurves:
+            data_path = [bone_label in channel.data_path,
+                channel_type in channel.data_path]
+            if all(data_path):
+                result.append(channel)
+
+    return result
+
+
+def _skeletal_animations(action, armature, options):
+    try:
+        current_context = context.area.type
+    except AttributeError:
+        logger.warning('No context, possibly running in batch mode')
+    else:
+        context.area.type = 'DOPESHEET_EDITOR'
+        context.space_data.mode = 'ACTION'
+        context.area.spaces.active.action = action
+    
+    armature_matrix = armature.matrix_world
+    fps = context.scene.render.fps
+
     end_frame = action.frame_range[1]
     start_frame = action.frame_range[0]
     frame_length = end_frame - start_frame
-    l,r,s = armature.matrix_world.decompose()
-    rotation_matrix = r.to_matrix()
-    hierarchy = []
-    parent_index = -1
+
     frame_step = options.get(constants.FRAME_STEP, 1)
-    fps = context.scene.render.fps
+    used_frames = int(frame_length / frame_step) + 1
+
+    keys = []
+    channels_location = []
+    channels_rotation = []
+    channels_scale = []
+
+    for pose_bone in armature.pose.bones:
+        logger.info('Processing channels for %s', 
+                    pose_bone.bone.name)
+        keys.append([])
+        channels_location.append(
+            _find_channels(action, 
+                           pose_bone.bone,
+                           'location'))
+        channels_rotation.append(
+            _find_channels(action, 
+                           pose_bone.bone,
+                           'rotation_quaternion'))
+        channels_rotation.append(
+            _find_channels(action, 
+                           pose_bone.bone,
+                           'rotation_euler'))
+        channels_scale.append(
+            _find_channels(action, 
+                           pose_bone.bone,
+                           'scale'))
+
+    frame_step = options[constants.FRAME_STEP]
+    frame_index_as_time = options[constants.FRAME_INDEX_AS_TIME]
+    for frame_index in range(0, used_frames):
+        if frame_index == used_frames - 1:
+            frame = end_frame
+        else:
+            frame = start_frame + frame_index * frame_step
 
-    start = int(start_frame)
-    end = int(end_frame / frame_step) + 1
+        logger.info('Processing frame %d', frame)
 
-    #@TODO need key constants
-    for bone in armature.data.bones:
-        if bone.use_deform is False:
-            logger.info('Skipping animation data for bone %s', bone.name)
-            continue
+        time = frame - start_frame
+        if frame_index_as_time is False:
+            time = time / fps
 
-        logger.info('Parsing animation data for bone %s', bone.name)
+        context.scene.frame_set(frame)
 
-        keys = []
-        for frame in range(start, end):
-            computed_frame = frame * frame_step
-            pos, pchange = _position(bone, computed_frame, 
-                action, armature.matrix_world)
-            rot, rchange = _rotation(bone, computed_frame, 
-                action, rotation_matrix)
+        bone_index = 0
+
+        def has_keyframe_at(channels, frame):
+            def find_keyframe_at(channel, frame):
+                for keyframe in channel.keyframe_points:
+                    if keyframe.co[0] == frame:
+                        return keyframe
+                return None
+
+            for channel in channels:
+                if not find_keyframe_at(channel, frame) is None:
+                    return True
+            return False
+
+        for pose_bone in armature.pose.bones:
+            logger.info('Processing bone %s', pose_bone.bone.name)
+            if pose_bone.parent is None:
+                bone_matrix = armature_matrix * pose_bone.matrix
+            else:
+                parent_matrix = armature_matrix * pose_bone.parent.matrix
+                bone_matrix = armature_matrix * pose_bone.matrix
+                bone_matrix = parent_matrix.inverted() * bone_matrix
+
+            pos, rot, scl = bone_matrix.decompose()
+            
+            pchange = True or has_keyframe_at(
+                channels_location[bone_index], frame)
+            rchange = True or has_keyframe_at(
+                channels_rotation[bone_index], frame)
+            schange = True or has_keyframe_at(
+                channels_scale[bone_index], frame)
 
-            # flip y and z
             px, py, pz = pos.x, pos.z, -pos.y
             rx, ry, rz, rw = rot.x, rot.z, -rot.y, rot.w
+            sx, sy, sz = scl.x, scl.z, scl.y
+
+            keyframe = {constants.TIME: time}
+            if frame == start_frame or frame == end_frame:
+                keyframe.update({
+                    constants.POS: [px, py, pz],
+                    constants.ROT: [rx, ry, rz, rw],
+                    constants.SCL: [sx, sy, sz]
+                })
+            elif any([pchange, rchange, schange]):
+                if pchange is True:
+                    keyframe[constants.POS] = [px, py, pz]
+                if rchange is True:
+                    keyframe[constants.ROT] = [rx, ry, rz, rw]
+                if schange is True:
+                    keyframe[constants.SCL] = [sx, sy, sz]
+
+            if len(keyframe.keys()) > 1:
+                logger.info('Recording keyframe data for %s %s',
+                            pose_bone.bone.name, str(keyframe))
+                keys[bone_index].append(keyframe)
+            else:
+                logger.info('No anim data to record for %s',
+                            pose_bone.bone.name)
 
-            if frame == start_frame:
-
-                time = (frame * frame_step - start_frame) / fps
-                keyframe = {
-                    'time': time,
-                    'pos': [px, py, pz],
-                    'rot': [rx, ry, rz, rw],
-                    'scl': [1, 1, 1]
-                }
-                keys.append(keyframe)
-
-            # END-FRAME: needs pos, rot and scl attributes 
-            # with animation length (required frame)
-
-            elif frame == end_frame / frame_step:
-
-                time = frame_length / fps
-                keyframe = {
-                    'time': time,
-                    'pos': [px, py, pz],
-                    'rot': [rx, ry, rz, rw],
-                    'scl': [1, 1, 1]
-                }
-                keys.append(keyframe)
-
-            # MIDDLE-FRAME: needs only one of the attributes, 
-            # can be an empty frame (optional frame)
-
-            elif pchange == True or rchange == True:
-
-                time = (frame * frame_step - start_frame) / fps
-
-                if pchange == True and rchange == True:
-                    keyframe = {
-                        'time': time, 
-                        'pos': [px, py, pz],
-                        'rot': [rx, ry, rz, rw]
-                    }
-                elif pchange == True:
-                    keyframe = {
-                        'time': time, 
-                        'pos': [px, py, pz]
-                    }
-                elif rchange == True:
-                    keyframe = {
-                        'time': time, 
-                        'rot': [rx, ry, rz, rw]
-                    }
-
-                keys.append(keyframe)
-
-        hierarchy.append({'keys': keys, 'parent': parent_index})
-        parent_index += 1
-
-    #@TODO key constants
+            bone_index += 1
+    
+    hierarchy = []
+    bone_index = 0
+    for pose_bone in armature.pose.bones:
+        hierarchy.append({
+            constants.PARENT: bone_index - 1,
+            constants.KEYS: keys[bone_index]
+        })
+        bone_index += 1
+
+    if frame_index_as_time is False:
+        frame_length = frame_length / fps
+
+    context.scene.frame_set(start_frame)
+    if context.area:
+        context.area.type = current_context
+    
     animation = {
-        'hierarchy': hierarchy, 
-        'length':frame_length / fps,
-        'fps': fps,
-        'name': action.name
+        constants.HIERARCHY: hierarchy, 
+        constants.LENGTH:frame_length,
+        constants.FPS: fps,
+        constants.NAME: action.name
     }
 
     return animation

+ 19 - 0
utils/exporters/blender/addons/io_three/exporter/io.py

@@ -1,4 +1,7 @@
+import os
+import sys
 import shutil
+import traceback
 from .. import constants, logger
 from . import _json
 
@@ -11,6 +14,22 @@ def copy_registered_textures(dest, registration):
 
 def copy(src, dst):
     logger.debug('io.copy(%s, %s)' % (src, dst))
+    if os.path.exists(dst) and os.path.isfile(src):
+        file_name = os.path.basename(src)
+        dst = os.path.join(dst, file_name)
+
+        logger.info('Destination file exists, attempting to remove %s', dst)
+        try:
+            os.remove(dst)
+        except:
+            logger.error('Failed to remove destination file')
+            info = sys.exc_info()
+            trace = traceback.format_exception(
+                info[0], info[1], info[2].tb_next)
+            trace = ''.join(trace)
+            logger.error(trace)
+            raise
+
     shutil.copy(src, dst)
 
 

BIN
utils/exporters/blender/tests/blend/anim.blend


+ 4 - 4
utils/exporters/blender/tests/scripts/js/review.js

@@ -131,14 +131,14 @@ function loadGeometry( data, url ) {
     var material = new THREE.MeshFaceMaterial( data.materials ); 
     var mesh;
 
-    if ( data.geometry.animation !== undefined ) {
+    if ( data.geometry.animations !== undefined && data.geometry.animations.length > 0 ) {
 
         console.log( 'loading animation' );
         data.materials[ 0 ].skinning = true;
-        mesh = new THREE.SkinnedMesh( data.geometry, material, false);
+        mesh = new THREE.SkinnedMesh( data.geometry, material, false );
 
-        var name = data.geometry.animation.name;
-        animation = new THREE.Animation( mesh, data.geometry.animation );
+        var name = data.geometry.animations[0].name;
+        animation = new THREE.Animation( mesh, data.geometry.animations[0] );
 
     } else {