Prechádzať zdrojové kódy

Added utility functions to generate ID using a base name
Abstracted uuid creation logic from base classes
Improved the layout of controls in the export panel
Added option for indenting the JSON output (space saver)
Animation option is now an enum that requires a user to select between POSE and REST
Abstracted animation logic into a new module reducing the size of mesh.py
Improved indexing speeds for vertex colours and normal vectors
Node uuid is now based on name and will be persistent ver multiple exports
Empty arrays will no longer assign None to key values
Copying files will no longer remove the destination, additionally destinations are tested to see if the same as the source

repsac 10 rokov pred
rodič
commit
91a9b61484

+ 99 - 46
utils/exporters/blender/addons/io_three/__init__.py

@@ -18,6 +18,7 @@
 
 import os
 import json
+import logging
 
 import bpy
 from bpy_extras.io_utils import ExportHelper
@@ -30,6 +31,10 @@ from bpy.props import (
 
 from . import constants
 
+logging.basicConfig(
+    format='%(levelname)s:THREE:%(message)s',
+    level=logging.DEBUG)
+
 SETTINGS_FILE_EXPORT = 'three_settings_export.js'
 
 
@@ -221,10 +226,11 @@ def save_settings_export(properties):
         constants.PRECISION: properties.option_round_value,
         constants.LOGGING: properties.option_logging,
         constants.COMPRESSION: properties.option_compression,
+        constants.INDENT: properties.option_indent,
         constants.COPY_TEXTURES: properties.option_copy_textures,
 
         constants.SCENE: properties.option_export_scene,
-        constants.EMBED_GEOMETRY: properties.option_embed_geometry,
+        #constants.EMBED_GEOMETRY: properties.option_embed_geometry,
         constants.EMBED_ANIMATION: properties.option_embed_animation,
         constants.LIGHTS: properties.option_lights,
         constants.CAMERAS: properties.option_cameras,
@@ -237,6 +243,7 @@ def save_settings_export(properties):
     }
 
     fname = get_settings_fullpath()
+    logging.debug('Saving settings to %s', fname)
     with open(fname, 'w') as stream:
         json.dump(settings, stream)
 
@@ -249,8 +256,11 @@ def restore_settings_export(properties):
 
     fname = get_settings_fullpath()
     if os.path.exists(fname) and os.access(fname, os.R_OK):
-        f = open(fname, 'r')
-        settings = json.load(f)
+        logging.debug('Settings cache found %s', fname)
+        with open(fname, 'r') as fs:
+            settings = json.load(fs)
+    else:
+        logging.debug('No settings file found, using defaults.')
 
     ## Geometry {
     properties.option_vertices = settings.get(
@@ -328,6 +338,10 @@ def restore_settings_export(properties):
         constants.COMPRESSION, 
         constants.NONE)
 
+    properties.option_indent = settings.get(
+        constants.INDENT,
+        constants.EXPORT_OPTIONS[constants.INDENT])
+
     properties.option_copy_textures = settings.get(
         constants.COPY_TEXTURES, 
         constants.EXPORT_OPTIONS[constants.COPY_TEXTURES])
@@ -342,9 +356,9 @@ def restore_settings_export(properties):
         constants.SCENE, 
         constants.EXPORT_OPTIONS[constants.SCENE])
 
-    properties.option_embed_geometry = settings.get(
-        constants.EMBED_GEOMETRY, 
-        constants.EXPORT_OPTIONS[constants.EMBED_GEOMETRY])
+    #properties.option_embed_geometry = settings.get(
+    #    constants.EMBED_GEOMETRY, 
+    #    constants.EXPORT_OPTIONS[constants.EMBED_GEOMETRY])
 
     properties.option_lights = settings.get(
         constants.LIGHTS, 
@@ -385,6 +399,15 @@ def compression_types():
 
     return types
 
+def animation_options():
+    anim = [
+        (constants.OFF, constants.OFF.title(), constants.OFF),
+        (constants.POSE, constants.POSE.title(), constants.POSE),
+        (constants.REST, constants.REST.title(), constants.REST)
+    ]
+
+    return anim
+
 class ExportThree(bpy.types.Operator, ExportHelper):
 
     bl_idname='export.three'
@@ -408,7 +431,7 @@ class ExportThree(bpy.types.Operator, ExportHelper):
         default=constants.EXPORT_OPTIONS[constants.NORMALS])
 
     option_colors = BoolProperty(
-        name='Colors', 
+        name='Vertex Colors', 
         description='Export vertex colors', 
         default=constants.EXPORT_OPTIONS[constants.COLORS])
 
@@ -476,7 +499,7 @@ class ExportThree(bpy.types.Operator, ExportHelper):
         (constants.CRITICAL, constants.CRITICAL, constants.CRITICAL)]
 
     option_logging = EnumProperty(
-        name='Logging', 
+        name='', 
         description = 'Logging verbosity level', 
         items=logging_types, 
         default=constants.DEBUG)
@@ -491,11 +514,13 @@ class ExportThree(bpy.types.Operator, ExportHelper):
         name='Scene', 
         description='Export scene', 
         default=constants.EXPORT_OPTIONS[constants.SCENE])
-
-    option_embed_geometry = BoolProperty(
-        name='Embed geometry', 
-        description='Embed geometry', 
-        default=constants.EXPORT_OPTIONS[constants.EMBED_GEOMETRY])
+    
+    #@TODO: removing this option since the ObjectLoader doesn't have
+    #       support for handling external geometry data
+    #option_embed_geometry = BoolProperty(
+    #    name='Embed geometry', 
+    #    description='Embed geometry', 
+    #    default=constants.EXPORT_OPTIONS[constants.EMBED_GEOMETRY])
 
     option_embed_animation = BoolProperty(
         name='Embed animation', 
@@ -522,10 +547,11 @@ class ExportThree(bpy.types.Operator, ExportHelper):
         description='Export animation (morphs)', 
         default=constants.EXPORT_OPTIONS[constants.MORPH_TARGETS])
 
