Newer
Older
Bastien Montagne
committed
# alayer.add_string(fbx_name_class(obj.name.encode(), b"AnimLayer"))
# alayer.add_string(b"")
Bastien Montagne
committed
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
# Animation curve node.
acurvenode = elem_data_single_int64(root, b"AnimationCurveNode", get_fbxuid_from_key(acurvenode_key))
acurvenode.add_string(fbx_name_class(acurvenode_name.encode(), b"AnimCurveNode"))
acurvenode.add_string(b"")
acn_tmpl = elem_props_template_init(scene_data.templates, b"AnimationCurveNode")
acn_props = elem_properties(acurvenode)
for fbx_item, (acurve_key, def_value, keys, _acurve_valid) in acurves.items():
elem_props_template_set(acn_tmpl, acn_props, "p_number", fbx_item.encode(), def_value, animatable=True)
# Only create Animation curve if needed!
if keys:
acurve = elem_data_single_int64(root, b"AnimationCurve", get_fbxuid_from_key(acurve_key))
acurve.add_string(fbx_name_class(b"", b"AnimCurve"))
acurve.add_string(b"")
# key attributes...
nbr_keys = len(keys)
# flags...
keyattr_flags = (
1 << 3 | # interpolation mode, 1 = constant, 2 = linear, 3 = cubic.
1 << 8 | # tangent mode, 8 = auto, 9 = TCB, 10 = user, 11 = generic break,
1 << 13 | # tangent mode, 12 = generic clamp, 13 = generic time independent,
1 << 14 | # tangent mode, 13 + 14 = generic clamp progressive.
0,
)
# Maybe values controlling TCB & co???
keyattr_datafloat = (0.0, 0.0, 9.419963346924634e-30, 0.0)
# And now, the *real* data!
elem_data_single_float64(acurve, b"Default", def_value)
elem_data_single_int32(acurve, b"KeyVer", FBX_ANIM_KEY_VERSION)
elem_data_single_int64_array(acurve, b"KeyTime", keys_to_ktimes(keys))
elem_data_single_float32_array(acurve, b"KeyValueFloat", (v for _f, v in keys))
elem_data_single_int32_array(acurve, b"KeyAttrFlags", keyattr_flags)
elem_data_single_float32_array(acurve, b"KeyAttrDataFloat", keyattr_datafloat)
elem_data_single_int32_array(acurve, b"KeyAttrRefCount", (nbr_keys,))
elem_props_template_finalize(acn_tmpl, acn_props)
Bastien Montagne
committed
##### Top-level FBX data container. #####
# Helper container gathering some data we need multiple times:
# * templates.
# * objects.
# * connections.
# * takes.
FBXData = namedtuple("FBXData", (
"templates", "templates_users", "connections",
"settings", "scene", "objects", "animations", "frame_start", "frame_end",
"data_empties", "data_lamps", "data_cameras", "data_meshes", "mesh_mat_indices",
"bones_to_posebones", "data_bones", "data_deformers",
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
"data_world", "data_materials", "data_textures", "data_videos",
))
def fbx_mat_properties_from_texture(tex):
"""
Returns a set of FBX metarial properties that are affected by the given texture.
Quite obviously, this is a fuzzy and far-from-perfect mapping! Amounts of influence are completely lost, e.g.
Note tex is actually expected to be a texture slot.
"""
# Tex influence does not exists in FBX, so assume influence < 0.5 = no influence... :/
INFLUENCE_THRESHOLD = 0.5
# Mapping Blender -> FBX (blend_use_name, blend_fact_name, fbx_name).
blend_to_fbx = (
# Lambert & Phong...
("diffuse", "diffuse", b"DiffuseFactor"),
("color_diffuse", "diffuse_color", b"DiffuseColor"),
("alpha", "alpha", b"TransparencyFactor"),
("diffuse", "diffuse", b"TransparentColor"), # Uses diffuse color in Blender!
("emit", "emit", b"EmissiveFactor"),
("diffuse", "diffuse", b"EmissiveColor"), # Uses diffuse color in Blender!
("ambient", "ambient", b"AmbientFactor"),
#("", "", b"AmbientColor"), # World stuff in Blender, for now ignore...
Bastien Montagne
committed
("normal", "normal", b"NormalMap"),
# Note: unsure about those... :/
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
#("", "", b"Bump"),
#("", "", b"BumpFactor"),
#("", "", b"DisplacementColor"),
#("", "", b"DisplacementFactor"),
# Phong only.
("specular", "specular", b"SpecularFactor"),
("color_spec", "specular_color", b"SpecularColor"),
# See Material template about those two!
("hardness", "hardness", b"Shininess"),
("hardness", "hardness", b"ShininessExponent"),
("mirror", "mirror", b"ReflectionColor"),
("raymir", "raymir", b"ReflectionFactor"),
)
tex_fbx_props = set()
for use_map_name, name_factor, fbx_prop_name in blend_to_fbx:
if getattr(tex, "use_map_" + use_map_name) and getattr(tex, name_factor + "_factor") >= INFLUENCE_THRESHOLD:
tex_fbx_props.add(fbx_prop_name)
return tex_fbx_props
Bastien Montagne
committed
def fbx_skeleton_from_armature(scene, settings, armature, objects, data_meshes, bones_to_posebones,
data_bones, data_deformers, arm_parents):
"""
Create skeleton from armature/bones (NodeAttribute/LimbNode and Model/LimbNode), and for each deformed mesh,
create Pose/BindPose(with sub PoseNode) and Deformer/Skin(with Deformer/SubDeformer/Cluster).
Also supports "parent to bone" (simple parent to Model/LimbNode).
arm_parents is a set of tuples (armature, object) for all successful armature bindings.
"""
arm = armature.data
Bastien Montagne
committed
bones = OrderedDict()
for bo, pbo in zip(arm.bones, armature.pose.bones):
key, data_key = get_blender_bone_key(armature, bo)
objects[bo] = key
bones_to_posebones[bo] = pbo
data_bones[bo] = (key, data_key, armature)
bones[bo.name] = bo
Bastien Montagne
committed
if not bones:
return
for obj in objects.keys():
if not isinstance(obj, Object):
continue
if obj.type not in {'MESH'}:
continue
if obj.parent != armature:
continue
# Always handled by an Armature modifier...
found = False
for mod in obj.modifiers:
if mod.type not in {'ARMATURE'}:
continue
# We only support vertex groups binding method, not bone envelopes one!
if mod.object == armature and mod.use_vertex_groups:
found = True
break
if not found:
continue
Bastien Montagne
committed
# Now we have a mesh using this armature.
# Note: bindpose have no relations at all (no connections), so no need for any preprocess for them.
# Create skin & clusters relations (note skins are connected to geometry, *not* model!).
Bastien Montagne
committed
_key, me, _free = data_meshes[obj]
Bastien Montagne
committed
clusters = OrderedDict((bo, get_blender_bone_cluster_key(armature, me, bo)) for bo in bones.values())
Bastien Montagne
committed
data_deformers.setdefault(armature, OrderedDict())[me] = (get_blender_armature_skin_key(armature, me),
obj, clusters)
# We don't want a regular parent relationship for those in FBX...
arm_parents.add((armature, obj))
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
def fbx_animations_simplify(scene_data, animdata):
"""
Simplifies FCurves!
"""
fac = scene_data.settings.bake_anim_simplify_factor
step = scene_data.settings.bake_anim_step
# So that, with default factor and step values (1), we get:
max_frame_diff = step * fac * 10 # max step of 10 frames.
value_diff_fac = fac / 1000 # min value evolution: 0.1% of whole range.
for obj, keys in animdata.items():
if not keys:
continue
extremums = [(min(values), max(values)) for values in zip(*(k[1] for k in keys))]
min_diffs = [max((mx - mn) * value_diff_fac, 0.000001) for mx, mn in extremums]
p_currframe, p_key, p_key_write = keys[0]
p_keyed = [(p_currframe - max_frame_diff, val) for val in p_key]
for currframe, key, key_write in keys:
for idx, (val, p_val) in enumerate(zip(key, p_key)):
p_keyedframe, p_keyedval = p_keyed[idx]
if val == p_val:
# Never write keyframe when value is exactly the same as prev one!
continue
if abs(val - p_val) >= min_diffs[idx]:
# If enough difference from previous sampled value, key this value *and* the previous one!
key_write[idx] = True
p_key_write[idx] = True
p_keyed[idx] = (currframe, val)
elif (abs(val - p_keyedval) >= min_diffs[idx]) or (currframe - p_keyedframe >= max_frame_diff):
# Else, if enough difference from previous keyed value (or max gap between keys is reached),
# key this value only!
key_write[idx] = True
p_keyed[idx] = (currframe, val)
p_currframe, p_key, p_key_write = currframe, key, key_write
# Always key last sampled values (we ignore curves with a single valid key anyway).
p_key_write[:] = [True] * len(p_key_write)
def fbx_animations_objects_do(scene_data, ref_id, f_start, f_end, start_zero):
Bastien Montagne
committed
Generate animation data (a single AnimStack) from objects, for a given frame range.
"""
objects = scene_data.objects
bake_step = scene_data.settings.bake_anim_step
scene = scene_data.scene
bone_map = scene_data.bones_to_posebones
# FBX mapping info: Property affected, and name of the "sub" property (to distinguish e.g. vector's channels).
fbx_names = (
Bastien Montagne
committed
("Lcl Translation", "T", "d|X"), ("Lcl Translation", "T", "d|Y"), ("Lcl Translation", "T", "d|Z"),
("Lcl Rotation", "R", "d|X"), ("Lcl Rotation", "R", "d|Y"), ("Lcl Rotation", "R", "d|Z"),
("Lcl Scaling", "S", "d|X"), ("Lcl Scaling", "S", "d|Y"), ("Lcl Scaling", "S", "d|Z"),
)
back_currframe = scene.frame_current
Bastien Montagne
committed
animdata = OrderedDict((obj, []) for obj in objects.keys())
Bastien Montagne
committed
p_rots = {}
Bastien Montagne
committed
currframe = f_start
while currframe < f_end:
real_currframe = currframe - f_start if start_zero else currframe
scene.frame_set(int(currframe), currframe - int(currframe))
for obj in objects.keys():
# Get PoseBone from bone...
tobj = bone_map[obj] if isinstance(obj, Bone) else obj
Bastien Montagne
committed
# We compute baked loc/rot/scale for all objects (rot being euler-compat with previous value!).
p_rot = p_rots.get(tobj, None)
loc, rot, scale, _m, _mr = fbx_object_tx(scene_data, tobj, p_rot)
p_rots[tobj] = rot
tx = tuple(loc) + tuple(units_convert_iter(rot, "radian", "degree")) + tuple(scale)
animdata[obj].append((real_currframe, tx, [False] * len(tx)))
currframe += bake_step
scene.frame_set(back_currframe, 0.0)
fbx_animations_simplify(scene_data, animdata)
Bastien Montagne
committed
animations = OrderedDict()
# And now, produce final data (usable by FBX export code)...
for obj, keys in animdata.items():
if not keys:
continue
curves = [[] for k in keys[0][1]]
for currframe, key, key_write in keys:
for idx, (val, wrt) in enumerate(zip(key, key_write)):
if wrt:
curves[idx].append((currframe, val))
# Get PoseBone from bone...
Bastien Montagne
committed
#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)
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).
Bastien Montagne
committed
final_keys = OrderedDict()
for idx, c in enumerate(curves):
Bastien Montagne
committed
fbx_group, fbx_gname, fbx_item = fbx_names[idx]
fbx_item_key = get_blender_anim_curve_key(scene, ref_id, obj, fbx_group, fbx_item)
if fbx_group not in final_keys:
fbx_group_key = get_blender_anim_curve_node_key(scene, ref_id, obj, fbx_group)
final_keys[fbx_group] = (fbx_group_key, OrderedDict(), fbx_gname)
Bastien Montagne
committed
final_keys[fbx_group][1][fbx_item] = (fbx_item_key, dtx[idx], c, True if len(c) > 1 else False)
# And now, remove anim groups (i.e. groups of curves affecting a single FBX property) with no curve at all!
del_groups = []
Bastien Montagne
committed
for grp, (_k, data, _n) in final_keys.items():
if True in (d[3] for d in data.values()):
continue
del_groups.append(grp)
for grp in del_groups:
del final_keys[grp]
if final_keys:
Bastien Montagne
committed
animations[obj] = (get_blender_anim_layer_key(scene, obj), final_keys)
astack_key = get_blender_anim_stack_key(scene, ref_id)
alayer_key = get_blender_anim_layer_key(scene, ref_id)
name = (ref_id.name if ref_id else scene.name).encode()
if start_zero:
f_end -= f_start
f_start = 0.0
Bastien Montagne
committed
return (astack_key, animations, alayer_key, name, f_start, f_end) if animations else None
def fbx_animations_objects(scene_data):
"""
Generate global animation data from objects.
"""
scene = scene_data.scene
animations = []
frame_start = 1e100
frame_end = -1e100
Bastien Montagne
committed
def add_anim(animations, anim):
nonlocal frame_start, frame_end
if anim is not None:
animations.append(anim)
f_start, f_end = anim[4:6]
if f_start < frame_start:
frame_start = f_start
if f_end > frame_end:
frame_end = f_end
Bastien Montagne
committed
# Per-NLA strip animstacks.
if scene_data.settings.bake_anim_use_nla_strips:
strips = []
for obj in scene_data.objects:
# NLA tracks only for objects, not bones!
if not isinstance(obj, Object) or not obj.animation_data:
continue
for track in obj.animation_data.nla_tracks:
if track.mute:
continue
for strip in track.strips:
if strip.mute:
continue
strips.append(strip)
strip.mute = True
for strip in strips:
strip.mute = False
add_anim(animations, fbx_animations_objects_do(scene_data, strip, strip.frame_start, strip.frame_end, True))
Bastien Montagne
committed
strip.mute = True
for strip in strips:
strip.mute = False
# Global (containing everything) animstack.
if not scene_data.settings.bake_anim_use_nla_strips or not animations:
add_anim(animations, fbx_animations_objects_do(scene_data, None, scene.frame_start, scene.frame_end, False))
return animations, frame_start, frame_end
def fbx_data_from_scene(scene, settings):
"""
Do some pre-processing over scene's data...
"""
objtypes = settings.object_types
Bastien Montagne
committed
objects = settings.context_objects
##### Gathering data...
# This is rather simple for now, maybe we could end generating templates with most-used values
# instead of default ones?
Bastien Montagne
committed
objects = OrderedDict((obj, get_blenderID_key(obj)) for obj in objects if obj.type in objtypes)
Bastien Montagne
committed
data_lamps = OrderedDict((obj.data, get_blenderID_key(obj.data)) for obj in objects if obj.type == 'LAMP')
# Unfortunately, FBX camera data contains object-level data (like position, orientation, etc.)...
Bastien Montagne
committed
data_cameras = OrderedDict((obj, get_blenderID_key(obj.data)) for obj in objects if obj.type == 'CAMERA')
# Yep! Contains nothing, but needed!
data_empties = OrderedDict((obj, get_blender_empty_key(obj)) for obj in objects if obj.type == 'EMPTY')
Bastien Montagne
committed
data_meshes = OrderedDict()
for obj in objects:
if obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
continue
Bastien Montagne
committed
if settings.use_mesh_modifiers or obj.type in BLENDER_OTHER_OBJECT_TYPES:
Bastien Montagne
committed
tmp_mods = []
if obj.type == 'MESH':
# No need to create a new mesh in this case, if no modifier is active!
use_org_data = True
Bastien Montagne
committed
for mod in obj.modifiers:
# For meshes, when anim export is enabled, disable Armature modifiers here!
if mod.type == 'ARMATURE' and settings.bake_anim:
Bastien Montagne
committed
tmp_mods.append((mod, mod.show_render))
mod.show_render = False
if mod.show_render:
use_org_data = False
if not use_org_data:
tmp_me = obj.to_mesh(scene, apply_modifiers=True, settings='RENDER')
data_meshes[obj] = (get_blenderID_key(tmp_me), tmp_me, True)
Bastien Montagne
committed
# Re-enable temporary disabled modifiers.
for mod, show_render in tmp_mods:
mod.show_render = show_render
Bastien Montagne
committed
data_meshes[obj] = (get_blenderID_key(obj.data), obj.data, False)
Bastien Montagne
committed
data_bones = OrderedDict()
data_deformers = OrderedDict()
bones_to_posebones = dict()
arm_parents = set()
for obj in tuple(objects.keys()):
if obj.type not in {'ARMATURE'}:
continue
Bastien Montagne
committed
fbx_skeleton_from_armature(scene, settings, obj, objects, data_meshes, bones_to_posebones,
data_bones, data_deformers, arm_parents)
# Some world settings are embedded in FBX materials...
if scene.world:
Bastien Montagne
committed
data_world = OrderedDict(((scene.world, get_blenderID_key(scene.world)),))
Bastien Montagne
committed
data_world = OrderedDict()
# TODO: Check all the mat stuff works even when mats are linked to Objects
# (we can then have the same mesh used with different materials...).
# *Should* work, as FBX always links its materials to Models (i.e. objects).
# XXX However, material indices would probably break...
Bastien Montagne
committed
data_materials = OrderedDict()
for obj in objects:
# Only meshes for now!
Bastien Montagne
committed
if not isinstance(obj, Object) or obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
continue
for mat_s in obj.material_slots:
mat = mat_s.material
# Note theoretically, FBX supports any kind of materials, even GLSL shaders etc.
# However, I doubt anything else than Lambert/Phong is really portable!
# 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(obj)
else:
data_materials[mat] = (get_blenderID_key(mat), [obj])
# Note FBX textures also hold their mapping info.
# TODO: Support layers?
Bastien Montagne
committed
data_textures = OrderedDict()
# FbxVideo also used to store static images...
Bastien Montagne
committed
data_videos = OrderedDict()
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
# For now, do not use world textures, don't think they can be linked to anything FBX wise...
for mat in data_materials.keys():
for tex in mat.texture_slots:
if tex is None:
continue
# For now, only consider image textures.
# Note FBX does has support for procedural, but this is not portable at all (opaque blob),
# so not useful for us.
# TODO I think ENVIRONMENT_MAP should be usable in FBX as well, but for now let it aside.
#if tex.texture.type not in {'IMAGE', 'ENVIRONMENT_MAP'}:
if tex.texture.type not in {'IMAGE'}:
continue
img = tex.texture.image
if img is None:
continue
# Find out whether we can actually use this texture for this material, in FBX context.
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
else:
Bastien Montagne
committed
data_textures[tex] = (get_blenderID_key(tex), OrderedDict(((mat, tex_fbx_props),)))
if img in data_videos:
data_videos[img][1].append(tex)
else:
data_videos[img] = (get_blenderID_key(img), [tex])
Bastien Montagne
committed
animations = ()
frame_start = scene.frame_start
frame_end = scene.frame_end
Bastien Montagne
committed
if settings.bake_anim:
# From objects & bones only for a start.
tmp_scdata = FBXData( # Kind of hack, we need a temp scene_data for object's space handling to bake animations...
None, None, None,
settings, scene, objects, None, 0.0, 0.0,
Bastien Montagne
committed
data_empties, data_lamps, data_cameras, data_meshes, None,
bones_to_posebones, data_bones, data_deformers,
data_world, data_materials, data_textures, data_videos,
)
animations, frame_start, frame_end = fbx_animations_objects(tmp_scdata)
##### Creation of templates...
templates = OrderedDict()
templates[b"GlobalSettings"] = fbx_template_def_globalsettings(scene, settings, nbr_users=1)
if data_empties:
templates[b"Null"] = fbx_template_def_null(scene, settings, nbr_users=len(data_empties))
if data_lamps:
templates[b"Light"] = fbx_template_def_light(scene, settings, nbr_users=len(data_lamps))
if data_cameras:
templates[b"Camera"] = fbx_template_def_camera(scene, settings, nbr_users=len(data_cameras))
if data_bones:
templates[b"Bone"] = fbx_template_def_bone(scene, settings, nbr_users=len(data_bones))
if data_meshes:
templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=len(data_meshes))
if objects:
templates[b"Model"] = fbx_template_def_model(scene, settings, nbr_users=len(objects))
if arm_parents:
# Number of Pose|BindPose elements should be the same as number of meshes-parented-to-armatures
templates[b"BindPose"] = fbx_template_def_pose(scene, settings, nbr_users=len(arm_parents))
if data_deformers:
nbr = len(data_deformers)
nbr += sum(len(clusters) for def_me in data_deformers.values() for a, b, clusters in def_me.values())
templates[b"Deformers"] = fbx_template_def_deformer(scene, settings, nbr_users=nbr)
# No world support in FBX...
"""
if data_world:
templates[b"World"] = fbx_template_def_world(scene, settings, nbr_users=len(data_world))
"""
if data_materials:
templates[b"Material"] = fbx_template_def_material(scene, settings, nbr_users=len(data_materials))
if data_textures:
templates[b"TextureFile"] = fbx_template_def_texture_file(scene, settings, nbr_users=len(data_textures))
if data_videos:
templates[b"Video"] = fbx_template_def_video(scene, settings, nbr_users=len(data_videos))
Bastien Montagne
committed
nbr_astacks = len(animations)
nbr_acnodes = 0
nbr_acurves = 0
for _astack_key, astack, _al, _n, _fs, _fe in animations:
for _alayer_key, alayer in astack.values():
for _acnode_key, acnode, _acnode_name in alayer.values():
nbr_acnodes += 1
for _acurve_key, _dval, acurve, acurve_valid in acnode.values():
if acurve:
nbr_acurves += 1
templates[b"AnimationStack"] = fbx_template_def_animstack(scene, settings, nbr_users=nbr_astacks)
Bastien Montagne
committed
# Would be nice to have one layer per animated object, but this seems tricky and not that well supported.
Bastien Montagne
committed
# So for now, only one layer per anim stack.
templates[b"AnimationLayer"] = fbx_template_def_animlayer(scene, settings, nbr_users=nbr_astacks)
templates[b"AnimationCurveNode"] = fbx_template_def_animcurvenode(scene, settings, nbr_users=nbr_acnodes)
templates[b"AnimationCurve"] = fbx_template_def_animcurve(scene, settings, nbr_users=nbr_acurves)
templates_users = sum(tmpl.nbr_users for tmpl in templates.values())
##### Creation of connections...
connections = []
# Objects (with classical parenting).
for obj, obj_key in objects.items():
# Bones are handled later.
if isinstance(obj, Object):
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
par = obj.parent
par_key = 0 # Convention, "root" node (never explicitly written).
if par and par in objects:
par_type = obj.parent_type
if par_type in {'OBJECT', 'BONE'}:
# Meshes parented to armature also have 'OBJECT' par_type, in FBX this is handled separately,
# we do not want an extra object parenting!
if (par, obj) not in arm_parents:
par_key = objects[par]
else:
print("Sorry, “{}” parenting type is not supported".format(par_type))
connections.append((b"OO", get_fbxuid_from_key(obj_key), get_fbxuid_from_key(par_key), None))
# Armature & Bone chains.
for bo, (bo_key, _bo_data_key, arm) in data_bones.items():
par = bo.parent
if not par: # Root bone.
par = arm
if par not in objects:
continue
connections.append((b"OO", get_fbxuid_from_key(bo_key), get_fbxuid_from_key(objects[par]), None))
# Empties
for empty_obj, empty_key in data_empties.items():
empty_obj_key = objects[empty_obj]
connections.append((b"OO", get_fbxuid_from_key(empty_key), get_fbxuid_from_key(empty_obj_key), None))
# Cameras
for obj_cam, cam_key in data_cameras.items():
cam_obj_key = objects[obj_cam]
connections.append((b"OO", get_fbxuid_from_key(cam_key), get_fbxuid_from_key(cam_obj_key), None))
# Object data.
for obj, obj_key in objects.items():
if isinstance(obj, Bone):
_bo_key, bo_data_key, _arm = data_bones[obj]
assert(_bo_key == obj_key)
connections.append((b"OO", get_fbxuid_from_key(bo_data_key), get_fbxuid_from_key(obj_key), None))
elif obj.type == 'LAMP':
lamp_key = data_lamps[obj.data]
connections.append((b"OO", get_fbxuid_from_key(lamp_key), get_fbxuid_from_key(obj_key), None))
Bastien Montagne
committed
elif obj.type in BLENDER_OBJECT_TYPES_MESHLIKE:
mesh_key, _me, _free = data_meshes[obj]
connections.append((b"OO", get_fbxuid_from_key(mesh_key), get_fbxuid_from_key(obj_key), None))
# Deformers (armature-to-geometry, only for meshes currently)...
for arm, deformed_meshes in data_deformers.items():
Bastien Montagne
committed
for me, (skin_key, obj, clusters) in deformed_meshes.items():
Bastien Montagne
committed
mesh_key, _me, _free = data_meshes[obj]
assert(me == _me)
connections.append((b"OO", get_fbxuid_from_key(skin_key), get_fbxuid_from_key(mesh_key), None))
for bo, clstr_key in clusters.items():
# cluster -> skin
connections.append((b"OO", get_fbxuid_from_key(clstr_key), get_fbxuid_from_key(skin_key), None))
# bone -> cluster
connections.append((b"OO", get_fbxuid_from_key(objects[bo]), get_fbxuid_from_key(clstr_key), None))
# Materials
Bastien Montagne
committed
mesh_mat_indices = OrderedDict()
_objs_indices = {}
for mat, (mat_key, objs) in data_materials.items():
for obj in objs:
obj_key = objects[obj]
connections.append((b"OO", get_fbxuid_from_key(mat_key), get_fbxuid_from_key(obj_key), None))
# Get index of this mat for this object.
# Mat indices for mesh faces are determined by their order in 'mat to ob' connections.
# Only mats for meshes currently...
me = obj.data
idx = _objs_indices[obj] = _objs_indices.get(obj, -1) + 1
Bastien Montagne
committed
mesh_mat_indices.setdefault(me, OrderedDict())[mat] = idx
del _objs_indices
# Textures
for tex, (tex_key, mats) in data_textures.items():
for mat, fbx_mat_props in mats.items():
mat_key, _objs = data_materials[mat]
for fbx_prop in fbx_mat_props:
# texture -> material properties
connections.append((b"OP", get_fbxuid_from_key(tex_key), get_fbxuid_from_key(mat_key), fbx_prop))
# Images
for vid, (vid_key, texs) in data_videos.items():
for tex in texs:
tex_key, _texs = data_textures[tex]
connections.append((b"OO", get_fbxuid_from_key(vid_key), get_fbxuid_from_key(tex_key), None))
Bastien Montagne
committed
for astack_key, astack, alayer_key, _name, _fstart, _fend in animations:
# Animstack itself is linked nowhere!
Bastien Montagne
committed
astack_id = get_fbxuid_from_key(astack_key)
Bastien Montagne
committed
# For now, only one layer!
Bastien Montagne
committed
alayer_id = get_fbxuid_from_key(alayer_key)
Bastien Montagne
committed
connections.append((b"OO", alayer_id, astack_id, None))
Bastien Montagne
committed
for obj, (alayer_key, acurvenodes) in astack.items():
obj_id = get_fbxuid_from_key(objects[obj])
# Animlayer -> animstack.
Bastien Montagne
committed
# alayer_id = get_fbxuid_from_key(alayer_key)
# connections.append((b"OO", alayer_id, astack_id, None))
Bastien Montagne
committed
for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
# Animcurvenode -> animalayer.
acurvenode_id = get_fbxuid_from_key(acurvenode_key)
connections.append((b"OO", acurvenode_id, alayer_id, None))
# Animcurvenode -> object property.
connections.append((b"OP", acurvenode_id, obj_id, fbx_prop.encode()))
Bastien Montagne
committed
for fbx_item, (acurve_key, dafault_value, acurve, acurve_valid) in acurves.items():
if acurve:
# Animcurve -> Animcurvenode.
connections.append((b"OP", get_fbxuid_from_key(acurve_key), acurvenode_id, fbx_item.encode()))
##### And pack all this!
return FBXData(
templates, templates_users, connections,
settings, scene, objects, animations, frame_start, frame_end,
data_empties, data_lamps, data_cameras, data_meshes, mesh_mat_indices,
bones_to_posebones, data_bones, data_deformers,
data_world, data_materials, data_textures, data_videos,
)
Bastien Montagne
committed
def fbx_scene_data_cleanup(scene_data):
"""
Some final cleanup...
"""
# Delete temp meshes.
for _key, me, free in scene_data.data_meshes.values():
if free:
bpy.data.meshes.remove(me)
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
##### Top-level FBX elements generators. #####
def fbx_header_elements(root, scene_data, time=None):
"""
Write boiling code of FBX root.
time is expected to be a datetime.datetime object, or None (using now() in this case).
"""
##### Start of FBXHeaderExtension element.
header_ext = elem_empty(root, b"FBXHeaderExtension")
elem_data_single_int32(header_ext, b"FBXHeaderVersion", FBX_HEADER_VERSION)
elem_data_single_int32(header_ext, b"FBXVersion", FBX_VERSION)
# No encryption!
elem_data_single_int32(header_ext, b"EncryptionType", 0)
if time is None:
time = datetime.datetime.now()
elem = elem_empty(header_ext, b"CreationTimeStamp")
elem_data_single_int32(elem, b"Version", 1000)
elem_data_single_int32(elem, b"Year", time.year)
elem_data_single_int32(elem, b"Month", time.month)
elem_data_single_int32(elem, b"Day", time.day)
elem_data_single_int32(elem, b"Hour", time.hour)
elem_data_single_int32(elem, b"Minute", time.minute)
elem_data_single_int32(elem, b"Second", time.second)
elem_data_single_int32(elem, b"Millisecond", time.microsecond // 1000)
elem_data_single_string_unicode(header_ext, b"Creator", "Blender version %s" % bpy.app.version_string)
# 'SceneInfo' seems mandatory to get a valid FBX file...
# TODO use real values!
# XXX Should we use scene.name.encode() here?
scene_info = elem_data_single_string(header_ext, b"SceneInfo", fbx_name_class(b"GlobalInfo", b"SceneInfo"))
scene_info.add_string(b"UserData")
elem_data_single_string(scene_info, b"Type", b"UserData")
elem_data_single_int32(scene_info, b"Version", FBX_SCENEINFO_VERSION)
meta_data = elem_empty(scene_info, b"MetaData")
elem_data_single_int32(meta_data, b"Version", FBX_SCENEINFO_VERSION)
elem_data_single_string(meta_data, b"Title", b"")
elem_data_single_string(meta_data, b"Subject", b"")
elem_data_single_string(meta_data, b"Author", b"")
elem_data_single_string(meta_data, b"Keywords", b"")
elem_data_single_string(meta_data, b"Revision", b"")
elem_data_single_string(meta_data, b"Comment", b"")
props = elem_properties(scene_info)
elem_props_set(props, "p_string_url", b"DocumentUrl", "/foobar.fbx")
elem_props_set(props, "p_string_url", b"SrcDocumentUrl", "/foobar.fbx")
original = elem_props_compound(props, b"Original")
original("p_string", b"ApplicationVendor", "Blender Foundation")
original("p_string", b"ApplicationName", "Blender")
original("p_string", b"ApplicationVersion", "2.70")
original("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
original("p_string", b"FileName", "/foobar.fbx")
lastsaved = elem_props_compound(props, b"LastSaved")
lastsaved("p_string", b"ApplicationVendor", "Blender Foundation")
lastsaved("p_string", b"ApplicationName", "Blender")
lastsaved("p_string", b"ApplicationVersion", "2.70")
lastsaved("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
##### End of FBXHeaderExtension element.
# FileID is replaced by dummy value currently...
elem_data_single_bytes(root, b"FileId", b"FooBar")
# CreationTime is replaced by dummy value currently, but anyway...
elem_data_single_string_unicode(root, b"CreationTime",
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}:{:03}"
"".format(time.year, time.month, time.day, time.hour, time.minute, time.second,
time.microsecond * 1000))
elem_data_single_string_unicode(root, b"Creator", "Blender version %s" % bpy.app.version_string)
##### Start of GlobalSettings element.
global_settings = elem_empty(root, b"GlobalSettings")
elem_data_single_int32(global_settings, b"Version", 1000)
props = elem_properties(global_settings)
up_axis, front_axis, coord_axis = RIGHT_HAND_AXES[scene_data.settings.to_axes]
elem_props_set(props, "p_integer", b"UpAxis", up_axis[0])
elem_props_set(props, "p_integer", b"UpAxisSign", up_axis[1])
elem_props_set(props, "p_integer", b"FrontAxis", front_axis[0])
elem_props_set(props, "p_integer", b"FrontAxisSign", front_axis[1])
elem_props_set(props, "p_integer", b"CoordAxis", coord_axis[0])
elem_props_set(props, "p_integer", b"CoordAxisSign", coord_axis[1])
elem_props_set(props, "p_integer", b"OriginalUpAxis", -1)
elem_props_set(props, "p_integer", b"OriginalUpAxisSign", 1)
Bastien Montagne
committed
elem_props_set(props, "p_double", b"UnitScaleFactor", 1.0)
elem_props_set(props, "p_double", b"OriginalUnitScaleFactor", 1.0)
elem_props_set(props, "p_color_rgb", b"AmbientColor", (0.0, 0.0, 0.0))
elem_props_set(props, "p_string", b"DefaultCamera", "Producer Perspective")
# Global timing data.
r = scene_data.scene.render
fps = r.fps / r.fps_base
Bastien Montagne
committed
fbx_fps, fbx_fps_mode = FBX_FRAMERATES[0] # Custom framerate.
for ref_fps, fps_mode in FBX_FRAMERATES:
if similar_values(fps, ref_fps):
fbx_fps = ref_fps
fbx_fps_mode = fps_mode
elem_props_set(props, "p_enum", b"TimeMode", fbx_fps_mode)
Bastien Montagne
committed
elem_props_set(props, "p_timestamp", b"TimeSpanStart", 0)
elem_props_set(props, "p_timestamp", b"TimeSpanStop", FBX_KTIME)
Bastien Montagne
committed
elem_props_set(props, "p_double", b"CustomFrameRate", fbx_fps)
##### End of GlobalSettings element.
def fbx_documents_elements(root, scene_data):
"""
Write 'Document' part of FBX root.
Seems like FBX support multiple documents, but until I find examples of such, we'll stick to single doc!
time is expected to be a datetime.datetime object, or None (using now() in this case).
"""
name = scene_data.scene.name
##### Start of Documents element.
docs = elem_empty(root, b"Documents")
elem_data_single_int32(docs, b"Count", 1)
doc_uid = get_fbxuid_from_key("__FBX_Document__" + name)
doc = elem_data_single_int64(docs, b"Document", doc_uid)
doc.add_string_unicode(name)
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
doc.add_string_unicode(name)
props = elem_properties(doc)
elem_props_set(props, "p_object", b"SourceObject")
elem_props_set(props, "p_string", b"ActiveAnimStackName", "")
# XXX Some kind of ID? Offset?
# Anyway, as long as we have only one doc, probably not an issue.
elem_data_single_int64(doc, b"RootNode", 0)
def fbx_references_elements(root, scene_data):
"""
Have no idea what references are in FBX currently... Just writing empty element.
"""
docs = elem_empty(root, b"References")
def fbx_definitions_elements(root, scene_data):
"""
Templates definitions. Only used by Objects data afaik (apart from dummy GlobalSettings one).
"""
definitions = elem_empty(root, b"Definitions")
elem_data_single_int32(definitions, b"Version", FBX_TEMPLATES_VERSION)
elem_data_single_int32(definitions, b"Count", scene_data.templates_users)
fbx_templates_generate(definitions, scene_data.templates)
def fbx_objects_elements(root, scene_data):
"""
Data (objects, geometry, material, textures, armatures, etc.
"""
objects = elem_empty(root, b"Objects")
for empty in scene_data.data_empties.keys():
fbx_data_empty_elements(objects, empty, scene_data)
for lamp in scene_data.data_lamps.keys():
fbx_data_lamp_elements(objects, lamp, scene_data)
for cam in scene_data.data_cameras.keys():
fbx_data_camera_elements(objects, cam, scene_data)
Bastien Montagne
committed
for me_obj in scene_data.data_meshes.keys():
fbx_data_mesh_elements(objects, me_obj, scene_data, done_meshes)
del done_meshes
for obj in scene_data.objects.keys():
fbx_data_object_elements(objects, obj, scene_data)
for obj in scene_data.objects.keys():
if not isinstance(obj, Object) or obj.type not in {'ARMATURE'}:
continue
fbx_data_armature_elements(objects, obj, scene_data)
for mat in scene_data.data_materials.keys():
fbx_data_material_elements(objects, mat, scene_data)
for tex in scene_data.data_textures.keys():
fbx_data_texture_file_elements(objects, tex, scene_data)
for vid in scene_data.data_videos.keys():
fbx_data_video_elements(objects, vid, scene_data)
fbx_data_animation_elements(objects, scene_data)
def fbx_connections_elements(root, scene_data):
"""
Relations between Objects (which material uses which texture, and so on).
"""
connections = elem_empty(root, b"Connections")
for c in scene_data.connections:
elem_connection(connections, *c)
def fbx_takes_elements(root, scene_data):
"""
Bastien Montagne
committed
Animations.
# XXX Pretty sure takes are no more needed...
takes = elem_empty(root, b"Takes")
elem_data_single_string(takes, b"Current", b"")
animations = scene_data.animations
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...
take = elem_data_single_string(takes, b"Take", name)
elem_data_single_string(take, b"FileName", name + b".tak")
take_loc_time = elem_data_single_int64(take, b"LocalTime", start_ktime)
take_loc_time.add_int64(end_ktime)
take_ref_time = elem_data_single_int64(take, b"ReferenceTime", start_ktime)
take_ref_time.add_int64(end_ktime)
##### "Main" functions. #####
FBXSettingsMedia = namedtuple("FBXSettingsMedia", (
"path_mode", "base_src", "base_dst", "subdir",
"embed_textures", "copy_set",
))
FBXSettings = namedtuple("FBXSettings", (
Bastien Montagne
committed
"report", "to_axes", "global_matrix", "global_scale",
Bastien Montagne
committed
"bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed",
"context_objects", "object_types", "use_mesh_modifiers",
"mesh_smooth_type", "use_mesh_edges", "use_tspace", "use_armature_deform_only",
Bastien Montagne
committed
"bake_anim", "bake_anim_use_nla_strips", "bake_anim_step", "bake_anim_simplify_factor",
"use_metadata", "media_settings", "use_custom_properties",
))
# This func can be called with just the filepath
def save_single(operator, scene, filepath="",
global_matrix=Matrix(),
axis_up="Z",
axis_forward="Y",
context_objects=None,
object_types=None,
use_mesh_modifiers=True,
mesh_smooth_type='FACE',
Bastien Montagne
committed
bake_anim_use_nla_strips=True,
bake_anim_step=1.0,
bake_anim_simplify_factor=1.0,
use_metadata=True,
path_mode='AUTO',
use_mesh_edges=True,
use_tspace=True,
embed_textures=False,
use_custom_properties=False,
bake_space_transform=False,
**kwargs
):
if object_types is None:
Bastien Montagne
committed
object_types = {'EMPTY', 'CAMERA', 'LAMP', 'ARMATURE', 'MESH', 'OTHER'}
if 'OTHER' in object_types:
object_types |= BLENDER_OTHER_OBJECT_TYPES
global_scale = global_matrix.median_scale
global_matrix_inv = global_matrix.inverted()
Bastien Montagne
committed
# For transforming mesh normals.
global_matrix_inv_transposed = global_matrix_inv.transposed()
# Only embed textures in COPY mode!
if embed_textures and path_mode != 'COPY':
embed_textures = False
media_settings = FBXSettingsMedia(
path_mode,
os.path.dirname(bpy.data.filepath), # base_src
os.path.dirname(filepath), # base_dst
# Local dir where to put images (medias), using FBX conventions.
os.path.splitext(os.path.basename(filepath))[0] + ".fbm", # subdir
embed_textures,
set(), # copy_set
)
settings = FBXSettings(
Bastien Montagne
committed
operator.report, (axis_up, axis_forward), global_matrix, global_scale,
Bastien Montagne
committed
bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
context_objects, object_types, use_mesh_modifiers,
mesh_smooth_type, use_mesh_edges, use_tspace, False,
Bastien Montagne
committed
bake_anim, bake_anim_use_nla_strips, bake_anim_step, bake_anim_simplify_factor,
False, media_settings, use_custom_properties,
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
)
import bpy_extras.io_utils
print('\nFBX export starting... %r' % filepath)
start_time = time.process_time()
# Generate some data about exported scene...
scene_data = fbx_data_from_scene(scene, settings)
root = elem_empty(None, b"") # Root element has no id, as it is not saved per se!
# Mostly FBXHeaderExtension and GlobalSettings.
fbx_header_elements(root, scene_data)
# Documents and References are pretty much void currently.
fbx_documents_elements(root, scene_data)
fbx_references_elements(root, scene_data)
# Templates definitions.
fbx_definitions_elements(root, scene_data)
# Actual data.
fbx_objects_elements(root, scene_data)