diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index 483f0dbe35120734324461536b2cd212ce598fc5..928c296a90f2ddcaaf0816b8ae4e07327574f3f3 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -59,7 +59,7 @@ from .fbx_utils import ( FBX_LIGHT_TYPES, FBX_LIGHT_DECAY_TYPES, RIGHT_HAND_AXES, FBX_FRAMERATES, # Miscellaneous utils. - units_convert, units_convert_iter, matrix_to_array, similar_values, + units_convertor, units_convertor_iter, matrix4_to_array, similar_values, # UUID from key. get_fbx_uuid_from_key, # Key generators. @@ -87,6 +87,16 @@ from .fbx_utils import ( FBXSettingsMedia, FBXSettings, FBXData, ) +# Units convertors! +convert_sec_to_ktime = units_convertor("second", "ktime") +convert_sec_to_ktime_iter = units_convertor_iter("second", "ktime") + +convert_mm_to_inch = units_convertor("millimeter", "inch") + +convert_rad_to_deg = units_convertor("radian", "degree") +convert_rad_to_deg_iter = units_convertor_iter("radian", "degree") + + ##### Templates ##### # TODO: check all those "default" values, they should match Blender's default as much as possible, I guess? @@ -609,8 +619,8 @@ def fbx_data_camera_elements(root, cam_obj, scene_data): height = render.resolution_y aspect = width / height # Film width & height from mm to inches - filmwidth = units_convert(cam_data.sensor_width, "millimeter", "inch") - filmheight = units_convert(cam_data.sensor_height, "millimeter", "inch") + filmwidth = convert_mm_to_inch(cam_data.sensor_width) + filmheight = convert_mm_to_inch(cam_data.sensor_height) filmaspect = filmwidth / filmheight # Film offset offsetx = filmwidth * cam_data.shift_x @@ -967,9 +977,8 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes): del _uvtuples_gen # Face's materials. - me_fbxmats_idx = None - if me in scene_data.mesh_mat_indices: - me_fbxmats_idx = scene_data.mesh_mat_indices[me] + me_fbxmats_idx = scene_data.mesh_mat_indices.get(me) + if me_fbxmats_idx is not None: me_blmats = me.materials if me_fbxmats_idx and me_blmats: lay_mat = elem_data_single_int32(geom, b"LayerElementMaterial", 0) @@ -1284,7 +1293,7 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): mat_world_obj = ob_obj.fbx_object_matrix(scene_data, global_space=True) fbx_posenode = elem_empty(fbx_pose, b"PoseNode") elem_data_single_int64(fbx_posenode, b"Node", ob_obj.fbx_uuid) - elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix_to_array(mat_world_obj)) + elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(mat_world_obj)) # And all bones of armature! mat_world_bones = {} for bo_obj in bones: @@ -1292,7 +1301,7 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): mat_world_bones[bo_obj] = bomat fbx_posenode = elem_empty(fbx_pose, b"PoseNode") elem_data_single_int64(fbx_posenode, b"Node", bo_obj.fbx_uuid) - elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix_to_array(bomat)) + elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(bomat)) # Deformer. fbx_skin = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(skin_key)) @@ -1341,9 +1350,9 @@ def fbx_data_armature_elements(root, arm_obj, scene_data): # http://area.autodesk.com/forum/autodesk-fbx/fbx-sdk/why-the-values-return- # by-fbxcluster-gettransformmatrix-x-not-same-with-the-value-in-ascii-fbx-file/ elem_data_single_float64_array(fbx_clstr, b"Transform", - matrix_to_array(mat_world_bones[bo_obj].inverted() * mat_world_obj)) - elem_data_single_float64_array(fbx_clstr, b"TransformLink", matrix_to_array(mat_world_bones[bo_obj])) - elem_data_single_float64_array(fbx_clstr, b"TransformAssociateModel", matrix_to_array(mat_world_arm)) + matrix4_to_array(mat_world_bones[bo_obj].inverted() * mat_world_obj)) + elem_data_single_float64_array(fbx_clstr, b"TransformLink", matrix4_to_array(mat_world_bones[bo_obj])) + elem_data_single_float64_array(fbx_clstr, b"TransformAssociateModel", matrix4_to_array(mat_world_arm)) def fbx_data_object_elements(root, ob_obj, scene_data): @@ -1368,7 +1377,7 @@ def fbx_data_object_elements(root, ob_obj, scene_data): # Object transform info. loc, rot, scale, matrix, matrix_rot = ob_obj.fbx_object_tx(scene_data) - rot = tuple(units_convert_iter(rot, "radian", "degree")) + rot = tuple(convert_rad_to_deg_iter(rot)) tmpl = elem_props_template_init(scene_data.templates, b"Model") # For now add only loc/rot/scale... @@ -1423,7 +1432,7 @@ def fbx_data_animation_elements(root, scene_data): fps = scene.render.fps / scene.render.fps_base def keys_to_ktimes(keys): - return (int(v) for v in units_convert_iter((f / fps for f, _v in keys), "second", "ktime")) + return (int(v) for v in convert_sec_to_ktime_iter((f / fps for f, _v in keys))) # Animation stacks. for astack_key, alayers, alayer_key, name, f_start, f_end in animations: @@ -1435,8 +1444,8 @@ def fbx_data_animation_elements(root, scene_data): astack_props = elem_properties(astack) r = scene_data.scene.render fps = r.fps / r.fps_base - start = int(units_convert(f_start / fps, "second", "ktime")) - end = int(units_convert(f_end / fps, "second", "ktime")) + start = int(convert_sec_to_ktime(f_start / fps)) + end = int(convert_sec_to_ktime(f_end / fps)) elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"LocalStart", start) elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"LocalStop", end) elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"ReferenceStart", start) @@ -1699,7 +1708,7 @@ def fbx_animations_objects_do(scene_data, ref_id, f_start, f_end, start_zero, ob p_rot = p_rots.get(ob_obj, None) loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data, rot_euler_compat=p_rot) p_rots[ob_obj] = rot - tx = tuple(loc) + tuple(units_convert_iter(rot, "radian", "degree")) + tuple(scale) + tx = tuple(loc) + tuple(convert_rad_to_deg_iter(rot)) + tuple(scale) animdata[ob_obj].append((real_currframe, tx, [False] * len(tx))) for ob_obj in objects: ob_obj.dupli_list_clear() @@ -1725,7 +1734,7 @@ def fbx_animations_objects_do(scene_data, ref_id, f_start, f_end, start_zero, ob # Get PoseBone from bone... #tobj = bone_map[obj] if isinstance(obj, Bone) else obj #loc, rot, scale, _m, _mr = fbx_object_tx(scene_data, tobj) - #tx = tuple(loc) + tuple(units_convert_iter(rot, "radian", "degree")) + tuple(scale) + #tx = tuple(loc) + tuple(convert_rad_to_deg_iter(rot)) + tuple(scale) dtx = (0.0, 0.0, 0.0) + (0.0, 0.0, 0.0) + (1.0, 1.0, 1.0) # If animation for a channel, (True, keyframes), else (False, current value). final_keys = OrderedDict() @@ -1980,8 +1989,9 @@ def fbx_data_from_scene(scene, settings): # We support any kind of 'surface' shader though, better to have some kind of default Lambert than nothing. # TODO: Support nodes (*BIG* todo!). if mat.type in {'SURFACE'} and not mat.use_nodes: - if mat in data_materials: - data_materials[mat][1].append(ob_obj) + mat_data = data_materials.get(mat) + if mat_data is not None: + mat_data[1].append(ob_obj) else: data_materials[mat] = (get_blenderID_key(mat), [ob_obj]) @@ -2009,12 +2019,14 @@ def fbx_data_from_scene(scene, settings): tex_fbx_props = fbx_mat_properties_from_texture(tex) if not tex_fbx_props: continue - if tex in data_textures: - data_textures[tex][1][mat] = tex_fbx_props + tex_data = data_textures.get(tex) + if tex_data is not None: + tex_data[1][mat] = tex_fbx_props else: data_textures[tex] = (get_blenderID_key(tex), OrderedDict(((mat, tex_fbx_props),))) - if img in data_videos: - data_videos[img][1].append(tex) + vid_data = data_videos.get(img) + if vid_data is not None: + vid_data[1].append(tex) else: data_videos[img] = (get_blenderID_key(img), [tex]) @@ -2461,8 +2473,8 @@ def fbx_takes_elements(root, scene_data): for astack_key, animations, alayer_key, name, f_start, f_end in animations: scene = scene_data.scene fps = scene.render.fps / scene.render.fps_base - start_ktime = int(units_convert(f_start / fps, "second", "ktime")) - end_ktime = int(units_convert(f_end / fps, "second", "ktime")) # +1 is unity hack... + start_ktime = int(convert_sec_to_ktime(f_start / fps)) + end_ktime = int(convert_sec_to_ktime(f_end / fps)) take = elem_data_single_string(takes, b"Take", name) elem_data_single_string(take, b"FileName", name + b".tak") diff --git a/io_scene_fbx/fbx_utils.py b/io_scene_fbx/fbx_utils.py index 465688ada7c1c497d3fd453afc479e39ab4218ae..d864229f0fc002674f0e9ea1a104670a996aa322 100644 --- a/io_scene_fbx/fbx_utils.py +++ b/io_scene_fbx/fbx_utils.py @@ -159,24 +159,33 @@ UNITS = { } -def units_convert(val, u_from, u_to): - """Convert value.""" +def units_convertor(u_from, u_to): + """Return a convertor between specified units.""" conv = UNITS[u_to] / UNITS[u_from] - return val * conv + return lambda v: v * conv -def units_convert_iter(it, u_from, u_to): - """Convert value.""" - conv = UNITS[u_to] / UNITS[u_from] - return (v * conv for v in it) +def units_convertor_iter(u_from, u_to): + """Return an iterable convertor between specified units.""" + conv = units_convertor(u_from, u_to) + def convertor(it): + for v in it: + yield(conv(v)) + return convertor -def matrix_to_array(mat): +def matrix4_to_array(mat): """Concatenate matrix's columns into a single, flat tuple""" # blender matrix is row major, fbx is col major so transpose on write return tuple(f for v in mat.transposed() for f in v) +def array_to_matrix4(arr): + """Convert a single 16-len tuple into a valid 4D Blender matrix""" + # Blender matrix is row major, fbx is col major so transpose on read + return Matrix(tuple(zip(*[iter(arr)]*4))).transposed() + + def similar_values(v1, v2, e=1e-6): """Return True if v1 and v2 are nearly the same.""" if v1 == v2: @@ -184,6 +193,16 @@ def similar_values(v1, v2, e=1e-6): return ((abs(v1 - v2) / max(abs(v1), abs(v2))) <= e) +def similar_values_iter(v1, v2, e=1e-6): + """Return True if iterables v1 and v2 are nearly the same.""" + if v1 == v2: + return True + for v1, v2 in zip(v1, v2): + if (abs(v1 - v2) / max(abs(v1), abs(v2))) > e: + return False + return True + + ##### UIDs code. ##### # ID class (mere int). @@ -489,13 +508,13 @@ def elem_props_template_init(templates, template_type): """ Init a writing template of given type, for *one* element's properties. """ - ret = None - if template_type in templates: - tmpl = templates[template_type] + ret = OrderedDict() + tmpl = templates.get(template_type) + if tmpl is not None: written = tmpl.written[0] props = tmpl.properties ret = OrderedDict((name, [val, ptype, anim, written]) for name, (val, ptype, anim) in props.items()) - return ret or OrderedDict() + return ret def elem_props_template_set(template, elem, ptype_name, name, value, animatable=False): @@ -547,11 +566,12 @@ def fbx_templates_generate(root, fbx_templates): templates = OrderedDict() for type_name, prop_type_name, properties, nbr_users, _written in fbx_templates.values(): - if type_name not in templates: + tmpl = templates.get(type_name) + if tmpl is None: templates[type_name] = [OrderedDict(((prop_type_name, (properties, nbr_users)),)), nbr_users] else: - templates[type_name][0][prop_type_name] = (properties, nbr_users) - templates[type_name][1] += nbr_users + tmpl[0][prop_type_name] = (properties, nbr_users) + tmpl[1] += nbr_users for type_name, (subprops, nbr_users) in templates.items(): template = elem_data_single_string(root, b"ObjectType", type_name) @@ -612,8 +632,8 @@ class MetaObjectWrapper(type): cache = getattr(cls, "_cache", None) if cache is None: cache = cls._cache = {} - if key in cache: - instance = cache[key] + instance = cache.get(key) + if instance is not None: # Duplis hack: since duplis are not persistent in Blender (we have to re-create them to get updated # info like matrix...), we *always* need to reset that matrix when calling ObjectWrapper() (all # other data is supposed valid during whole cache live, so we can skip resetting it). diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index 0ee770f7d00a5f20f879c90fc41a731fcc4aee05..e42cbae77c70dd12b611ddf055efb0d2beca1773 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -34,18 +34,21 @@ import bpy # ----- # Utils -from . import parse_fbx +from . import parse_fbx, fbx_utils from .parse_fbx import data_types, FBXElem +from .fbx_utils import ( + units_convertor_iter, + array_to_matrix4, + similar_values, + similar_values_iter, +) # global singleton, assign on execution fbx_elem_nil = None - -def tuple_deg_to_rad(eul): - return (eul[0] / 57.295779513, - eul[1] / 57.295779513, - eul[2] / 57.295779513) +# Units convertors... +convert_deg_to_rad_iter = units_convertor_iter("degree", "radian") def elem_find_first(elem, id_search, default=None): @@ -95,6 +98,13 @@ def elem_split_name_class(elem): return elem_name, elem_class +def elem_name_ensure_class(elem, clss=...): + elem_name, elem_class = elem_split_name_class(elem) + if clss is not ...: + assert(elem_class == clss) + return elem_name.decode('utf-8') + + def elem_split_name_class_nodeattr(elem): assert(elem.props_type[-2] == data_types.STRING) elem_name, elem_class = elem.props[-2].split(b'\x00\x01') @@ -109,8 +119,8 @@ def elem_uuid(elem): return elem.props[0] -def elem_prop_first(elem): - return elem.props[0] if (elem is not None) and elem.props else None +def elem_prop_first(elem, default=None): + return elem.props[0] if (elem is not None) and elem.props else default # ---- @@ -317,9 +327,9 @@ def blen_read_object(fbx_tmpl, fbx_obj, object_data): rot_alt_mat = Matrix() # rotation - lcl_rot = Euler(tuple_deg_to_rad(rot), rot_ord).to_matrix().to_4x4() * rot_alt_mat - pre_rot = Euler(tuple_deg_to_rad(pre_rot), rot_ord).to_matrix().to_4x4() - pst_rot = Euler(tuple_deg_to_rad(pst_rot), rot_ord).to_matrix().to_4x4() + lcl_rot = Euler(convert_deg_to_rad_iter(rot), rot_ord).to_matrix().to_4x4() * rot_alt_mat + pre_rot = Euler(convert_deg_to_rad_iter(pre_rot), rot_ord).to_matrix().to_4x4() + pst_rot = Euler(convert_deg_to_rad_iter(pst_rot), rot_ord).to_matrix().to_4x4() rot_ofs = Matrix.Translation(rot_ofs) rot_piv = Matrix.Translation(rot_piv) @@ -646,9 +656,7 @@ def blen_read_geom_layer_normal(fbx_obj, mesh): def blen_read_geom(fbx_tmpl, fbx_obj): # TODO, use 'fbx_tmpl' - elem_name, elem_class = elem_split_name_class(fbx_obj) - assert(elem_class == b'Geometry') - elem_name_utf8 = elem_name.decode('utf-8') + elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Geometry') fbx_verts = elem_prop_first(elem_find_first(fbx_obj, b'Vertices')) fbx_polys = elem_prop_first(elem_find_first(fbx_obj, b'PolygonVertexIndex')) @@ -733,14 +741,45 @@ def blen_read_geom(fbx_tmpl, fbx_obj): return mesh +def blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene, global_matrix): + from mathutils import Vector + + elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry') + indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes'), default=()) + dvcos = tuple(co for co in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata, b'Vertices'), default=()))] * 3)) + # We completely ignore normals here! + weight = elem_prop_first(elem_find_first(fbx_bcdata, b'DeformPercent'), default=100.0) / 100.0 + vgweights = tuple(vgw / 100.0 for vgw in elem_prop_first(elem_find_first(fbx_bcdata, b'FullWeights'), default=())) + + assert(len(vgweights) == len(indices) == len(dvcos)) + create_vg = bool(set(vgweights) - {1.0}) + + for me, objects in meshes: + vcos = tuple((idx, me.vertices[idx].co + Vector(dvco)) for idx, dvco in zip(indices, dvcos)) + objects = list({blen_o for fbx_o, blen_o in objects}) + assert(objects) + + if me.shape_keys is None: + objects[0].shape_key_add(name="Basis", from_mix=False) + objects[0].shape_key_add(name=elem_name_utf8, from_mix=False) + me.shape_keys.use_relative = True # Should already be set as such. + + kb = me.shape_keys.key_blocks[elem_name_utf8] + for idx, co in vcos: + kb.data[idx].co[:] = co + kb.value = weight + + # Add vgroup if necessary. + if create_vg: + add_vgroup_to_objects(indices, vgweights, elem_name_utf8, objects) + kb.vertex_group = elem_name_utf8 + + # -------- # Material -def blen_read_material(fbx_tmpl, fbx_obj, - cycles_material_wrap_map, use_cycles): - elem_name, elem_class = elem_split_name_class(fbx_obj) - assert(elem_class == b'Material') - elem_name_utf8 = elem_name.decode('utf-8') +def blen_read_material(fbx_tmpl, fbx_obj, cycles_material_wrap_map, use_cycles): + elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Material') ma = bpy.data.materials.new(name=elem_name_utf8) @@ -796,9 +835,7 @@ def blen_read_texture(fbx_tmpl, fbx_obj, basedir, image_cache, import os from bpy_extras import image_utils - elem_name, elem_class = elem_split_name_class(fbx_obj) - assert(elem_class == b'Texture') - elem_name_utf8 = elem_name.decode('utf-8') + elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Texture') filepath = elem_find_first_string(fbx_obj, b'FileName') if os.sep == '/': @@ -828,9 +865,7 @@ def blen_read_camera(fbx_tmpl, fbx_obj, global_scale): # meters to inches M2I = 0.0393700787 - elem_name, elem_class = elem_split_name_class_nodeattr(fbx_obj) - assert(elem_class == b'Camera') - elem_name_utf8 = elem_name.decode('utf-8') + elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'NodeAttribute') fbx_props = (elem_find_first(fbx_obj, b'Properties70'), elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil)) @@ -855,9 +890,7 @@ def blen_read_camera(fbx_tmpl, fbx_obj, global_scale): def blen_read_light(fbx_tmpl, fbx_obj, global_scale): import math - elem_name, elem_class = elem_split_name_class_nodeattr(fbx_obj) - assert(elem_class == b'Light') - elem_name_utf8 = elem_name.decode('utf-8') + elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'NodeAttribute') fbx_props = (elem_find_first(fbx_obj, b'Properties70'), elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil)) @@ -920,13 +953,15 @@ def load(operator, context, filepath="", global fbx_elem_nil fbx_elem_nil = FBXElem('', (), (), ()) - import os + import os, time from bpy_extras.io_utils import axis_conversion from mathutils import Matrix from . import parse_fbx from .fbx_utils import RIGHT_HAND_AXES, FBX_FRAMERATES + start_time = time.process_time() + # detect ascii files if is_ascii(filepath, 24): operator.report({'ERROR'}, "ASCII FBX files are not supported %r" % filepath) @@ -1483,4 +1518,5 @@ def load(operator, context, filepath="", material.use_raytrace = False _(); del _ + print('Import finished in %.4f sec.' % (time.process_time() - start_time)) return {'FINISHED'}