-    option_animation_skeletal = BoolProperty(
-        name='Skeletal animation', 
+    option_animation_skeletal = EnumProperty(
+        name='', 
         description='Export animation (skeletal)', 
-        default=constants.EXPORT_OPTIONS[constants.ANIMATION])
+        items=animation_options(),
+        default=constants.OFF)
 
     option_frame_index_as_time = BoolProperty(
         name='Frame index as time',
@@ -541,8 +567,13 @@ class ExportThree(bpy.types.Operator, ExportHelper):
         soft_max=1000, 
         default=1)
  
+    option_indent = BoolProperty(
+        name='Indent JSON',
+        description='Disable this to reduce the file size',
+        default=constants.EXPORT_OPTIONS[constants.INDENT])
+
     option_compression = EnumProperty(
-        name='Compression', 
+        name='', 
         description = 'Compression options', 
         items=compression_types(), 
         default=constants.NONE)
@@ -585,7 +616,7 @@ class ExportThree(bpy.types.Operator, ExportHelper):
 
         ## Geometry {
         row = layout.row()
-        row.label(text='Geometry:')
+        row.label(text='GEOMETRY:')
 
         row = layout.row()
         row.prop(self.properties, 'option_vertices')
@@ -593,6 +624,7 @@ class ExportThree(bpy.types.Operator, ExportHelper):
 
         row = layout.row()
         row.prop(self.properties, 'option_normals')
+        row.prop(self.properties, 'option_uv_coords')
 
         row = layout.row()
         row.prop(self.properties, 'option_bones')
@@ -601,69 +633,66 @@ class ExportThree(bpy.types.Operator, ExportHelper):
         row = layout.row()
         row.prop(self.properties, 'option_geometry_type')
 
-        row = layout.row()
-        row.prop(self.properties, 'option_influences')
         ## }
 
         layout.separator()
 
         ## Materials {
         row = layout.row()
-        row.label(text='Materials:')
-
-        row = layout.row()
-        row.prop(self.properties, 'option_materials')
-        row.prop(self.properties, 'option_uv_coords')
+        row.label(text='- Shading:')
 
         row = layout.row()
         row.prop(self.properties, 'option_face_materials')
-        row.prop(self.properties, 'option_maps')
 
         row = layout.row()
         row.prop(self.properties, 'option_colors')
+
+        row = layout.row()
         row.prop(self.properties, 'option_mix_colors')
         ## }
     
         layout.separator()
 
-        ## Settings {
+        ## Animation {
         row = layout.row()
-        row.label(text='Settings:')
+        row.label(text='- Animation:')
 
         row = layout.row()
-        row.prop(self.properties, 'option_scale')
-        
-        row = layout.row()
-        row = layout.row()
-        row.prop(self.properties, 'option_round_off')
+        row.prop(self.properties, 'option_animation_morph')
+
         row = layout.row()
-        row.prop(self.properties, 'option_round_value')
+        row.label(text='Skeletal animations:')
 
         row = layout.row()
+        row.prop(self.properties, 'option_animation_skeletal')
+
+        layout.row()
         row = layout.row()
-        row.prop(self.properties, 'option_logging')
+        row.prop(self.properties, 'option_influences')
 
         row = layout.row()
-        row.prop(self.properties, 'option_compression')
+        row.prop(self.properties, 'option_frame_step')
 
         row = layout.row()
-        row.prop(self.properties, 'option_copy_textures')
+        row.prop(self.properties, 'option_frame_index_as_time')
 
         row = layout.row()
         row.prop(self.properties, 'option_embed_animation')
+
         ## }
 
         layout.separator()
 
         ## Scene {
         row = layout.row()
-        row.label(text='Scene:')
+        row.label(text='SCENE:')
 
         row = layout.row()
         row.prop(self.properties, 'option_export_scene')
+        row.prop(self.properties, 'option_materials')
 
-        row = layout.row()
-        row.prop(self.properties, 'option_embed_geometry')
+        #row = layout.row()
+        #row.prop(self.properties, 'option_embed_geometry')
 
         row = layout.row()
         row.prop(self.properties, 'option_lights')
@@ -672,20 +701,44 @@ class ExportThree(bpy.types.Operator, ExportHelper):
 
         layout.separator()
 
-        ## Animation {
+        ## Settings {
         row = layout.row()
-        row.label(text='Animation:')
+        row.label(text='SETTINGS:')
 
         row = layout.row()
-        row.prop(self.properties, 'option_animation_morph')
+        row.prop(self.properties, 'option_maps')
+
         row = layout.row()
-        row.prop(self.properties, 'option_animation_skeletal')
+        row.prop(self.properties, 'option_copy_textures')
+
         row = layout.row()
-        row.prop(self.properties, 'option_frame_step')
+        row.prop(self.properties, 'option_scale')
+        
+        layout.row()
         row = layout.row()
-        row.prop(self.properties, 'option_frame_index_as_time')
+        row.prop(self.properties, 'option_round_off')
+        row = layout.row()
+        row.prop(self.properties, 'option_round_value')
+
+        layout.row()
+        row = layout.row()
+        row.label(text='Logging verbosity:')
+
+        row = layout.row()
+        row.prop(self.properties, 'option_logging')
+
+        row = layout.row()
+        row.label(text='File compression format:')
+
+        row = layout.row()
+        row.prop(self.properties, 'option_compression')
+
+        row = layout.row()
+        row.prop(self.properties, 'option_indent')
         ## }
 
