diff --git a/utils/exporters/blender/.gitignore b/utils/exporters/blender/.gitignore index 90e54781632294b72fbf8daf8a2068cbcbba7dfb..c1c6da1922d327bdeb30d154a06ee998cfccc0db 100644 --- a/utils/exporters/blender/.gitignore +++ b/utils/exporters/blender/.gitignore @@ -1,2 +1,3 @@ tests/review __pycache__/ +tmp/ diff --git a/utils/exporters/blender/addons/io_three/__init__.py b/utils/exporters/blender/addons/io_three/__init__.py index b334a26866595f57c2410934d1d4ad58003a2692..3d162ec217480ed0943c36413bf43029753586d1 100644 --- a/utils/exporters/blender/addons/io_three/__init__.py +++ b/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,136 +31,185 @@ 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' bl_info = { - 'name': 'Three.js Format', - 'author': 'Ed Caspersen (repsac)', - 'version': (1, 0, 0), - 'blender': (2, 7, 2), - 'location': 'File > Import-Export', - 'description': 'Export Three.js formatted JSON files.', - 'warning': '', - 'wiki_url': 'https://github.com/mrdoob/three.js/tree/'\ - 'master/utils/exporters/blender', - 'tracker_url': 'https://github.com/mrdoob/three.js/issues', + 'name': "Three.js Format", + 'author': "repsac, mrdoob, yomotsu, mpk, jpweeks", + 'version': (1, 2, 1), + 'blender': (2, 7, 3), + 'location': "File > Import-Export", + 'description': "Export Three.js formatted JSON files.", + 'warning': "Importer not included.", + 'wiki_url': "https://github.com/mrdoob/three.js/tree/"\ + "master/utils/exporters/blender", + 'tracker_url': "https://github.com/mrdoob/three.js/issues", 'category': 'Import-Export' } def _geometry_types(): - types = [ - (constants.GLOBAL, constants.GLOBAL.title(), - constants.GLOBAL), - (constants.GEOMETRY, constants.GEOMETRY.title(), - constants.GEOMETRY), - (constants.BUFFER_GEOMETRY, constants.BUFFER_GEOMETRY, - constants.BUFFER_GEOMETRY), - ] + """The valid geometry types that are supported by Three.js + + :return: list of tuples + + """ + keys = (constants.GLOBAL, + constants.GEOMETRY, + constants.BUFFER_GEOMETRY) + types = [] + for key in keys: + types.append((key, key.title(), key)) return types bpy.types.Mesh.THREE_geometry_type = EnumProperty( - name='Geometry type', - description='Geometry type', + name="Geometry type", + description="Geometry type", items=_geometry_types(), default=constants.GLOBAL) -class MESH_PT_hello(bpy.types.Panel): +class ThreeMesh(bpy.types.Panel): + """Creates custom properties on a mesh node""" bl_label = 'THREE' bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = 'data' - + def draw(self, context): + """ + + :param context: + + """ row = self.layout.row() if context.mesh: - row.prop(context.mesh, 'THREE_geometry_type', text='Type') + row.prop(context.mesh, + 'THREE_geometry_type', + text="Type") def _blending_types(index): - types = ( - constants.BLENDING_TYPES.NONE, - constants.BLENDING_TYPES.NORMAL, - constants.BLENDING_TYPES.ADDITIVE, - constants.BLENDING_TYPES.SUBTRACTIVE, - constants.BLENDING_TYPES.MULTIPLY, - constants.BLENDING_TYPES.CUSTOM) + """Supported blending types for Three.js + + :param index: + :type index: int + :returns: tuple if types (str, str, str) + + """ + types = (constants.BLENDING_TYPES.NONE, + constants.BLENDING_TYPES.NORMAL, + constants.BLENDING_TYPES.ADDITIVE, + constants.BLENDING_TYPES.SUBTRACTIVE, + constants.BLENDING_TYPES.MULTIPLY, + constants.BLENDING_TYPES.CUSTOM) return (types[index], types[index], types[index]) bpy.types.Material.THREE_blending_type = EnumProperty( - name='Blending type', - description='Blending type', - items=[_blending_types(x) for x in range(5)], + name="Blending type", + description="Blending type", + items=[_blending_types(x) for x in range(5)], default=constants.BLENDING_TYPES.NORMAL) bpy.types.Material.THREE_depth_write = BoolProperty(default=True) bpy.types.Material.THREE_depth_test = BoolProperty(default=True) -class MATERIAL_PT_hello(bpy.types.Panel): +class ThreeMaterial(bpy.types.Panel): + """Adds custom properties to the Materials of an object""" bl_label = 'THREE' bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = 'material' - + def draw(self, context): + """ + + :param context: + + """ layout = self.layout mat = context.material - + if mat is not None: row = layout.row() - row.label(text='Selected material: %s' % mat.name ) + row.label(text="Selected material: %s" % mat.name) row = layout.row() - row.prop(mat, 'THREE_blending_type', - text='Blending type' ) + row.prop(mat, 'THREE_blending_type', + text="Blending type") row = layout.row() - row.prop(mat, 'THREE_depth_write', - text='Enable depth writing' ) + row.prop(mat, 'THREE_depth_write', + text="Enable depth writing") row = layout.row() - row.prop(mat, 'THREE_depth_test', - text='Enable depth testing' ) + row.prop(mat, 'THREE_depth_test', + text="Enable depth testing") def _mag_filters(index): + """Three.js mag filters + + :param index: + :type index: int + :returns: tuple with the filter values + + """ types = (constants.LINEAR_FILTERS.LINEAR, - constants.NEAREST_FILTERS.NEAREST) + constants.NEAREST_FILTERS.NEAREST) return (types[index], types[index], types[index]) bpy.types.Texture.THREE_mag_filter = EnumProperty( - name='Mag Filter', - items = [_mag_filters(x) for x in range(2)], + name="Mag Filter", + items=[_mag_filters(x) for x in range(2)], default=constants.LINEAR_FILTERS.LINEAR) def _min_filters(index): + """Three.js min filters + + :param index: + :type index: int + :returns: tuple with the filter values + + """ types = (constants.LINEAR_FILTERS.LINEAR, - constants.LINEAR_FILTERS.MIP_MAP_NEAREST, - constants.LINEAR_FILTERS.MIP_MAP_LINEAR, - constants.NEAREST_FILTERS.NEAREST, - constants.NEAREST_FILTERS.MIP_MAP_NEAREST, - constants.NEAREST_FILTERS.MIP_MAP_LINEAR) + constants.LINEAR_FILTERS.MIP_MAP_NEAREST, + constants.LINEAR_FILTERS.MIP_MAP_LINEAR, + constants.NEAREST_FILTERS.NEAREST, + constants.NEAREST_FILTERS.MIP_MAP_NEAREST, + constants.NEAREST_FILTERS.MIP_MAP_LINEAR) return (types[index], types[index], types[index]) bpy.types.Texture.THREE_min_filter = EnumProperty( - name='Min Filter', - items = [_min_filters(x) for x in range(6)], + name="Min Filter", + items=[_min_filters(x) for x in range(6)], default=constants.LINEAR_FILTERS.MIP_MAP_LINEAR) def _mapping(index): + """Three.js texture mappings types + + :param index: + :type index: int + :returns: tuple with the mapping values + + """ types = (constants.MAPPING_TYPES.UV, - constants.MAPPING_TYPES.CUBE_REFLECTION, - constants.MAPPING_TYPES.CUBE_REFRACTION, - constants.MAPPING_TYPES.SPHERICAL_REFLECTION) + constants.MAPPING_TYPES.CUBE_REFLECTION, + constants.MAPPING_TYPES.CUBE_REFRACTION, + constants.MAPPING_TYPES.SPHERICAL_REFLECTION) return (types[index], types[index], types[index]) bpy.types.Texture.THREE_mapping = EnumProperty( - name='Mapping', - items = [_mapping(x) for x in range(4)], + name="Mapping", + items=[_mapping(x) for x in range(4)], default=constants.MAPPING_TYPES.UV) -class TEXTURE_PT_hello(bpy.types.Panel): +class ThreeTexture(bpy.types.Panel): + """Adds custom properties to a texture""" bl_label = 'THREE' bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' @@ -167,28 +217,39 @@ class TEXTURE_PT_hello(bpy.types.Panel): #@TODO: possible to make cycles compatible? def draw(self, context): + """ + + :param context: + + """ layout = self.layout tex = context.texture if tex is not None: row = layout.row() - row.prop(tex, 'THREE_mapping', text='Mapping') + row.prop(tex, 'THREE_mapping', text="Mapping") row = layout.row() - row.prop(tex, 'THREE_mag_filter', text='Mag Filter') + row.prop(tex, 'THREE_mag_filter', text="Mag Filter") row = layout.row() - row.prop(tex, 'THREE_min_filter', text='Min Filter') + row.prop(tex, 'THREE_min_filter', text="Min Filter") bpy.types.Object.THREE_export = bpy.props.BoolProperty(default=True) -class OBJECT_PT_hello(bpy.types.Panel): +class ThreeObject(bpy.types.Panel): + """Adds custom properties to an object""" bl_label = 'THREE' bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = 'object' def draw(self, context): + """ + + :param context: + + """ layout = self.layout obj = context.object @@ -196,11 +257,22 @@ class OBJECT_PT_hello(bpy.types.Panel): row.prop(obj, 'THREE_export', text='Export') def get_settings_fullpath(): + """ + + :returns: Full path to the settings file (temp directory) + + """ return os.path.join(bpy.app.tempdir, SETTINGS_FILE_EXPORT) def save_settings_export(properties): + """Save the current export settings to disk. + + :param properties: + :returns: settings + :rtype: dict + """ settings = { constants.VERTICES: properties.option_vertices, constants.FACES: properties.option_faces, @@ -221,10 +293,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, @@ -232,10 +305,12 @@ 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 } fname = get_settings_fullpath() + logging.debug("Saving settings to %s", fname) with open(fname, 'w') as stream: json.dump(settings, stream) @@ -243,32 +318,42 @@ def save_settings_export(properties): def restore_settings_export(properties): + """Restore the settings (if settings file is found on disk) + If not found thend default to paramgers defined in + constants.EXPORT_OPTIONS + + :param properties: + + """ settings = {} 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( - constants.VERTICES, + constants.VERTICES, constants.EXPORT_OPTIONS[constants.VERTICES]) properties.option_faces = settings.get( - constants.FACES, + constants.FACES, constants.EXPORT_OPTIONS[constants.FACES]) properties.option_normals = settings.get( - constants.NORMALS, + constants.NORMALS, constants.EXPORT_OPTIONS[constants.NORMALS]) properties.option_skinning = settings.get( - constants.SKINNING, + constants.SKINNING, constants.EXPORT_OPTIONS[constants.SKINNING]) properties.option_bones = settings.get( - constants.BONES, + constants.BONES, constants.EXPORT_OPTIONS[constants.BONES]) properties.option_influences = settings.get( @@ -282,185 +367,213 @@ def restore_settings_export(properties): ## Materials { properties.option_materials = settings.get( - constants.MATERIALS, + constants.MATERIALS, constants.EXPORT_OPTIONS[constants.MATERIALS]) properties.option_uv_coords = settings.get( - constants.UVS, + constants.UVS, constants.EXPORT_OPTIONS[constants.UVS]) properties.option_face_materials = settings.get( - constants.FACE_MATERIALS, + constants.FACE_MATERIALS, constants.EXPORT_OPTIONS[constants.FACE_MATERIALS]) properties.option_maps = settings.get( - constants.MAPS, + constants.MAPS, constants.EXPORT_OPTIONS[constants.MAPS]) properties.option_colors = settings.get( - constants.COLORS, + constants.COLORS, constants.EXPORT_OPTIONS[constants.COLORS]) properties.option_mix_colors = settings.get( - constants.MIX_COLORS, + constants.MIX_COLORS, constants.EXPORT_OPTIONS[constants.MIX_COLORS]) ## } ## Settings { properties.option_scale = settings.get( - constants.SCALE, + constants.SCALE, constants.EXPORT_OPTIONS[constants.SCALE]) properties.option_round_off = settings.get( - constants.ENABLE_PRECISION, + constants.ENABLE_PRECISION, constants.EXPORT_OPTIONS[constants.ENABLE_PRECISION]) properties.option_round_value = settings.get( - constants.PRECISION, + constants.PRECISION, constants.EXPORT_OPTIONS[constants.PRECISION]) properties.option_logging = settings.get( - constants.LOGGING, + constants.LOGGING, constants.EXPORT_OPTIONS[constants.LOGGING]) properties.option_compression = settings.get( - constants.COMPRESSION, + 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.COPY_TEXTURES, constants.EXPORT_OPTIONS[constants.COPY_TEXTURES]) properties.option_embed_animation = settings.get( - constants.EMBED_ANIMATION, + constants.EMBED_ANIMATION, constants.EXPORT_OPTIONS[constants.EMBED_ANIMATION]) ## } ## Scene { properties.option_export_scene = settings.get( - constants.SCENE, + 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, + constants.LIGHTS, constants.EXPORT_OPTIONS[constants.LIGHTS]) properties.option_cameras = settings.get( - constants.CAMERAS, + constants.CAMERAS, constants.EXPORT_OPTIONS[constants.CAMERAS]) ## } ## Animation { properties.option_animation_morph = settings.get( - constants.MORPH_TARGETS, + constants.MORPH_TARGETS, constants.EXPORT_OPTIONS[constants.MORPH_TARGETS]) properties.option_animation_skeletal = settings.get( - constants.ANIMATION, + constants.ANIMATION, constants.EXPORT_OPTIONS[constants.ANIMATION]) properties.option_frame_step = settings.get( - constants.FRAME_STEP, + 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(): + """Supported compression formats + + :rtype: tuple + + """ types = [(constants.NONE, constants.NONE, constants.NONE)] try: import msgpack - types.append((constants.MSGPACK, constants.MSGPACK, - constants.MSGPACK)) + types.append((constants.MSGPACK, constants.MSGPACK, + constants.MSGPACK)) except ImportError: pass return types +def animation_options(): + """The supported skeletal animation types + + :returns: list of tuples + + """ + 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): + """Class that handles the export properties""" - bl_idname='export.three' + bl_idname = 'export.three' bl_label = 'Export THREE' filename_ext = constants.EXTENSION option_vertices = BoolProperty( - name='Vertices', - description='Export vertices', + name="Vertices", + description="Export vertices", default=constants.EXPORT_OPTIONS[constants.VERTICES]) option_faces = BoolProperty( - name='Faces', - description='Export faces', + name="Faces", + description="Export faces", default=constants.EXPORT_OPTIONS[constants.FACES]) option_normals = BoolProperty( - name='Normals', - description='Export normals', + name="Normals", + description="Export normals", default=constants.EXPORT_OPTIONS[constants.NORMALS]) option_colors = BoolProperty( - name='Colors', - description='Export vertex colors', + name="Vertex Colors", + description="Export vertex colors", default=constants.EXPORT_OPTIONS[constants.COLORS]) option_mix_colors = BoolProperty( - name='Mix Colors', - description='Mix material and vertex colors', + name="Mix Colors", + description="Mix material and vertex colors", default=constants.EXPORT_OPTIONS[constants.MIX_COLORS]) option_uv_coords = BoolProperty( - name='UVs', - description='Export texture coordinates', + name="UVs", + description="Export texture coordinates", default=constants.EXPORT_OPTIONS[constants.UVS]) option_materials = BoolProperty( - name='Materials', - description='Export materials', + name="Materials", + description="Export materials", default=constants.EXPORT_OPTIONS[constants.MATERIALS]) option_face_materials = BoolProperty( - name='Face Materials', - description='Face mapping materials', + name="Face Materials", + description="Face mapping materials", default=constants.EXPORT_OPTIONS[constants.FACE_MATERIALS]) option_maps = BoolProperty( - name='Textures', - description='Include texture maps', + name="Textures", + description="Include texture maps", default=constants.EXPORT_OPTIONS[constants.MAPS]) option_skinning = BoolProperty( - name='Skinning', - description='Export skin data', + name="Skinning", + description="Export skin data", default=constants.EXPORT_OPTIONS[constants.SKINNING]) option_bones = BoolProperty( - name='Bones', - description='Export bones', + name="Bones", + description="Export bones", default=constants.EXPORT_OPTIONS[constants.BONES]) option_scale = FloatProperty( - name='Scale', - description='Scale vertices', - min=0.01, - max=1000.0, - soft_min=0.01, - soft_max=1000.0, + name="Scale", + description="Scale vertices", + min=0.01, + max=1000.0, + soft_min=0.01, + soft_max=1000.0, default=constants.EXPORT_OPTIONS[constants.SCALE]) option_round_off = BoolProperty( - name='Enable Precision', - description='Round off floating point values', + name="Enable Precision", + description="Round off floating point values", default=constants.EXPORT_OPTIONS[constants.ENABLE_PRECISION]) option_round_value = IntProperty( - name='Precision', + name="Precision", min=0, max=16, - description='Floating point precision', + description="Floating point precision", default=constants.EXPORT_OPTIONS[constants.PRECISION]) logging_types = [ @@ -471,75 +584,88 @@ class ExportThree(bpy.types.Operator, ExportHelper): (constants.CRITICAL, constants.CRITICAL, constants.CRITICAL)] option_logging = EnumProperty( - name='Logging', - description = 'Logging verbosity level', - items=logging_types, + name="", + description="Logging verbosity level", + items=logging_types, default=constants.DEBUG) option_geometry_type = EnumProperty( - name='Type', - description='Geometry type', + name="Type", + description="Geometry type", items=_geometry_types()[1:], default=constants.GEOMETRY) option_export_scene = BoolProperty( - name='Scene', - description='Export scene', + 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', - description='Embed animation data with the geometry data', + name="Embed animation", + description="Embed animation data with the geometry data", default=constants.EXPORT_OPTIONS[constants.EMBED_ANIMATION]) option_copy_textures = BoolProperty( - name='Copy textures', - description='Copy textures', + name="Copy textures", + description="Copy textures", default=constants.EXPORT_OPTIONS[constants.COPY_TEXTURES]) option_lights = BoolProperty( - name='Lights', - description='Export default scene lights', + name="Lights", + description="Export default scene lights", default=False) option_cameras = BoolProperty( - name='Cameras', - description='Export default scene cameras', + name="Cameras", + description="Export default scene cameras", default=False) option_animation_morph = BoolProperty( - name='Morph animation', - description='Export animation (morphs)', + name="Morph animation", + description="Export animation (morphs)", default=constants.EXPORT_OPTIONS[constants.MORPH_TARGETS]) - option_animation_skeletal = BoolProperty( - name='Skeletal animation', - description='Export animation (skeletal)', - default=constants.EXPORT_OPTIONS[constants.ANIMATION]) + option_animation_skeletal = EnumProperty( + name="", + description="Export animation (skeletal)", + items=animation_options(), + default=constants.OFF) + + 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', - min=1, - max=1000, - soft_min=1, - soft_max=1000, + name="Frame step", + description="Animation frame step", + min=1, + max=1000, + soft_min=1, + 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', - description = 'Compression options', - items=compression_types(), + name="", + description="Compression options", + items=compression_types(), default=constants.NONE) option_influences = IntProperty( - name='Influences', - description='Maximum number of bone influences', + name="Influences", + description="Maximum number of bone influences", min=1, max=4, default=2) @@ -550,17 +676,27 @@ class ExportThree(bpy.types.Operator, ExportHelper): @classmethod def poll(cls, context): + """ + + :param context: + + """ return context.active_object is not None def execute(self, context): + """ + + :param context: + + """ if not self.properties.filepath: - raise Exception('filename not set') + raise Exception("filename not set") settings = save_settings_export(self.properties) filepath = self.filepath if settings[constants.COMPRESSION] == constants.MSGPACK: - filepath = '%s%s' % (filepath[:-4], constants.PACK) + filepath = "%s%s" % (filepath[:-4], constants.PACK) from io_three import exporter if settings[constants.SCENE]: @@ -571,11 +707,16 @@ class ExportThree(bpy.types.Operator, ExportHelper): return {'FINISHED'} def draw(self, context): + """ + + :param context: + + """ layout = self.layout ## Geometry { row = layout.row() - row.label(text='Geometry:') + row.label(text="GEOMETRY:") row = layout.row() row.prop(self.properties, 'option_vertices') @@ -583,6 +724,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') @@ -591,66 +733,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.prop(self.properties, 'option_animation_morph') + row = layout.row() - row.prop(self.properties, 'option_round_off') - row.prop(self.properties, 'option_round_value') + row.label(text="Skeletal animations:") row = layout.row() - row.prop(self.properties, 'option_logging') + row.prop(self.properties, 'option_animation_skeletal') + layout.row() row = layout.row() - row.prop(self.properties, 'option_compression') + row.prop(self.properties, 'option_influences') row = layout.row() - row.prop(self.properties, 'option_copy_textures') + row.prop(self.properties, 'option_frame_step') + + row = layout.row() + 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') @@ -659,34 +801,68 @@ 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_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): + """ + + :param self: + :param 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 def register(): + """Registers the addon (Blender boilerplate)""" bpy.utils.register_module(__name__) bpy.types.INFO_MT_file_export.append(menu_func_export) def unregister(): + """Unregisters the addon (Blender boilerplate)""" bpy.utils.unregister_module(__name__) bpy.types.INFO_MT_file_export.remove(menu_func_export) if __name__ == '__main__': - register() + register() diff --git a/utils/exporters/blender/addons/io_three/constants.py b/utils/exporters/blender/addons/io_three/constants.py index 18b514d1ed8abbb9123f59881a117d734c8608b4..b183cf0c54d2fde90aaf78833847416393f4b427 100644 --- a/utils/exporters/blender/addons/io_three/constants.py +++ b/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' @@ -49,8 +50,11 @@ SCALE = 'scale' COMPRESSION = 'compression' MAPS = 'maps' FRAME_STEP = 'frameStep' -ANIMATION = 'animation' +FRAME_INDEX_AS_TIME = 'frameIndexAsTime' +ANIMATION = 'animations' MORPH_TARGETS = 'morphTargets' +POSE = 'pose' +REST = 'rest' SKIN_INDICES = 'skinIndices' SKIN_WEIGHTS = 'skinWeights' LOGGING = 'logging' @@ -64,6 +68,7 @@ PRECISION = 'precision' DEFAULT_PRECISION = 6 EMBED_GEOMETRY = 'embedGeometry' EMBED_ANIMATION = 'embedAnimation' +OFF = 'off' GLOBAL = 'global' BUFFER_GEOMETRY = 'BufferGeometry' @@ -93,11 +98,12 @@ EXPORT_OPTIONS = { FACE_MATERIALS: False, SCALE: 1, FRAME_STEP: 1, - SCENE: True, + FRAME_INDEX_AS_TIME: False, + SCENE: False, MIX_COLORS: False, COMPRESSION: None, MAPS: False, - ANIMATION: False, + ANIMATION: OFF, BONES: False, SKINNING: False, MORPH_TARGETS: False, @@ -105,12 +111,13 @@ EXPORT_OPTIONS = { LIGHTS: False, COPY_TEXTURES: True, LOGGING: DEBUG, - ENABLE_PRECISION: False, + ENABLE_PRECISION: True, PRECISION: DEFAULT_PRECISION, EMBED_GEOMETRY: True, EMBED_ANIMATION: True, GEOMETRY_TYPE: GEOMETRY, - INFLUENCES_PER_VERTEX: 2 + INFLUENCES_PER_VERTEX: 2, + INDENT: True } @@ -210,10 +217,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' diff --git a/utils/exporters/blender/addons/io_three/exporter/api/animation.py b/utils/exporters/blender/addons/io_three/exporter/api/animation.py new file mode 100644 index 0000000000000000000000000000000000000000..13af9003945092006290709ed1bd81f0996336a8 --- /dev/null +++ b/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 diff --git a/utils/exporters/blender/addons/io_three/exporter/api/mesh.py b/utils/exporters/blender/addons/io_three/exporter/api/mesh.py index 70ad1bf2c9d942ba526e4ba19f1b197b6dc0843c..269c0ee7a1c505781f209db2515f6ca3f139b966 100644 --- a/utils/exporters/blender/addons/io_three/exporter/api/mesh.py +++ b/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,60 +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) - if armature and armature.animation_data: - return _skeletal_animations(armature, options) + + 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) - if not armature: return - bones = [] - bone_map = {} - bone_count = 0 - bone_index_rel = 0 + if not armature: + return [], {} - for bone in armature.data.bones: - logger.info('Parsing bone %s', bone.name) - - if bone.parent is None or bone.parent.use_deform is False: - 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 - - 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) + 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() - bone_count += 1 + bones, bone_map = func(armature, round_off, round_val) + #armature.data.pose_position = pose_position return (bones, bone_map) @@ -101,8 +97,6 @@ def buffer_normal(mesh, options): return normals_ - - @_mesh def buffer_position(mesh, options): position = [] @@ -144,6 +138,7 @@ def buffer_uv(mesh, options): return uvs_ + @_mesh def faces(mesh, options): logger.debug('mesh.faces(%s, %s)', mesh, options) @@ -152,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) @@ -170,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: @@ -185,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] @@ -207,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)]) - return face_data + face_data.insert(0, utilities.bit_mask(mask)) + faces_data.extend(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) @@ -242,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) @@ -249,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) @@ -367,12 +386,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 @@ -587,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: @@ -595,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 @@ -631,12 +655,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 +673,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,254 +690,118 @@ def _skinning_data(mesh, bone_map, influences, array_index): return manifest -def _skeletal_animations(armature, options): - action = armature.animation_data.action - 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 - - #@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 - - 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 - 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 - animation = { - 'hierarchy': hierarchy, - 'length':frame_length / fps, - 'fps': fps, - '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 - - -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 +def _pose_bones(armature, round_off, round_val): + bones = [] + bone_map = {} + bone_count = 0 - value = channel.evaluate(frame) + armature_matrix = armature.matrix_world + for bone_count, pose_bone in enumerate(armature.pose.bones): + armature_bone = pose_bone.bone + bone_index = None - if channel.array_index == 1: - rotation.x = value + 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 - elif channel.array_index == 2: - rotation.y = value + bone_map[bone_count] = bone_count - elif channel.array_index == 3: - rotation.z = value + 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 + }) - elif channel.array_index == 0: - rotation.w = value + return bones, bone_map - 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 diff --git a/utils/exporters/blender/addons/io_three/exporter/api/object.py b/utils/exporters/blender/addons/io_three/exporter/api/object.py index 4c4716e58095ee3e20e93612d2c7110efcce24e1..e104422bf2317ae2f29ab864622ebea0abf49a02 100644 --- a/utils/exporters/blender/addons/io_three/exporter/api/object.py +++ b/utils/exporters/blender/addons/io_three/exporter/api/object.py @@ -17,7 +17,6 @@ from .constants import ( PERSP, ORTHO, RENDER, - ZYX, NO_SHADOW ) @@ -46,14 +45,16 @@ def _object(func): return inner -def assemblies(valid_types): +def assemblies(valid_types, options): logger.debug('object.assemblies(%s)', valid_types) for obj in data.objects: - if not obj.parent and obj.type in valid_types: - yield obj.name - elif obj.parent and not obj.parent.parent \ - and obj.parent.type == ARMATURE: + + # rigged assets are parented under armature nodes + if obj.parent and obj.parent.type != ARMATURE: + continue + if obj.parent and obj.parent.type == ARMATURE: logger.info('Has armature parent %s', obj.name) + if _valid_node(obj, valid_types, options): yield obj.name @@ -152,38 +153,15 @@ def node_type(obj): def nodes(valid_types, options): - visible_layers = _visible_scene_layers() for obj in data.objects: - # skip objects that are not on visible layers - if not _on_visible_layer(obj, visible_layers): - continue - try: - export = obj.THREE_export - except AttributeError: - export = True - - mesh_node = mesh(obj, options) - is_mesh = obj.type == MESH - - # skip objects that a mesh could not be resolved - if is_mesh and not mesh_node: - continue - - # secondary test; if a mesh node was resolved but no - # faces are detected then bow out - if is_mesh: - mesh_node = data.meshes[mesh_node] - if len(mesh_node.tessfaces) is 0: - continue - - if obj.type in valid_types and export: + if _valid_node(obj, valid_types, options): yield obj.name - @_object def position(obj, options): logger.debug('object.position(%s)', obj) - vector = _matrix(obj)[0] + parent = obj.parent is None + vector = _decompose_matrix(obj, local=not parent)[0] vector = (vector.x, vector.y, vector.z) round_off, round_val = utilities.rounding(options) @@ -206,8 +184,8 @@ def receive_shadow(obj): @_object def rotation(obj, options): logger.debug('object.rotation(%s)', obj) - vector = _matrix(obj)[1].to_euler(ZYX) - vector = (vector.x, vector.y, vector.z) + vector = _decompose_matrix(obj)[1] + vector = (vector.x, vector.y, vector.z, vector.w) round_off, round_val = utilities.rounding(options) if round_off: @@ -219,7 +197,7 @@ def rotation(obj, options): @_object def scale(obj, options): logger.debug('object.scale(%s)', obj) - vector = _matrix(obj)[2] + vector = _decompose_matrix(obj)[2] vector = (vector.x, vector.y, vector.z) round_off, round_val = utilities.rounding(options) @@ -381,8 +359,11 @@ def extracted_meshes(): return [key for key in _MESH_MAP.keys()] -def _matrix(obj): - matrix = ROTATE_X_PI2 * obj.matrix_world +def _decompose_matrix(obj, local=False): + if local: + matrix = ROTATE_X_PI2 * obj.matrix_local + else: + matrix = ROTATE_X_PI2 * obj.matrix_world return matrix.decompose() @@ -401,3 +382,40 @@ def _visible_scene_layers(): for index, layer in enumerate(context.scene.layers): if layer: visible_layers.append(index) return visible_layers + + +def _valid_node(obj, valid_types, options): + if obj.type not in valid_types: + return False + + # skip objects that are not on visible layers + visible_layers = _visible_scene_layers() + if not _on_visible_layer(obj, visible_layers): + return False + + try: + export = obj.THREE_export + except AttributeError: + export = True + if not export: + return False + + mesh_node = mesh(obj, options) + is_mesh = obj.type == MESH + + # skip objects that a mesh could not be resolved + if is_mesh and not mesh_node: + return False + + # secondary test; if a mesh node was resolved but no + # faces are detected then bow out + if is_mesh: + mesh_node = data.meshes[mesh_node] + if len(mesh_node.tessfaces) is 0: + return False + + # if we get this far assume that the mesh is valid + return True + + + diff --git a/utils/exporters/blender/addons/io_three/exporter/base_classes.py b/utils/exporters/blender/addons/io_three/exporter/base_classes.py index 984785d7f3842af76e47b2e91f45d0faec3078d4..4953f3b2094c402aaaad367afedaa45a5853fd3c 100644 --- a/utils/exporters/blender/addons/io_three/exporter/base_classes.py +++ b/utils/exporters/blender/addons/io_three/exporter/base_classes.py @@ -1,40 +1,68 @@ -import uuid -from .. import constants, exceptions +from . import utilities +from .. import constants, exceptions class BaseClass(constants.BASE_DICT): + """Base class which inherits from a base dictionary object.""" _defaults = {} def __init__(self, parent=None, type=None): constants.BASE_DICT.__init__(self) - self.__type = type + self._type = type - self.__parent = parent + self._parent = parent constants.BASE_DICT.update(self, self._defaults.copy()) - + def __setitem__(self, key, value): if not isinstance(value, constants.VALID_DATA_TYPES): - msg = 'Value is an invalid data type: %s' % type(value) - raise exceptions.ThreeValueError(msg) + msg = "Value is an invalid data type: %s" % type(value) + raise exceptions.ThreeValueError(msg) constants.BASE_DICT.__setitem__(self, key, value) @property def count(self): + """ + + :return: number of keys + :rtype: int + + """ return len(self.keys()) @property def parent(self): - return self.__parent + """ + + :return: parent object + + """ + return self._parent @property def type(self): - return self.__type - + """ + + :return: the type (if applicable) + + """ + return self._type + def copy(self): + """Copies the items to a standard dictionary object. + + :rtype: dict + + """ data = {} def _dict_copy(old, new): + """Recursive function for processing all values + + :param old: + :param new: + + """ for key, value in old.items(): if isinstance(value, (str, list)): new[key] = value[:] @@ -51,12 +79,16 @@ class BaseClass(constants.BASE_DICT): return data -class BaseNode(BaseClass): +class BaseNode(BaseClass): + """Base class for all nodes for the current platform.""" def __init__(self, node, parent, type): BaseClass.__init__(self, parent=parent, type=type) - self.__node = node - if node is not None: + self._node = node + 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 @@ -65,35 +97,51 @@ class BaseNode(BaseClass): else: scene = None - self.__scene = scene + self._scene = scene - self[constants.UUID] = str(uuid.uuid4()).upper() - @property def node(self): - return self.__node + """ + + :return: name of the node + + """ + return self._node @property def scene(self): - return self.__scene + """ + + :return: returns the scene point + + """ + + return self._scene @property def options(self): + """ + + :return: export options + :retype: dict + + """ return self.scene.options class BaseScene(BaseClass): + """Base class that scenes inherit from.""" def __init__(self, filepath, options): BaseClass.__init__(self, type=constants.SCENE) - self.__filepath = filepath + self._filepath = filepath - self.__options = options.copy() + self._options = options.copy() @property def filepath(self): - return self.__filepath + return self._filepath @property def options(self): - return self.__options + return self._options diff --git a/utils/exporters/blender/addons/io_three/exporter/geometry.py b/utils/exporters/blender/addons/io_three/exporter/geometry.py index 563cdf1973ff966019f4d9735b76c36b53087c01..0e6fe8bad1c0731fe5cea63e52bccee94171e733 100644 --- a/utils/exporters/blender/addons/io_three/exporter/geometry.py +++ b/utils/exporters/blender/addons/io_three/exporter/geometry.py @@ -7,9 +7,10 @@ FORMAT_VERSION = 3 class Geometry(base_classes.BaseNode): + """Class that wraps a single mesh/geometry node.""" def __init__(self, node, parent=None): - logger.debug('Geometry().__init__(%s)', node) - + logger.debug("Geometry().__init__(%s)", node) + #@TODO: maybe better to have `three` constants for # strings that are specific to `three` properties geo_type = constants.GEOMETRY.title() @@ -18,23 +19,28 @@ class Geometry(base_classes.BaseNode): if opt_type == constants.BUFFER_GEOMETRY: geo_type = constants.BUFFER_GEOMETRY elif opt_type != constants.GEOMETRY: - logger.error('Unknown geometry type %s', opt_type) + logger.error("Unknown geometry type %s", opt_type) - logger.info('Setting %s to "%s"', node, geo_type) + logger.info("Setting %s to '%s'", node, geo_type) self._defaults[constants.TYPE] = geo_type - base_classes.BaseNode.__init__(self, node, - parent=parent, - type=geo_type) + base_classes.BaseNode.__init__(self, node, + parent=parent, + type=geo_type) @property def animation_filename(self): + """Calculate the file name for the animation file + + :return: base name for the file + """ compression = self.options.get(constants.COMPRESSION) if compression in (None, constants.NONE): ext = constants.JSON elif compression == constants.MSGPACK: ext = constants.PACK + key = '' for key in (constants.MORPH_TARGETS, constants.ANIMATION): try: self[key] @@ -42,27 +48,32 @@ class Geometry(base_classes.BaseNode): except KeyError: pass else: - logger.info('%s has no animation data', self.node) + logger.info("%s has no animation data", self.node) return return '%s.%s.%s' % (self.node, key, ext) @property def face_count(self): + """Parse the bit masks of the `faces` array. + + :rtype: int + + """ try: faces = self[constants.FACES] except KeyError: - logger.debug('No parsed faces found') + logger.debug("No parsed faces found") return 0 length = len(faces) offset = 0 - bitset = lambda x,y: x & ( 1 << y ) + bitset = lambda x, y: x & (1 << y) face_count = 0 masks = (constants.MASK[constants.UVS], - constants.MASK[constants.NORMALS], - constants.MASK[constants.COLORS]) + constants.MASK[constants.NORMALS], + constants.MASK[constants.COLORS]) while offset < length: bit = faces[offset] @@ -84,20 +95,32 @@ class Geometry(base_classes.BaseNode): @property def metadata(self): + """Metadata for the current node. + + :rtype: dict + + """ metadata = { constants.GENERATOR: constants.THREE, constants.VERSION: FORMAT_VERSION } if self[constants.TYPE] == constants.GEOMETRY.title(): - self.__geometry_metadata(metadata) + self._geometry_metadata(metadata) else: - self.__buffer_geometry_metadata(metadata) + self._buffer_geometry_metadata(metadata) return metadata def copy(self, scene=True): - logger.debug('Geometry().copy(scene=%s)', scene) + """Copy the geometry definitions to a standard dictionary. + + :param scene: toggle for scene formatting (Default = True) + :type scene: bool + :rtype: dict + + """ + logger.debug("Geometry().copy(scene=%s)", scene) dispatch = { True: self._scene_format, False: self._geometry_format @@ -107,47 +130,68 @@ class Geometry(base_classes.BaseNode): try: data[constants.MATERIALS] = self[constants.MATERIALS].copy() except KeyError: - logger.debug('No materials to copy') + logger.debug("No materials to copy") return data def copy_textures(self): - logger.debug('Geometry().copy_textures()') + """Copy the textures to the destination directory.""" + logger.debug("Geometry().copy_textures()") if self.options.get(constants.COPY_TEXTURES): texture_registration = self.register_textures() if texture_registration: - logger.info('%s has registered textures', self.node) + logger.info("%s has registered textures", self.node) io.copy_registered_textures( os.path.dirname(self.scene.filepath), texture_registration) def parse(self): - logger.debug('Geometry().parse()') + """Parse the current node""" + logger.debug("Geometry().parse()") if self[constants.TYPE] == constants.GEOMETRY.title(): - logger.info('Parsing Geometry format') - self.__parse_geometry() + logger.info("Parsing Geometry format") + self._parse_geometry() else: - logger.info('Parsing BufferGeometry format') - self.__parse_buffer_geometry() + logger.info("Parsing BufferGeometry format") + self._parse_buffer_geometry() def register_textures(self): - logger.debug('Geometry().register_textures()') - return api.mesh.texture_registration(self.node) + """Obtain a texture registration object. + + :rtype: dict + + """ + logger.debug("Geometry().register_textures()") + return api.mesh.texture_registration(self.node) def write(self, filepath=None): - logger.debug('Geometry().write(filepath=%s)', filepath) + """Write the geometry definitions to disk. Uses the + desitnation path of the scene. + + :param filepath: optional output file path (Default=None) + :type filepath: str + + """ + logger.debug("Geometry().write(filepath=%s)", filepath) filepath = filepath or self.scene.filepath - io.dump(filepath, self.copy(scene=False), - options=self.scene.options) + io.dump(filepath, self.copy(scene=False), + options=self.scene.options) if self.options.get(constants.MAPS): - logger.info('Copying textures for %s', self.node) + logger.info("Copying textures for %s", self.node) self.copy_textures() def write_animation(self, filepath): - logger.debug('Geometry().write_animation(%s)', filepath) + """Write the animation definitions to a separate file + on disk. This helps optimize the geometry file size. + + :param filepath: destination path + :type filepath: str + + """ + logger.debug("Geometry().write_animation(%s)", filepath) for key in (constants.MORPH_TARGETS, constants.ANIMATION): try: @@ -156,29 +200,35 @@ class Geometry(base_classes.BaseNode): except KeyError: pass else: - logger.info('%s has no animation data', self.node) + logger.info("%s has no animation data", self.node) return filepath = os.path.join(filepath, self.animation_filename) if filepath: - logger.info('Dumping animation data to %s', filepath) + logger.info("Dumping animation data to %s", filepath) io.dump(filepath, data, options=self.scene.options) return filepath else: - logger.warning('Could not determine a filepath for '\ - 'animation data. Nothing written to disk.') + logger.warning("Could not determine a filepath for "\ + "animation data. Nothing written to disk.") def _component_data(self): - logger.debug('Geometry()._component_data()') - + """Query the component data only + + :rtype: dict + + """ + logger.debug("Geometry()._component_data()") + if self[constants.TYPE] != constants.GEOMETRY.title(): return self[constants.ATTRIBUTES] - components = [constants.VERTICES, constants.FACES, - constants.UVS, constants.COLORS, constants.NORMALS, - constants.BONES, constants.SKIN_WEIGHTS, - constants.SKIN_INDICES, constants.NAME, - constants.INFLUENCES_PER_VERTEX] + components = [constants.VERTICES, constants.FACES, + constants.UVS, constants.COLORS, + constants.NORMALS, constants.BONES, + constants.SKIN_WEIGHTS, + constants.SKIN_INDICES, constants.NAME, + constants.INFLUENCES_PER_VERTEX] data = {} anim_components = [constants.MORPH_TARGETS, constants.ANIMATION] @@ -192,20 +242,25 @@ class Geometry(base_classes.BaseNode): pass else: data[component] = os.path.basename( - self.animation_filename) + self.animation_filename) + break else: - logger.info('No animation data found for %s', self.node) + logger.info("No animation data found for %s", self.node) for component in components: try: data[component] = self[component] except KeyError: - logger.debug('Component %s not found', component) - pass + logger.debug("Component %s not found", component) return data def _geometry_format(self): + """Three.Geometry formatted definitions + + :rtype: dict + + """ data = self._component_data() if self[constants.TYPE] != constants.GEOMETRY.title(): @@ -219,17 +274,27 @@ class Geometry(base_classes.BaseNode): return data - def __buffer_geometry_metadata(self, metadata): + def _buffer_geometry_metadata(self, metadata): + """Three.BufferGeometry metadata + + :rtype: dict + + """ for key, value in self[constants.ATTRIBUTES].items(): size = value[constants.ITEM_SIZE] array = value[constants.ARRAY] metadata[key] = len(array)/size - - def __geometry_metadata(self, metadata): + + def _geometry_metadata(self, metadata): + """Three.Geometry metadat + + :rtype: dict + + """ skip = (constants.TYPE, constants.FACES, constants.UUID, - constants.ANIMATION, constants.SKIN_INDICES, - constants.SKIN_WEIGHTS, constants.NAME, - constants.INFLUENCES_PER_VERTEX) + constants.ANIMATION, constants.SKIN_INDICES, + constants.SKIN_WEIGHTS, constants.NAME, + constants.INFLUENCES_PER_VERTEX) vectors = (constants.VERTICES, constants.NORMALS) for key in self.keys(): @@ -249,6 +314,11 @@ class Geometry(base_classes.BaseNode): metadata[constants.FACES] = faces def _scene_format(self): + """Format the output for Scene compatability + + :rtype: dict + + """ data = { constants.UUID: self[constants.UUID], constants.TYPE: self[constants.TYPE] @@ -267,34 +337,35 @@ class Geometry(base_classes.BaseNode): } else: data[constants.ATTRIBUTES] = component_data - data[constants.METADATA] = self.metadata + data[constants.METADATA] = self.metadata data[constants.NAME] = self[constants.NAME] - return data + return data - def __parse_buffer_geometry(self): + def _parse_buffer_geometry(self): + """Parse the geometry to Three.BufferGeometry specs""" self[constants.ATTRIBUTES] = {} options_vertices = self.options.get(constants.VERTICES) option_normals = self.options.get(constants.NORMALS) option_uvs = self.options.get(constants.UVS) - dispatch = ( - (constants.POSITION, options_vertices, - api.mesh.buffer_position, 3), - (constants.UV, option_uvs, api.mesh.buffer_uv, 2), - (constants.NORMAL, option_normals, - api.mesh.buffer_normal, 3) - ) + pos_tuple = (constants.POSITION, options_vertices, + api.mesh.buffer_position, 3) + uvs_tuple = (constants.UV, option_uvs, + api.mesh.buffer_uv, 2) + normals_tuple = (constants.NORMAL, option_normals, + api.mesh.buffer_normal, 3) + dispatch = (pos_tuple, uvs_tuple, normals_tuple) - for key, option, func, size in dispatch: + for key, option, func, size in dispatch: if not option: continue - array = func(self.node, self.options) - if not array: - logger.warning('No array could be made for %s', key) + array = func(self.node, self.options) or [] + if not array: + logger.warning("No array could be made for %s", key) continue self[constants.ATTRIBUTES][key] = { @@ -303,53 +374,55 @@ class Geometry(base_classes.BaseNode): constants.ARRAY: array } - def __parse_geometry(self): + def _parse_geometry(self): + """Parse the geometry to Three.Geometry specs""" if self.options.get(constants.VERTICES): - logger.info('Parsing %s', 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) + 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) + 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) + 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) + 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): - logger.info('Parsing %s', constants.ANIMATION) - self[constants.ANIMATION] = api.mesh.animation( - 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) 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.skeletal_animation( + self.node, self.options) or [] #@TODO: considering making bones data implied when # querying skinning data bone_map = {} if self.options.get(constants.BONES): - logger.info('Parsing %s', constants.BONES) - bones, bone_map = api.mesh.bones(self.node) + logger.info("Parsing %s", constants.BONES) + bones, bone_map = api.mesh.bones(self.node, self.options) self[constants.BONES] = bones if self.options.get(constants.SKINNING): - logger.info('Parsing %s', constants.SKINNING) + logger.info("Parsing %s", constants.SKINNING) influences = self.options.get( constants.INFLUENCES_PER_VERTEX, 2) @@ -360,7 +433,7 @@ class Geometry(base_classes.BaseNode): self.node, bone_map, influences) if self.options.get(constants.MORPH_TARGETS): - logger.info('Parsing %s', constants.MORPH_TARGETS) + logger.info("Parsing %s", constants.MORPH_TARGETS) self[constants.MORPH_TARGETS] = api.mesh.morph_targets( self.node, self.options) diff --git a/utils/exporters/blender/addons/io_three/exporter/image.py b/utils/exporters/blender/addons/io_three/exporter/image.py index 64af5c9c31339d534bcae23b1290589be63e33d0..a91bb79de782c11a727053902e5b16c29fb71537 100644 --- a/utils/exporters/blender/addons/io_three/exporter/image.py +++ b/utils/exporters/blender/addons/io_three/exporter/image.py @@ -4,22 +4,43 @@ from . import base_classes, io, api class Image(base_classes.BaseNode): + """Class the wraps an image node. This is the node that + represent that actual file on disk. + """ def __init__(self, node, parent): - logger.debug('Image().__init__(%s)', node) + logger.debug("Image().__init__(%s)", node) base_classes.BaseNode.__init__(self, node, parent, constants.IMAGE) self[constants.URL] = api.image.file_name(self.node) @property def destination(self): + """ + + :return: full destination path (when copied) + + """ dirname = os.path.dirname(self.scene.filepath) return os.path.join(dirname, self[constants.URL]) @property def filepath(self): + """ + + :return: source file path + + """ return api.image.file_path(self.node) def copy_texture(self, func=io.copy): - logger.debug('Image().copy_texture()') + """Copy the texture. + self.filepath > self.destination + + :param func: Optional function override (Default = io.copy) + arguments are (, ) + :return: path the texture was copied to + + """ + logger.debug("Image().copy_texture()") func(self.filepath, self.destination) return self.destination diff --git a/utils/exporters/blender/addons/io_three/exporter/io.py b/utils/exporters/blender/addons/io_three/exporter/io.py index 749d02c1e1660d4481d3b29b872b04501d975977..81ba137ef67271cf5278e8ab42b2c54274561296 100644 --- a/utils/exporters/blender/addons/io_three/exporter/io.py +++ b/utils/exporters/blender/addons/io_three/exporter/io.py @@ -1,33 +1,61 @@ +import os import shutil from .. import constants, logger from . import _json def copy_registered_textures(dest, registration): - logger.debug('io.copy_registered_textures(%s, %s)', dest, registration) + """Copy the registered textures to the destination (root) path + + :param dest: destination directory + :param registration: registered textures + :type dest: str + :type registration: dict + + """ + logger.debug("io.copy_registered_textures(%s, %s)", dest, registration) for value in registration.values(): copy(value['file_path'], dest) def copy(src, dst): - logger.debug('io.copy(%s, %s)' % (src, dst)) - shutil.copy(src, dst) + """Copy a file to a destination + + :param src: source file + :param dst: destination file/path + + """ + logger.debug("io.copy(%s, %s)" % (src, dst)) + if os.path.isdir(dst): + file_name = os.path.basename(src) + dst = os.path.join(dst, file_name) + + if src != dst: + shutil.copy(src, dst) def dump(filepath, data, options=None): + """Dump the output to disk (JSON, msgpack, etc) + + :param filepath: output file path + :param data: serializable data to write to disk + :param options: (Default = None) + :type options: dict + + """ options = options or {} - logger.debug('io.dump(%s, data, options=%s)', filepath, options) + logger.debug("io.dump(%s, data, options=%s)", filepath, options) compress = options.get(constants.COMPRESSION, constants.NONE) if compress == constants.MSGPACK: try: import msgpack except ImportError: - logger.error('msgpack module not found') + logger.error("msgpack module not found") raise - logger.info('Dumping to msgpack') - func = lambda x,y: msgpack.dump(x, y) + logger.info("Dumping to msgpack") + func = lambda x, y: msgpack.dump(x, y) mode = 'wb' else: round_off = options.get(constants.ENABLE_PRECISION) @@ -36,28 +64,37 @@ def dump(filepath, data, options=None): else: _json.ROUND = None - logger.info('Dumping to JSON') - func = lambda x,y: _json.json.dump(x, y, indent=4) + 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=indent) mode = 'w' - logger.info('Writing to %s', filepath) + logger.info("Writing to %s", filepath) with open(filepath, mode=mode) as stream: func(data, stream) def load(filepath, options): - logger.debug('io.load(%s, %s)', filepath, options) + """Load the contents of the file path with the correct parser + + :param filepath: input file path + :param options: + :type options: dict + + """ + logger.debug("io.load(%s, %s)", filepath, options) compress = options.get(constants.COMPRESSION, constants.NONE) if compress == constants.MSGPACK: try: import msgpack except ImportError: - logger.error('msgpack module not found') + logger.error("msgpack module not found") raise module = msgpack mode = 'rb' else: - logger.info('Loading JSON') + logger.info("Loading JSON") module = _json.json mode = 'r' diff --git a/utils/exporters/blender/addons/io_three/exporter/material.py b/utils/exporters/blender/addons/io_three/exporter/material.py index 122b410ffc2337caed43d3137595eb13f7285e4e..0a289ad6a61620d24d3ff6fdc307b005606099f8 100644 --- a/utils/exporters/blender/addons/io_three/exporter/material.py +++ b/utils/exporters/blender/addons/io_three/exporter/material.py @@ -3,21 +3,23 @@ from . import base_classes, utilities, api class Material(base_classes.BaseNode): + """Class that wraps material nodes""" def __init__(self, node, parent): - logger.debug('Material().__init__(%s)', node) - base_classes.BaseNode.__init__(self, node, parent, - constants.MATERIAL) - - self.__common_attributes() + logger.debug("Material().__init__(%s)", node) + base_classes.BaseNode.__init__(self, node, parent, + constants.MATERIAL) + + self._common_attributes() if self[constants.TYPE] == constants.THREE_PHONG: - self.__phong_attributes() + self._phong_attributes() textures = self.parent.options.get(constants.MAPS) if textures: - self.__update_maps() + self._update_maps() - def __common_attributes(self): - logger.debug('Material().__common_attributes()') + def _common_attributes(self): + """Parse the common material attributes""" + logger.debug('Material()._common_attributes()') dispatch = { constants.PHONG: constants.THREE_PHONG, constants.LAMBERT: constants.THREE_LAMBERT, @@ -26,14 +28,15 @@ class Material(base_classes.BaseNode): shader_type = api.material.type(self.node) self[constants.TYPE] = dispatch[shader_type] - ambient = api.material.ambient_color(self.node) - self[constants.AMBIENT] = utilities.rgb2int(ambient) - diffuse = api.material.diffuse_color(self.node) self[constants.COLOR] = utilities.rgb2int(diffuse) - - emissive = api.material.emissive_color(self.node) - self[constants.EMISSIVE] = utilities.rgb2int(emissive) + + if self[constants.TYPE] != constants.THREE_BASIC: + ambient = api.material.ambient_color(self.node) + self[constants.AMBIENT] = utilities.rgb2int(ambient) + + emissive = api.material.emissive_color(self.node) + self[constants.EMISSIVE] = utilities.rgb2int(emissive) vertex_color = api.material.use_vertex_colors(self.node) self[constants.VERTEX_COLORS] = vertex_color @@ -44,14 +47,18 @@ class Material(base_classes.BaseNode): self[constants.DEPTH_WRITE] = api.material.depth_write(self.node) - def __phong_attributes(self): - logger.debug('Material().__phong_attributes()') + def _phong_attributes(self): + """Parse phong specific attributes""" + logger.debug("Material()._phong_attributes()") specular = api.material.specular_color(self.node) self[constants.SPECULAR] = utilities.rgb2int(specular) self[constants.SHININESS] = api.material.specular_coef(self.node) - def __update_maps(self): - logger.debug('Material().__update_maps()') + def _update_maps(self): + """Parses maps/textures and updates the textures array + with any new nodes found. + """ + logger.debug("Material()._update_maps()") mapping = ( (api.material.diffuse_map, constants.MAP), @@ -59,14 +66,14 @@ class Material(base_classes.BaseNode): (api.material.light_map, constants.LIGHT_MAP) ) - for func,key in mapping: + for func, key in mapping: map_node = func(self.node) if map_node: logger.info('Found map node %s for %s', map_node, key) tex_inst = self.scene.texture(map_node.name) - self[key] = tex_inst[constants.UUID] + self[key] = tex_inst[constants.UUID] - if self[constants.TYPE] == constants.THREE_PHONG: + if self[constants.TYPE] == constants.THREE_PHONG: mapping = ( (api.material.bump_map, constants.BUMP_MAP, constants.BUMP_SCALE, api.material.bump_scale), @@ -76,8 +83,9 @@ class Material(base_classes.BaseNode): for func, map_key, scale_key, scale_func in mapping: map_node = func(self.node) - if not map_node: continue - logger.info('Found map node %s for %s', map_node, map_key) + if not map_node: + continue + logger.info("Found map node %s for %s", map_node, map_key) tex_inst = self.scene.texture(map_node.name) - self[map_key] = tex_inst[constants.UUID] + self[map_key] = tex_inst[constants.UUID] self[scale_key] = scale_func(self.node) diff --git a/utils/exporters/blender/addons/io_three/exporter/object.py b/utils/exporters/blender/addons/io_three/exporter/object.py index 1f43024a3535743c8505af1c330ab84ef04473e4..220c6383cc3875ae5d8037a0473cdcad7bd42099 100644 --- a/utils/exporters/blender/addons/io_three/exporter/object.py +++ b/utils/exporters/blender/addons/io_three/exporter/object.py @@ -3,18 +3,19 @@ from . import base_classes, api class Object(base_classes.BaseNode): - + """Class that wraps an object node""" def __init__(self, node, parent=None, type=None): - logger.debug('Object().__init__(%s)', node) + logger.debug("Object().__init__(%s)", node) base_classes.BaseNode.__init__(self, node, parent=parent, type=type) if self.node: - self.__node_setup() + self._node_setup() else: - self.__root_setup() + self._root_setup() - def __init_camera(self): - logger.debug('Object().__init_camera()') + def _init_camera(self): + """Initialize camera attributes""" + logger.debug("Object()._init_camera()") self[constants.FAR] = api.camera.far(self.node) self[constants.NEAR] = api.camera.near(self.node) @@ -29,29 +30,32 @@ class Object(base_classes.BaseNode): #@TODO: need more light attributes. Some may have to come from # custom blender attributes. - def __init_light(self): - logger.debug('Object().__init_light()') + def _init_light(self): + """Initialize light attributes""" + logger.debug("Object()._init_light()") self[constants.COLOR] = api.light.color(self.node) self[constants.INTENSITY] = api.light.intensity(self.node) if self[constants.TYPE] != constants.DIRECTIONAL_LIGHT: self[constants.DISTANCE] = api.light.distance(self.node) - + if self[constants.TYPE] == constants.SPOT_LIGHT: self[constants.ANGLE] = api.light.angle(self.node) - def __init_mesh(self): - logger.debug('Object().__init_mesh()') + def _init_mesh(self): + """Initialize mesh attributes""" + logger.debug("Object()._init_mesh()") mesh = api.object.mesh(self.node, self.options) node = self.scene.geometry(mesh) if node: self[constants.GEOMETRY] = node[constants.UUID] else: - msg = 'Could not find Geometry() node for %s' + msg = "Could not find Geometry() node for %s" logger.error(msg, self.node) - def __node_setup(self): - logger.debug('Object().__node_setup()') + def _node_setup(self): + """Parse common node attributes of all objects""" + logger.debug("Object()._node_setup()") self[constants.NAME] = api.object.name(self.node) self[constants.POSITION] = api.object.position( @@ -68,49 +72,52 @@ class Object(base_classes.BaseNode): self[constants.TYPE] = api.object.node_type(self.node) if self.options.get(constants.MATERIALS): - logger.info('Parsing materials for %s', self.node) + logger.info("Parsing materials for %s", self.node) material_name = api.object.material(self.node) if material_name: - logger.info('Material found %s', material_name) + logger.info("Material found %s", material_name) material_inst = self.scene.material(material_name) self[constants.MATERIAL] = material_inst[constants.UUID] else: - logger.info('%s has no materials', self.node) + logger.info("%s has no materials", self.node) - casts_shadow = (constants.MESH, - constants.DIRECTIONAL_LIGHT, - constants.SPOT_LIGHT) + casts_shadow = (constants.MESH, + constants.DIRECTIONAL_LIGHT, + constants.SPOT_LIGHT) if self[constants.TYPE] in casts_shadow: - logger.info('Querying shadow casting for %s', self.node) + logger.info("Querying shadow casting for %s", self.node) self[constants.CAST_SHADOW] = \ api.object.cast_shadow(self.node) - + if self[constants.TYPE] == constants.MESH: - logger.info('Querying shadow receive for %s', self.node) + logger.info("Querying shadow receive for %s", self.node) self[constants.RECEIVE_SHADOW] = \ api.object.receive_shadow(self.node) camera = (constants.PERSPECTIVE_CAMERA, - constants.ORTHOGRAPHIC_CAMERA) + constants.ORTHOGRAPHIC_CAMERA) - lights = (constants.AMBIENT_LIGHT, constants.DIRECTIONAL_LIGHT, - constants.AREA_LIGHT, constants.POINT_LIGHT, - constants.SPOT_LIGHT, constants.HEMISPHERE_LIGHT) + lights = (constants.AMBIENT_LIGHT, + constants.DIRECTIONAL_LIGHT, + constants.AREA_LIGHT, constants.POINT_LIGHT, + constants.SPOT_LIGHT, constants.HEMISPHERE_LIGHT) if self[constants.TYPE] == constants.MESH: - self.__init_mesh() + self._init_mesh() elif self[constants.TYPE] in camera: - self.__init_camera() + self._init_camera() elif self[constants.TYPE] in lights: - self.__init_light() + self._init_light() - #for child in api.object.children(self.node, self.scene.valid_types): - # if not self.get(constants.CHILDREN): - # self[constants.CHILDREN] = [Object(child, parent=self)] - # else: - # self[constants.CHILDREN].append(Object(child, parent=self)) + for child in api.object.children(self.node, self.scene.valid_types): + if not self.get(constants.CHILDREN): + self[constants.CHILDREN] = [Object(child, parent=self)] + else: + self[constants.CHILDREN].append(Object(child, parent=self)) - def __root_setup(self): - logger.debug('Object().__root_setup()') - self[constants.MATRIX] = [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1] + def _root_setup(self): + """Applies to a root/scene object""" + logger.debug("Object()._root_setup()") + self[constants.MATRIX] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 1] diff --git a/utils/exporters/blender/addons/io_three/exporter/scene.py b/utils/exporters/blender/addons/io_three/exporter/scene.py index cb182a8b401afb7455e2599d7a5899fc2b917330..cf021d469e8fae44922ca5be2142b894d69ae635 100644 --- a/utils/exporters/blender/addons/io_three/exporter/scene.py +++ b/utils/exporters/blender/addons/io_three/exporter/scene.py @@ -4,14 +4,16 @@ from . import ( base_classes, texture, material, - geometry, - object, + geometry, + object as object_, + utilities, io, api ) class Scene(base_classes.BaseScene): + """Class that handles the contruction of a Three scene""" _defaults = { constants.METADATA: constants.DEFAULT_METADATA.copy(), constants.GEOMETRIES: [], @@ -21,7 +23,7 @@ class Scene(base_classes.BaseScene): } def __init__(self, filepath, options=None): - logger.debug('Scene().__init__(%s, %s)', filepath, options) + logger.debug("Scene().__init__(%s, %s)", filepath, options) base_classes.BaseScene.__init__(self, filepath, options or {}) source_file = api.scene_name() @@ -30,73 +32,107 @@ class Scene(base_classes.BaseScene): @property def valid_types(self): + """ + + :return: list of valid node types + + """ valid_types = [api.constants.MESH] if self.options.get(constants.CAMERAS): - logger.info('Adding cameras to valid object types') + logger.info("Adding cameras to valid object types") valid_types.append(api.constants.CAMERA) if self.options.get(constants.LIGHTS): - logger.info('Adding lights to valid object types') + logger.info("Adding lights to valid object types") valid_types.append(api.constants.LAMP) return valid_types - def geometry(self, arg): - logger.debug('Scene().geometry(%s)', arg) - return self._find_node(arg, self[constants.GEOMETRIES]) + def geometry(self, value): + """Find a geometry node that matches either a name + or uuid value. + + :param value: name or uuid + :type value: str + + """ + logger.debug("Scene().geometry(%s)", value) + return _find_node(value, self[constants.GEOMETRIES]) + + def image(self, value): + """Find a image node that matches either a name + or uuid value. - def image(self, arg): - logger.debug('Scene().image%s)', arg) - return self._find_node(arg, self[constants.IMAGES]) + :param value: name or uuid + :type value: str - def material(self, arg): - logger.debug('Scene().material(%s)', arg) - return self._find_node(arg, self[constants.MATERIALS]) + """ + logger.debug("Scene().image%s)", value) + return _find_node(value, self[constants.IMAGES]) + + def material(self, value): + """Find a material node that matches either a name + or uuid value. + + :param value: name or uuid + :type value: str + + """ + logger.debug("Scene().material(%s)", value) + return _find_node(value, self[constants.MATERIALS]) def parse(self): - logger.debug('Scene().parse()') + """Execute the parsing of the scene""" + logger.debug("Scene().parse()") if self.options.get(constants.MAPS): - self.__parse_textures() + self._parse_textures() if self.options.get(constants.MATERIALS): - self.__parse_materials() + self._parse_materials() + + self._parse_geometries() + self._parse_objects() + + def texture(self, value): + """Find a texture node that matches either a name + or uuid value. - self.__parse_geometries() - self.__parse_objects() + :param value: name or uuid + :type value: str - def texture(self, arg): - logger.debug('Scene().texture(%s)', arg) - return self._find_node(arg, self[constants.TEXTURES]) + """ + logger.debug("Scene().texture(%s)", value) + return _find_node(value, self[constants.TEXTURES]) def write(self): - logger.debug('Scene().write()') + """Write the parsed scene to disk.""" + logger.debug("Scene().write()") 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, + extension = constants.EXTENSIONS.get( + compression, constants.EXTENSIONS[constants.JSON]) export_dir = os.path.dirname(self.filepath) for key, value in self.items(): - + if key == constants.GEOMETRIES: geometries = [] - for geometry in value: + for geom in value: if not embed_anim: - geometry.write_animation(export_dir) + geom.write_animation(export_dir) + geom_data = geom.copy() if embed: - for each in value: - geometries.append(each.copy()) + geometries.append(geom_data) continue - geom_data = geometry.copy() - geo_type = geom_data[constants.TYPE].lower() if geo_type == constants.GEOMETRY.lower(): geom_data.pop(constants.DATA) @@ -104,10 +140,10 @@ class Scene(base_classes.BaseScene): geom_data.pop(constants.ATTRIBUTES) geom_data.pop(constants.METADATA) - url = 'geometry.%s%s' % (geometry.node, extension) + url = 'geometry.%s%s' % (geom.node, extension) geometry_file = os.path.join(export_dir, url) - geometry.write(filepath=geometry_file) + geom.write(filepath=geometry_file) geom_data[constants.URL] = os.path.basename(url) geometries.append(geom_data) @@ -124,20 +160,12 @@ class Scene(base_classes.BaseScene): if self.options.get(constants.COPY_TEXTURES): for geo in self[constants.GEOMETRIES]: - logger.info('Copying textures from %s', geo.node) + logger.info("Copying textures from %s", geo.node) geo.copy_textures() - def _find_node(self, arg, manifest): - for index in manifest: - uuid = index.get(constants.UUID) == arg - name = index.node == arg - if uuid or name: - return index - else: - logger.debug('No matching node for %s', arg) - - def __parse_geometries(self): - logger.debug('Scene().__parse_geometries()') + def _parse_geometries(self): + """Locate all geometry nodes and parse them""" + logger.debug("Scene()._parse_geometries()") # this is an important step. please refer to the doc string # on the function for more information @@ -146,47 +174,75 @@ class Scene(base_classes.BaseScene): # now iterate over all the extracted mesh nodes and parse each one for mesh in api.object.extracted_meshes(): - logger.info('Parsing geometry %s', mesh) + logger.info("Parsing geometry %s", mesh) geo = geometry.Geometry(mesh, self) geo.parse() geometries.append(geo) - logger.info('Added %d geometry nodes', len(geometries)) + logger.info("Added %d geometry nodes", len(geometries)) self[constants.GEOMETRIES] = geometries - def __parse_materials(self): - logger.debug('Scene().__parse_materials()') + def _parse_materials(self): + """Locate all non-orphaned materials and parse them""" + logger.debug("Scene()._parse_materials()") materials = [] for material_name in api.material.used_materials(): - logger.info('Parsing material %s', material_name) - materials.append(material.Material(material_name, parent=self)) + logger.info("Parsing material %s", material_name) + materials.append(material.Material(material_name, parent=self)) - logger.info('Added %d material nodes', len(materials)) + logger.info("Added %d material nodes", len(materials)) self[constants.MATERIALS] = materials - def __parse_objects(self): - logger.debug('Scene().__parse_objects()') - self[constants.OBJECT] = object.Object(None, parent=self) + def _parse_objects(self): + """Locate all valid objects in the scene and parse them""" + 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): - logger.info('Parsing object %s', node) - obj = object.Object(node, parent=self[constants.OBJECT]) + objects = [] + for node in api.object.assemblies(self.valid_types, self.options): + logger.info("Parsing object %s", node) + obj = object_.Object(node, parent=self[constants.OBJECT]) objects.append(obj) - logger.info('Added %d object nodes', len(objects)) + logger.info("Added %d object nodes", len(objects)) self[constants.OBJECT][constants.CHILDREN] = objects - def __parse_textures(self): - logger.debug('Scene().__parse_textures()') + def _parse_textures(self): + """Locate all non-orphaned textures and parse them""" + logger.debug("Scene()._parse_textures()") textures = [] for texture_name in api.texture.textures(): - logger.info('Parsing texture %s', texture_name) + logger.info("Parsing texture %s", texture_name) tex_inst = texture.Texture(texture_name, self) textures.append(tex_inst) - logger.info('Added %d texture nodes', len(textures)) + logger.info("Added %d texture nodes", len(textures)) self[constants.TEXTURES] = textures + + +def _find_node(value, manifest): + """Find a node that matches either a name + or uuid value. + + :param value: name or uuid + :param manifest: manifest of nodes to search + :type value: str + :type manifest: list + + """ + for index in manifest: + uuid = index.get(constants.UUID) == value + name = index.node == value + if uuid or name: + return index + else: + logger.debug("No matching node for %s", value) + diff --git a/utils/exporters/blender/addons/io_three/exporter/texture.py b/utils/exporters/blender/addons/io_three/exporter/texture.py index 2eafe25ff67cfba8740d408af0c3ef9170496e33..cdf866b83612f2c46b2637a566939c16eb958b1c 100644 --- a/utils/exporters/blender/addons/io_three/exporter/texture.py +++ b/utils/exporters/blender/addons/io_three/exporter/texture.py @@ -3,8 +3,9 @@ from . import base_classes, image, api class Texture(base_classes.BaseNode): + """Class that wraps a texture node""" def __init__(self, node, parent): - logger.debug('Texture().__init__(%s)', node) + logger.debug("Texture().__init__(%s)", node) base_classes.BaseNode.__init__(self, node, parent, constants.TEXTURE) img_inst = self.scene.image(api.texture.file_name(self.node)) @@ -29,4 +30,10 @@ class Texture(base_classes.BaseNode): @property def image(self): + """ + + :return: the image object of the current texture + :rtype: image.Image + + """ return self.scene.image(self[constants.IMAGE]) diff --git a/utils/exporters/blender/addons/io_three/exporter/utilities.py b/utils/exporters/blender/addons/io_three/exporter/utilities.py index 7bf5238617fd337a6b05decc2260169f63a28838..605371d41cba8480acabcfce9699e382db0ed3b4 100644 --- a/utils/exporters/blender/addons/io_three/exporter/utilities.py +++ b/utils/exporters/blender/addons/io_three/exporter/utilities.py @@ -8,9 +8,15 @@ ROUND = constants.DEFAULT_PRECISION def bit_mask(flags): + """Generate a bit mask. + + :type flags: dict + :return: int + + """ bit = 0 - true = lambda x,y: (x | (1 << y)) - false = lambda x,y: (x & (~(1 << y))) + true = lambda x, y: (x | (1 << y)) + false = lambda x, y: (x & (~(1 << y))) for mask, position in constants.MASK.items(): func = true if flags.get(mask) else false @@ -20,16 +26,43 @@ def bit_mask(flags): def hash(value): + """Generate a hash from a given value + + :param value: + :rtype: str + + """ hash_ = hashlib.md5() hash_.update(repr(value).encode('utf8')) return hash_.hexdigest() def id(): + """Generate a random UUID + + :rtype: str + + """ return str(uuid.uuid4()).upper() +def id_from_name(name): + """Generate a UUID using a name as the namespace + + :type name: str + :rtype: str + + """ + return str(uuid.uuid3(uuid.NAMESPACE_DNS, name)).upper() + + def rgb2int(rgb): + """Convert a given rgb value to an integer + + :type rgb: list|tuple + :rtype: int + + """ is_tuple = isinstance(rgb, tuple) rgb = list(rgb) if is_tuple else rgb @@ -38,6 +71,15 @@ def rgb2int(rgb): def round_off(value, ndigits=ROUND): + """Round off values to specified limit + + :param value: value(s) to round off + :param ndigits: limit (Default = ROUND) + :type value: float|list|tuple + :return: the same data type that was passed + :rtype: float|list|tuple + + """ is_tuple = isinstance(value, tuple) is_list = isinstance(value, list) @@ -55,10 +97,17 @@ def round_off(value, ndigits=ROUND): def rounding(options): - round_off = options.get(constants.ENABLE_PRECISION) - if round_off: + """By evaluation the options determine if precision was + enabled and what the value is + + :type options: dict + :rtype: bool, int + + """ + round_off_ = options.get(constants.ENABLE_PRECISION) + if round_off_: round_val = options[constants.PRECISION] else: round_val = None - return (round_off, round_val) + return (round_off_, round_val) diff --git a/utils/exporters/blender/addons/io_three/logger.py b/utils/exporters/blender/addons/io_three/logger.py index 7fd51f03e9fdfb7a91680a8c0b1129c5bc04ce8b..cf6e178cac96e9f0ba330ba5bd9db453e54de226 100644 --- a/utils/exporters/blender/addons/io_three/logger.py +++ b/utils/exporters/blender/addons/io_three/logger.py @@ -15,7 +15,14 @@ LEVELS = { constants.CRITICAL: logging.CRITICAL } + def init(filename, level=constants.DEBUG): + """Initialize the logger. + + :param filename: base name of the log file + :param level: logging level (Default = DEBUG) + + """ global LOG_FILE LOG_FILE = os.path.join(tempfile.gettempdir(), filename) with open(LOG_FILE, 'w'): diff --git a/utils/exporters/blender/tests/blend/anim.blend b/utils/exporters/blender/tests/blend/anim.blend index 8a136463e860ebd8fa5866ab9794272335097e90..c8e5095735d90a05661cee7c65854cd554ce9a3a 100644 Binary files a/utils/exporters/blender/tests/blend/anim.blend and b/utils/exporters/blender/tests/blend/anim.blend differ diff --git a/utils/exporters/blender/tests/blend/scene_children.blend b/utils/exporters/blender/tests/blend/scene_children.blend new file mode 100644 index 0000000000000000000000000000000000000000..5d857d8b0db436642f0873f38a4005d362253e73 Binary files /dev/null and b/utils/exporters/blender/tests/blend/scene_children.blend differ diff --git a/utils/exporters/blender/tests/scripts/js/review.js b/utils/exporters/blender/tests/scripts/js/review.js index f8d48b6e1b1cf746b33a126790e53bb70910c28f..b48530c08fd9a68fe85023d85be5ade57299a56e 100644 --- a/utils/exporters/blender/tests/scripts/js/review.js +++ b/utils/exporters/blender/tests/scripts/js/review.js @@ -99,6 +99,10 @@ function loadObject( data ) { camera = scene.children[ i ]; var container = document.getElementById( 'viewport' ); + + orbit = new THREE.OrbitControls( camera, container ); + orbit.addEventListener( 'change', render ); + var aspect = container.offsetWidth / container.offsetHeight; camera.aspect = aspect; camera.updateProjectionMatrix(); @@ -131,14 +135,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 { diff --git a/utils/exporters/blender/tests/scripts/test_geometry_normals.bash b/utils/exporters/blender/tests/scripts/test_geometry_normals.bash index 03d9b08b3dbcfb8b9eb611ecdb1222ebd888cb95..89b923b96b8d56bedbf4af04daa639d7bd498ca0 100755 --- a/utils/exporters/blender/tests/scripts/test_geometry_normals.bash +++ b/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) diff --git a/utils/exporters/blender/tests/scripts/test_scene_children.bash b/utils/exporters/blender/tests/scripts/test_scene_children.bash new file mode 100755 index 0000000000000000000000000000000000000000..f51c718e694be8e3bf66f33d40c2982171238a53 --- /dev/null +++ b/utils/exporters/blender/tests/scripts/test_scene_children.bash @@ -0,0 +1,9 @@ +#!/bin/bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "$DIR/setup_test_env.bash" + +blender --background $BLEND/scene_children.blend \ + --python $PYSCRIPT -- $JSON --vertices --faces --scene \ + --cameras --materials --embedGeometry --lights --cameras +makereview $@ --tag $(tagname) diff --git a/utils/exporters/blender/tests/scripts/test_scene_instancing.bash b/utils/exporters/blender/tests/scripts/test_scene_instancing.bash index 4e6cbed83ac8b5ffe74a258387dd542b300ab638..5e93d6e1d0dfda19d2c1aba80d318c0185852c85 100755 --- a/utils/exporters/blender/tests/scripts/test_scene_instancing.bash +++ b/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)