Newer
Older
for idx, (sv, v) in enumerate(zip(shape.data, me.vertices)):
if similar_values_iter(sv.co, v.co):
# Note: Maybe this is a bit too simplistic, should we use real shape base here? Though FBX does not
# have this at all... Anyway, this should cover most common cases imho.
continue
shape_verts_co.extend(sv.co - v.co);
shape_verts_idx.append(idx);
if not shape_verts_co:
continue
channel_key, geom_key = get_blender_mesh_shape_channel_key(me, shape)
data = (channel_key, geom_key, shape_verts_co, shape_verts_idx)
data_deformers_shape.setdefault(me, (me_key, shapes_key, OrderedDict()))[2][shape] = data
Bastien Montagne
committed
data_deformers_skin = OrderedDict()
Bastien Montagne
committed
data_bones = OrderedDict()
Bastien Montagne
committed
for ob_obj in tuple(objects):
if not (ob_obj.is_object and ob_obj.type in {'ARMATURE'}):
Bastien Montagne
committed
fbx_skeleton_from_armature(scene, settings, ob_obj, objects, data_meshes,
data_bones, data_deformers_skin, 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()
Bastien Montagne
committed
for ob_obj in objects:
# If obj is not a valid object for materials, wrapper will just return an empty tuple...
for mat_s in ob_obj.material_slots:
if mat is None:
continue # Empty slots!
# 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:
mat_data = data_materials.get(mat)
if mat_data is not None:
mat_data[1].append(ob_obj)
Bastien Montagne
committed
data_materials[mat] = (get_blenderID_key(mat), [ob_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()
# 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, use_tex in zip(mat.texture_slots, mat.use_textures):
if tex is None or not use_tex:
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
tex_data = data_textures.get(tex)
if tex_data is not None:
tex_data[1][mat] = tex_fbx_props
Bastien Montagne
committed
data_textures[tex] = (get_blenderID_key(tex), OrderedDict(((mat, tex_fbx_props),)))
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])
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,
data_bones, data_deformers_skin, data_deformers_shape,
Bastien Montagne
committed
data_world, data_materials, data_textures, data_videos,
)
animations, frame_start, frame_end = fbx_animations(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:
nbr = len(data_meshes)
if data_deformers_shape:
nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values())
templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=nbr)
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_skin or data_deformers_shape:
nbr = 0
if data_deformers_skin:
nbr += len(data_deformers_skin)
nbr += sum(len(clusters) for def_me in data_deformers_skin.values() for a, b, clusters in def_me.values())
if data_deformers_shape:
nbr += len(data_deformers_shape)
nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values())
assert(nbr != 0)
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).
Bastien Montagne
committed
for ob_obj in objects:
# Bones are handled later.
Bastien Montagne
committed
if not ob_obj.is_bone:
par_obj = ob_obj.parent
# Meshes parented to armature are handled separately, yet we want the 'no parent' connection (0).
if par_obj and ob_obj.has_valid_parent(objects) and (par_obj, ob_obj) not in arm_parents:
connections.append((b"OO", ob_obj.fbx_uuid, par_obj.fbx_uuid, None))
else:
connections.append((b"OO", ob_obj.fbx_uuid, 0, None))
# Armature & Bone chains.
Bastien Montagne
committed
for bo_obj in data_bones.keys():
par_obj = bo_obj.parent
if par_obj not in objects:
Bastien Montagne
committed
connections.append((b"OO", bo_obj.fbx_uuid, par_obj.fbx_uuid, None))
Bastien Montagne
committed
for ob_obj in objects:
if ob_obj.is_bone:
bo_data_key = data_bones[ob_obj]
connections.append((b"OO", get_fbx_uuid_from_key(bo_data_key), ob_obj.fbx_uuid, None))
Bastien Montagne
committed
else:
Bastien Montagne
committed
if ob_obj.type == 'LAMP':
lamp_key = data_lamps[ob_obj.bdata.data]
connections.append((b"OO", get_fbx_uuid_from_key(lamp_key), ob_obj.fbx_uuid, None))
Bastien Montagne
committed
elif ob_obj.type == 'CAMERA':
cam_key = data_cameras[ob_obj]
connections.append((b"OO", get_fbx_uuid_from_key(cam_key), ob_obj.fbx_uuid, None))
Bastien Montagne
committed
elif ob_obj.type == 'EMPTY':
empty_key = data_empties[ob_obj]
connections.append((b"OO", get_fbx_uuid_from_key(empty_key), ob_obj.fbx_uuid, None))
Bastien Montagne
committed
elif ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE:
mesh_key, _me, _free = data_meshes[ob_obj]
connections.append((b"OO", get_fbx_uuid_from_key(mesh_key), ob_obj.fbx_uuid, None))
# 'Shape' deformers (shape keys, only for meshes currently)...
for me_key, shapes_key, shapes in data_deformers_shape.values():
# shape -> geometry
connections.append((b"OO", get_fbx_uuid_from_key(shapes_key), get_fbx_uuid_from_key(me_key), None))
for channel_key, geom_key, _shape_verts_co, _shape_verts_idx in shapes.values():
# shape channel -> shape
connections.append((b"OO", get_fbx_uuid_from_key(channel_key), get_fbx_uuid_from_key(shapes_key), None))
# geometry (keys) -> shape channel
connections.append((b"OO", get_fbx_uuid_from_key(geom_key), get_fbx_uuid_from_key(channel_key), None))
# 'Skin' deformers (armature-to-geometry, only for meshes currently)...
for arm, deformed_meshes in data_deformers_skin.items():
Bastien Montagne
committed
for me, (skin_key, ob_obj, clusters) in deformed_meshes.items():
mesh_key, _me, _free = data_meshes[ob_obj]
Bastien Montagne
committed
assert(me == _me)
connections.append((b"OO", get_fbx_uuid_from_key(skin_key), get_fbx_uuid_from_key(mesh_key), None))
Bastien Montagne
committed
for bo_obj, clstr_key in clusters.items():
connections.append((b"OO", get_fbx_uuid_from_key(clstr_key), get_fbx_uuid_from_key(skin_key), None))
connections.append((b"OO", bo_obj.fbx_uuid, get_fbx_uuid_from_key(clstr_key), None))
Bastien Montagne
committed
mesh_mat_indices = OrderedDict()
Bastien Montagne
committed
for mat, (mat_key, ob_objs) in data_materials.items():
for ob_obj in ob_objs:
connections.append((b"OO", get_fbx_uuid_from_key(mat_key), ob_obj.fbx_uuid, None))
# Get index of this mat for this object (or dupliobject).
# Mat indices for mesh faces are determined by their order in 'mat to ob' connections.
# Only mats for meshes currently...
# Note in case of dupliobjects a same me/mat idx will be generated several times...
# Should not be an issue in practice, and it's needed in case we export duplis but not the original!
if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
continue
_mesh_key, me, _free = data_meshes[ob_obj]
idx = _objs_indices[ob_obj] = _objs_indices.get(ob_obj, -1) + 1
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():
Bastien Montagne
committed
mat_key, _ob_objs = data_materials[mat]
for fbx_prop in fbx_mat_props:
# texture -> material properties
connections.append((b"OP", get_fbx_uuid_from_key(tex_key), get_fbx_uuid_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_fbx_uuid_from_key(vid_key), get_fbx_uuid_from_key(tex_key), None))
Bastien Montagne
committed
for astack_key, astack, alayer_key, _name, _fstart, _fend in animations:
# Animstack itself is linked nowhere!
astack_id = get_fbx_uuid_from_key(astack_key)
Bastien Montagne
committed
# For now, only one layer!
alayer_id = get_fbx_uuid_from_key(alayer_key)
Bastien Montagne
committed
connections.append((b"OO", alayer_id, astack_id, None))
for elem_key, (alayer_key, acurvenodes) in astack.items():
elem_id = get_fbx_uuid_from_key(elem_key)
# alayer_id = get_fbx_uuid_from_key(alayer_key)
Bastien Montagne
committed
# 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_fbx_uuid_from_key(acurvenode_key)
connections.append((b"OO", acurvenode_id, alayer_id, None))
# Animcurvenode -> object property.
connections.append((b"OP", acurvenode_id, elem_id, fbx_prop.encode()))
for fbx_item, (acurve_key, default_value, acurve, acurve_valid) in acurves.items():
if acurve:
# Animcurve -> Animcurvenode.
connections.append((b"OP", get_fbx_uuid_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,
data_bones, data_deformers_skin, data_deformers_shape,
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)
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
##### 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")
scene = scene_data.scene
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]
# Currently not sure about that, but looks like default unit of FBX is cm...
scale_factor = (1.0 if scene.unit_settings.system == 'NONE' else scene.unit_settings.scale_length) * 100
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)
elem_props_set(props, "p_double", b"UnitScaleFactor", scale_factor)
elem_props_set(props, "p_double", b"OriginalUnitScaleFactor", scale_factor)
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.render
_, fbx_fps_mode = FBX_FRAMERATES[0] # Custom framerate.
fbx_fps = fps = r.fps / r.fps_base
Bastien Montagne
committed
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_fbx_uuid_from_key("__FBX_Document__" + name)
doc = elem_data_single_int64(docs, b"Document", doc_uid)
doc.add_string_unicode(name)
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
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")
Bastien Montagne
committed
for empty in scene_data.data_empties:
fbx_data_empty_elements(objects, empty, scene_data)
Bastien Montagne
committed
for lamp in scene_data.data_lamps:
fbx_data_lamp_elements(objects, lamp, scene_data)
Bastien Montagne
committed
for cam in scene_data.data_cameras:
fbx_data_camera_elements(objects, cam, scene_data)
for me_obj in scene_data.data_meshes:
fbx_data_mesh_elements(objects, me_obj, scene_data, done_meshes)
Bastien Montagne
committed
for ob_obj in scene_data.objects:
if ob_obj.is_dupli:
Bastien Montagne
committed
continue
Bastien Montagne
committed
fbx_data_object_elements(objects, ob_obj, scene_data)
ob_obj.dupli_list_create(scene_data.scene, 'RENDER')
for dp_obj in ob_obj.dupli_list:
if dp_obj not in scene_data.objects:
continue
fbx_data_object_elements(objects, dp_obj, scene_data)
ob_obj.dupli_list_clear()
Bastien Montagne
committed
for ob_obj in scene_data.objects:
if not (ob_obj.is_object and ob_obj.type == 'ARMATURE'):
Bastien Montagne
committed
fbx_data_armature_elements(objects, ob_obj, scene_data)
Bastien Montagne
committed
for mat in scene_data.data_materials:
fbx_data_material_elements(objects, mat, scene_data)
Bastien Montagne
committed
for tex in scene_data.data_textures:
fbx_data_texture_file_elements(objects, tex, scene_data)
Bastien Montagne
committed
for vid in scene_data.data_videos:
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(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")
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. #####
# 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',
use_armature_deform_only=False,
Bastien Montagne
committed
bake_anim_use_nla_strips=True,
bake_anim_use_all_actions=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_props=False,
bake_space_transform=False,
Bastien Montagne
committed
# Clear cached ObjectWrappers (just in case...).
ObjectWrapper.cache_clear()
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, use_armature_deform_only,
bake_anim, bake_anim_use_nla_strips, bake_anim_use_all_actions, bake_anim_step, bake_anim_simplify_factor,
False, media_settings, use_custom_props,
2622
2623
2624
2625
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
)
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)
Bastien Montagne
committed
# Cleanup!
fbx_scene_data_cleanup(scene_data)
# And we are down, we can write the whole thing!
encode_bin.write(filepath, root, FBX_VERSION)
Bastien Montagne
committed
# Clear cached ObjectWrappers!
ObjectWrapper.cache_clear()
# 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 {
# These options seem to produce the same result as the old Ascii exporter in Unity3D:
"version": 'BIN7400',
"axis_up": 'Y',
"axis_forward": '-Z',
"global_matrix": Matrix.Rotation(-math.pi / 2.0, 4, 'X'),
# 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,
"use_selection": False,
"object_types": {'ARMATURE', 'EMPTY', 'MESH', 'OTHER'},
"use_mesh_modifiers": True,
"use_mesh_edges": False,
"mesh_smooth_type": 'FACE',
"use_tspace": False, # XXX Why? Unity is expected to support tspace import...
"use_armature_deform_only": True,
"use_custom_props": True,
"bake_anim_simplify_factor": 1.0,
"bake_anim_step": 1.0,
"bake_anim_use_nla_strips": True,
"bake_anim_use_all_actions": True,
"path_mode": 'AUTO',
"embed_textures": False,
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
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
"batch_mode": 'OFF',
}
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