+
+
 def menu_func_export(self, context):
     default_path = bpy.data.filepath.replace('.blend', constants.EXTENSION)
     text = 'Three.js (%s)' % constants.EXTENSION

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

@@ -34,6 +34,7 @@ MAPPING_TYPES = type('Mapping', (), {
 
 JSON = 'json'
 EXTENSION = '.%s' % JSON
+INDENT = 'indent'
 
 
 MATERIALS = 'materials'
@@ -52,6 +53,8 @@ FRAME_STEP = 'frameStep'
 FRAME_INDEX_AS_TIME = 'frameIndexAsTime'
 ANIMATION = 'animations'
 MORPH_TARGETS = 'morphTargets'
+POSE = 'pose'
+REST = 'rest'
 SKIN_INDICES = 'skinIndices'
 SKIN_WEIGHTS = 'skinWeights'
 LOGGING = 'logging'
@@ -65,6 +68,7 @@ PRECISION = 'precision'
 DEFAULT_PRECISION = 6
 EMBED_GEOMETRY = 'embedGeometry'
 EMBED_ANIMATION = 'embedAnimation'
+OFF = 'off'
 
 GLOBAL = 'global'
 BUFFER_GEOMETRY = 'BufferGeometry'
@@ -95,11 +99,11 @@ EXPORT_OPTIONS = {
     SCALE: 1,
     FRAME_STEP: 1,
     FRAME_INDEX_AS_TIME: False,
-    SCENE: True,
+    SCENE: False,
     MIX_COLORS: False,
     COMPRESSION: None,
     MAPS: False,
-    ANIMATION: False,
+    ANIMATION: OFF,
     BONES: False,
     SKINNING: False,
     MORPH_TARGETS: False,
@@ -112,7 +116,8 @@ EXPORT_OPTIONS = {
     EMBED_GEOMETRY: True,
     EMBED_ANIMATION: True,
     GEOMETRY_TYPE: GEOMETRY,
-    INFLUENCES_PER_VERTEX: 2
+    INFLUENCES_PER_VERTEX: 2,
+    INDENT: True
 }
 
 

+ 476 - 0
utils/exporters/blender/addons/io_three/exporter/api/animation.py

@@ -0,0 +1,476 @@
+import mathutils
+from bpy import data, context
+from .. import constants, logger, utilities
+
+
+def pose_animation(armature, options):
+    logger.debug('animation.pose_animation %s', armature)
+    func = _parse_pose_action
+    return _parse_action(func, armature, options)
+
+
+def rest_animation(armature, options):
+    logger.debug('animation.rest_animation %s', armature)
+    func = _parse_rest_action
+    return _parse_action(func, armature, options)
+
+
+def _parse_action(func, armature, options):
+    animations = []
+    logger.info('Parsing %d actions', len(data.actions))
+    round_off, round_val = utilities.rounding(options)
+    for action in data.actions:
+        logger.info('Parsing action %s', action.name)
+        animation = func(action, armature, options, round_off, round_val)
+        animations.append(animation)
+    return animations
+
+
+def _parse_rest_action(action, armature, options, round_off, round_val):
+    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
+
+    start = int(start_frame)
+    end = int(end_frame / frame_step) + 1
+
+    for bone in armature.data.bones:
+        # I believe this was meant to skip control bones, may
+        # not be useful. needs more testing
+        if bone.use_deform is False:
+            logger.info('Skipping animation data for bone %s', bone.name)
+            continue
+
+        logger.info('Parsing animation data for bone %s', bone.name)
+
+        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)
+
+            # 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
+
+            if frame == start_frame:
+
+                time = (frame * frame_step - start_frame) / fps
+                #@TODO: missing scale values
+                keyframe = {
+                    constants.TIME: time,
+                    constants.POS: [px, py, pz],
+                    constants.ROT: [rx, ry, rz, rw],
+                    constants.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 = {
+                    constants.TIME: time,
+                    constants.POS: [px, py, pz],
+                    constants.ROT: [rx, ry, rz, rw],
+                    constants.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 = {
+                        constants.TIME: time, 
+                        constants.POS: [px, py, pz],
+                        constants.ROT: [rx, ry, rz, rw]
+                    }
+                elif pchange == True:
+                    keyframe = {
+                        constants.TIME: time, 
+                        constants.POS: [px, py, pz]
+                    }
+                elif rchange == True:
+                    keyframe = {
+                        constants.TIME: time, 
+                        constants.ROT: [rx, ry, rz, rw]
+                    }
+
+                keys.append(keyframe)
+
+        hierarchy.append({
+            constants.KEYS: keys, 
+            constants.PARENT: parent_index
+        })
+        parent_index += 1
+
+    animation = {
+        constants.HIERARCHY: hierarchy, 
+        constants.LENGTH:frame_length / fps,
+        constants.FPS: fps,
+        constants.NAME: action.name
+    }
+
+    return animation
+
+
+def _parse_pose_action(action, armature, options, round_off, round_val):
+    #@TODO: this seems to fail in batch mode meaning the
+    #       user has to have th GUI open. need to improve
+    #       this logic to allow batch processing, if Blender
+    #       chooses to behave....
+    current_context = context.area.type
+    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
+
+    frame_step = options.get(constants.FRAME_STEP, 1)
+    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
+
+        logger.info('Processing frame %d', frame)
+
+        time = frame - start_frame
+        if frame_index_as_time is False:
+            time = time / fps
+
+        context.scene.frame_set(frame)
+
+        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)
+
+            if round_off:
+                pos = (
+                    utilities.round_off(pos.x, round_val),
+                    utilities.round_off(pos.z, round_val),
+                    -utilities.round_off(pos.y, round_val)
+                )
+                rot = (
+                    utilities.round_off(rot.x, round_val),
+                    utilities.round_off(rot.z, round_val), 
+                    -utilities.round_off(rot.y, round_val),
+                    utilities.round_off(rot.w, round_val)
+                )
+                scl = (
+                    utilities.round_off(scl.x, round_val),
+                    utilities.round_off(scl.z, round_val),
+                    utilities.round_off(scl.y, round_val)
+                )
+            else:
+                pos = (pos.x, pos.z, -pos.y)
+                rot = (rot.x, rot.z, -rot.y, rot.w)
+                scl = (scl.x, scl.z, scl.y)
+
+            keyframe = {constants.TIME: time}
+            if frame == start_frame or frame == end_frame:
+                keyframe.update({
+                    constants.POS: pos,
+                    constants.ROT: rot,
+                    constants.SCL: scl
+                })
+            elif any([pchange, rchange, schange]):
+                if pchange is True:
+                    keyframe[constants.POS] = pos
+                if rchange is True:
+                    keyframe[constants.ROT] = rot
+                if schange is True:
+                    keyframe[constants.SCL] = scl
+
+            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)
+
+            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)
+    context.area.type = current_context
+    
+    animation = {
+        constants.HIERARCHY: hierarchy, 
+        constants.LENGTH:frame_length,
+        constants.FPS: fps,
+        constants.NAME: action.name
+    }
+
+    return animation
+
+
+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 _position(bone, frame, action, armature_matrix):
+
+    position = mathutils.Vector((0,0,0))
+    change = False
+
+    ngroups = len(action.groups)
+
+    if ngroups > 0:
+
+        index = 0
+
+        for i in range(ngroups):
+            if action.groups[i].name == bone.name:
+                index = i
+
+        for channel in action.groups[index].channels:
+            if "location" in channel.data_path:
+                has_changed = _handle_position_channel(
+                    channel, frame, position)
+                change = change or has_changed
+
+    else:
+
+        bone_label = '"%s"' % bone.name
+
+        for channel in action.fcurves:
+            data_path = channel.data_path
+            if bone_label in data_path and \
+            "location" in data_path:
+                has_changed = _handle_position_channel(
+                    channel, frame, position)
+                change = change or has_changed
+
+    position = position * bone.matrix_local.inverted()
+
+    if bone.parent is None:
+
+        position.x += bone.head.x
+        position.y += bone.head.y
+        position.z += bone.head.z
+
+    else:
+
+        parent = bone.parent
+
+        parent_matrix = parent.matrix_local.inverted()
+        diff = parent.tail_local - parent.head_local
+
+        position.x += (bone.head * parent_matrix).x + diff.x
+        position.y += (bone.head * parent_matrix).y + diff.y
+        position.z += (bone.head * parent_matrix).z + diff.z
+
+    return armature_matrix*position, change
+
+
+def _rotation(bone, frame, action, armature_matrix):
+
+    # TODO: calculate rotation also from rotation_euler channels
+
+    rotation = mathutils.Vector((0,0,0,1))
+
+    change = False
+
+    ngroups = len(action.groups)
+
+    # animation grouped by bones
+
+    if ngroups > 0:
+
+        index = -1
+
+        for i in range(ngroups):
+            if action.groups[i].name == bone.name:
+                index = i
+
+        if index > -1:
+            for channel in action.groups[index].channels:
+                if "quaternion" in channel.data_path:
+                    has_changed = _handle_rotation_channel(
+                        channel, frame, rotation)
+                    change = change or has_changed
+
+    # animation in raw fcurves
+
+    else:
+
+        bone_label = '"%s"' % bone.name
+
+        for channel in action.fcurves:
+            data_path = channel.data_path
+            if bone_label in data_path and \
+            "quaternion" in data_path:
+                has_changed = _handle_rotation_channel(
+                    channel, frame, rotation)
+                change = change or has_changed
+
+    rot3 = rotation.to_3d()
+    rotation.xyz = rot3 * bone.matrix_local.inverted()
+    rotation.xyz = armature_matrix * rotation.xyz
+
+    return rotation, change
+
+
+def _handle_rotation_channel(channel, frame, rotation):
+
+    change = False
+
+    if channel.array_index in [0, 1, 2, 3]:
+
+        for keyframe in channel.keyframe_points:
+            if keyframe.co[0] == frame:
+                change = True
+
+        value = channel.evaluate(frame)
+
+        if channel.array_index == 1:
+            rotation.x = value
+
+        elif channel.array_index == 2:
+            rotation.y = value
+
+        elif channel.array_index == 3:
+            rotation.z = value
+
+        elif channel.array_index == 0:
+            rotation.w = value
+
+    return change
+
+
+def _handle_position_channel(channel, frame, position):
+
+    change = False
+
+    if channel.array_index in [0, 1, 2]:
+        for keyframe in channel.keyframe_points:
+            if keyframe.co[0] == frame:
+                change = True
+
+        value = channel.evaluate(frame)
+
+        if channel.array_index == 0:
+            position.x = value
+
+        if channel.array_index == 1:
+            position.y = value
+
+        if channel.array_index == 2:
+            position.z = value
+
+    return change

+ 184 - 374
utils/exporters/blender/addons/io_three/exporter/api/mesh.py

@@ -1,7 +1,6 @@
 import operator
-import mathutils
 from bpy import data, types, context
-from . import material, texture
+from . import material, texture, animation
 from . import object as object_
 from .. import constants, utilities, logger, exceptions
 
@@ -21,64 +20,57 @@ def _mesh(func):
 
 
 @_mesh
-def animation(mesh, options):
+def skeletal_animation(mesh, options):
     logger.debug('mesh.animation(%s, %s)', mesh, options)
     armature = _armature(mesh)
-    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')
+
+    if not armature:
+        logger.warning('No armature found (%s)', mesh)
+        return []
+
+    anim_type = options.get(constants.ANIMATION)
+    #pose_position = armature.data.pose_position
+    dispatch = {
+        constants.POSE: animation.pose_animation,
+        constants.REST: animation.rest_animation
+    }
+    
+    func = dispatch[anim_type]
+    #armature.data.pose_position = anim_type.upper()
+    animations = func(armature, options)
+    #armature.data.pose_position = pose_position
         
     return animations
 
 
 @_mesh
-def bones(mesh):
+def bones(mesh, options):
     logger.debug('mesh.bones(%s)', mesh)
     armature = _armature(mesh)
-    bones = []
-    bone_map = {}
 
     if not armature: 
-        return bones, bone_map
+        return [], {}
 
-    bone_count = 0
-
-    armature_matrix = armature.matrix_world
-    for bone_count, pose_bone in enumerate(armature.pose.bones):
-        armature_bone = pose_bone.bone
-        bone_index = None
-
-        if armature_bone.parent is None:
-            bone_matrix = armature_matrix * armature_bone.matrix_local
-            bone_index = -1
-        else:
-            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_map[bone_count] = bone_count
+    round_off, round_val = utilities.rounding(options)
+    anim_type = options.get(constants.ANIMATION)
+    #pose_position = armature.data.pose_position
+
+    if anim_type == constants.OFF:
+        logger.info('Animation type not set, defaulting '\
+            'to using REST position for the armature.')
+        func = _rest_bones
+        #armature.data.pose_position = 'REST'
+    else:
+        dispatch = {
+            constants.REST: _rest_bones,
+            constants.POSE: _pose_bones
+        }
+        logger.info('Using %s for the armature', anim_type)
+        func = dispatch[anim_type]
+        #armature.data.pose_position = anim_type.upper()
 
-        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)
-        })
+    bones, bone_map = func(armature, round_off, round_val)
+    #armature.data.pose_position = pose_position
 
     return (bones, bone_map)
 
