Newer
Older
return obj
def link_skeleton_children(self, fbx_tmpl, settings, scene):
if self.is_bone:
for child in self.children:
if child.ignore:
continue
child_obj = child.bl_obj
Bastien Montagne
committed
if child_obj and child_obj != self.bl_obj:
Bastien Montagne
committed
child_obj.parent = self.bl_obj # get the armature the bone belongs to
child_obj.parent_bone = self.bl_bone
child_obj.parent_type = 'BONE'
child_obj.matrix_parent_inverse = Matrix()
Bastien Montagne
committed
# Blender attaches to the end of a bone, while FBX attaches to the start.
# bone_child_matrix corrects for that.
Bastien Montagne
committed
if child.pre_matrix:
child.pre_matrix = self.bone_child_matrix @ child.pre_matrix
Bastien Montagne
committed
else:
child.pre_matrix = self.bone_child_matrix
child_obj.matrix_basis = child.get_matrix()
child.link_skeleton_children(fbx_tmpl, settings, scene)
Bastien Montagne
committed
else:
obj = self.bl_obj
Bastien Montagne
committed
for child in self.children:
if child.ignore:
continue
child_obj = child.link_skeleton_children(fbx_tmpl, settings, scene)
Bastien Montagne
committed
if child_obj:
child_obj.parent = obj
return obj
def set_pose_matrix(self, arm):
pose_bone = arm.bl_obj.pose.bones[self.bl_bone]
pose_bone.matrix_basis = self.get_bind_matrix().inverted_safe() @ self.get_matrix()
Bastien Montagne
committed
for child in self.children:
if child.ignore:
continue
if child.is_bone:
child.set_pose_matrix(arm)
def merge_weights(self, combined_weights, fbx_cluster):
indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=())
weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=())
for index, weight in zip(indices, weights):
w = combined_weights.get(index)
if w is None:
combined_weights[index] = [weight]
else:
w.append(weight)
def set_bone_weights(self):
ignored_children = tuple(child for child in self.children
if child.is_bone and child.ignore and len(child.clusters) > 0)
Bastien Montagne
committed
if len(ignored_children) > 0:
# If we have an ignored child bone we need to merge their weights into the current bone weights.
# This can happen both intentionally and accidentally when skinning a model. Either way, they
# need to be moved into a parent bone or they cause animation glitches.
Bastien Montagne
committed
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
2099
2100
2101
2102
for fbx_cluster, meshes in self.clusters:
combined_weights = {}
self.merge_weights(combined_weights, fbx_cluster)
for child in ignored_children:
for child_cluster, child_meshes in child.clusters:
if not meshes.isdisjoint(child_meshes):
self.merge_weights(combined_weights, child_cluster)
# combine child weights
indices = []
weights = []
for i, w in combined_weights.items():
indices.append(i)
if len(w) > 1:
weights.append(sum(w) / len(w))
else:
weights.append(w[0])
add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes])
# clusters that drive meshes not included in a parent don't need to be merged
all_meshes = set().union(*[meshes for _, meshes in self.clusters])
for child in ignored_children:
for child_cluster, child_meshes in child.clusters:
if all_meshes.isdisjoint(child_meshes):
indices = elem_prop_first(elem_find_first(child_cluster, b'Indexes', default=None), default=())
weights = elem_prop_first(elem_find_first(child_cluster, b'Weights', default=None), default=())
add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in child_meshes])
else:
# set the vertex weights on meshes
for fbx_cluster, meshes in self.clusters:
indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=())
weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=())
add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes])
for child in self.children:
Bastien Montagne
committed
child.set_bone_weights()
def build_hierarchy(self, fbx_tmpl, settings, scene, view_layer):
Bastien Montagne
committed
if self.is_armature:
# create when linking since we need object data
elem_name_utf8 = self.fbx_name
self.bl_data = arm_data = bpy.data.armatures.new(name=elem_name_utf8)
# Object data must be created already
self.bl_obj = arm = bpy.data.objects.new(name=elem_name_utf8, object_data=arm_data)
arm.matrix_basis = self.get_matrix()
if self.fbx_elem:
fbx_props = (elem_find_first(self.fbx_elem, b'Properties70'),
elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
if settings.use_custom_props:
blen_read_custom_properties(self.fbx_elem, arm, settings)
Bastien Montagne
committed
# instance in scene
view_layer.active_layer_collection.collection.objects.link(arm)
Bastien Montagne
committed
# Add bones:
# Switch to Edit mode.
is_hidden = arm.hide_viewport
arm.hide_viewport = False # Can't switch to Edit mode hidden objects...
Bastien Montagne
committed
bpy.ops.object.mode_set(mode='EDIT')
for child in self.children:
if child.ignore:
continue
if child.is_bone:
Bastien Montagne
committed
child.build_skeleton(self, Matrix(), force_connect_children=settings.force_connect_children)
Bastien Montagne
committed
bpy.ops.object.mode_set(mode='OBJECT')
arm.hide_viewport = is_hidden
Bastien Montagne
committed
# Set pose matrix
for child in self.children:
if child.ignore:
continue
if child.is_bone:
child.set_pose_matrix(self)
# Add bone children:
for child in self.children:
if child.ignore:
continue
child_obj = child.build_skeleton_children(fbx_tmpl, settings, scene, view_layer)
return arm
Bastien Montagne
committed
elif self.fbx_elem and not self.is_bone:
obj = self.build_node_obj(fbx_tmpl, settings)
# walk through children
for child in self.children:
child.build_hierarchy(fbx_tmpl, settings, scene, view_layer)
# instance in scene
view_layer.active_layer_collection.collection.objects.link(obj)
obj.select_set(True)
return obj
else:
for child in self.children:
child.build_hierarchy(fbx_tmpl, settings, scene, view_layer)
return None
def link_hierarchy(self, fbx_tmpl, settings, scene):
if self.is_armature:
arm = self.bl_obj
# Link bone children:
for child in self.children:
if child.ignore:
continue
child_obj = child.link_skeleton_children(fbx_tmpl, settings, scene)
Bastien Montagne
committed
if child_obj:
child_obj.parent = arm
# Add armature modifiers to the meshes
if self.meshes:
for mesh in self.meshes:
(mmat, amat) = mesh.armature_setup[self]
me_obj = mesh.bl_obj
Bastien Montagne
committed
# bring global armature & mesh matrices into *Blender* global space.
# Note: Usage of matrix_geom (local 'diff' transform) here is quite brittle.
# Among other things, why in hell isn't it taken into account by bindpose & co???
# Probably because org app (max) handles it completely aside from any parenting stuff,
# which we obviously cannot do in Blender. :/
Bastien Montagne
committed
amat = self.bind_matrix
amat = settings.global_matrix @ (Matrix() if amat is None else amat)
if self.matrix_geom:
amat = amat @ self.matrix_geom
mmat = settings.global_matrix @ mmat
if mesh.matrix_geom:
Bastien Montagne
committed
# Now that we have armature and mesh in there (global) bind 'state' (matrix),
# we can compute inverse parenting matrix of the mesh.
me_obj.matrix_parent_inverse = amat.inverted_safe() @ mmat @ me_obj.matrix_basis.inverted_safe()
Bastien Montagne
committed
mod = mesh.bl_obj.modifiers.new(arm.name, 'ARMATURE')
Bastien Montagne
committed
mod.object = arm
# Add bone weights to the deformers
for child in self.children:
if child.ignore:
continue
if child.is_bone:
child.set_bone_weights()
return arm
elif self.bl_obj:
obj = self.bl_obj
Bastien Montagne
committed
# walk through children
for child in self.children:
child_obj = child.link_hierarchy(fbx_tmpl, settings, scene)
Bastien Montagne
committed
if child_obj:
child_obj.parent = obj
Bastien Montagne
committed
return obj
else:
for child in self.children:
child.link_hierarchy(fbx_tmpl, settings, scene)
return None
Bastien Montagne
committed
Campbell Barton
committed
def is_ascii(filepath, size):
with open(filepath, 'r', encoding="utf-8") as f:
try:
f.read(size)
return True
except UnicodeDecodeError:
pass
return False
def load(operator, context, filepath="",
use_manual_orientation=False,
axis_forward='-Z',
axis_up='Y',
global_scale=1.0,
Bastien Montagne
committed
bake_space_transform=False,
use_custom_normals=True,
Campbell Barton
committed
use_image_search=False,
use_alpha_decals=False,
use_anim=True,
Bastien Montagne
committed
anim_offset=1.0,
use_custom_props=True,
Bastien Montagne
committed
use_custom_props_enum_as_string=True,
ignore_leaf_bones=False,
Bastien Montagne
committed
force_connect_children=False,
Bastien Montagne
committed
automatic_bone_orientation=False,
primary_bone_axis='Y',
Bastien Montagne
committed
secondary_bone_axis='X',
use_prepost_rot=True):
global fbx_elem_nil
fbx_elem_nil = FBXElem('', (), (), ())
from bpy_extras.io_utils import axis_conversion
from . import parse_fbx
from .fbx_utils import RIGHT_HAND_AXES, FBX_FRAMERATES
Bastien Montagne
committed
start_time_proc = time.process_time()
start_time_sys = time.time()
Bastien Montagne
committed
perfmon = PerfMon()
perfmon.level_up()
perfmon.step("FBX Import: start importing %s" % filepath)
perfmon.level_up()
Campbell Barton
committed
# detect ascii files
if is_ascii(filepath, 24):
operator.report({'ERROR'}, "ASCII FBX files are not supported %r" % filepath)
return {'CANCELLED'}
try:
elem_root, version = parse_fbx.parse(filepath)
Bastien Montagne
committed
except Exception as e:
import traceback
traceback.print_exc()
Bastien Montagne
committed
operator.report({'ERROR'}, "Couldn't open file %r (%s)" % (filepath, e))
return {'CANCELLED'}
if version < 7100:
operator.report({'ERROR'}, "Version %r unsupported, must be %r or later" % (version, 7100))
return {'CANCELLED'}
Bastien Montagne
committed
print("FBX version: %r" % version)
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
# deselect all
if bpy.ops.object.select_all.poll():
bpy.ops.object.select_all(action='DESELECT')
basedir = os.path.dirname(filepath)
nodal_material_wrap_map = {}
image_cache = {}
# Tables: (FBX_byte_id -> [FBX_data, None or Blender_datablock])
fbx_table_nodes = {}
Campbell Barton
committed
if use_alpha_decals:
material_decals = set()
else:
material_decals = None
scene = context.scene
# #### Get some info from GlobalSettings.
Bastien Montagne
committed
perfmon.step("FBX import: Prepare...")
fbx_settings = elem_find_first(elem_root, b'GlobalSettings')
fbx_settings_props = elem_find_first(fbx_settings, b'Properties70')
if fbx_settings is None or fbx_settings_props is None:
operator.report({'ERROR'}, "No 'GlobalSettings' found in file %r" % filepath)
return {'CANCELLED'}
Bastien Montagne
committed
# FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
unit_scale = elem_props_get_number(fbx_settings_props, b'UnitScaleFactor', 1.0)
unit_scale_org = elem_props_get_number(fbx_settings_props, b'OriginalUnitScaleFactor', 1.0)
global_scale *= (unit_scale / units_blender_to_fbx_factor(context.scene))
# Compute global matrix and scale.
if not use_manual_orientation:
axis_forward = (elem_props_get_integer(fbx_settings_props, b'FrontAxis', 1),
elem_props_get_integer(fbx_settings_props, b'FrontAxisSign', 1))
axis_up = (elem_props_get_integer(fbx_settings_props, b'UpAxis', 2),
elem_props_get_integer(fbx_settings_props, b'UpAxisSign', 1))
axis_coord = (elem_props_get_integer(fbx_settings_props, b'CoordAxis', 0),
elem_props_get_integer(fbx_settings_props, b'CoordAxisSign', 1))
axis_key = (axis_up, axis_forward, axis_coord)
axis_up, axis_forward = {v: k for k, v in RIGHT_HAND_AXES.items()}.get(axis_key, ('Z', 'Y'))
global_matrix = (Matrix.Scale(global_scale, 4) @
axis_conversion(from_forward=axis_forward, from_up=axis_up).to_4x4())
Bastien Montagne
committed
# To cancel out unwanted rotation/scale on nodes.
global_matrix_inv = global_matrix.inverted()
# For transforming mesh normals.
global_matrix_inv_transposed = global_matrix_inv.transposed()
# Compute bone correction matrix
bone_correction_matrix = None # None means no correction/identity
if not automatic_bone_orientation:
if (primary_bone_axis, secondary_bone_axis) != ('Y', 'X'):
bone_correction_matrix = axis_conversion(from_forward='X',
from_up='Y',
to_forward=secondary_bone_axis,
to_up=primary_bone_axis,
).to_4x4()
# Compute framerate settings.
custom_fps = elem_props_get_number(fbx_settings_props, b'CustomFrameRate', 25.0)
time_mode = elem_props_get_enum(fbx_settings_props, b'TimeMode')
real_fps = {eid: val for val, eid in FBX_FRAMERATES[1:]}.get(time_mode, custom_fps)
Bastien Montagne
committed
if real_fps <= 0.0:
real_fps = 25.0
scene.render.fps = round(real_fps)
scene.render.fps_base = scene.render.fps / real_fps
Jens Ch. Restemeier
committed
# store global settings that need to be accessed during conversion
settings = FBXImportSettings(
operator.report, (axis_up, axis_forward), global_matrix, global_scale,
Bastien Montagne
committed
bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
use_custom_normals, use_image_search,
Jens Ch. Restemeier
committed
use_alpha_decals, decal_offset,
use_anim, anim_offset,
use_custom_props, use_custom_props_enum_as_string,
nodal_material_wrap_map, image_cache,
Bastien Montagne
committed
ignore_leaf_bones, force_connect_children, automatic_bone_orientation, bone_correction_matrix,
Bastien Montagne
committed
use_prepost_rot,
Jens Ch. Restemeier
committed
)
Bastien Montagne
committed
perfmon.step("FBX import: Templates...")
fbx_defs = elem_find_first(elem_root, b'Definitions') # can be None
fbx_nodes = elem_find_first(elem_root, b'Objects')
fbx_connections = elem_find_first(elem_root, b'Connections')
if fbx_nodes is None:
operator.report({'ERROR'}, "No 'Objects' found in file %r" % filepath)
return {'CANCELLED'}
if fbx_connections is None:
operator.report({'ERROR'}, "No 'Connections' found in file %r" % filepath)
return {'CANCELLED'}
# ----
# First load property templates
# Load 'PropertyTemplate' values.
# Key is a tuple, (ObjectType, FBXNodeType)
# eg, (b'Texture', b'KFbxFileTexture')
# (b'Geometry', b'KFbxMesh')
fbx_templates = {}
def _():
if fbx_defs is not None:
for fbx_def in fbx_defs.elems:
if fbx_def.id == b'ObjectType':
for fbx_subdef in fbx_def.elems:
if fbx_subdef.id == b'PropertyTemplate':
assert(fbx_def.props_type == b'S')
assert(fbx_subdef.props_type == b'S')
# (b'Texture', b'KFbxFileTexture') - eg.
key = fbx_def.props[0], fbx_subdef.props[0]
fbx_templates[key] = fbx_subdef
_(); del _
def fbx_template_get(key):
Bastien Montagne
committed
ret = fbx_templates.get(key, fbx_elem_nil)
Bastien Montagne
committed
if ret is fbx_elem_nil:
Bastien Montagne
committed
# Newest FBX (7.4 and above) use no more 'K' in their type names...
key = (key[0], key[1][1:])
return fbx_templates.get(key, fbx_elem_nil)
return ret
Bastien Montagne
committed
perfmon.step("FBX import: Nodes...")
# ----
# Build FBX node-table
def _():
for fbx_obj in fbx_nodes.elems:
# TODO, investigate what other items after first 3 may be
assert(fbx_obj.props_type[:3] == b'LSS')
fbx_uuid = elem_uuid(fbx_obj)
fbx_table_nodes[fbx_uuid] = [fbx_obj, None]
_(); del _
# ----
# Load in the data
# http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=
# WS73099cc142f487551fea285e1221e4f9ff8-7fda.htm,topicNumber=d0e6388
Bastien Montagne
committed
perfmon.step("FBX import: Connections...")
fbx_connection_map = {}
fbx_connection_map_reverse = {}
def _():
for fbx_link in fbx_connections.elems:
c_type = fbx_link.props[0]
if fbx_link.props_type[1:3] == b'LL':
c_src, c_dst = fbx_link.props[1:3]
fbx_connection_map.setdefault(c_src, []).append((c_dst, fbx_link))
fbx_connection_map_reverse.setdefault(c_dst, []).append((c_src, fbx_link))
_(); del _
Bastien Montagne
committed
perfmon.step("FBX import: Meshes...")
# ----
# Load mesh data
def _():
fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxMesh'))
for fbx_uuid, fbx_item in fbx_table_nodes.items():
fbx_obj, blen_data = fbx_item
if fbx_obj.id != b'Geometry':
continue
if fbx_obj.props[-1] == b'Mesh':
assert(blen_data is None)
Jens Ch. Restemeier
committed
fbx_item[1] = blen_read_geom(fbx_tmpl, fbx_obj, settings)
_(); del _
Bastien Montagne
committed
perfmon.step("FBX import: Materials & Textures...")
# ----
# Load material data
def _():
fbx_tmpl = fbx_template_get((b'Material', b'KFbxSurfacePhong'))
# b'KFbxSurfaceLambert'
for fbx_uuid, fbx_item in fbx_table_nodes.items():
fbx_obj, blen_data = fbx_item
if fbx_obj.id != b'Material':
continue
assert(blen_data is None)
Jens Ch. Restemeier
committed
fbx_item[1] = blen_read_material(fbx_tmpl, fbx_obj, settings)
_(); del _
# ----
Bastien Montagne
committed
# Load image & textures data
Bastien Montagne
committed
fbx_tmpl_tex = fbx_template_get((b'Texture', b'KFbxFileTexture'))
fbx_tmpl_img = fbx_template_get((b'Video', b'KFbxVideo'))
Bastien Montagne
committed
# Important to run all 'Video' ones first, embedded images are stored in those nodes.
# XXX Note we simplify things here, assuming both matching Video and Texture will use same file path,
# this may be a bit weak, if issue arise we'll fallback to plain connection stuff...
for fbx_uuid, fbx_item in fbx_table_nodes.items():
fbx_obj, blen_data = fbx_item
if fbx_obj.id != b'Video':
continue
fbx_item[1] = blen_read_texture_image(fbx_tmpl_img, fbx_obj, basedir, settings)
for fbx_uuid, fbx_item in fbx_table_nodes.items():
fbx_obj, blen_data = fbx_item
if fbx_obj.id != b'Texture':
continue
Bastien Montagne
committed
fbx_item[1] = blen_read_texture_image(fbx_tmpl_tex, fbx_obj, basedir, settings)
_(); del _
Bastien Montagne
committed
perfmon.step("FBX import: Cameras & Lamps...")
# ----
# Load camera data
def _():
fbx_tmpl = fbx_template_get((b'NodeAttribute', b'KFbxCamera'))
for fbx_uuid, fbx_item in fbx_table_nodes.items():
fbx_obj, blen_data = fbx_item
if fbx_obj.id != b'NodeAttribute':
continue
if fbx_obj.props[-1] == b'Camera':
assert(blen_data is None)
fbx_item[1] = blen_read_camera(fbx_tmpl, fbx_obj, global_scale)
_(); del _
# ----
# Load lamp data
def _():
fbx_tmpl = fbx_template_get((b'NodeAttribute', b'KFbxLight'))
for fbx_uuid, fbx_item in fbx_table_nodes.items():
fbx_obj, blen_data = fbx_item
if fbx_obj.id != b'NodeAttribute':
continue
if fbx_obj.props[-1] == b'Light':
assert(blen_data is None)
fbx_item[1] = blen_read_light(fbx_tmpl, fbx_obj, global_scale)
# ----
# Connections
def connection_filter_ex(fbx_uuid, fbx_id, dct):
return [(c_found[0], c_found[1], c_type)
for (c_uuid, c_type) in dct.get(fbx_uuid, ())
# 0 is used for the root node, which isnt in fbx_table_nodes
for c_found in (() if c_uuid is 0 else (fbx_table_nodes.get(c_uuid, (None, None)),))
if (fbx_id is None) or (c_found[0] and c_found[0].id == fbx_id)]
def connection_filter_forward(fbx_uuid, fbx_id):
return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map)
def connection_filter_reverse(fbx_uuid, fbx_id):
return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map_reverse)
Bastien Montagne
committed
perfmon.step("FBX import: Objects & Armatures...")
Bastien Montagne
committed
# -- temporary helper hierarchy to build armatures and objects from
# lookup from uuid to helper node. Used to build parent-child relations and later to look up animated nodes.
fbx_helper_nodes = {}
def _():
# We build an intermediate hierarchy used to:
# - Calculate and store bone orientation correction matrices. The same matrices will be reused for animation.
# - Find/insert armature nodes.
# - Filter leaf bones.
Bastien Montagne
committed
# create scene root
fbx_helper_nodes[0] = root_helper = FbxImportHelperNode(None, None, None, False)
root_helper.is_root = True
# add fbx nodes
fbx_tmpl = fbx_template_get((b'Model', b'KFbxNode'))
for a_uuid, a_item in fbx_table_nodes.items():
Bastien Montagne
committed
if fbx_obj is None or fbx_obj.id != b'Model':
continue
Bastien Montagne
committed
fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
Bastien Montagne
committed
transform_data = blen_read_object_transform_preprocess(fbx_props, fbx_obj, Matrix(), use_prepost_rot)
# Note: 'Root' "bones" are handled as (armature) objects.
# Note: See T46912 for first FBX file I ever saw with 'Limb' bones - thought those were totally deprecated.
is_bone = fbx_obj.props[2] in {b'LimbNode', b'Limb'}
Bastien Montagne
committed
fbx_helper_nodes[a_uuid] = FbxImportHelperNode(fbx_obj, bl_data, transform_data, is_bone)
# add parent-child relations and add blender data to the node
for fbx_link in fbx_connections.elems:
if fbx_link.props[0] != b'OO':
continue
if fbx_link.props_type[1:3] == b'LL':
c_src, c_dst = fbx_link.props[1:3]
parent = fbx_helper_nodes.get(c_dst)
if parent is None:
continue
Bastien Montagne
committed
child = fbx_helper_nodes.get(c_src)
if child is None:
# add blender data (meshes, lights, cameras, etc.) to a helper node
fbx_sdata, bl_data = p_item = fbx_table_nodes.get(c_src, (None, None))
if fbx_sdata is None:
continue
Bastien Montagne
committed
if fbx_sdata.id not in {b'Geometry', b'NodeAttribute'}:
continue
Bastien Montagne
committed
parent.bl_data = bl_data
else:
# set parent
child.parent = parent
Bastien Montagne
committed
# find armatures (either an empty below a bone or a new node inserted at the bone
root_helper.find_armatures()
Bastien Montagne
committed
# mark nodes that have bone children
root_helper.find_bone_children()
Bastien Montagne
committed
# mark nodes that need a bone to attach child-bones to
root_helper.find_fake_bones()
# mark leaf nodes that are only required to mark the end of their parent bone
if settings.ignore_leaf_bones:
root_helper.mark_leaf_bones()
# What a mess! Some bones have several BindPoses, some have none, clusters contain a bind pose as well,
# and you can have several clusters per bone!
Bastien Montagne
committed
# Maybe some conversion can be applied to put them all into the same frame of reference?
# get the bind pose from pose elements
for a_uuid, a_item in fbx_table_nodes.items():
Bastien Montagne
committed
if fbx_obj is None:
continue
Bastien Montagne
committed
if fbx_obj.id != b'Pose':
continue
if fbx_obj.props[2] != b'BindPose':
continue
for fbx_pose_node in fbx_obj.elems:
if fbx_pose_node.id != b'PoseNode':
continue
node_elem = elem_find_first(fbx_pose_node, b'Node')
node = elem_uuid(node_elem)
matrix_elem = elem_find_first(fbx_pose_node, b'Matrix')
matrix = array_to_matrix4(matrix_elem.props[0]) if matrix_elem else None
bone = fbx_helper_nodes.get(node)
if bone and matrix:
# Store the matrix in the helper node.
# There may be several bind pose matrices for the same node, but in tests they seem to be identical.
Bastien Montagne
committed
bone.bind_matrix = matrix # global space
# get clusters and bind pose
for helper_uuid, helper_node in fbx_helper_nodes.items():
if not helper_node.is_bone:
Bastien Montagne
committed
for cluster_uuid, cluster_link in fbx_connection_map.get(helper_uuid, ()):
if cluster_link.props[0] != b'OO':
continue
fbx_cluster, _ = fbx_table_nodes.get(cluster_uuid, (None, None))
if fbx_cluster is None or fbx_cluster.id != b'Deformer' or fbx_cluster.props[2] != b'Cluster':
continue
Bastien Montagne
committed
# Get the bind pose from the cluster:
tx_mesh_elem = elem_find_first(fbx_cluster, b'Transform', default=None)
tx_mesh = array_to_matrix4(tx_mesh_elem.props[0]) if tx_mesh_elem else Matrix()
tx_bone_elem = elem_find_first(fbx_cluster, b'TransformLink', default=None)
tx_bone = array_to_matrix4(tx_bone_elem.props[0]) if tx_bone_elem else None
Bastien Montagne
committed
tx_arm_elem = elem_find_first(fbx_cluster, b'TransformAssociateModel', default=None)
tx_arm = array_to_matrix4(tx_arm_elem.props[0]) if tx_arm_elem else None
Bastien Montagne
committed
mesh_matrix = tx_mesh
armature_matrix = tx_arm
Bastien Montagne
committed
helper_node.bind_matrix = tx_bone # overwrite the bind matrix
Bastien Montagne
committed
# Get the meshes driven by this cluster: (Shouldn't that be only one?)
meshes = set()
for skin_uuid, skin_link in fbx_connection_map.get(cluster_uuid):
if skin_link.props[0] != b'OO':
Bastien Montagne
committed
fbx_skin, _ = fbx_table_nodes.get(skin_uuid, (None, None))
if fbx_skin is None or fbx_skin.id != b'Deformer' or fbx_skin.props[2] != b'Skin':
continue
Bastien Montagne
committed
for mesh_uuid, mesh_link in fbx_connection_map.get(skin_uuid):
if mesh_link.props[0] != b'OO':
continue
fbx_mesh, _ = fbx_table_nodes.get(mesh_uuid, (None, None))
if fbx_mesh is None or fbx_mesh.id != b'Geometry' or fbx_mesh.props[2] != b'Mesh':
continue
for object_uuid, object_link in fbx_connection_map.get(mesh_uuid):
if object_link.props[0] != b'OO':
continue
mesh_node = fbx_helper_nodes[object_uuid]
if mesh_node:
# ----
# If we get a valid mesh matrix (in bone space), store armature and
# mesh global matrices, we need them to compute mesh's matrix_parent_inverse
# when actually binding them via the modifier.
# Note we assume all bones were bound with the same mesh/armature (global) matrix,
# we do not support otherwise in Blender anyway!
mesh_node.armature_setup[helper_node.armature] = (mesh_matrix, armature_matrix)
Bastien Montagne
committed
meshes.add(mesh_node)
helper_node.clusters.append((fbx_cluster, meshes))
# convert bind poses from global space into local space
root_helper.make_bind_pose_local()
# collect armature meshes
root_helper.collect_armature_meshes()
Bastien Montagne
committed
# find the correction matrices to align FBX objects with their Blender equivalent
root_helper.find_correction_matrix(settings)
Bastien Montagne
committed
# build the Object/Armature/Bone hierarchy
root_helper.build_hierarchy(fbx_tmpl, settings, scene, view_layer)
Bastien Montagne
committed
# Link the Object/Armature/Bone hierarchy
root_helper.link_hierarchy(fbx_tmpl, settings, scene)
Bastien Montagne
committed
# root_helper.print_info(0)
_(); del _
Bastien Montagne
committed
perfmon.step("FBX import: ShapeKeys...")
Bastien Montagne
committed
# We can handle shapes.
blend_shape_channels = {} # We do not need Shapes themselves, but keyblocks, for anim.
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
def _():
fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxShape'))
for s_uuid, s_item in fbx_table_nodes.items():
fbx_sdata, bl_sdata = s_item = fbx_table_nodes.get(s_uuid, (None, None))
if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape':
continue
# shape -> blendshapechannel -> blendshape -> mesh.
for bc_uuid, bc_ctype in fbx_connection_map.get(s_uuid, ()):
if bc_ctype.props[0] != b'OO':
continue
fbx_bcdata, _bl_bcdata = fbx_table_nodes.get(bc_uuid, (None, None))
if fbx_bcdata is None or fbx_bcdata.id != b'Deformer' or fbx_bcdata.props[2] != b'BlendShapeChannel':
continue
meshes = []
objects = []
for bs_uuid, bs_ctype in fbx_connection_map.get(bc_uuid, ()):
if bs_ctype.props[0] != b'OO':
continue
fbx_bsdata, _bl_bsdata = fbx_table_nodes.get(bs_uuid, (None, None))
if fbx_bsdata is None or fbx_bsdata.id != b'Deformer' or fbx_bsdata.props[2] != b'BlendShape':
continue
for m_uuid, m_ctype in fbx_connection_map.get(bs_uuid, ()):
if m_ctype.props[0] != b'OO':
continue
fbx_mdata, bl_mdata = fbx_table_nodes.get(m_uuid, (None, None))
if fbx_mdata is None or fbx_mdata.id != b'Geometry' or fbx_mdata.props[2] != b'Mesh':
continue
# Blenmeshes are assumed already created at that time!
assert(isinstance(bl_mdata, bpy.types.Mesh))
# And we have to find all objects using this mesh!
objects = []
for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()):
if o_ctype.props[0] != b'OO':
continue
Bastien Montagne
committed
node = fbx_helper_nodes[o_uuid]
if node:
objects.append(node)
meshes.append((bl_mdata, objects))
# BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
# keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
Bastien Montagne
committed
keyblocks = blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene)
blend_shape_channels[bc_uuid] = keyblocks
_(); del _
if use_anim:
perfmon.step("FBX import: Animations...")
Bastien Montagne
committed
# Animation!
def _():
fbx_tmpl_astack = fbx_template_get((b'AnimationStack', b'FbxAnimStack'))
fbx_tmpl_alayer = fbx_template_get((b'AnimationLayer', b'FbxAnimLayer'))
stacks = {}
# AnimationStacks.
for as_uuid, fbx_asitem in fbx_table_nodes.items():
fbx_asdata, _blen_data = fbx_asitem
if fbx_asdata.id != b'AnimationStack' or fbx_asdata.props[2] != b'':
stacks[as_uuid] = (fbx_asitem, {})
# AnimationLayers
# (mixing is completely ignored for now, each layer results in an independent set of actions).
def get_astacks_from_alayer(al_uuid):
for as_uuid, as_ctype in fbx_connection_map.get(al_uuid, ()):
if as_ctype.props[0] != b'OO':
fbx_asdata, _bl_asdata = fbx_table_nodes.get(as_uuid, (None, None))
if (fbx_asdata is None or fbx_asdata.id != b'AnimationStack' or
fbx_asdata.props[2] != b'' or as_uuid not in stacks):
yield as_uuid
for al_uuid, fbx_alitem in fbx_table_nodes.items():
fbx_aldata, _blen_data = fbx_alitem
if fbx_aldata.id != b'AnimationLayer' or fbx_aldata.props[2] != b'':
continue
for as_uuid in get_astacks_from_alayer(al_uuid):
_fbx_asitem, alayers = stacks[as_uuid]
alayers[al_uuid] = (fbx_alitem, {})
# AnimationCurveNodes (also the ones linked to actual animated data!).
curvenodes = {}
for acn_uuid, fbx_acnitem in fbx_table_nodes.items():
fbx_acndata, _blen_data = fbx_acnitem
if fbx_acndata.id != b'AnimationCurveNode' or fbx_acndata.props[2] != b'':
cnode = curvenodes[acn_uuid] = {}
items = []
for n_uuid, n_ctype in fbx_connection_map.get(acn_uuid, ()):
if n_ctype.props[0] != b'OP':
continue
lnk_prop = n_ctype.props[3]
if lnk_prop in {b'Lcl Translation', b'Lcl Rotation', b'Lcl Scaling'}:
# n_uuid can (????) be linked to root '0' node, instead of a mere object node... See T41712.
ob = fbx_helper_nodes.get(n_uuid, None)
if ob is None or ob.is_root:
continue
items.append((ob, lnk_prop))
elif lnk_prop == b'DeformPercent': # Shape keys.
keyblocks = blend_shape_channels.get(n_uuid, None)
if keyblocks is None:
continue
items += [(kb, lnk_prop) for kb in keyblocks]
Bastien Montagne
committed
elif lnk_prop == b'FocalLength': # Camera lens.
from bpy.types import Camera
fbx_item = fbx_table_nodes.get(n_uuid, None)
if fbx_item is None or not isinstance(fbx_item[1], Camera):
continue
cam = fbx_item[1]
items.append((cam, lnk_prop))
elif lnk_prop == b'DiffuseColor':
from bpy.types import Material
fbx_item = fbx_table_nodes.get(n_uuid, None)
if fbx_item is None or not isinstance(fbx_item[1], Material):
continue
mat = fbx_item[1]
items.append((mat, lnk_prop))
print("WARNING! Importing material's animation is not supported for Nodal materials...")
for al_uuid, al_ctype in fbx_connection_map.get(acn_uuid, ()):
if al_ctype.props[0] != b'OO':
continue
fbx_aldata, _blen_aldata = fbx_alitem = fbx_table_nodes.get(al_uuid, (None, None))
if fbx_aldata is None or fbx_aldata.id != b'AnimationLayer' or fbx_aldata.props[2] != b'':
continue
for as_uuid in get_astacks_from_alayer(al_uuid):
_fbx_alitem, anim_items = stacks[as_uuid][1][al_uuid]
assert(_fbx_alitem == fbx_alitem)
for item, item_prop in items:
# No need to keep curvenode FBX data here, contains nothing useful for us.
anim_items.setdefault(item, {})[acn_uuid] = (cnode, item_prop)
# AnimationCurves (real animation data).
for ac_uuid, fbx_acitem in fbx_table_nodes.items():
fbx_acdata, _blen_data = fbx_acitem
if fbx_acdata.id != b'AnimationCurve' or fbx_acdata.props[2] != b'':
for acn_uuid, acn_ctype in fbx_connection_map.get(ac_uuid, ()):
if acn_ctype.props[0] != b'OP':
continue
fbx_acndata, _bl_acndata = fbx_table_nodes.get(acn_uuid, (None, None))
if (fbx_acndata is None or fbx_acndata.id != b'AnimationCurveNode' or
fbx_acndata.props[2] != b'' or acn_uuid not in curvenodes):
continue
# Note this is an infamous simplification of the compound props stuff,
# seems to be standard naming but we'll probably have to be smarter to handle more exotic files?
Bastien Montagne
committed
channel = {
b'd|X': 0, b'd|Y': 1, b'd|Z': 2,
b'd|DeformPercent': 0,
b'd|FocalLength': 0
}.get(acn_ctype.props[3], None)
if channel is None:
continue
curvenodes[acn_uuid][ac_uuid] = (fbx_acitem, channel)
# And now that we have sorted all this, apply animations!
blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, settings.anim_offset)
_(); del _
Bastien Montagne
committed
perfmon.step("FBX import: Assign materials...")
def _():
# link Material's to Geometry (via Model's)
for fbx_uuid, fbx_item in fbx_table_nodes.items():
fbx_obj, blen_data = fbx_item
if fbx_obj.id != b'Geometry':
continue
mesh = fbx_table_nodes.get(fbx_uuid, (None, None))[1]
# can happen in rare cases
if mesh is None:
continue
Bastien Montagne
committed
# In Blender, we link materials to data, typically (meshes), while in FBX they are linked to objects...
# So we have to be careful not to re-add endlessly the same material to a mesh!
# This can easily happen with 'baked' dupliobjects, see T44386.
# TODO: add an option to link materials to objects in Blender instead?
done_materials = set()
Bastien Montagne
committed
for (fbx_lnk, fbx_lnk_item, fbx_lnk_type) in connection_filter_forward(fbx_uuid, b'Model'):
# link materials
fbx_lnk_uuid = elem_uuid(fbx_lnk)
Bastien Montagne
committed
for (fbx_lnk_material, material, fbx_lnk_material_type) in connection_filter_reverse(fbx_lnk_uuid, b'Material'):
if material not in done_materials:
Bastien Montagne
committed
mesh.materials.append(material)
done_materials.add(material)
Bastien Montagne
committed
# We have to validate mesh polygons' ma_idx, see T41015!
Bastien Montagne
committed
# Some FBX seem to have an extra 'default' material which is not defined in FBX file.
if mesh.validate_material_indices():
print("WARNING: mesh '%s' had invalid material indices, those were reset to first material" % mesh.name)
_(); del _
Bastien Montagne
committed
perfmon.step("FBX import: Assign textures...")
Campbell Barton
committed
material_images = {}
fbx_tmpl = fbx_template_get((b'Material', b'KFbxSurfacePhong'))
# b'KFbxSurfaceLambert'
def texture_mapping_set(fbx_obj, node_texture):
assert(fbx_obj.id == b'Texture')
fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
loc = elem_props_get_vector_3d(fbx_props, b'Translation', (0.0, 0.0, 0.0))
rot = tuple(-r for r in elem_props_get_vector_3d(fbx_props, b'Rotation', (0.0, 0.0, 0.0)))
scale = tuple(((1.0 / s) if s != 0.0 else 1.0)
for s in elem_props_get_vector_3d(fbx_props, b'Scaling', (1.0, 1.0, 1.0)))
clamp_uv = (bool(elem_props_get_enum(fbx_props, b'WrapModeU', 0)),
bool(elem_props_get_enum(fbx_props, b'WrapModeV', 0)))
if (loc == (0.0, 0.0, 0.0) and
rot == (0.0, 0.0, 0.0) and
scale == (1.0, 1.0, 1.0) and
clamp_uv == (False, False)):
return
node_texture.translation = loc
node_texture.rotation = rot
node_texture.scale = scale
# awkward conversion UV clamping to min/max
node_texture.min = (0.0, 0.0, 0.0)
node_texture.max = (1.0, 1.0, 1.0)
node_texture.use_min = node_texture.use_max = clamp_uv[0] or clamp_uv[1]
if clamp_uv[0] != clamp_uv[1]:
# use bool as index
node_texture.min[not clamp[0]] = -1e9
node_texture.max[not clamp[0]] = 1e9
for fbx_uuid, fbx_item in fbx_table_nodes.items():
fbx_obj, blen_data = fbx_item
if fbx_obj.id != b'Material':
continue
material = fbx_table_nodes.get(fbx_uuid, (None, None))[1]
for (fbx_lnk,
image,
fbx_lnk_type) in connection_filter_reverse(fbx_uuid, b'Texture'):
if fbx_lnk_type.props[0] == b'OP':
lnk_type = fbx_lnk_type.props[3]
ma_wrap = nodal_material_wrap_map[material]
if lnk_type in {b'DiffuseColor', b'3dsMax|maps|texmap_diffuse'}:
ma_wrap.base_color_texture.image = image
texture_mapping_set(fbx_lnk, ma_wrap.base_color_texture)