FBX io: more cleanup.

* Rework unit conversion system to avoid dict lookup each time, also use it in import;
* Optimize a bit by removing some double-dict-lookups;
* Add basic timing info to importer as well.
This commit is contained in:
Bastien Montagne 2014-07-06 18:59:52 +02:00
parent a1d1012a0c
commit fbce6598ea
3 changed files with 139 additions and 71 deletions

View File

@ -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")

View File

@ -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).

View File

@ -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'}