@@ -146,6 +138,7 @@ def buffer_uv(mesh, options):
     
     return uvs_
 
+
 @_mesh
 def faces(mesh, options):
     logger.debug('mesh.faces(%s, %s)', mesh, options)
@@ -154,7 +147,6 @@ def faces(mesh, options):
     logger.info('Has UVs = %s', vertex_uv)
     logger.info('Has vertex colours = %s', has_colors)
 
-
     round_off, round_val = utilities.rounding(options)
     if round_off:
         logger.debug('Rounding off of vectors set to %s', round_off)
@@ -172,7 +164,19 @@ def faces(mesh, options):
     vertex_normals = _normals(mesh, options) if opt_normals else None
     vertex_colours = vertex_colors(mesh) if opt_colours else None
 
-    face_data = []
+    faces_data = []
+
+    colour_indices = {}
+    if vertex_colours:
+        logger.debug('Indexing colours')
+        for index, colour in enumerate(vertex_colours):
+            colour_indices[str(colour)] = index
+
+    normal_indices = {}
+    if vertex_normals:
+        logger.debug('Indexing normals')
+        for index, normal in enumerate(vertex_normals):
+            normal_indices[str(normal)] = index
 
     logger.info('Parsing %d faces', len(mesh.tessfaces))
     for face in mesh.tessfaces:
