Newer
Older
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
# Script copyright (C) Blender Foundation
# FBX 7.1.0 -> 7.4.0 loader for Blender
Campbell Barton
committed
# Not totally pep8 compliant.
# pep8 import_fbx.py --ignore=E501,E123,E702,E125
if "bpy" in locals():
import importlib
if "parse_fbx" in locals():
importlib.reload(parse_fbx)
if "fbx_utils" in locals():
importlib.reload(fbx_utils)
Campbell Barton
committed
# -----
# Utils
from .parse_fbx import data_types, FBXElem
from .fbx_utils import (
units_convertor_iter,
array_to_matrix4,
similar_values,
similar_values_iter,
Jens Ch. Restemeier
committed
FBXImportSettings
# global singleton, assign on execution
fbx_elem_nil = None
# Units convertors...
convert_deg_to_rad_iter = units_convertor_iter("degree", "radian")
MAT_CONVERT_BONE = fbx_utils.MAT_CONVERT_BONE.inverted()
MAT_CONVERT_LAMP = fbx_utils.MAT_CONVERT_LAMP.inverted()
MAT_CONVERT_CAMERA = fbx_utils.MAT_CONVERT_CAMERA.inverted()
def elem_find_first(elem, id_search, default=None):
for fbx_item in elem.elems:
if fbx_item.id == id_search:
return fbx_item
return default
def elem_find_iter(elem, id_search):
for fbx_item in elem.elems:
if fbx_item.id == id_search:
yield fbx_item
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
def elem_find_first_string(elem, id_search):
fbx_item = elem_find_first(elem, id_search)
if fbx_item is not None:
assert(len(fbx_item.props) == 1)
assert(fbx_item.props_type[0] == data_types.STRING)
return fbx_item.props[0].decode('utf-8')
return None
def elem_find_first_bytes(elem, id_search, decode=True):
fbx_item = elem_find_first(elem, id_search)
if fbx_item is not None:
assert(len(fbx_item.props) == 1)
assert(fbx_item.props_type[0] == data_types.STRING)
return fbx_item.props[0]
return None
def elem_repr(elem):
return "%s: props[%d=%r], elems=(%r)" % (
elem.id,
len(elem.props),
", ".join([repr(p) for p in elem.props]),
# elem.props_type,
b", ".join([e.id for e in elem.elems]),
)
def elem_split_name_class(elem):
assert(elem.props_type[-2] == data_types.STRING)
elem_name, elem_class = elem.props[-2].split(b'\x00\x01')
return elem_name, elem_class
def elem_name_ensure_class(elem, clss=...):
elem_name, elem_class = elem_split_name_class(elem)
if clss is not ...:
assert(elem_class == clss)
return elem_name.decode('utf-8')
def elem_split_name_class_nodeattr(elem):
assert(elem.props_type[-2] == data_types.STRING)
elem_name, elem_class = elem.props[-2].split(b'\x00\x01')
assert(elem_class == b'NodeAttribute')
assert(elem.props_type[-1] == data_types.STRING)
elem_class = elem.props[-1]
return elem_name, elem_class
def elem_uuid(elem):
assert(elem.props_type[0] == data_types.INT64)
return elem.props[0]
def elem_prop_first(elem, default=None):
return elem.props[0] if (elem is not None) and elem.props else default
# ----
# Support for
# Properties70: { ... P:
def elem_props_find_first(elem, elem_prop_id):
# support for templates (tuple of elems)
if type(elem) is not FBXElem:
assert(type(elem) is tuple)
for e in elem:
result = elem_props_find_first(e, elem_prop_id)
if result is not None:
return result
assert(len(elem) > 0)
return None
for subelem in elem.elems:
assert(subelem.id == b'P')
if subelem.props[0] == elem_prop_id:
return subelem
return None
def elem_props_get_color_rgb(elem, elem_prop_id, default=None):
elem_prop = elem_props_find_first(elem, elem_prop_id)
if elem_prop is not None:
assert(elem_prop.props[0] == elem_prop_id)
if elem_prop.props[1] == b'Color':
# FBX version 7300
assert(elem_prop.props[1] == b'Color')
assert(elem_prop.props[2] == b'')
assert(elem_prop.props[3] in {b'A', b'A+', b'AU'})
else:
assert(elem_prop.props[1] == b'ColorRGB')
assert(elem_prop.props[2] == b'Color')
assert(elem_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
return elem_prop.props[4:7]
return default
def elem_props_get_vector_3d(elem, elem_prop_id, default=None):
elem_prop = elem_props_find_first(elem, elem_prop_id)
if elem_prop is not None:
assert(elem_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
return elem_prop.props[4:7]
return default
def elem_props_get_number(elem, elem_prop_id, default=None):
elem_prop = elem_props_find_first(elem, elem_prop_id)
if elem_prop is not None:
assert(elem_prop.props[0] == elem_prop_id)
if elem_prop.props[1] == b'double':
assert(elem_prop.props[1] == b'double')
assert(elem_prop.props[2] == b'Number')
else:
assert(elem_prop.props[1] == b'Number')
assert(elem_prop.props[2] == b'')
assert(elem_prop.props[3] in {b'A', b'A+', b'AU'})
# we could allow other number types
assert(elem_prop.props_type[4] == data_types.FLOAT64)
return elem_prop.props[4]
return default
def elem_props_get_integer(elem, elem_prop_id, default=None):
elem_prop = elem_props_find_first(elem, elem_prop_id)
if elem_prop is not None:
assert(elem_prop.props[0] == elem_prop_id)
if elem_prop.props[1] == b'int':
assert(elem_prop.props[1] == b'int')
assert(elem_prop.props[2] == b'Integer')
elif elem_prop.props[1] == b'ULongLong':
assert(elem_prop.props[1] == b'ULongLong')
assert(elem_prop.props[2] == b'')
# we could allow other number types
assert(elem_prop.props_type[4] in {data_types.INT32, data_types.INT64})
return elem_prop.props[4]
return default
Campbell Barton
committed
def elem_props_get_bool(elem, elem_prop_id, default=None):
elem_prop = elem_props_find_first(elem, elem_prop_id)
if elem_prop is not None:
assert(elem_prop.props[0] == elem_prop_id)
assert(elem_prop.props[1] == b'bool')
assert(elem_prop.props[2] == b'')
assert(elem_prop.props[3] == b'')
# we could allow other number types
assert(elem_prop.props_type[4] == data_types.INT32)
assert(elem_prop.props[4] in {0, 1})
Campbell Barton
committed
return bool(elem_prop.props[4])
return default
def elem_props_get_enum(elem, elem_prop_id, default=None):
elem_prop = elem_props_find_first(elem, elem_prop_id)
if elem_prop is not None:
assert(elem_prop.props[0] == elem_prop_id)
assert(elem_prop.props[1] == b'enum')
assert(elem_prop.props[2] == b'')
assert(elem_prop.props[3] == b'')
# we could allow other number types
assert(elem_prop.props_type[4] == data_types.INT32)
return elem_prop.props[4]
return default
Bastien Montagne
committed
def elem_props_get_visibility(elem, elem_prop_id, default=None):
elem_prop = elem_props_find_first(elem, elem_prop_id)
if elem_prop is not None:
assert(elem_prop.props[0] == elem_prop_id)
assert(elem_prop.props[1] == b'Visibility')
assert(elem_prop.props[2] == b'')
assert(elem_prop.props[3] in {b'A', b'A+', b'AU'})
# we could allow other number types
assert(elem_prop.props_type[4] == data_types.FLOAT64)
return elem_prop.props[4]
return default
# ----------------------------------------------------------------------------
# Blender
# ------
# Object
Bastien Montagne
committed
from collections import namedtuple
Bastien Montagne
committed
FBXTransformData = namedtuple("FBXTransformData", (
"loc",
"rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat",
"sca", "sca_ofs", "sca_piv",
))
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
def blen_read_custom_properties(fbx_obj, blen_obj, settings):
# There doesn't seem to be a way to put user properties into templates, so this only get the object properties:
fbx_obj_props = elem_find_first(fbx_obj, b'Properties70')
if fbx_obj_props:
for fbx_prop in fbx_obj_props.elems:
assert(fbx_prop.id == b'P')
if b'U' in fbx_prop.props[3]:
if fbx_prop.props[0] == b'UDP3DSMAX':
# Special case for 3DS Max user properties:
assert(fbx_prop.props[1] == b'KString')
assert(fbx_prop.props_type[4] == data_types.STRING)
items = fbx_prop.props[4].decode('utf-8')
for item in items.split('\r\n'):
if item:
prop_name, prop_value = item.split('=', 1)
blen_obj[prop_name.strip()] = prop_value.strip()
else:
prop_name = fbx_prop.props[0].decode('utf-8')
prop_type = fbx_prop.props[1]
if prop_type in {b'Vector', b'Vector3D', b'Color', b'ColorRGB'}:
assert(fbx_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
blen_obj[prop_name] = fbx_prop.props[4:7]
elif prop_type in {b'Vector4', b'ColorRGBA'}:
assert(fbx_prop.props_type[4:8] == bytes((data_types.FLOAT64,)) * 4)
blen_obj[prop_name] = fbx_prop.props[4:8]
elif prop_type == b'Vector2D':
assert(fbx_prop.props_type[4:6] == bytes((data_types.FLOAT64,)) * 2)
blen_obj[prop_name] = fbx_prop.props[4:6]
elif prop_type in {b'Integer', b'int'}:
assert(fbx_prop.props_type[4] == data_types.INT32)
blen_obj[prop_name] = fbx_prop.props[4]
elif prop_type == b'KString':
assert(fbx_prop.props_type[4] == data_types.STRING)
blen_obj[prop_name] = fbx_prop.props[4].decode('utf-8')
elif prop_type in {b'Number', b'double', b'Double'}:
assert(fbx_prop.props_type[4] == data_types.FLOAT64)
blen_obj[prop_name] = fbx_prop.props[4]
elif prop_type in {b'Float', b'float'}:
assert(fbx_prop.props_type[4] == data_types.FLOAT32)
blen_obj[prop_name] = fbx_prop.props[4]
elif prop_type in {b'Bool', b'bool'}:
assert(fbx_prop.props_type[4] == data_types.INT32)
blen_obj[prop_name] = fbx_prop.props[4] != 0
elif prop_type in {b'Enum', b'enum'}:
assert(fbx_prop.props_type[4:6] == bytes((data_types.INT32, data_types.STRING)))
val = fbx_prop.props[4]
if settings.use_custom_props_enum_as_string:
enum_items = fbx_prop.props[5].decode('utf-8').split('~')
assert(val >= 0 and val < len(enum_items))
blen_obj[prop_name] = enum_items[val]
else:
blen_obj[prop_name] = val
else:
print ("WARNING: User property type '%s' is not supported" % prop_type.decode('utf-8'))
Bastien Montagne
committed
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
def blen_read_object_transform_do(transform_data):
# translation
lcl_translation = Matrix.Translation(transform_data.loc)
# rotation
to_rot = lambda rot, rot_ord: Euler(convert_deg_to_rad_iter(rot), rot_ord).to_matrix().to_4x4()
lcl_rot = to_rot(transform_data.rot, transform_data.rot_ord) * transform_data.rot_alt_mat
pre_rot = to_rot(transform_data.pre_rot, transform_data.rot_ord)
pst_rot = to_rot(transform_data.pst_rot, transform_data.rot_ord)
rot_ofs = Matrix.Translation(transform_data.rot_ofs)
rot_piv = Matrix.Translation(transform_data.rot_piv)
sca_ofs = Matrix.Translation(transform_data.sca_ofs)
sca_piv = Matrix.Translation(transform_data.sca_piv)
# scale
lcl_scale = Matrix()
lcl_scale[0][0], lcl_scale[1][1], lcl_scale[2][2] = transform_data.sca
return (
lcl_translation *
rot_ofs *
rot_piv *
pre_rot *
lcl_rot *
pst_rot *
rot_piv.inverted_safe() *
Bastien Montagne
committed
sca_ofs *
sca_piv *
lcl_scale *
sca_piv.inverted_safe()
Bastien Montagne
committed
)
Bastien Montagne
committed
# XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become
# more likely, will have to make this more robust!!!
def add_vgroup_to_objects(vg_indices, vg_weights, vg_name, objects):
assert(len(vg_indices) == len(vg_weights))
if vg_indices:
for obj in objects:
# We replace/override here...
vg = obj.vertex_groups.get(vg_name)
if vg is None:
vg = obj.vertex_groups.new(vg_name)
for i, w in zip(vg_indices, vg_weights):
vg.add((i,), w, 'REPLACE')
Bastien Montagne
committed
def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_prepost_rot):
# This is quite involved, 'fbxRNode.cpp' from openscenegraph used as a reference
Bastien Montagne
committed
const_vector_zero_3d = 0.0, 0.0, 0.0
const_vector_one_3d = 1.0, 1.0, 1.0
Bastien Montagne
committed
loc = list(elem_props_get_vector_3d(fbx_props, b'Lcl Translation', const_vector_zero_3d))
rot = list(elem_props_get_vector_3d(fbx_props, b'Lcl Rotation', const_vector_zero_3d))
sca = list(elem_props_get_vector_3d(fbx_props, b'Lcl Scaling', const_vector_one_3d))
rot_ofs = elem_props_get_vector_3d(fbx_props, b'RotationOffset', const_vector_zero_3d)
rot_piv = elem_props_get_vector_3d(fbx_props, b'RotationPivot', const_vector_zero_3d)
sca_ofs = elem_props_get_vector_3d(fbx_props, b'ScalingOffset', const_vector_zero_3d)
sca_piv = elem_props_get_vector_3d(fbx_props, b'ScalingPivot', const_vector_zero_3d)
is_rot_act = elem_props_get_bool(fbx_props, b'RotationActive', False)
if is_rot_act:
Bastien Montagne
committed
if use_prepost_rot:
pre_rot = elem_props_get_vector_3d(fbx_props, b'PreRotation', const_vector_zero_3d)
pst_rot = elem_props_get_vector_3d(fbx_props, b'PostRotation', const_vector_zero_3d)
else:
pre_rot = const_vector_zero_3d
pst_rot = const_vector_zero_3d
rot_ord = {
0: 'XYZ',
1: 'XYZ',
2: 'XZY',
3: 'YZX',
4: 'YXZ',
5: 'ZXY',
6: 'ZYX',
}.get(elem_props_get_enum(fbx_props, b'RotationOrder', 0))
else:
pre_rot = const_vector_zero_3d
pst_rot = const_vector_zero_3d
rot_ord = 'XYZ'
Bastien Montagne
committed
return FBXTransformData(loc,
rot, rot_ofs, rot_piv, pre_rot, pst_rot, rot_ord, rot_alt_mat,
sca, sca_ofs, sca_piv)
# ---------
# Animation
def blen_read_animations_curves_iter(fbx_curves, blen_start_offset, fbx_start_offset, fps):
"""
Get raw FBX AnimCurve list, and yield values for all curves at each singular curves' keyframes,
together with (blender) timing, in frames.
blen_start_offset is expected in frames, while fbx_start_offset is expected in FBX ktime.
"""
# As a first step, assume linear interpolation between key frames, we'll (try to!) handle more
# of FBX curves later.
from .fbx_utils import FBX_KTIME
timefac = fps / FBX_KTIME
curves = tuple([0,
elem_prop_first(elem_find_first(c[2], b'KeyTime')),
elem_prop_first(elem_find_first(c[2], b'KeyValueFloat')),
c]
allkeys = sorted({item for sublist in curves for item in sublist[1]})
for curr_fbxktime in allkeys:
curr_values = []
for item in curves:
idx, times, values, fbx_curve = item
if times[idx] < curr_fbxktime:
if idx >= 0:
idx += 1
if idx >= len(times):
# We have reached our last element for this curve, stay on it from now on...
idx = -1
item[0] = idx
if times[idx] >= curr_fbxktime:
if idx == 0:
curr_values.append((values[idx], fbx_curve))
else:
# Interpolate between this key and the previous one.
ifac = (curr_fbxktime - times[idx - 1]) / (times[idx] - times[idx - 1])
curr_values.append(((values[idx] - values[idx - 1]) * ifac + values[idx - 1], fbx_curve))
curr_blenkframe = (curr_fbxktime - fbx_start_offset) * timefac + blen_start_offset
yield (curr_blenkframe, curr_values)
Bastien Montagne
committed
def blen_read_animations_action_item(action, item, cnodes, fps):
'Bake' loc/rot/scale into the action,
taking any pre_ and post_ matrix into account to transform from fbx into blender space.
"""
from bpy.types import Object, PoseBone, ShapeKey
from itertools import chain
fbx_curves = []
Bastien Montagne
committed
for curves, fbxprop in cnodes.values():
for (fbx_acdata, _blen_data), channel in curves.values():
fbx_curves.append((fbxprop, channel, fbx_acdata))
# Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
if len(fbx_curves) == 0:
return
blen_curves = []
props = []
if isinstance(item, ShapeKey):
props = [(item.path_from_id("value"), 1, "Key")]
else: # Object or PoseBone:
Bastien Montagne
committed
if item.is_bone:
bl_obj = item.bl_obj.pose.bones[item.bl_bone]
else:
bl_obj = item.bl_obj
# We want to create actions for objects, but for bones we 'reuse' armatures' actions!
Bastien Montagne
committed
grpname = item.bl_obj.name
# Since we might get other channels animated in the end, due to all FBX transform magic,
# we need to add curves for whole loc/rot/scale in any case.
Bastien Montagne
committed
props = [(bl_obj.path_from_id("location"), 3, grpname or "Location"),
Bastien Montagne
committed
(bl_obj.path_from_id("scale"), 3, grpname or "Scale")]
rot_mode = bl_obj.rotation_mode
if rot_mode == 'QUATERNION':
Bastien Montagne
committed
props[1] = (bl_obj.path_from_id("rotation_quaternion"), 4, grpname or "Quaternion Rotation")
elif rot_mode == 'AXIS_ANGLE':
Bastien Montagne
committed
props[1] = (bl_obj.path_from_id("rotation_axis_angle"), 4, grpname or "Axis Angle Rotation")
Bastien Montagne
committed
props[1] = (bl_obj.path_from_id("rotation_euler"), 3, grpname or "Euler Rotation")
blen_curves = [action.fcurves.new(prop, channel, grpname)
for prop, nbr_channels, grpname in props for channel in range(nbr_channels)]
if isinstance(item, ShapeKey):
# We assume for now blen init point is frame 1.0, while FBX ktime init point is 0.
for frame, values in blen_read_animations_curves_iter(fbx_curves, 1.0, 0, fps):
value = 0.0
for v, (fbxprop, channel, _fbx_acdata) in values:
assert(fbxprop == b'DeformPercent')
assert(channel == 0)
value = v / 100.0
for fc, v in zip(blen_curves, (value,)):
fc.keyframe_points.insert(frame, v, {'NEEDED', 'FAST'}).interpolation = 'LINEAR'
else: # Object or PoseBone:
Bastien Montagne
committed
if item.is_bone:
bl_obj = item.bl_obj.pose.bones[item.bl_bone]
else:
bl_obj = item.bl_obj
transform_data = item.fbx_transform_data
rot_prev = bl_obj.rotation_euler.copy()
# Pre-compute inverted local rest matrix of the bone, if relevant.
restmat_inv = item.get_bind_matrix().inverted_safe() if item.is_bone else None
# We assume for now blen init point is frame 1.0, while FBX ktime init point is 0.
for frame, values in blen_read_animations_curves_iter(fbx_curves, 1.0, 0, fps):
for v, (fbxprop, channel, _fbx_acdata) in values:
if fbxprop == b'Lcl Translation':
transform_data.loc[channel] = v
elif fbxprop == b'Lcl Rotation':
transform_data.rot[channel] = v
elif fbxprop == b'Lcl Scaling':
transform_data.sca[channel] = v
mat = blen_read_object_transform_do(transform_data)
Bastien Montagne
committed
# compensate for changes in the local matrix during processing
if item.anim_compensation_matrix:
mat = mat * item.anim_compensation_matrix
# apply pre- and post matrix
# post-matrix will contain any correction for lights, camera and bone orientation
# pre-matrix will contain any correction for a parent's correction matrix or the global matrix
if item.pre_matrix:
mat = item.pre_matrix * mat
if item.post_matrix:
mat = mat * item.post_matrix
# And now, remove that rest pose matrix from current mat (also in parent space).
if restmat_inv:
# Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
loc, rot, sca = mat.decompose()
if rot_mode == 'QUATERNION':
pass # nothing to do!
elif rot_mode == 'AXIS_ANGLE':
vec, ang = rot.to_axis_angle()
rot = ang, vec.x, vec.y, vec.z
else: # Euler
rot = rot.to_euler(rot_mode, rot_prev)
rot_prev = rot
for fc, value in zip(blen_curves, chain(loc, rot, sca)):
fc.keyframe_points.insert(frame, value, {'NEEDED', 'FAST'}).interpolation = 'LINEAR'
# Since we inserted our keyframes in 'FAST' mode, we have to update the fcurves now.
for fc in blen_curves:
fc.update()
Bastien Montagne
committed
def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene):
"""
Recreate an action per stack/layer/object combinations.
Bastien Montagne
committed
Only the first found action is linked to objects, more complex setups are not handled,
it's up to user to reproduce them!
Bastien Montagne
committed
from bpy.types import ShapeKey
actions = {}
for as_uuid, ((fbx_asdata, _blen_data), alayers) in stacks.items():
stack_name = elem_name_ensure_class(fbx_asdata, b'AnimStack')
for al_uuid, ((fbx_aldata, _blen_data), items) in alayers.items():
layer_name = elem_name_ensure_class(fbx_aldata, b'AnimLayer')
for item, cnodes in items.items():
Bastien Montagne
committed
if isinstance(item, ShapeKey):
id_data = item.id_data
else:
id_data = item.bl_obj
if id_data is None:
continue
Bastien Montagne
committed
# Create new action if needed (should always be needed!
key = (as_uuid, al_uuid, id_data)
action = actions.get(key)
if action is None:
action_name = "|".join((id_data.name, stack_name, layer_name))
actions[key] = action = bpy.data.actions.new(action_name)
action.use_fake_user = True
Bastien Montagne
committed
# If none yet assigned, assign this action to id_data.
if not id_data.animation_data:
id_data.animation_data_create()
if not id_data.animation_data.action:
id_data.animation_data.action = action
# And actually populate the action!
Bastien Montagne
committed
blen_read_animations_action_item(action, item, cnodes, scene.render.fps)
# ----
# Mesh
def blen_read_geom_layerinfo(fbx_layer):
return (
elem_find_first_string(fbx_layer, b'Name'),
elem_find_first_bytes(fbx_layer, b'MappingInformationType'),
elem_find_first_bytes(fbx_layer, b'ReferenceInformationType'),
)
def blen_read_geom_array_setattr(generator, blen_data, blen_attr, fbx_data, stride, item_size, descr, xform):
"""Generic fbx_layer to blen_data setter, generator is expected to yield tuples (ble_idx, fbx_idx)."""
if xform is not None:
for blen_idx, fbx_idx in generator:
if fbx_idx == -1:
continue
Bastien Montagne
committed
setattr(blen_data[blen_idx], blen_attr,
xform(fbx_data[fbx_idx] if (item_size == 1) else fbx_data[fbx_idx:fbx_idx + item_size]))
else:
for blen_idx, fbx_idx in generator:
if fbx_idx == -1:
continue
Bastien Montagne
committed
setattr(blen_data[blen_idx], blen_attr,
fbx_data[fbx_idx] if (item_size == 1) else fbx_data[fbx_idx:fbx_idx + item_size])
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
# generic generators.
def blen_read_geom_array_gen_direct(fbx_data, stride):
fbx_data_len = len(fbx_data)
return zip(*(range(fbx_data_len // stride), range(0, fbx_data_len, stride)))
def blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride):
return ((bi, fi * stride) for bi, fi in enumerate(fbx_layer_index))
def blen_read_geom_array_gen_direct_looptovert(mesh, fbx_data, stride):
fbx_data_len = len(fbx_data) // stride
loops = mesh.loops
for p in mesh.polygons:
for lidx in p.loop_indices:
vidx = loops[lidx].vertex_index
if vidx < fbx_data_len:
yield lidx, vidx * stride
# generic error printers.
def blen_read_geom_array_error_mapping(descr, fbx_layer_mapping):
print("warning layer %r mapping type unsupported: %r" % (descr, fbx_layer_mapping))
def blen_read_geom_array_error_ref(descr, fbx_layer_ref):
print("warning layer %r ref type unsupported: %r" % (descr, fbx_layer_ref))
def blen_read_geom_array_mapped_vert(
mesh, blen_data, blen_attr,
fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref,
stride, item_size, descr,
if fbx_layer_mapping == b'ByVertice':
if fbx_layer_ref == b'Direct':
assert(fbx_layer_index is None)
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
blen_read_geom_array_error_ref(descr, fbx_layer_ref)
blen_read_geom_array_error_mapping(descr, fbx_layer_mapping)
def blen_read_geom_array_mapped_edge(
mesh, blen_data, blen_attr,
fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref,
stride, item_size, descr,
xform=None,
):
if fbx_layer_mapping == b'ByEdge':
if fbx_layer_ref == b'Direct':
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
blen_read_geom_array_error_ref(descr, fbx_layer_ref)
blen_read_geom_array_error_mapping(descr, fbx_layer_mapping)
return False
mesh, blen_data, blen_attr,
fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref,
stride, item_size, descr,
xform=None,
):
Bastien Montagne
committed
if fbx_layer_ref == b'IndexToDirect':
# XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
# We fallback to 'Direct' mapping in this case.
#~ assert(fbx_layer_index is not None)
if fbx_layer_index is None:
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride),
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
elif fbx_layer_ref == b'Direct':
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
blen_read_geom_array_error_ref(descr, fbx_layer_ref)
blen_read_geom_array_error_mapping(descr, fbx_layer_mapping)
mesh, blen_data, blen_attr,
fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref,
stride, item_size, descr,
if fbx_layer_mapping == b'ByPolygonVertex':
if fbx_layer_ref == b'IndexToDirect':
assert(fbx_layer_index is not None)
blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride),
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
blen_read_geom_array_error_ref(descr, fbx_layer_ref)
elif fbx_layer_mapping == b'ByVertice':
if fbx_layer_ref == b'Direct':
assert(fbx_layer_index is None)
blen_read_geom_array_setattr(blen_read_geom_array_gen_direct_looptovert(mesh, fbx_layer_data, stride),
blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
return True
blen_read_geom_array_error_ref(descr, fbx_layer_ref)
blen_read_geom_array_error_mapping(descr, fbx_layer_mapping)
def blen_read_geom_layer_material(fbx_obj, mesh):
fbx_layer = elem_find_first(fbx_obj, b'LayerElementMaterial')
if fbx_layer is None:
return
(fbx_layer_name,
fbx_layer_mapping,
fbx_layer_ref,
) = blen_read_geom_layerinfo(fbx_layer)
if fbx_layer_mapping == b'AllSame':
# only to quiet warning
return
layer_id = b'Materials'
fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
blen_data = mesh.polygons
mesh, blen_data, "material_index",
fbx_layer_data, None,
fbx_layer_mapping, fbx_layer_ref,
def blen_read_geom_layer_uv(fbx_obj, mesh):
for layer_id in (b'LayerElementUV',):
for fbx_layer in elem_find_iter(fbx_obj, layer_id):
# all should be valid
(fbx_layer_name,
fbx_layer_mapping,
fbx_layer_ref,
) = blen_read_geom_layerinfo(fbx_layer)
fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, b'UV'))
fbx_layer_index = elem_prop_first(elem_find_first(fbx_layer, b'UVIndex'))
uv_tex = mesh.uv_textures.new(name=fbx_layer_name)
uv_lay = mesh.uv_layers[-1]
# some valid files omit this data
if fbx_layer_data is None:
print("%r %r missing data" % (layer_id, fbx_layer_name))
continue
blen_read_geom_array_mapped_polyloop(
mesh, blen_data, "uv",
fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref,
def blen_read_geom_layer_color(fbx_obj, mesh):
# almost same as UV's
for layer_id in (b'LayerElementColor',):
for fbx_layer in elem_find_iter(fbx_obj, layer_id):
# all should be valid
(fbx_layer_name,
fbx_layer_mapping,
fbx_layer_ref,
) = blen_read_geom_layerinfo(fbx_layer)
fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, b'Colors'))
fbx_layer_index = elem_prop_first(elem_find_first(fbx_layer, b'ColorIndex'))
color_lay = mesh.vertex_colors.new(name=fbx_layer_name)
blen_data = color_lay.data[:]
# some valid files omit this data
if fbx_layer_data is None:
print("%r %r missing data" % (layer_id, fbx_layer_name))
continue
# ignore alpha layer (read 4 items into 3)
blen_read_geom_array_mapped_polyloop(
mesh, blen_data, "color",
fbx_layer_data, fbx_layer_index,
fbx_layer_mapping, fbx_layer_ref,
4, 3, layer_id,
)
def blen_read_geom_layer_smooth(fbx_obj, mesh):
fbx_layer = elem_find_first(fbx_obj, b'LayerElementSmoothing')
if fbx_layer is None:
return False
# all should be valid
(fbx_layer_name,
fbx_layer_mapping,
fbx_layer_ref,
) = blen_read_geom_layerinfo(fbx_layer)
layer_id = b'Smoothing'
fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
# udk has 'Direct' mapped, with no Smoothing, not sure why, but ignore these
if fbx_layer_data is None:
return False
if fbx_layer_mapping == b'ByEdge':
# some models have bad edge data, we cant use this info...
if not mesh.edges:
return False
blen_data = mesh.edges
ok_smooth = blen_read_geom_array_mapped_edge(
mesh, blen_data, "use_edge_sharp",
fbx_layer_data, None,
fbx_layer_mapping, fbx_layer_ref,
xform=lambda s: not s,
)
return ok_smooth
elif fbx_layer_mapping == b'ByPolygon':
blen_data = mesh.polygons
return blen_read_geom_array_mapped_polygon(
mesh, blen_data, "use_smooth",
fbx_layer_data, None,
fbx_layer_mapping, fbx_layer_ref,
xform=lambda s: (s != 0), # smoothgroup bitflags, treat as booleans for now
)
else:
print("warning layer %r mapping type unsupported: %r" % (fbx_layer.id, fbx_layer_mapping))
return False
Bastien Montagne
committed
def blen_read_geom_layer_normal(fbx_obj, mesh, xform=None):
fbx_layer = elem_find_first(fbx_obj, b'LayerElementNormal')
if fbx_layer is None:
return False
(fbx_layer_name,
fbx_layer_mapping,
fbx_layer_ref,
) = blen_read_geom_layerinfo(fbx_layer)
layer_id = b'Normals'
fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
return blen_read_geom_array_mapped_vert(
fbx_layer_data, None,
fbx_layer_mapping, fbx_layer_ref,
Bastien Montagne
committed
xform
Jens Ch. Restemeier
committed
def blen_read_geom(fbx_tmpl, fbx_obj, settings):
Bastien Montagne
committed
from itertools import chain
import array
# Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
# global matrix, so we need to apply the global matrix to the vertices to get the correct result.
geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
# We need to apply the inverse transpose of the global matrix when transforming normals.
geom_mat_no = Matrix(settings.global_matrix_inv_transposed) if settings.bake_space_transform else None
if geom_mat_no is not None:
# Remove translation & scaling!
geom_mat_no.translation = Vector()
geom_mat_no.normalize()
# TODO, use 'fbx_tmpl'
elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Geometry')
fbx_verts = elem_prop_first(elem_find_first(fbx_obj, b'Vertices'))
fbx_polys = elem_prop_first(elem_find_first(fbx_obj, b'PolygonVertexIndex'))
fbx_edges = elem_prop_first(elem_find_first(fbx_obj, b'Edges'))
Bastien Montagne
committed
if geom_mat_co is not None:
def _vcos_transformed_gen(raw_cos, m=None):
# Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
return chain(*(m * Vector(v) for v in zip(*(iter(raw_cos),) * 3)))
fbx_verts = array.array(fbx_verts.typecode, _vcos_transformed_gen(fbx_verts, geom_mat_co))
if fbx_verts is None:
fbx_verts = ()
if fbx_polys is None:
fbx_polys = ()
mesh = bpy.data.meshes.new(name=elem_name_utf8)
mesh.vertices.add(len(fbx_verts) // 3)
mesh.vertices.foreach_set("co", fbx_verts)
if fbx_polys:
mesh.loops.add(len(fbx_polys))
poly_loop_starts = []
poly_loop_totals = []
poly_loop_prev = 0
for i, l in enumerate(mesh.loops):
index = fbx_polys[i]
if index < 0:
poly_loop_starts.append(poly_loop_prev)
poly_loop_totals.append((i - poly_loop_prev) + 1)
poly_loop_prev = i + 1
l.vertex_index = index
mesh.polygons.add(len(poly_loop_starts))
mesh.polygons.foreach_set("loop_start", poly_loop_starts)
mesh.polygons.foreach_set("loop_total", poly_loop_totals)
blen_read_geom_layer_material(fbx_obj, mesh)
blen_read_geom_layer_uv(fbx_obj, mesh)
blen_read_geom_layer_color(fbx_obj, mesh)
# edges in fact index the polygons (NOT the vertices)
import array
tot_edges = len(fbx_edges)
edges_conv = array.array('i', [0]) * (tot_edges * 2)
edge_index = 0
for i in fbx_edges:
e_a = fbx_polys[i]
if e_a >= 0:
e_b = fbx_polys[i + 1]