Newer
Older
"""
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... :/
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
#("", "", 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
def fbx_skeleton_from_armature(scene, settings, armature, objects, 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
for obj in objects.keys():
if not isinstance(obj, Object):
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
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
# Now we have a mesh using this armature. First, find out which bones are concerned!
# XXX Assuming here non-used bones can have no cluster, this has to be checked!
used_bones = tuple(bones[vg.name] for vg in obj.vertex_groups if vg.name in bones)
if not used_bones:
continue
# 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!).
me = obj.data
clusters = {bo: get_blender_bone_cluster_key(armature, me, bo) for bo in used_bones}
data_deformers.setdefault(armature, {})[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))
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
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(scene_data):
"""
Generate animation data from objects.
"""
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())
currframe = scene.frame_start
while currframe < scene.frame_end:
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
# We compute baked loc/rot/scale for all objects.
loc, rot, scale, _m, _mr = fbx_object_tx(scene_data, tobj)
tx = tuple(loc) + tuple(units_convert_iter(rot, "radian", "degree")) + tuple(scale)
animdata[obj].append((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...
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)
# 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(obj, fbx_group, fbx_item)
if fbx_group not in final_keys:
Bastien Montagne
committed
final_keys[fbx_group] = (get_blender_anim_curve_node_key(obj, fbx_group), OrderedDict(), fbx_gname)
final_keys[fbx_group][1][fbx_item] = (fbx_item_key, tx[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:
animations[obj] = (get_blender_anim_layer_key(obj), final_keys)
return (get_blender_anim_stack_key(scene), animations) if animations else None
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')
data_meshes = OrderedDict((obj.data, (get_blenderID_key(obj.data), obj)) for obj in objects if obj.type == 'MESH')
# 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_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
fbx_skeleton_from_armature(scene, settings, obj, objects, 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!
if not isinstance(obj, Object) or obj.type not in {'MESH'}:
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()
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
# 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])
# Animation...
# From objects 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,
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 = 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))
if animations:
# One stack!
templates[b"AnimationStack"] = fbx_template_def_animstack(scene, settings, nbr_users=1)
# One layer per animated object.
templates[b"AnimationLayer"] = fbx_template_def_animlayer(scene, settings, nbr_users=len(animations[1]))
# As much curve node as animated properties.
nbr = sum(len(al) for _kal, al in animations[1].values())
templates[b"AnimationCurveNode"] = fbx_template_def_animcurvenode(scene, settings, nbr_users=nbr)
# And the number of curves themselves...
nbr = sum(1 if ac else 0 for _kal, al in animations[1].values()
Bastien Montagne
committed
for _kacn, acn, _acn_n in al.values()
for _kac, _dv, ac, _acv in acn.values())
templates[b"AnimationCurve"] = fbx_template_def_animcurve(scene, settings, nbr_users=nbr)
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):
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
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))
elif obj.type == 'MESH':
mesh_key, _obj = data_meshes[obj.data]
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():
for me, (skin_key, _obj, clusters) in deformed_meshes.items():
# skin -> geometry
mesh_key, _obj = data_meshes[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))
#Animations
if animations:
# Animstack itself is linked nowhere!
astack_id = get_fbxuid_from_key(animations[0])
for obj, (alayer_key, acurvenodes) in animations[1].items():
obj_id = get_fbxuid_from_key(objects[obj])
# Animlayer -> animstack.
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,
data_empties, data_lamps, data_cameras, data_meshes, mesh_mat_indices,
bones_to_posebones, data_bones, data_deformers,
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
data_world, data_materials, data_textures, data_videos,
)
##### 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
f_start = scene_data.scene.frame_start
f_end = scene_data.scene.frame_end
elem_props_set(props, "p_enum", b"TimeMode", 14) # FPS, 14 = custom...
Bastien Montagne
committed
#elem_props_set(props, "p_timestamp", b"TimeSpanStart", int(units_convert(f_start / fps, "second", "ktime")))
#elem_props_set(props, "p_timestamp", b"TimeSpanStop", int(units_convert(f_end / fps, "second", "ktime")))
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", 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)
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
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)
for mesh in scene_data.data_meshes.keys():
fbx_data_mesh_elements(objects, mesh, scene_data)
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):
"""
Animations. Have yet to check how this work...
"""
# XXX Are takes needed at all in new anim system?
takes = elem_empty(root, b"Takes")
elem_data_single_string(takes, b"Current", b"")
animations = scene_data.animations
if animations is None:
return
scene = scene_data.scene
take_name = scene.name.encode()
fps = scene.render.fps / scene.render.fps_base
scene_start_ktime = int(units_convert(scene.frame_start / fps, "second", "ktime"))
scene_end_ktime = int(units_convert(scene.frame_end / fps, "second", "ktime"))
take = elem_data_single_string(takes, b"Take", take_name)
elem_data_single_string(take, b"FileName", take_name + b".tak")
take_loc_time = elem_data_single_int64(take, b"LocalTime", scene_start_ktime)
take_loc_time.add_int64(scene_end_ktime)
take_ref_time = elem_data_single_int64(take, b"ReferenceTime", scene_start_ktime)
take_ref_time.add_int64(scene_end_ktime)
##### "Main" functions. #####
FBXSettingsMedia = namedtuple("FBXSettingsMedia", (
"path_mode", "base_src", "base_dst", "subdir",
"embed_textures", "copy_set",
))
FBXSettings = namedtuple("FBXSettings", (
"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",
"bake_anim", "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',
bake_anim=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:
object_types = {'EMPTY', 'CAMERA', 'LAMP', 'ARMATURE', 'MESH'}
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(
(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,
bake_anim, bake_anim_step, bake_anim_simplify_factor,
False, media_settings, use_custom_properties,
2795
2796
2797
2798
2799
2800
2801
2802
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
2839
2840
2841
2842
2843
)
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)
# How data are inter-connected.
fbx_connections_elements(root, scene_data)
# Animation.
fbx_takes_elements(root, scene_data)
# And we are down, we can write the whole thing!
encode_bin.write(filepath, root, FBX_VERSION)
# copy all collected files, if we did not embed them.
if not media_settings.embed_textures:
bpy_extras.io_utils.path_reference_copy(media_settings.copy_set)
print('export finished in %.4f sec.' % (time.process_time() - start_time))
return {'FINISHED'}
# defaults for applications, currently only unity but could add others.
def defaults_unity3d():
return {
"global_matrix": Matrix.Rotation(-math.pi / 2.0, 4, 'X'),
"use_selection": False,
"object_types": {'ARMATURE', 'EMPTY', 'MESH'},
"use_mesh_modifiers": True,
#"use_armature_deform_only": True,
"bake_anim": True,
#"use_anim_optimize": False,
#"use_anim_action_all": True,
# Should really be True, but it can cause problems if a model is already in a scene or prefab
# with the old transforms.
"bake_space_transform": False,
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
}
def save(operator, context,
filepath="",
use_selection=False,
batch_mode='OFF',
use_batch_own_dir=False,
**kwargs
):
"""
This is a wrapper around save_single, which handles multi-scenes (or groups) cases, when batch-exporting a whole
.blend file.
"""
ret = None
org_mode = None
if context.active_object and context.active_object.mode != 'OBJECT' and bpy.ops.object.mode_set.poll():
org_mode = context.active_object.mode
bpy.ops.object.mode_set(mode='OBJECT')
if batch_mode == 'OFF':
kwargs_mod = kwargs.copy()
if use_selection:
kwargs_mod["context_objects"] = context.selected_objects
else:
kwargs_mod["context_objects"] = context.scene.objects
ret = save_single(operator, context.scene, filepath, **kwargs_mod)
else:
fbxpath = filepath
prefix = os.path.basename(fbxpath)
if prefix:
fbxpath = os.path.dirname(fbxpath)
if batch_mode == 'GROUP':
data_seq = bpy.data.groups
else:
data_seq = bpy.data.scenes
# call this function within a loop with BATCH_ENABLE == False
# no scene switching done at the moment.
# orig_sce = context.scene
new_fbxpath = fbxpath # own dir option modifies, we need to keep an original
for data in data_seq: # scene or group
newname = "_".join((prefix, bpy.path.clean_name(data.name)))
if use_batch_own_dir:
new_fbxpath = os.path.join(fbxpath, newname)
# path may already exist
# TODO - might exist but be a file. unlikely but should probably account for it.
if not os.path.exists(new_fbxpath):
os.makedirs(new_fbxpath)
filepath = os.path.join(new_fbxpath, newname + '.fbx')
print('\nBatch exporting %s as...\n\t%r' % (data, filepath))
if batch_mode == 'GROUP': # group
# group, so objects update properly, add a dummy scene.
scene = bpy.data.scenes.new(name="FBX_Temp")
scene.layers = [True] * 20
# bpy.data.scenes.active = scene # XXX, cant switch
for ob_base in data.objects:
scene.objects.link(ob_base)
scene.update()
# TODO - BUMMER! Armatures not in the group wont animate the mesh
else:
scene = data
kwargs_batch = kwargs.copy()
kwargs_batch["context_objects"] = data.objects
save_single(operator, scene, filepath, **kwargs_batch)
if batch_mode == 'GROUP':
# remove temp group scene
bpy.data.scenes.remove(scene)
# no active scene changing!
# bpy.data.scenes.active = orig_sce
ret = {'FINISHED'} # so the script wont run after we have batch exported.
if context.active_object and org_mode and bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode=org_mode)
return ret