@@ -187,20 +191,21 @@ def faces(mesh, options):
         mask = {
             constants.QUAD: vert_count is 4,
             constants.MATERIALS: materials,
-            constants.UVS: opt_uvs,
-            constants.NORMALS: opt_normals,
-            constants.COLORS: opt_colours
+            constants.UVS: False,
+            constants.NORMALS: False,
+            constants.COLORS: False
         }
 
-        face_data.append(utilities.bit_mask(mask))
+        face_data = []
         
         face_data.extend([v for v in face.vertices])
         
         if mask[constants.MATERIALS]:
             face_data.append(face.material_index)
         
-        if mask[constants.UVS] and uv_layers:
-
+        #@TODO: this needs the same optimization as what
+        #       was done for colours and normals
+        if uv_layers:
             for index, uv_layer in enumerate(uv_layers):
                 layer = mesh.tessface_uv_textures[index]
 
@@ -209,34 +214,38 @@ def faces(mesh, options):
                     if round_off:
                         uv = utilities.round_off(uv, round_val)
                     face_data.append(uv_layer.index(uv))
+                    mask[constants.UVS] = True
 
-        if mask[constants.NORMALS] and vertex_normals:
+        if vertex_normals:
             for vertex in face.vertices:
                 normal = mesh.vertices[vertex].normal
                 normal = (normal.x, normal.y, normal.z)
                 if round_off:
                     normal = utilities.round_off(normal, round_val)
-                face_data.append(vertex_normals.index(normal))
+                face_data.append(normal_indices[str(normal)])
+                mask[constants.NORMALS] = True
         
-        if mask[constants.COLORS]:
+        if vertex_colours:
             colours = mesh.tessface_vertex_colors.active.data[face.index]
 
             for each in (colours.color1, colours.color2, colours.color3):
                 each = utilities.rgb2int(each)
-                face_data.append(vertex_colours.index(each))
+                face_data.append(colour_indices[str(each)])
+                mask[constants.COLORS] = True
 
             if mask[constants.QUAD]:
                 colour = utilities.rgb2int(colours.color4)
-                face_data.append(vertex_colours.index(colour))
+                face_data.append(colour_indices[str(colour)])
+
+        face_data.insert(0, utilities.bit_mask(mask))
+        faces_data.extend(face_data)
 
-    return face_data
+    return faces_data
  
 
 @_mesh
 def morph_targets(mesh, options):
     logger.debug('mesh.morph_targets(%s, %s)', mesh, options)
-    #@TODO: consider an attribute for the meshes for determining
-    #       morphs, which would save on so much overhead
     obj = object_.objects_using_mesh(mesh)[0]
     original_frame = context.scene.frame_current
     frame_step = options.get(constants.FRAME_STEP, 1)
@@ -244,6 +253,8 @@ def morph_targets(mesh, options):
         context.scene.frame_end+1, frame_step)
 
     morphs = []
+    round_off, round_val = utilities.rounding(options)
+
     for frame in scene_frames:
         logger.info('Processing data at frame %d', frame)
         context.scene.frame_set(frame, 0.0)
@@ -251,8 +262,14 @@ def morph_targets(mesh, options):
         vertices = object_.extract_mesh(obj, options).vertices[:]
 
         for vertex in vertices:
-            vectors = [round(vertex.co.x, 6), round(vertex.co.y, 6), 
-                round(vertex.co.z, 6)]
+            if round_off:
+                vectors = [
+                    utilities.round_off(vertex.co.x, round_val), 
+                    utilities.round_off(vertex.co.y, round_val), 
+                    utilities.round_off(vertex.co.z, round_val)
+                ]
+            else:
+                vectors = [vertex.co.x, vertex.co.y, vertex.co.z]
             morphs[-1].extend(vectors)
     
     context.scene.frame_set(original_frame, 0.0)
@@ -589,6 +606,7 @@ def _normals(mesh, options):
     vectors = []
     round_off, round_val = utilities.rounding(options)
 
+    vectors_ = {}
     for face in mesh.tessfaces:
 
         for vertex_index in face.vertices:
@@ -597,8 +615,12 @@ def _normals(mesh, options):
             if round_off:
                 vector = utilities.round_off(vector, round_val)
 
-            if vector not in vectors:
+            str_vec = str(vector)
+            try:
+                vectors_[str_vec]
+            except KeyError:
                 vectors.append(vector)
+                vectors_[str_vec] = True
 
     return vectors
 
@@ -668,330 +690,118 @@ def _skinning_data(mesh, bone_map, influences, array_index):
     return manifest
 
 
-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 _pose_bones(armature, round_off, round_val):
+    bones = []
+    bone_map = {}
+    bone_count = 0
 
-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
+    for bone_count, pose_bone in enumerate(armature.pose.bones):
+        armature_bone = pose_bone.bone
+        bone_index = None
 
-    frame_step = options.get(constants.FRAME_STEP, 1)
-    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
+        if armature_bone.parent is None:
+            bone_matrix = armature_matrix * armature_bone.matrix_local
+            bone_index = -1
         else:
-            frame = start_frame + frame_index * frame_step
-
-        logger.info('Processing frame %d', frame)
-
-        time = frame - start_frame
-        if frame_index_as_time is False:
-            time = time / fps
-
-        context.scene.frame_set(frame)
-
-        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
+            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 channel in channels:
-                if not find_keyframe_at(channel, frame) is None:
-                    return True
-            return False
+            for pose_parent in armature.pose.bones:
+                armature_parent = pose_parent.bone.name
+                if armature_parent == parent_bone.name:
+                    bone_index = index
+                index += 1
 
-        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)
-
-            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)
+        bone_map[bone_count] = bone_count
 
-            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]
+        pos, rot, scl = bone_matrix.decompose()
+        if round_off:
+            pos = (
+                utilities.round_off(pos.x, round_val),
+                utilities.round_off(pos.z, round_val),
+                -utilities.round_off(pos.y, round_val)
+            )
+            rot = (
+                utilities.round_off(rot.x, round_val),
+                utilities.round_off(rot.z, round_val), 
+                -utilities.round_off(rot.y, round_val),
+                utilities.round_off(rot.w, round_val)
+            )
+            scl = (
+                utilities.round_off(scl.x, round_val),
+                utilities.round_off(scl.z, round_val),
+                utilities.round_off(scl.y, round_val)
+            )
+        else:
+            pos = (pos.x, pos.z, -pos.y)
+            rot = (rot.x, rot.z, -rot.y, rot.w)
+            scl = (scl.x, scl.z, scl.y)
+        bones.append({
+            constants.PARENT: bone_index,
+            constants.NAME: armature_bone.name,
+            constants.POS: pos,
+            constants.ROTQ: rot,
+            constants.SCL: scl 
         })
-        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 = {
-        constants.HIERARCHY: hierarchy, 
-        constants.LENGTH:frame_length,
-        constants.FPS: fps,
-        constants.NAME: action.name
-    }
-
-    return animation
-
-
-def _position(bone, frame, action, armature_matrix):
-
-    position = mathutils.Vector((0,0,0))
-    change = False
-
-    ngroups = len(action.groups)
-
-    if ngroups > 0:
-
-        index = 0
-
-        for i in range(ngroups):
-            if action.groups[i].name == bone.name:
-                index = i
-
-        for channel in action.groups[index].channels:
-            if "location" in channel.data_path:
-                has_changed = _handle_position_channel(
-                    channel, frame, position)
-                change = change or has_changed
-
-    else:
-
-        bone_label = '"%s"' % bone.name
-
-        for channel in action.fcurves:
-            data_path = channel.data_path
-            if bone_label in data_path and \
-            "location" in data_path:
-                has_changed = _handle_position_channel(
-                    channel, frame, position)
-                change = change or has_changed
-
-    position = position * bone.matrix_local.inverted()
-
-    if bone.parent is None:
-
-        position.x += bone.head.x
-        position.y += bone.head.y
-        position.z += bone.head.z
-
-    else:
-
-        parent = bone.parent
-
-        parent_matrix = parent.matrix_local.inverted()
-        diff = parent.tail_local - parent.head_local
-
-        position.x += (bone.head * parent_matrix).x + diff.x
-        position.y += (bone.head * parent_matrix).y + diff.y
-        position.z += (bone.head * parent_matrix).z + diff.z
 
-    return armature_matrix*position, change
+    return bones, bone_map
 
 
-def _rotation(bone, frame, action, armature_matrix):
-
-    # TODO: calculate rotation also from rotation_euler channels
-
-    rotation = mathutils.Vector((0,0,0,1))
-
-    change = False
-
-    ngroups = len(action.groups)
-
-    # animation grouped by bones
-
-    if ngroups > 0:
-
-        index = -1
-
-        for i in range(ngroups):
-            if action.groups[i].name == bone.name:
-                index = i
-
-        if index > -1:
-            for channel in action.groups[index].channels:
-                if "quaternion" in channel.data_path:
-                    has_changed = _handle_rotation_channel(
-                        channel, frame, rotation)
-                    change = change or has_changed
-
-    # animation in raw fcurves
-
-    else:
-
-        bone_label = '"%s"' % bone.name
-
-        for channel in action.fcurves:
-            data_path = channel.data_path
-            if bone_label in data_path and \
-            "quaternion" in data_path:
-                has_changed = _handle_rotation_channel(
-                    channel, frame, rotation)
-                change = change or has_changed
-
-    rot3 = rotation.to_3d()
-    rotation.xyz = rot3 * bone.matrix_local.inverted()
-    rotation.xyz = armature_matrix * rotation.xyz
-
-    return rotation, change
-
-
-def _handle_rotation_channel(channel, frame, rotation):
-
-    change = False
-
-    if channel.array_index in [0, 1, 2, 3]:
-
-        for keyframe in channel.keyframe_points:
-            if keyframe.co[0] == frame:
-                change = True
-
-        value = channel.evaluate(frame)
-
-        if channel.array_index == 1:
-            rotation.x = value
-
-        elif channel.array_index == 2:
-            rotation.y = value
-
-        elif channel.array_index == 3:
-            rotation.z = value
-
-        elif channel.array_index == 0:
-            rotation.w = value
-
-    return change
-
+def _rest_bones(armature, round_off, round_val):
+    bones = []
+    bone_map = {}
+    bone_count = 0
+    bone_index_rel = 0
 
-def _handle_position_channel(channel, frame, position):
+    for bone in armature.data.bones:
+        logger.info('Parsing bone %s', bone.name)
 
-    change = False
+        if not bone.use_deform:
+            logger.debug('Ignoring bone %s at: %d', 
+                bone.name, bone_index_rel)
+            continue
 
-    if channel.array_index in [0, 1, 2]:
-        for keyframe in channel.keyframe_points:
-            if keyframe.co[0] == frame:
-                change = True
+        if bone.parent is None:
+            bone_pos = bone.head_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)
+                index += 1
 
-        value = channel.evaluate(frame)
+        bone_world_pos = armature.matrix_world * bone_pos
+        if round_off:
+            x_axis = utilities.round_off(bone_world_pos.x, round_val)
+            y_axis = utilities.round_off(bone_world_pos.z, round_val)
+            z_axis = -utilities.round_off(bone_world_pos.y, round_val)
+        else:
+            x_axis = bone_world_pos.x
+            y_axis = bone_world_pos.z
+            z_axis = -bone_world_pos.y
+
+        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
+        #@TODO: the rotq probably should not have these
+        #       hard coded values
+        bones.append({
+            constants.PARENT: bone_index,
+            constants.NAME: bone.name,
+            constants.POS: (x_axis, y_axis, z_axis),
+            constants.ROTQ: (0,0,0,1)
+        })
 
-        if channel.array_index == 0:
-            position.x = value
+        bone_count += 1
 
-        if channel.array_index == 1:
-            position.y = value
+    return (bones, bone_map)
 
-        if channel.array_index == 2:
-            position.z = value
 
-    return change

+ 6 - 5
utils/exporters/blender/addons/io_three/exporter/base_classes.py

@@ -1,4 +1,4 @@
-import uuid
+from . import utilities
 from .. import constants, exceptions 
 
 
@@ -55,8 +55,11 @@ class BaseNode(BaseClass):
     def __init__(self, node, parent, type):
         BaseClass.__init__(self, parent=parent, type=type)
         self.__node = node
-        if node is not None:
+        if node is None:
+            self[constants.UUID] = utilities.id()
+        else:
             self[constants.NAME] = node
+            self[constants.UUID] = utilities.id_from_name(node)
 
         if isinstance(parent, BaseScene):
             scene = parent
@@ -66,9 +69,7 @@ class BaseNode(BaseClass):
             scene = None
 
         self.__scene = scene
-
-        self[constants.UUID] = str(uuid.uuid4()).upper()
-    
+ 
     @property
     def node(self):
         return self.__node

+ 16 - 15
utils/exporters/blender/addons/io_three/exporter/geometry.py

@@ -292,7 +292,7 @@ class Geometry(base_classes.BaseNode):
             if not option:
                 continue
 
-            array = func(self.node, self.options)
+            array = func(self.node, self.options) or []
             if not array: 
                 logger.warning('No array could be made for %s', key)
                 continue
@@ -307,37 +307,38 @@ class Geometry(base_classes.BaseNode):
         if self.options.get(constants.VERTICES):
             logger.info('Parsing %s', constants.VERTICES)
             self[constants.VERTICES] = api.mesh.vertices(
-                self.node, self.options)
-
-        if self.options.get(constants.FACES):
-            logger.info('Parsing %s', constants.FACES)
-            self[constants.FACES] = api.mesh.faces(
-                self.node, self.options)
+                self.node, self.options) or []
 
         if self.options.get(constants.NORMALS):
             logger.info('Parsing %s', constants.NORMALS)
             self[constants.NORMALS] = api.mesh.normals(
-                self.node, self.options)
+                self.node, self.options) or []
 
         if self.options.get(constants.COLORS):
             logger.info('Parsing %s', constants.COLORS)
             self[constants.COLORS] = api.mesh.vertex_colors(
-                self.node)
+                self.node) or []
         
         if self.options.get(constants.FACE_MATERIALS):
             logger.info('Parsing %s', constants.FACE_MATERIALS)
             self[constants.MATERIALS] = api.mesh.materials(
-                self.node, self.options)
+                self.node, self.options) or []
 
         if self.options.get(constants.UVS):
             logger.info('Parsing %s', constants.UVS)
             self[constants.UVS] = api.mesh.uvs(
-                self.node, self.options)
+                self.node, self.options) or []
 
-        if self.options.get(constants.ANIMATION):
+        if self.options.get(constants.FACES):
+            logger.info('Parsing %s', constants.FACES)
+            self[constants.FACES] = api.mesh.faces(
+                self.node, self.options) or []
+
+        no_anim = (None, False, constants.OFF)
+        if self.options.get(constants.ANIMATION) not in no_anim:
             logger.info('Parsing %s', constants.ANIMATION)
-            self[constants.ANIMATION] = api.mesh.animation(
-                self.node, self.options)
+            self[constants.ANIMATION] = api.mesh.skeletal_animation(
+                self.node, self.options) or []
 
         #@TODO: considering making bones data implied when
         #       querying skinning data
@@ -345,7 +346,7 @@ class Geometry(base_classes.BaseNode):
         bone_map = {}
         if self.options.get(constants.BONES):
             logger.info('Parsing %s', constants.BONES)
-            bones, bone_map = api.mesh.bones(self.node)
+            bones, bone_map = api.mesh.bones(self.node, self.options)
             self[constants.BONES] = bones
 
         if self.options.get(constants.SKINNING):

+ 5 - 18
utils/exporters/blender/addons/io_three/exporter/io.py

@@ -1,7 +1,5 @@
 import os
-import sys
 import shutil
-import traceback
 from .. import constants, logger
 from . import _json
 
@@ -18,21 +16,8 @@ def copy(src, dst):
         file_name = os.path.basename(src)
         dst = os.path.join(dst, file_name)
 
-    if os.path.exists(dst):
-
-        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)
+    if src != dst:
+        shutil.copy(src, dst)
 
 
 def dump(filepath, data, options=None):
@@ -57,8 +42,10 @@ def dump(filepath, data, options=None):
         else:
             _json.ROUND = None
 
+        indent = options.get(constants.INDENT, True)
+        indent = 4 if indent else None
         logger.info('Dumping to JSON')
-        func = lambda x,y: _json.json.dump(x, y, indent=4)
+        func = lambda x,y: _json.json.dump(x, y, indent=indent)
         mode = 'w'
 
     logger.info('Writing to %s', filepath)

+ 7 - 1
utils/exporters/blender/addons/io_three/exporter/scene.py

@@ -6,6 +6,7 @@ from . import (
     material,
     geometry, 
     object,
+    utilities,
     io,
     api
 )
@@ -74,7 +75,7 @@ class Scene(base_classes.BaseScene):
         data = {}
         
         embed_anim = self.options.get(constants.EMBED_ANIMATION, True)
-        embed = self.options[constants.EMBED_GEOMETRY]
+        embed = self.options.get(constants.EMBED_GEOMETRY, True)
 
         compression = self.options.get(constants.COMPRESSION)
         extension = constants.EXTENSIONS.get(compression, 
@@ -167,8 +168,13 @@ class Scene(base_classes.BaseScene):
 
     def __parse_objects(self): 
         logger.debug('Scene().__parse_objects()')
+        try:
+            scene_name = self[constants.METADATA][constants.SOURCE_FILE]
+        except KeyError:
+            scene_name = constants.SCENE
         self[constants.OBJECT] = object.Object(None, parent=self)
         self[constants.OBJECT][constants.TYPE] = constants.SCENE.title()
+        self[constants.UUID] = utilities.id_from_name(scene_name)
 
         objects = [] 
         for node in api.object.nodes(self.valid_types, self.options):

+ 3 - 0
utils/exporters/blender/addons/io_three/exporter/utilities.py

@@ -28,6 +28,9 @@ def hash(value):
 def id():
     return str(uuid.uuid4()).upper()
 
+def id_from_name(name):
+    return str(uuid.uuid3(uuid.NAMESPACE_DNS, name)).upper()
+
 
 def rgb2int(rgb):
     is_tuple = isinstance(rgb, tuple)

+ 1 - 1
utils/exporters/blender/tests/scripts/test_geometry_normals.bash

@@ -4,5 +4,5 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 source "$DIR/setup_test_env.bash"
 
 blender --background $BLEND/torusA.blend --python $PYSCRIPT -- \
-    $JSON --vertices --faces --normals
+    $JSON --vertices --faces --normals --indent
 makereview $@ --tag $(tagname)

+ 1 - 1
utils/exporters/blender/tests/scripts/test_scene_instancing.bash

@@ -5,5 +5,5 @@ source "$DIR/setup_test_env.bash"
 
 blender --background $BLEND/scene_instancing.blend --python $PYSCRIPT -- \
     $JSON --vertices --faces --scene --materials --enablePrecision \
-    --precision 4 --embedGeometry
+    --precision 4 --embedGeometry --indent
 makereview $@ --tag $(tagname)