1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
21 # Script copyright (C) Blender Foundation
23 # FBX 7.1.0 -> 7.4.0 loader for Blender
25 # Not totally pep8 compliant.
26 # pep8 import_fbx.py --ignore=E501,E123,E702,E125
30 if "parse_fbx" in locals():
31 importlib
.reload(parse_fbx
)
32 if "fbx_utils" in locals():
33 importlib
.reload(fbx_utils
)
36 from mathutils
import Matrix
, Euler
, Vector
40 from . import parse_fbx
, fbx_utils
42 from .parse_fbx
import (
46 from .fbx_utils
import (
48 units_blender_to_fbx_factor
,
56 # global singleton, assign on execution
60 convert_deg_to_rad_iter
= units_convertor_iter("degree", "radian")
62 MAT_CONVERT_BONE
= fbx_utils
.MAT_CONVERT_BONE
.inverted()
63 MAT_CONVERT_LIGHT
= fbx_utils
.MAT_CONVERT_LIGHT
.inverted()
64 MAT_CONVERT_CAMERA
= fbx_utils
.MAT_CONVERT_CAMERA
.inverted()
67 def validate_blend_names(name
):
68 assert(type(name
) == bytes
)
69 # Blender typically does not accept names over 63 bytes...
72 h
= hashlib
.sha1(name
).hexdigest()
73 return name
[:55].decode('utf-8', 'replace') + "_" + h
[:7]
75 # We use 'replace' even though FBX 'specs' say it should always be utf8, see T53841.
76 return name
.decode('utf-8', 'replace')
79 def elem_find_first(elem
, id_search
, default
=None):
80 for fbx_item
in elem
.elems
:
81 if fbx_item
.id == id_search
:
86 def elem_find_iter(elem
, id_search
):
87 for fbx_item
in elem
.elems
:
88 if fbx_item
.id == id_search
:
92 def elem_find_first_string(elem
, id_search
):
93 fbx_item
= elem_find_first(elem
, id_search
)
94 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
95 assert(len(fbx_item
.props
) == 1)
96 assert(fbx_item
.props_type
[0] == data_types
.STRING
)
97 return fbx_item
.props
[0].decode('utf-8', 'replace')
101 def elem_find_first_string_as_bytes(elem
, id_search
):
102 fbx_item
= elem_find_first(elem
, id_search
)
103 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
104 assert(len(fbx_item
.props
) == 1)
105 assert(fbx_item
.props_type
[0] == data_types
.STRING
)
106 return fbx_item
.props
[0] # Keep it as bytes as requested...
110 def elem_find_first_bytes(elem
, id_search
, decode
=True):
111 fbx_item
= elem_find_first(elem
, id_search
)
112 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
113 assert(len(fbx_item
.props
) == 1)
114 assert(fbx_item
.props_type
[0] == data_types
.BYTES
)
115 return fbx_item
.props
[0]
120 return "%s: props[%d=%r], elems=(%r)" % (
123 ", ".join([repr(p
) for p
in elem
.props
]),
125 b
", ".join([e
.id for e
in elem
.elems
]),
129 def elem_split_name_class(elem
):
130 assert(elem
.props_type
[-2] == data_types
.STRING
)
131 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
132 return elem_name
, elem_class
135 def elem_name_ensure_class(elem
, clss
=...):
136 elem_name
, elem_class
= elem_split_name_class(elem
)
138 assert(elem_class
== clss
)
139 return validate_blend_names(elem_name
)
142 def elem_name_ensure_classes(elem
, clss
=...):
143 elem_name
, elem_class
= elem_split_name_class(elem
)
145 assert(elem_class
in clss
)
146 return validate_blend_names(elem_name
)
149 def elem_split_name_class_nodeattr(elem
):
150 assert(elem
.props_type
[-2] == data_types
.STRING
)
151 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
152 assert(elem_class
== b
'NodeAttribute')
153 assert(elem
.props_type
[-1] == data_types
.STRING
)
154 elem_class
= elem
.props
[-1]
155 return elem_name
, elem_class
159 assert(elem
.props_type
[0] == data_types
.INT64
)
163 def elem_prop_first(elem
, default
=None):
164 return elem
.props
[0] if (elem
is not None) and elem
.props
else default
169 # Properties70: { ... P:
170 def elem_props_find_first(elem
, elem_prop_id
):
172 # When properties are not found... Should never happen, but happens - as usual.
174 # support for templates (tuple of elems)
175 if type(elem
) is not FBXElem
:
176 assert(type(elem
) is tuple)
178 result
= elem_props_find_first(e
, elem_prop_id
)
179 if result
is not None:
181 assert(len(elem
) > 0)
184 for subelem
in elem
.elems
:
185 assert(subelem
.id == b
'P')
186 if subelem
.props
[0] == elem_prop_id
:
191 def elem_props_get_color_rgb(elem
, elem_prop_id
, default
=None):
192 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
193 if elem_prop
is not None:
194 assert(elem_prop
.props
[0] == elem_prop_id
)
195 if elem_prop
.props
[1] == b
'Color':
197 assert(elem_prop
.props
[1] == b
'Color')
198 assert(elem_prop
.props
[2] == b
'')
200 assert(elem_prop
.props
[1] == b
'ColorRGB')
201 assert(elem_prop
.props
[2] == b
'Color')
202 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
203 return elem_prop
.props
[4:7]
207 def elem_props_get_vector_3d(elem
, elem_prop_id
, default
=None):
208 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
209 if elem_prop
is not None:
210 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
211 return elem_prop
.props
[4:7]
215 def elem_props_get_number(elem
, elem_prop_id
, default
=None):
216 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
217 if elem_prop
is not None:
218 assert(elem_prop
.props
[0] == elem_prop_id
)
219 if elem_prop
.props
[1] == b
'double':
220 assert(elem_prop
.props
[1] == b
'double')
221 assert(elem_prop
.props
[2] == b
'Number')
223 assert(elem_prop
.props
[1] == b
'Number')
224 assert(elem_prop
.props
[2] == b
'')
226 # we could allow other number types
227 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
229 return elem_prop
.props
[4]
233 def elem_props_get_integer(elem
, elem_prop_id
, default
=None):
234 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
235 if elem_prop
is not None:
236 assert(elem_prop
.props
[0] == elem_prop_id
)
237 if elem_prop
.props
[1] == b
'int':
238 assert(elem_prop
.props
[1] == b
'int')
239 assert(elem_prop
.props
[2] == b
'Integer')
240 elif elem_prop
.props
[1] == b
'ULongLong':
241 assert(elem_prop
.props
[1] == b
'ULongLong')
242 assert(elem_prop
.props
[2] == b
'')
244 # we could allow other number types
245 assert(elem_prop
.props_type
[4] in {data_types
.INT32
, data_types
.INT64
})
247 return elem_prop
.props
[4]
251 def elem_props_get_bool(elem
, elem_prop_id
, default
=None):
252 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
253 if elem_prop
is not None:
254 assert(elem_prop
.props
[0] == elem_prop_id
)
255 # b'Bool' with a capital seems to be used for animated property... go figure...
256 assert(elem_prop
.props
[1] in {b
'bool', b
'Bool'})
257 assert(elem_prop
.props
[2] == b
'')
259 # we could allow other number types
260 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
261 assert(elem_prop
.props
[4] in {0, 1})
263 return bool(elem_prop
.props
[4])
267 def elem_props_get_enum(elem
, elem_prop_id
, default
=None):
268 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
269 if elem_prop
is not None:
270 assert(elem_prop
.props
[0] == elem_prop_id
)
271 assert(elem_prop
.props
[1] == b
'enum')
272 assert(elem_prop
.props
[2] == b
'')
273 assert(elem_prop
.props
[3] == b
'')
275 # we could allow other number types
276 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
278 return elem_prop
.props
[4]
282 def elem_props_get_visibility(elem
, elem_prop_id
, default
=None):
283 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
284 if elem_prop
is not None:
285 assert(elem_prop
.props
[0] == elem_prop_id
)
286 assert(elem_prop
.props
[1] == b
'Visibility')
287 assert(elem_prop
.props
[2] == b
'')
289 # we could allow other number types
290 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
292 return elem_prop
.props
[4]
296 # ----------------------------------------------------------------------------
301 from collections
import namedtuple
304 FBXTransformData
= namedtuple("FBXTransformData", (
306 "rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat", "geom_rot",
307 "sca", "sca_ofs", "sca_piv", "geom_sca",
311 def blen_read_custom_properties(fbx_obj
, blen_obj
, settings
):
312 # There doesn't seem to be a way to put user properties into templates, so this only get the object properties:
313 fbx_obj_props
= elem_find_first(fbx_obj
, b
'Properties70')
315 for fbx_prop
in fbx_obj_props
.elems
:
316 assert(fbx_prop
.id == b
'P')
318 if b
'U' in fbx_prop
.props
[3]:
319 if fbx_prop
.props
[0] == b
'UDP3DSMAX':
320 # Special case for 3DS Max user properties:
321 assert(fbx_prop
.props
[1] == b
'KString')
322 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
323 items
= fbx_prop
.props
[4].decode('utf-8', 'replace')
324 for item
in items
.split('\r\n'):
326 prop_name
, prop_value
= item
.split('=', 1)
327 prop_name
= validate_blend_names(prop_name
.strip().encode('utf-8'))
328 blen_obj
[prop_name
] = prop_value
.strip()
330 prop_name
= validate_blend_names(fbx_prop
.props
[0])
331 prop_type
= fbx_prop
.props
[1]
332 if prop_type
in {b
'Vector', b
'Vector3D', b
'Color', b
'ColorRGB'}:
333 assert(fbx_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
334 blen_obj
[prop_name
] = fbx_prop
.props
[4:7]
335 elif prop_type
in {b
'Vector4', b
'ColorRGBA'}:
336 assert(fbx_prop
.props_type
[4:8] == bytes((data_types
.FLOAT64
,)) * 4)
337 blen_obj
[prop_name
] = fbx_prop
.props
[4:8]
338 elif prop_type
== b
'Vector2D':
339 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.FLOAT64
,)) * 2)
340 blen_obj
[prop_name
] = fbx_prop
.props
[4:6]
341 elif prop_type
in {b
'Integer', b
'int'}:
342 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
343 blen_obj
[prop_name
] = fbx_prop
.props
[4]
344 elif prop_type
== b
'KString':
345 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
346 blen_obj
[prop_name
] = fbx_prop
.props
[4].decode('utf-8', 'replace')
347 elif prop_type
in {b
'Number', b
'double', b
'Double'}:
348 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT64
)
349 blen_obj
[prop_name
] = fbx_prop
.props
[4]
350 elif prop_type
in {b
'Float', b
'float'}:
351 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT32
)
352 blen_obj
[prop_name
] = fbx_prop
.props
[4]
353 elif prop_type
in {b
'Bool', b
'bool'}:
354 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
355 blen_obj
[prop_name
] = fbx_prop
.props
[4] != 0
356 elif prop_type
in {b
'Enum', b
'enum'}:
357 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.INT32
, data_types
.STRING
)))
358 val
= fbx_prop
.props
[4]
359 if settings
.use_custom_props_enum_as_string
and fbx_prop
.props
[5]:
360 enum_items
= fbx_prop
.props
[5].decode('utf-8', 'replace').split('~')
361 assert(val
>= 0 and val
< len(enum_items
))
362 blen_obj
[prop_name
] = enum_items
[val
]
364 blen_obj
[prop_name
] = val
366 print ("WARNING: User property type '%s' is not supported" % prop_type
.decode('utf-8', 'replace'))
369 def blen_read_object_transform_do(transform_data
):
370 # This is a nightmare. FBX SDK uses Maya way to compute the transformation matrix of a node - utterly simple:
372 # WorldTransform = ParentWorldTransform @ T @ Roff @ Rp @ Rpre @ R @ Rpost @ Rp-1 @ Soff @ Sp @ S @ Sp-1
374 # Where all those terms are 4 x 4 matrices that contain:
375 # WorldTransform: Transformation matrix of the node in global space.
376 # ParentWorldTransform: Transformation matrix of the parent node in global space.
378 # Roff: Rotation offset
382 # Rpost: Post-rotation
383 # Rp-1: Inverse of the rotation pivot
384 # Soff: Scaling offset
387 # Sp-1: Inverse of the scaling pivot
389 # But it was still too simple, and FBX notion of compatibility is... quite specific. So we also have to
390 # support 3DSMax way:
392 # WorldTransform = ParentWorldTransform @ T @ R @ S @ OT @ OR @ OS
394 # Where all those terms are 4 x 4 matrices that contain:
395 # WorldTransform: Transformation matrix of the node in global space
396 # ParentWorldTransform: Transformation matrix of the parent node in global space
400 # OT: Geometric transform translation
401 # OR: Geometric transform rotation
402 # OS: Geometric transform translation
405 # Geometric transformations ***are not inherited***: ParentWorldTransform does not contain the OT, OR, OS
406 # of WorldTransform's parent node.
408 # Taken from http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/
409 # index.html?url=WS1a9193826455f5ff1f92379812724681e696651.htm,topicNumber=d0e7429
412 lcl_translation
= Matrix
.Translation(transform_data
.loc
)
413 geom_loc
= Matrix
.Translation(transform_data
.geom_loc
)
416 to_rot
= lambda rot
, rot_ord
: Euler(convert_deg_to_rad_iter(rot
), rot_ord
).to_matrix().to_4x4()
417 lcl_rot
= to_rot(transform_data
.rot
, transform_data
.rot_ord
) @ transform_data
.rot_alt_mat
418 pre_rot
= to_rot(transform_data
.pre_rot
, transform_data
.rot_ord
)
419 pst_rot
= to_rot(transform_data
.pst_rot
, transform_data
.rot_ord
)
420 geom_rot
= to_rot(transform_data
.geom_rot
, transform_data
.rot_ord
)
422 rot_ofs
= Matrix
.Translation(transform_data
.rot_ofs
)
423 rot_piv
= Matrix
.Translation(transform_data
.rot_piv
)
424 sca_ofs
= Matrix
.Translation(transform_data
.sca_ofs
)
425 sca_piv
= Matrix
.Translation(transform_data
.sca_piv
)
429 lcl_scale
[0][0], lcl_scale
[1][1], lcl_scale
[2][2] = transform_data
.sca
430 geom_scale
= Matrix();
431 geom_scale
[0][0], geom_scale
[1][1], geom_scale
[2][2] = transform_data
.geom_sca
440 rot_piv
.inverted_safe() @
444 sca_piv
.inverted_safe()
446 geom_mat
= geom_loc
@ geom_rot
@ geom_scale
447 # We return mat without 'geometric transforms' too, because it is to be used for children, sigh...
448 return (base_mat
@ geom_mat
, base_mat
, geom_mat
)
451 # XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become
452 # more likely, will have to make this more robust!!!
453 def add_vgroup_to_objects(vg_indices
, vg_weights
, vg_name
, objects
):
454 assert(len(vg_indices
) == len(vg_weights
))
457 # We replace/override here...
458 vg
= obj
.vertex_groups
.get(vg_name
)
460 vg
= obj
.vertex_groups
.new(name
=vg_name
)
461 for i
, w
in zip(vg_indices
, vg_weights
):
462 vg
.add((i
,), w
, 'REPLACE')
465 def blen_read_object_transform_preprocess(fbx_props
, fbx_obj
, rot_alt_mat
, use_prepost_rot
):
466 # This is quite involved, 'fbxRNode.cpp' from openscenegraph used as a reference
467 const_vector_zero_3d
= 0.0, 0.0, 0.0
468 const_vector_one_3d
= 1.0, 1.0, 1.0
470 loc
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Translation', const_vector_zero_3d
))
471 rot
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Rotation', const_vector_zero_3d
))
472 sca
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Scaling', const_vector_one_3d
))
474 geom_loc
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricTranslation', const_vector_zero_3d
))
475 geom_rot
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricRotation', const_vector_zero_3d
))
476 geom_sca
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricScaling', const_vector_one_3d
))
478 rot_ofs
= elem_props_get_vector_3d(fbx_props
, b
'RotationOffset', const_vector_zero_3d
)
479 rot_piv
= elem_props_get_vector_3d(fbx_props
, b
'RotationPivot', const_vector_zero_3d
)
480 sca_ofs
= elem_props_get_vector_3d(fbx_props
, b
'ScalingOffset', const_vector_zero_3d
)
481 sca_piv
= elem_props_get_vector_3d(fbx_props
, b
'ScalingPivot', const_vector_zero_3d
)
483 is_rot_act
= elem_props_get_bool(fbx_props
, b
'RotationActive', False)
487 pre_rot
= elem_props_get_vector_3d(fbx_props
, b
'PreRotation', const_vector_zero_3d
)
488 pst_rot
= elem_props_get_vector_3d(fbx_props
, b
'PostRotation', const_vector_zero_3d
)
490 pre_rot
= const_vector_zero_3d
491 pst_rot
= const_vector_zero_3d
499 6: 'XYZ', # XXX eSphericXYZ, not really supported...
500 }.get(elem_props_get_enum(fbx_props
, b
'RotationOrder', 0))
502 pre_rot
= const_vector_zero_3d
503 pst_rot
= const_vector_zero_3d
506 return FBXTransformData(loc
, geom_loc
,
507 rot
, rot_ofs
, rot_piv
, pre_rot
, pst_rot
, rot_ord
, rot_alt_mat
, geom_rot
,
508 sca
, sca_ofs
, sca_piv
, geom_sca
)
513 def blen_read_animations_curves_iter(fbx_curves
, blen_start_offset
, fbx_start_offset
, fps
):
515 Get raw FBX AnimCurve list, and yield values for all curves at each singular curves' keyframes,
516 together with (blender) timing, in frames.
517 blen_start_offset is expected in frames, while fbx_start_offset is expected in FBX ktime.
519 # As a first step, assume linear interpolation between key frames, we'll (try to!) handle more
520 # of FBX curves later.
521 from .fbx_utils
import FBX_KTIME
522 timefac
= fps
/ FBX_KTIME
525 elem_prop_first(elem_find_first(c
[2], b
'KeyTime')),
526 elem_prop_first(elem_find_first(c
[2], b
'KeyValueFloat')),
530 allkeys
= sorted({item
for sublist
in curves
for item
in sublist
[1]})
531 for curr_fbxktime
in allkeys
:
534 idx
, times
, values
, fbx_curve
= item
536 if times
[idx
] < curr_fbxktime
:
539 if idx
>= len(times
):
540 # We have reached our last element for this curve, stay on it from now on...
544 if times
[idx
] >= curr_fbxktime
:
546 curr_values
.append((values
[idx
], fbx_curve
))
548 # Interpolate between this key and the previous one.
549 ifac
= (curr_fbxktime
- times
[idx
- 1]) / (times
[idx
] - times
[idx
- 1])
550 curr_values
.append(((values
[idx
] - values
[idx
- 1]) * ifac
+ values
[idx
- 1], fbx_curve
))
551 curr_blenkframe
= (curr_fbxktime
- fbx_start_offset
) * timefac
+ blen_start_offset
552 yield (curr_blenkframe
, curr_values
)
555 def blen_read_animations_action_item(action
, item
, cnodes
, fps
, anim_offset
):
557 'Bake' loc/rot/scale into the action,
558 taking any pre_ and post_ matrix into account to transform from fbx into blender space.
560 from bpy
.types
import Object
, PoseBone
, ShapeKey
, Material
, Camera
561 from itertools
import chain
564 for curves
, fbxprop
in cnodes
.values():
565 for (fbx_acdata
, _blen_data
), channel
in curves
.values():
566 fbx_curves
.append((fbxprop
, channel
, fbx_acdata
))
568 # Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
569 if len(fbx_curves
) == 0:
575 if isinstance(item
, Material
):
577 props
= [("diffuse_color", 3, grpname
or "Diffuse Color")]
578 elif isinstance(item
, ShapeKey
):
579 props
= [(item
.path_from_id("value"), 1, "Key")]
580 elif isinstance(item
, Camera
):
581 props
= [(item
.path_from_id("lens"), 1, "Camera")]
582 else: # Object or PoseBone:
584 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
588 # We want to create actions for objects, but for bones we 'reuse' armatures' actions!
589 grpname
= item
.bl_obj
.name
591 # Since we might get other channels animated in the end, due to all FBX transform magic,
592 # we need to add curves for whole loc/rot/scale in any case.
593 props
= [(bl_obj
.path_from_id("location"), 3, grpname
or "Location"),
595 (bl_obj
.path_from_id("scale"), 3, grpname
or "Scale")]
596 rot_mode
= bl_obj
.rotation_mode
597 if rot_mode
== 'QUATERNION':
598 props
[1] = (bl_obj
.path_from_id("rotation_quaternion"), 4, grpname
or "Quaternion Rotation")
599 elif rot_mode
== 'AXIS_ANGLE':
600 props
[1] = (bl_obj
.path_from_id("rotation_axis_angle"), 4, grpname
or "Axis Angle Rotation")
602 props
[1] = (bl_obj
.path_from_id("rotation_euler"), 3, grpname
or "Euler Rotation")
604 blen_curves
= [action
.fcurves
.new(prop
, index
=channel
, action_group
=grpname
)
605 for prop
, nbr_channels
, grpname
in props
for channel
in range(nbr_channels
)]
607 if isinstance(item
, Material
):
608 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
610 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
611 assert(fbxprop
== b
'DiffuseColor')
612 assert(channel
in {0, 1, 2})
615 for fc
, v
in zip(blen_curves
, value
):
616 fc
.keyframe_points
.insert(frame
, v
, options
={'NEEDED', 'FAST'}).interpolation
= 'LINEAR'
618 elif isinstance(item
, ShapeKey
):
619 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
621 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
622 assert(fbxprop
== b
'DeformPercent')
626 for fc
, v
in zip(blen_curves
, (value
,)):
627 fc
.keyframe_points
.insert(frame
, v
, options
={'NEEDED', 'FAST'}).interpolation
= 'LINEAR'
629 elif isinstance(item
, Camera
):
630 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
632 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
633 assert(fbxprop
== b
'FocalLength')
637 for fc
, v
in zip(blen_curves
, (value
,)):
638 fc
.keyframe_points
.insert(frame
, v
, options
={'NEEDED', 'FAST'}).interpolation
= 'LINEAR'
640 else: # Object or PoseBone:
642 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
646 transform_data
= item
.fbx_transform_data
647 rot_prev
= bl_obj
.rotation_euler
.copy()
649 # Pre-compute inverted local rest matrix of the bone, if relevant.
650 restmat_inv
= item
.get_bind_matrix().inverted_safe() if item
.is_bone
else None
652 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
653 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
654 if fbxprop
== b
'Lcl Translation':
655 transform_data
.loc
[channel
] = v
656 elif fbxprop
== b
'Lcl Rotation':
657 transform_data
.rot
[channel
] = v
658 elif fbxprop
== b
'Lcl Scaling':
659 transform_data
.sca
[channel
] = v
660 mat
, _
, _
= blen_read_object_transform_do(transform_data
)
662 # compensate for changes in the local matrix during processing
663 if item
.anim_compensation_matrix
:
664 mat
= mat
@ item
.anim_compensation_matrix
666 # apply pre- and post matrix
667 # post-matrix will contain any correction for lights, camera and bone orientation
668 # pre-matrix will contain any correction for a parent's correction matrix or the global matrix
670 mat
= item
.pre_matrix
@ mat
672 mat
= mat
@ item
.post_matrix
674 # And now, remove that rest pose matrix from current mat (also in parent space).
676 mat
= restmat_inv
@ mat
678 # Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
679 loc
, rot
, sca
= mat
.decompose()
680 if rot_mode
== 'QUATERNION':
681 pass # nothing to do!
682 elif rot_mode
== 'AXIS_ANGLE':
683 vec
, ang
= rot
.to_axis_angle()
684 rot
= ang
, vec
.x
, vec
.y
, vec
.z
686 rot
= rot
.to_euler(rot_mode
, rot_prev
)
688 for fc
, value
in zip(blen_curves
, chain(loc
, rot
, sca
)):
689 fc
.keyframe_points
.insert(frame
, value
, options
={'NEEDED', 'FAST'}).interpolation
= 'LINEAR'
691 # Since we inserted our keyframes in 'FAST' mode, we have to update the fcurves now.
692 for fc
in blen_curves
:
696 def blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, anim_offset
):
698 Recreate an action per stack/layer/object combinations.
699 Only the first found action is linked to objects, more complex setups are not handled,
700 it's up to user to reproduce them!
702 from bpy
.types
import ShapeKey
, Material
, Camera
705 for as_uuid
, ((fbx_asdata
, _blen_data
), alayers
) in stacks
.items():
706 stack_name
= elem_name_ensure_class(fbx_asdata
, b
'AnimStack')
707 for al_uuid
, ((fbx_aldata
, _blen_data
), items
) in alayers
.items():
708 layer_name
= elem_name_ensure_class(fbx_aldata
, b
'AnimLayer')
709 for item
, cnodes
in items
.items():
710 if isinstance(item
, Material
):
712 elif isinstance(item
, ShapeKey
):
713 id_data
= item
.id_data
714 elif isinstance(item
, Camera
):
717 id_data
= item
.bl_obj
718 # XXX Ignore rigged mesh animations - those are a nightmare to handle, see note about it in
719 # FbxImportHelperNode class definition.
720 if id_data
and id_data
.type == 'MESH' and id_data
.parent
and id_data
.parent
.type == 'ARMATURE':
725 # Create new action if needed (should always be needed!
726 key
= (as_uuid
, al_uuid
, id_data
)
727 action
= actions
.get(key
)
729 action_name
= "|".join((id_data
.name
, stack_name
, layer_name
))
730 actions
[key
] = action
= bpy
.data
.actions
.new(action_name
)
731 action
.use_fake_user
= True
732 # If none yet assigned, assign this action to id_data.
733 if not id_data
.animation_data
:
734 id_data
.animation_data_create()
735 if not id_data
.animation_data
.action
:
736 id_data
.animation_data
.action
= action
737 # And actually populate the action!
738 blen_read_animations_action_item(action
, item
, cnodes
, scene
.render
.fps
, anim_offset
)
744 def blen_read_geom_layerinfo(fbx_layer
):
746 validate_blend_names(elem_find_first_string_as_bytes(fbx_layer
, b
'Name')),
747 elem_find_first_string_as_bytes(fbx_layer
, b
'MappingInformationType'),
748 elem_find_first_string_as_bytes(fbx_layer
, b
'ReferenceInformationType'),
752 def blen_read_geom_array_setattr(generator
, blen_data
, blen_attr
, fbx_data
, stride
, item_size
, descr
, xform
):
753 """Generic fbx_layer to blen_data setter, generator is expected to yield tuples (ble_idx, fbx_idx)."""
754 max_idx
= len(blen_data
) - 1
757 def check_skip(blen_idx
, fbx_idx
):
759 if fbx_idx
< 0: # Negative values mean 'skip'.
761 if blen_idx
> max_idx
:
763 print("ERROR: too much data in this layer, compared to elements in mesh, skipping!")
768 if xform
is not None:
769 if isinstance(blen_data
, list):
771 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
772 blen_data
[blen_idx
] = xform(fbx_data
[fbx_idx
])
774 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
775 blen_data
[blen_idx
] = xform(fbx_data
[fbx_idx
:fbx_idx
+ item_size
])
778 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
779 setattr(blen_data
[blen_idx
], blen_attr
, xform(fbx_data
[fbx_idx
]))
781 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
782 setattr(blen_data
[blen_idx
], blen_attr
, xform(fbx_data
[fbx_idx
:fbx_idx
+ item_size
]))
784 if isinstance(blen_data
, list):
786 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
787 blen_data
[blen_idx
] = fbx_data
[fbx_idx
]
789 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
790 blen_data
[blen_idx
] = fbx_data
[fbx_idx
:fbx_idx
+ item_size
]
793 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
794 setattr(blen_data
[blen_idx
], blen_attr
, fbx_data
[fbx_idx
])
796 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
797 setattr(blen_data
[blen_idx
], blen_attr
, fbx_data
[fbx_idx
:fbx_idx
+ item_size
])
799 for blen_idx
, fbx_idx
in generator
:
800 if check_skip(blen_idx
, fbx_idx
):
802 _process(blen_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
)
805 # generic generators.
806 def blen_read_geom_array_gen_allsame(data_len
):
807 return zip(*(range(data_len
), (0,) * data_len
))
810 def blen_read_geom_array_gen_direct(fbx_data
, stride
):
811 fbx_data_len
= len(fbx_data
)
812 return zip(*(range(fbx_data_len
// stride
), range(0, fbx_data_len
, stride
)))
815 def blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
):
816 return ((bi
, fi
* stride
) for bi
, fi
in enumerate(fbx_layer_index
))
819 def blen_read_geom_array_gen_direct_looptovert(mesh
, fbx_data
, stride
):
820 fbx_data_len
= len(fbx_data
) // stride
822 for p
in mesh
.polygons
:
823 for lidx
in p
.loop_indices
:
824 vidx
= loops
[lidx
].vertex_index
825 if vidx
< fbx_data_len
:
826 yield lidx
, vidx
* stride
829 # generic error printers.
830 def blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
=False):
832 print("warning layer %r mapping type unsupported: %r" % (descr
, fbx_layer_mapping
))
835 def blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
=False):
837 print("warning layer %r ref type unsupported: %r" % (descr
, fbx_layer_ref
))
840 def blen_read_geom_array_mapped_vert(
841 mesh
, blen_data
, blen_attr
,
842 fbx_layer_data
, fbx_layer_index
,
843 fbx_layer_mapping
, fbx_layer_ref
,
844 stride
, item_size
, descr
,
845 xform
=None, quiet
=False,
847 if fbx_layer_mapping
== b
'ByVertice':
848 if fbx_layer_ref
== b
'Direct':
849 assert(fbx_layer_index
is None)
850 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
851 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
853 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
854 elif fbx_layer_mapping
== b
'AllSame':
855 if fbx_layer_ref
== b
'IndexToDirect':
856 assert(fbx_layer_index
is None)
857 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
858 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
860 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
862 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
867 def blen_read_geom_array_mapped_edge(
868 mesh
, blen_data
, blen_attr
,
869 fbx_layer_data
, fbx_layer_index
,
870 fbx_layer_mapping
, fbx_layer_ref
,
871 stride
, item_size
, descr
,
872 xform
=None, quiet
=False,
874 if fbx_layer_mapping
== b
'ByEdge':
875 if fbx_layer_ref
== b
'Direct':
876 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
877 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
879 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
880 elif fbx_layer_mapping
== b
'AllSame':
881 if fbx_layer_ref
== b
'IndexToDirect':
882 assert(fbx_layer_index
is None)
883 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
884 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
886 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
888 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
893 def blen_read_geom_array_mapped_polygon(
894 mesh
, blen_data
, blen_attr
,
895 fbx_layer_data
, fbx_layer_index
,
896 fbx_layer_mapping
, fbx_layer_ref
,
897 stride
, item_size
, descr
,
898 xform
=None, quiet
=False,
900 if fbx_layer_mapping
== b
'ByPolygon':
901 if fbx_layer_ref
== b
'IndexToDirect':
902 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
903 # We fallback to 'Direct' mapping in this case.
904 #~ assert(fbx_layer_index is not None)
905 if fbx_layer_index
is None:
906 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
907 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
909 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
),
910 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
912 elif fbx_layer_ref
== b
'Direct':
913 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
914 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
916 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
917 elif fbx_layer_mapping
== b
'AllSame':
918 if fbx_layer_ref
== b
'IndexToDirect':
919 assert(fbx_layer_index
is None)
920 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
921 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
923 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
925 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
930 def blen_read_geom_array_mapped_polyloop(
931 mesh
, blen_data
, blen_attr
,
932 fbx_layer_data
, fbx_layer_index
,
933 fbx_layer_mapping
, fbx_layer_ref
,
934 stride
, item_size
, descr
,
935 xform
=None, quiet
=False,
937 if fbx_layer_mapping
== b
'ByPolygonVertex':
938 if fbx_layer_ref
== b
'IndexToDirect':
939 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
940 # We fallback to 'Direct' mapping in this case.
941 #~ assert(fbx_layer_index is not None)
942 if fbx_layer_index
is None:
943 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
944 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
946 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
),
947 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
949 elif fbx_layer_ref
== b
'Direct':
950 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
951 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
953 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
954 elif fbx_layer_mapping
== b
'ByVertice':
955 if fbx_layer_ref
== b
'Direct':
956 assert(fbx_layer_index
is None)
957 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct_looptovert(mesh
, fbx_layer_data
, stride
),
958 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
960 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
961 elif fbx_layer_mapping
== b
'AllSame':
962 if fbx_layer_ref
== b
'IndexToDirect':
963 assert(fbx_layer_index
is None)
964 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
965 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
967 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
969 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
974 def blen_read_geom_layer_material(fbx_obj
, mesh
):
975 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementMaterial')
977 if fbx_layer
is None:
983 ) = blen_read_geom_layerinfo(fbx_layer
)
985 layer_id
= b
'Materials'
986 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
988 blen_data
= mesh
.polygons
989 blen_read_geom_array_mapped_polygon(
990 mesh
, blen_data
, "material_index",
991 fbx_layer_data
, None,
992 fbx_layer_mapping
, fbx_layer_ref
,
997 def blen_read_geom_layer_uv(fbx_obj
, mesh
):
998 for layer_id
in (b
'LayerElementUV',):
999 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
1000 # all should be valid
1004 ) = blen_read_geom_layerinfo(fbx_layer
)
1006 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'UV'))
1007 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'UVIndex'))
1009 # Always init our new layers with (0, 0) UVs.
1010 uv_lay
= mesh
.uv_layers
.new(name
=fbx_layer_name
, do_init
=False)
1012 print("Failed to add {%r %r} UVLayer to %r (probably too many of them?)"
1013 "" % (layer_id
, fbx_layer_name
, mesh
.name
))
1016 blen_data
= uv_lay
.data
1018 # some valid files omit this data
1019 if fbx_layer_data
is None:
1020 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
1023 blen_read_geom_array_mapped_polyloop(
1024 mesh
, blen_data
, "uv",
1025 fbx_layer_data
, fbx_layer_index
,
1026 fbx_layer_mapping
, fbx_layer_ref
,
1031 def blen_read_geom_layer_color(fbx_obj
, mesh
):
1032 # almost same as UV's
1033 for layer_id
in (b
'LayerElementColor',):
1034 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
1035 # all should be valid
1039 ) = blen_read_geom_layerinfo(fbx_layer
)
1041 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'Colors'))
1042 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'ColorIndex'))
1044 # Always init our new layers with full white opaque color.
1045 color_lay
= mesh
.vertex_colors
.new(name
=fbx_layer_name
, do_init
=False)
1046 blen_data
= color_lay
.data
1048 # some valid files omit this data
1049 if fbx_layer_data
is None:
1050 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
1053 blen_read_geom_array_mapped_polyloop(
1054 mesh
, blen_data
, "color",
1055 fbx_layer_data
, fbx_layer_index
,
1056 fbx_layer_mapping
, fbx_layer_ref
,
1061 def blen_read_geom_layer_smooth(fbx_obj
, mesh
):
1062 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementSmoothing')
1064 if fbx_layer
is None:
1067 # all should be valid
1071 ) = blen_read_geom_layerinfo(fbx_layer
)
1073 layer_id
= b
'Smoothing'
1074 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1076 # udk has 'Direct' mapped, with no Smoothing, not sure why, but ignore these
1077 if fbx_layer_data
is None:
1080 if fbx_layer_mapping
== b
'ByEdge':
1081 # some models have bad edge data, we cant use this info...
1083 print("warning skipping sharp edges data, no valid edges...")
1086 blen_data
= mesh
.edges
1087 blen_read_geom_array_mapped_edge(
1088 mesh
, blen_data
, "use_edge_sharp",
1089 fbx_layer_data
, None,
1090 fbx_layer_mapping
, fbx_layer_ref
,
1092 xform
=lambda s
: not s
,
1094 # We only set sharp edges here, not face smoothing itself...
1095 mesh
.use_auto_smooth
= True
1097 elif fbx_layer_mapping
== b
'ByPolygon':
1098 blen_data
= mesh
.polygons
1099 return blen_read_geom_array_mapped_polygon(
1100 mesh
, blen_data
, "use_smooth",
1101 fbx_layer_data
, None,
1102 fbx_layer_mapping
, fbx_layer_ref
,
1104 xform
=lambda s
: (s
!= 0), # smoothgroup bitflags, treat as booleans for now
1107 print("warning layer %r mapping type unsupported: %r" % (fbx_layer
.id, fbx_layer_mapping
))
1111 def blen_read_geom_layer_normal(fbx_obj
, mesh
, xform
=None):
1112 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementNormal')
1114 if fbx_layer
is None:
1120 ) = blen_read_geom_layerinfo(fbx_layer
)
1122 layer_id
= b
'Normals'
1123 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1124 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'NormalsIndex'))
1126 # try loops, then vertices.
1127 tries
= ((mesh
.loops
, "Loops", False, blen_read_geom_array_mapped_polyloop
),
1128 (mesh
.polygons
, "Polygons", True, blen_read_geom_array_mapped_polygon
),
1129 (mesh
.vertices
, "Vertices", True, blen_read_geom_array_mapped_vert
))
1130 for blen_data
, blen_data_type
, is_fake
, func
in tries
:
1131 bdata
= [None] * len(blen_data
) if is_fake
else blen_data
1132 if func(mesh
, bdata
, "normal",
1133 fbx_layer_data
, fbx_layer_index
, fbx_layer_mapping
, fbx_layer_ref
, 3, 3, layer_id
, xform
, True):
1134 if blen_data_type
== "Polygons":
1135 for pidx
, p
in enumerate(mesh
.polygons
):
1136 for lidx
in range(p
.loop_start
, p
.loop_start
+ p
.loop_total
):
1137 mesh
.loops
[lidx
].normal
[:] = bdata
[pidx
]
1138 elif blen_data_type
is "Vertices":
1139 # We have to copy vnors to lnors! Far from elegant, but simple.
1140 for l
in mesh
.loops
:
1141 l
.normal
[:] = bdata
[l
.vertex_index
]
1144 blen_read_geom_array_error_mapping("normal", fbx_layer_mapping
)
1145 blen_read_geom_array_error_ref("normal", fbx_layer_ref
)
1149 def blen_read_geom(fbx_tmpl
, fbx_obj
, settings
):
1150 from itertools
import chain
1153 # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
1154 # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
1155 geom_mat_co
= settings
.global_matrix
if settings
.bake_space_transform
else None
1156 # We need to apply the inverse transpose of the global matrix when transforming normals.
1157 geom_mat_no
= Matrix(settings
.global_matrix_inv_transposed
) if settings
.bake_space_transform
else None
1158 if geom_mat_no
is not None:
1159 # Remove translation & scaling!
1160 geom_mat_no
.translation
= Vector()
1161 geom_mat_no
.normalize()
1163 # TODO, use 'fbx_tmpl'
1164 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Geometry')
1166 fbx_verts
= elem_prop_first(elem_find_first(fbx_obj
, b
'Vertices'))
1167 fbx_polys
= elem_prop_first(elem_find_first(fbx_obj
, b
'PolygonVertexIndex'))
1168 fbx_edges
= elem_prop_first(elem_find_first(fbx_obj
, b
'Edges'))
1170 if geom_mat_co
is not None:
1171 def _vcos_transformed_gen(raw_cos
, m
=None):
1172 # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
1173 return chain(*(m
@ Vector(v
) for v
in zip(*(iter(raw_cos
),) * 3)))
1174 fbx_verts
= array
.array(fbx_verts
.typecode
, _vcos_transformed_gen(fbx_verts
, geom_mat_co
))
1176 if fbx_verts
is None:
1178 if fbx_polys
is None:
1181 mesh
= bpy
.data
.meshes
.new(name
=elem_name_utf8
)
1182 mesh
.vertices
.add(len(fbx_verts
) // 3)
1183 mesh
.vertices
.foreach_set("co", fbx_verts
)
1186 mesh
.loops
.add(len(fbx_polys
))
1187 poly_loop_starts
= []
1188 poly_loop_totals
= []
1190 for i
, l
in enumerate(mesh
.loops
):
1191 index
= fbx_polys
[i
]
1193 poly_loop_starts
.append(poly_loop_prev
)
1194 poly_loop_totals
.append((i
- poly_loop_prev
) + 1)
1195 poly_loop_prev
= i
+ 1
1197 l
.vertex_index
= index
1199 mesh
.polygons
.add(len(poly_loop_starts
))
1200 mesh
.polygons
.foreach_set("loop_start", poly_loop_starts
)
1201 mesh
.polygons
.foreach_set("loop_total", poly_loop_totals
)
1203 blen_read_geom_layer_material(fbx_obj
, mesh
)
1204 blen_read_geom_layer_uv(fbx_obj
, mesh
)
1205 blen_read_geom_layer_color(fbx_obj
, mesh
)
1208 # edges in fact index the polygons (NOT the vertices)
1210 tot_edges
= len(fbx_edges
)
1211 edges_conv
= array
.array('i', [0]) * (tot_edges
* 2)
1217 e_b
= fbx_polys
[i
+ 1]
1221 # Last index of polygon, wrap back to the start.
1223 # ideally we wouldn't have to search back,
1224 # but it should only be 2-3 iterations.
1226 while j
>= 0 and fbx_polys
[j
] >= 0:
1229 e_b
= fbx_polys
[j
+ 1]
1231 edges_conv
[edge_index
] = e_a
1232 edges_conv
[edge_index
+ 1] = e_b
1235 mesh
.edges
.add(tot_edges
)
1236 mesh
.edges
.foreach_set("vertices", edges_conv
)
1238 # must be after edge, face loading.
1239 ok_smooth
= blen_read_geom_layer_smooth(fbx_obj
, mesh
)
1242 if settings
.use_custom_normals
:
1243 # Note: we store 'temp' normals in loops, since validate() may alter final mesh,
1244 # we can only set custom lnors *after* calling it.
1245 mesh
.create_normals_split()
1246 if geom_mat_no
is None:
1247 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
)
1250 return geom_mat_no
@ Vector(v
)
1251 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
, nortrans
)
1253 mesh
.validate(clean_customdata
=False) # *Very* important to not remove lnors here!
1256 clnors
= array
.array('f', [0.0] * (len(mesh
.loops
) * 3))
1257 mesh
.loops
.foreach_get("normal", clnors
)
1260 mesh
.polygons
.foreach_set("use_smooth", [True] * len(mesh
.polygons
))
1263 mesh
.normals_split_custom_set(tuple(zip(*(iter(clnors
),) * 3)))
1264 mesh
.use_auto_smooth
= True
1268 if settings
.use_custom_normals
:
1269 mesh
.free_normals_split()
1272 mesh
.polygons
.foreach_set("use_smooth", [True] * len(mesh
.polygons
))
1274 if settings
.use_custom_props
:
1275 blen_read_custom_properties(fbx_obj
, mesh
, settings
)
1280 def blen_read_shape(fbx_tmpl
, fbx_sdata
, fbx_bcdata
, meshes
, scene
):
1281 elem_name_utf8
= elem_name_ensure_class(fbx_sdata
, b
'Geometry')
1282 indices
= elem_prop_first(elem_find_first(fbx_sdata
, b
'Indexes'), default
=())
1283 dvcos
= tuple(co
for co
in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata
, b
'Vertices'), default
=()))] * 3))
1284 # We completely ignore normals here!
1285 weight
= elem_prop_first(elem_find_first(fbx_bcdata
, b
'DeformPercent'), default
=100.0) / 100.0
1286 vgweights
= tuple(vgw
/ 100.0 for vgw
in elem_prop_first(elem_find_first(fbx_bcdata
, b
'FullWeights'), default
=()))
1288 # Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
1289 nbr_indices
= len(indices
)
1290 if len(vgweights
) == 1 and nbr_indices
> 1:
1291 vgweights
= (vgweights
[0],) * nbr_indices
1293 assert(len(vgweights
) == nbr_indices
== len(dvcos
))
1294 create_vg
= bool(set(vgweights
) - {1.0})
1298 for me
, objects
in meshes
:
1299 vcos
= tuple((idx
, me
.vertices
[idx
].co
+ Vector(dvco
)) for idx
, dvco
in zip(indices
, dvcos
))
1300 objects
= list({node
.bl_obj
for node
in objects
})
1303 if me
.shape_keys
is None:
1304 objects
[0].shape_key_add(name
="Basis", from_mix
=False)
1305 kb
= objects
[0].shape_key_add(name
=elem_name_utf8
, from_mix
=False)
1306 me
.shape_keys
.use_relative
= True # Should already be set as such.
1308 for idx
, co
in vcos
:
1309 kb
.data
[idx
].co
[:] = co
1312 # Add vgroup if necessary.
1314 vgoups
= add_vgroup_to_objects(indices
, vgweights
, kb
.name
, objects
)
1315 kb
.vertex_group
= kb
.name
1317 keyblocks
.append(kb
)
1325 def blen_read_material(fbx_tmpl
, fbx_obj
, settings
):
1326 from bpy_extras
import node_shader_utils
1327 from math
import sqrt
1329 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Material')
1331 nodal_material_wrap_map
= settings
.nodal_material_wrap_map
1332 ma
= bpy
.data
.materials
.new(name
=elem_name_utf8
)
1334 const_color_white
= 1.0, 1.0, 1.0
1336 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1337 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1339 ma_wrap
= node_shader_utils
.PrincipledBSDFWrapper(ma
, is_readonly
=False, use_nodes
=True)
1340 ma_wrap
.base_color
= elem_props_get_color_rgb(fbx_props
, b
'DiffuseColor', const_color_white
)
1341 # No specular color in Principled BSDF shader, assumed to be either white or take some tint from diffuse one...
1342 # TODO: add way to handle tint option (guesstimate from spec color + intensity...)?
1343 ma_wrap
.specular
= elem_props_get_number(fbx_props
, b
'SpecularFactor', 0.25) * 2.0
1344 # XXX Totally empirical conversion, trying to adapt it
1345 # (from 1.0 - 0.0 Principled BSDF range to 0.0 - 100.0 FBX shininess range)...
1346 fbx_shininess
= elem_props_get_number(fbx_props
, b
'Shininess', 20.0)
1347 ma_wrap
.roughness
= 1.0 - (sqrt(fbx_shininess
) / 10.0)
1348 ma_wrap
.transmission
= 1.0 - elem_props_get_number(fbx_props
, b
'Opacity', 1.0)
1349 ma_wrap
.metallic
= elem_props_get_number(fbx_props
, b
'ReflectionFactor', 0.0)
1350 # We have no metallic (a.k.a. reflection) color...
1351 # elem_props_get_color_rgb(fbx_props, b'ReflectionColor', const_color_white)
1352 # (x / 7.142) is only a guess, cycles usable range is (0.0 -> 0.5)
1353 ma_wrap
.normalmap_strength
= elem_props_get_number(fbx_props
, b
'BumpFactor', 2.5) / 7.142
1355 nodal_material_wrap_map
[ma
] = ma_wrap
1357 if settings
.use_custom_props
:
1358 blen_read_custom_properties(fbx_obj
, ma
, settings
)
1366 def blen_read_texture_image(fbx_tmpl
, fbx_obj
, basedir
, settings
):
1368 from bpy_extras
import image_utils
1370 def pack_data_from_content(image
, fbx_obj
):
1371 data
= elem_find_first_bytes(fbx_obj
, b
'Content')
1373 data_len
= len(data
)
1375 image
.pack(data
=data
, data_len
=data_len
)
1377 elem_name_utf8
= elem_name_ensure_classes(fbx_obj
, {b
'Texture', b
'Video'})
1379 image_cache
= settings
.image_cache
1381 # Yet another beautiful logic demonstration by Master FBX:
1382 # * RelativeFilename in both Video and Texture nodes.
1383 # * FileName in texture nodes.
1384 # * Filename in video nodes.
1385 # Aaaaaaaarrrrrrrrgggggggggggg!!!!!!!!!!!!!!
1386 filepath
= elem_find_first_string(fbx_obj
, b
'RelativeFilename')
1388 filepath
= os
.path
.join(basedir
, filepath
)
1390 filepath
= elem_find_first_string(fbx_obj
, b
'FileName')
1392 filepath
= elem_find_first_string(fbx_obj
, b
'Filename')
1394 print("Error, could not find any file path in ", fbx_obj
)
1395 print(" Falling back to: ", elem_name_utf8
)
1396 filepath
= elem_name_utf8
1398 filepath
= filepath
.replace('\\', '/') if (os
.sep
== '/') else filepath
.replace('/', '\\')
1400 image
= image_cache
.get(filepath
)
1401 if image
is not None:
1402 # Data is only embedded once, we may have already created the image but still be missing its data!
1403 if not image
.has_data
:
1404 pack_data_from_content(image
, fbx_obj
)
1407 image
= image_utils
.load_image(
1411 recursive
=settings
.use_image_search
,
1414 # Try to use embedded data, if available!
1415 pack_data_from_content(image
, fbx_obj
)
1417 image_cache
[filepath
] = image
1418 # name can be ../a/b/c
1419 image
.name
= os
.path
.basename(elem_name_utf8
)
1421 if settings
.use_custom_props
:
1422 blen_read_custom_properties(fbx_obj
, image
, settings
)
1427 def blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
):
1431 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1433 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1434 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1436 camera
= bpy
.data
.cameras
.new(name
=elem_name_utf8
)
1438 camera
.type = 'ORTHO' if elem_props_get_enum(fbx_props
, b
'CameraProjectionType', 0) == 1 else 'PERSP'
1440 camera
.lens
= elem_props_get_number(fbx_props
, b
'FocalLength', 35.0)
1441 camera
.sensor_width
= elem_props_get_number(fbx_props
, b
'FilmWidth', 32.0 * M2I
) / M2I
1442 camera
.sensor_height
= elem_props_get_number(fbx_props
, b
'FilmHeight', 32.0 * M2I
) / M2I
1444 camera
.ortho_scale
= elem_props_get_number(fbx_props
, b
'OrthoZoom', 1.0)
1446 filmaspect
= camera
.sensor_width
/ camera
.sensor_height
1448 camera
.shift_x
= elem_props_get_number(fbx_props
, b
'FilmOffsetX', 0.0) / (M2I
* camera
.sensor_width
)
1449 camera
.shift_y
= elem_props_get_number(fbx_props
, b
'FilmOffsetY', 0.0) / (M2I
* camera
.sensor_height
* filmaspect
)
1451 camera
.clip_start
= elem_props_get_number(fbx_props
, b
'NearPlane', 0.01) * global_scale
1452 camera
.clip_end
= elem_props_get_number(fbx_props
, b
'FarPlane', 100.0) * global_scale
1457 def blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
):
1459 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1461 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1462 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1467 2: 'SPOT'}.get(elem_props_get_enum(fbx_props
, b
'LightType', 0), 'POINT')
1469 lamp
= bpy
.data
.lights
.new(name
=elem_name_utf8
, type=light_type
)
1471 if light_type
== 'SPOT':
1472 spot_size
= elem_props_get_number(fbx_props
, b
'OuterAngle', None)
1473 if spot_size
is None:
1475 spot_size
= elem_props_get_number(fbx_props
, b
'Cone angle', 45.0)
1476 lamp
.spot_size
= math
.radians(spot_size
)
1478 spot_blend
= elem_props_get_number(fbx_props
, b
'InnerAngle', None)
1479 if spot_blend
is None:
1481 spot_blend
= elem_props_get_number(fbx_props
, b
'HotSpot', 45.0)
1482 lamp
.spot_blend
= 1.0 - (spot_blend
/ spot_size
)
1484 # TODO, cycles nodes???
1485 lamp
.color
= elem_props_get_color_rgb(fbx_props
, b
'Color', (1.0, 1.0, 1.0))
1486 lamp
.energy
= elem_props_get_number(fbx_props
, b
'Intensity', 100.0) / 100.0
1487 lamp
.distance
= elem_props_get_number(fbx_props
, b
'DecayStart', 25.0) * global_scale
1488 lamp
.use_shadow
= elem_props_get_bool(fbx_props
, b
'CastShadow', True)
1489 if hasattr(lamp
, "cycles"):
1490 lamp
.cycles
.cast_shadow
= lamp
.use_shadow
1491 # Keeping this for now, but this is not used nor exposed anymore afaik...
1492 lamp
.shadow_color
= elem_props_get_color_rgb(fbx_props
, b
'ShadowColor', (0.0, 0.0, 0.0))
1497 # ### Import Utility class
1498 class FbxImportHelperNode
:
1500 Temporary helper node to store a hierarchy of fbxNode objects before building Objects, Armatures and Bones.
1501 It tries to keep the correction data in one place so it can be applied consistently to the imported data.
1505 '_parent', 'anim_compensation_matrix', 'is_global_animation', 'armature_setup', 'armature', 'bind_matrix',
1506 'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix', 'children', 'clusters',
1507 'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type',
1508 'is_armature', 'has_bone_children', 'is_bone', 'is_root', 'is_leaf',
1509 'matrix', 'matrix_as_parent', 'matrix_geom', 'meshes', 'post_matrix', 'pre_matrix')
1511 def __init__(self
, fbx_elem
, bl_data
, fbx_transform_data
, is_bone
):
1512 self
.fbx_name
= elem_name_ensure_class(fbx_elem
, b
'Model') if fbx_elem
else 'Unknown'
1513 self
.fbx_type
= fbx_elem
.props
[2] if fbx_elem
else None
1514 self
.fbx_elem
= fbx_elem
1516 self
.bl_data
= bl_data
1517 self
.bl_bone
= None # Name of bone if this is a bone (this may be different to fbx_name if there was a name conflict in Blender!)
1518 self
.fbx_transform_data
= fbx_transform_data
1519 self
.is_root
= False
1520 self
.is_bone
= is_bone
1521 self
.is_armature
= False
1522 self
.armature
= None # For bones only, relevant armature node.
1523 self
.has_bone_children
= False # True if the hierarchy below this node contains bones, important to support mixed hierarchies.
1524 self
.is_leaf
= False # True for leaf-bones added to the end of some bone chains to set the lengths.
1525 self
.pre_matrix
= None # correction matrix that needs to be applied before the FBX transform
1526 self
.bind_matrix
= None # for bones this is the matrix used to bind to the skin
1527 if fbx_transform_data
:
1528 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= blen_read_object_transform_do(fbx_transform_data
)
1530 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= (None, None, None)
1531 self
.post_matrix
= None # correction matrix that needs to be applied after the FBX transform
1532 self
.bone_child_matrix
= None # Objects attached to a bone end not the beginning, this matrix corrects for that
1534 # XXX Those two are to handle the fact that rigged meshes are not linked to their armature in FBX, which implies
1535 # that their animation is in global space (afaik...).
1536 # This is actually not really solvable currently, since anim_compensation_matrix is not valid if armature
1537 # itself is animated (we'd have to recompute global-to-local anim_compensation_matrix for each frame,
1538 # and for each armature action... beyond being an insane work).
1539 # Solution for now: do not read rigged meshes animations at all! sic...
1540 self
.anim_compensation_matrix
= None # a mesh moved in the hierarchy may have a different local matrix. This compensates animations for this.
1541 self
.is_global_animation
= False
1543 self
.meshes
= None # List of meshes influenced by this bone.
1544 self
.clusters
= [] # Deformer Cluster nodes
1545 self
.armature_setup
= {} # mesh and armature matrix when the mesh was bound
1555 def parent(self
, value
):
1556 if self
._parent
is not None:
1557 self
._parent
.children
.remove(self
)
1558 self
._parent
= value
1559 if self
._parent
is not None:
1560 self
._parent
.children
.append(self
)
1564 # Separating leaf status from ignore status itself.
1565 # Currently they are equivalent, but this may change in future.
1570 return self
.fbx_elem
.props
[1].decode()
1574 def print_info(self
, indent
=0):
1575 print(" " * indent
+ (self
.fbx_name
if self
.fbx_name
else "(Null)")
1576 + ("[root]" if self
.is_root
else "")
1577 + ("[leaf]" if self
.is_leaf
else "")
1578 + ("[ignore]" if self
.ignore
else "")
1579 + ("[armature]" if self
.is_armature
else "")
1580 + ("[bone]" if self
.is_bone
else "")
1581 + ("[HBC]" if self
.has_bone_children
else "")
1583 for c
in self
.children
:
1584 c
.print_info(indent
+ 1)
1586 def mark_leaf_bones(self
):
1587 if self
.is_bone
and len(self
.children
) == 1:
1588 child
= self
.children
[0]
1589 if child
.is_bone
and len(child
.children
) == 0:
1590 child
.is_leaf
= True
1591 for child
in self
.children
:
1592 child
.mark_leaf_bones()
1594 def do_bake_transform(self
, settings
):
1595 return (settings
.bake_space_transform
and self
.fbx_type
in (b
'Mesh', b
'Null') and
1596 not self
.is_armature
and not self
.is_bone
)
1598 def find_correction_matrix(self
, settings
, parent_correction_inv
=None):
1599 from bpy_extras
.io_utils
import axis_conversion
1601 if self
.parent
and (self
.parent
.is_root
or self
.parent
.do_bake_transform(settings
)):
1602 self
.pre_matrix
= settings
.global_matrix
1604 if parent_correction_inv
:
1605 self
.pre_matrix
= parent_correction_inv
@ (self
.pre_matrix
if self
.pre_matrix
else Matrix())
1607 correction_matrix
= None
1610 if settings
.automatic_bone_orientation
:
1611 # find best orientation to align bone with
1612 bone_children
= tuple(child
for child
in self
.children
if child
.is_bone
)
1613 if len(bone_children
) == 0:
1614 # no children, inherit the correction from parent (if possible)
1615 if self
.parent
and self
.parent
.is_bone
:
1616 correction_matrix
= parent_correction_inv
.inverted() if parent_correction_inv
else None
1618 # else find how best to rotate the bone to align the Y axis with the children
1619 best_axis
= (1, 0, 0)
1620 if len(bone_children
) == 1:
1621 vec
= bone_children
[0].get_bind_matrix().to_translation()
1622 best_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
1623 if abs(vec
[0]) > abs(vec
[1]):
1624 if abs(vec
[0]) > abs(vec
[2]):
1625 best_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
1626 elif abs(vec
[1]) > abs(vec
[2]):
1627 best_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
1629 # get the child directions once because they may be checked several times
1630 child_locs
= (child
.get_bind_matrix().to_translation() for child
in bone_children
)
1631 child_locs
= tuple(loc
.normalized() for loc
in child_locs
if loc
.magnitude
> 0.0)
1633 # I'm not sure which one I like better...
1638 s
= -1 if i
% 2 == 1 else 1
1639 test_axis
= Vector((s
if a
== 0 else 0, s
if a
== 1 else 0, s
if a
== 2 else 0))
1641 # find max angle to children
1643 for loc
in child_locs
:
1644 max_angle
= min(max_angle
, test_axis
.dot(loc
))
1646 # is it better than the last one?
1647 if best_angle
< max_angle
:
1648 best_angle
= max_angle
1649 best_axis
= test_axis
1652 for vec
in child_locs
:
1653 test_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
1654 if abs(vec
[0]) > abs(vec
[1]):
1655 if abs(vec
[0]) > abs(vec
[2]):
1656 test_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
1657 elif abs(vec
[1]) > abs(vec
[2]):
1658 test_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
1660 # find max angle to children
1662 for loc
in child_locs
:
1663 max_angle
= min(max_angle
, test_axis
.dot(loc
))
1665 # is it better than the last one?
1666 if best_angle
< max_angle
:
1667 best_angle
= max_angle
1668 best_axis
= test_axis
1670 # convert best_axis to axis string
1671 to_up
= 'Z' if best_axis
[2] >= 0 else '-Z'
1672 if abs(best_axis
[0]) > abs(best_axis
[1]):
1673 if abs(best_axis
[0]) > abs(best_axis
[2]):
1674 to_up
= 'X' if best_axis
[0] >= 0 else '-X'
1675 elif abs(best_axis
[1]) > abs(best_axis
[2]):
1676 to_up
= 'Y' if best_axis
[1] >= 0 else '-Y'
1677 to_forward
= 'X' if to_up
not in {'X', '-X'} else 'Y'
1679 # Build correction matrix
1680 if (to_up
, to_forward
) != ('Y', 'X'):
1681 correction_matrix
= axis_conversion(from_forward
='X',
1683 to_forward
=to_forward
,
1687 correction_matrix
= settings
.bone_correction_matrix
1689 # camera and light can be hard wired
1690 if self
.fbx_type
== b
'Camera':
1691 correction_matrix
= MAT_CONVERT_CAMERA
1692 elif self
.fbx_type
== b
'Light':
1693 correction_matrix
= MAT_CONVERT_LIGHT
1695 self
.post_matrix
= correction_matrix
1697 if self
.do_bake_transform(settings
):
1698 self
.post_matrix
= settings
.global_matrix_inv
@ (self
.post_matrix
if self
.post_matrix
else Matrix())
1701 correction_matrix_inv
= correction_matrix
.inverted_safe() if correction_matrix
else None
1702 for child
in self
.children
:
1703 child
.find_correction_matrix(settings
, correction_matrix_inv
)
1705 def find_armature_bones(self
, armature
):
1706 for child
in self
.children
:
1708 child
.armature
= armature
1709 child
.find_armature_bones(armature
)
1711 def find_armatures(self
):
1712 needs_armature
= False
1713 for child
in self
.children
:
1715 needs_armature
= True
1718 if self
.fbx_type
in {b
'Null', b
'Root'}:
1719 # if empty then convert into armature
1720 self
.is_armature
= True
1723 # otherwise insert a new node
1724 # XXX Maybe in case self is virtual FBX root node, we should instead add one armature per bone child?
1725 armature
= FbxImportHelperNode(None, None, None, False)
1726 armature
.fbx_name
= "Armature"
1727 armature
.is_armature
= True
1729 for child
in tuple(self
.children
):
1731 child
.parent
= armature
1733 armature
.parent
= self
1735 armature
.find_armature_bones(armature
)
1737 for child
in self
.children
:
1738 if child
.is_armature
or child
.is_bone
:
1740 child
.find_armatures()
1742 def find_bone_children(self
):
1743 has_bone_children
= False
1744 for child
in self
.children
:
1745 has_bone_children |
= child
.find_bone_children()
1746 self
.has_bone_children
= has_bone_children
1747 return self
.is_bone
or has_bone_children
1749 def find_fake_bones(self
, in_armature
=False):
1750 if in_armature
and not self
.is_bone
and self
.has_bone_children
:
1752 # if we are not a null node we need an intermediate node for the data
1753 if self
.fbx_type
not in {b
'Null', b
'Root'}:
1754 node
= FbxImportHelperNode(self
.fbx_elem
, self
.bl_data
, None, False)
1755 self
.fbx_elem
= None
1759 for child
in self
.children
:
1760 if child
.is_bone
or child
.has_bone_children
:
1767 if self
.is_armature
:
1769 for child
in self
.children
:
1770 child
.find_fake_bones(in_armature
)
1772 def get_world_matrix_as_parent(self
):
1773 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
1774 if self
.matrix_as_parent
:
1775 matrix
= matrix
@ self
.matrix_as_parent
1778 def get_world_matrix(self
):
1779 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
1781 matrix
= matrix
@ self
.matrix
1784 def get_matrix(self
):
1785 matrix
= self
.matrix
if self
.matrix
else Matrix()
1787 matrix
= self
.pre_matrix
@ matrix
1788 if self
.post_matrix
:
1789 matrix
= matrix
@ self
.post_matrix
1792 def get_bind_matrix(self
):
1793 matrix
= self
.bind_matrix
if self
.bind_matrix
else Matrix()
1795 matrix
= self
.pre_matrix
@ matrix
1796 if self
.post_matrix
:
1797 matrix
= matrix
@ self
.post_matrix
1800 def make_bind_pose_local(self
, parent_matrix
=None):
1801 if parent_matrix
is None:
1802 parent_matrix
= Matrix()
1804 if self
.bind_matrix
:
1805 bind_matrix
= parent_matrix
.inverted_safe() @ self
.bind_matrix
1807 bind_matrix
= self
.matrix
.copy() if self
.matrix
else None
1809 self
.bind_matrix
= bind_matrix
1811 parent_matrix
= parent_matrix
@ bind_matrix
1813 for child
in self
.children
:
1814 child
.make_bind_pose_local(parent_matrix
)
1816 def collect_skeleton_meshes(self
, meshes
):
1817 for _
, m
in self
.clusters
:
1819 for child
in self
.children
:
1820 child
.collect_skeleton_meshes(meshes
)
1822 def collect_armature_meshes(self
):
1823 if self
.is_armature
:
1824 armature_matrix_inv
= self
.get_world_matrix().inverted_safe()
1827 for child
in self
.children
:
1828 child
.collect_skeleton_meshes(meshes
)
1830 old_matrix
= m
.matrix
1831 m
.matrix
= armature_matrix_inv
@ m
.get_world_matrix()
1832 m
.anim_compensation_matrix
= old_matrix
.inverted_safe() @ m
.matrix
1833 m
.is_global_animation
= True
1835 self
.meshes
= meshes
1837 for child
in self
.children
:
1838 child
.collect_armature_meshes()
1840 def build_skeleton(self
, arm
, parent_matrix
, parent_bone_size
=1, force_connect_children
=False):
1841 def child_connect(par_bone
, child_bone
, child_head
, connect_ctx
):
1842 # child_bone or child_head may be None.
1843 force_connect_children
, connected
= connect_ctx
1844 if child_bone
is not None:
1845 child_bone
.parent
= par_bone
1846 child_head
= child_bone
.head
1848 if similar_values_iter(par_bone
.tail
, child_head
):
1849 if child_bone
is not None:
1850 child_bone
.use_connect
= True
1851 # Disallow any force-connection at this level from now on, since that child was 'really'
1852 # connected, we do not want to move current bone's tail anymore!
1854 elif force_connect_children
and connected
is not None:
1855 # We only store position where tail of par_bone should be in the end.
1856 # Actual tail moving and force connection of compatible child bones will happen
1857 # once all have been checked.
1858 if connected
is ...:
1859 connected
= ([child_head
.copy(), 1], [child_bone
] if child_bone
is not None else [])
1861 connected
[0][0] += child_head
1862 connected
[0][1] += 1
1863 if child_bone
is not None:
1864 connected
[1].append(child_bone
)
1865 connect_ctx
[1] = connected
1867 def child_connect_finalize(par_bone
, connect_ctx
):
1868 force_connect_children
, connected
= connect_ctx
1869 # Do nothing if force connection is not enabled!
1870 if force_connect_children
and connected
is not None and connected
is not ...:
1871 # Here again we have to be wary about zero-length bones!!!
1872 par_tail
= connected
[0][0] / connected
[0][1]
1873 if (par_tail
- par_bone
.head
).magnitude
< 1e-2:
1874 par_bone_vec
= (par_bone
.tail
- par_bone
.head
).normalized()
1875 par_tail
= par_bone
.head
+ par_bone_vec
* 0.01
1876 par_bone
.tail
= par_tail
1877 for child_bone
in connected
[1]:
1878 if similar_values_iter(par_tail
, child_bone
.head
):
1879 child_bone
.use_connect
= True
1881 # Create the (edit)bone.
1882 bone
= arm
.bl_data
.edit_bones
.new(name
=self
.fbx_name
)
1884 self
.bl_obj
= arm
.bl_obj
1885 self
.bl_data
= arm
.bl_data
1886 self
.bl_bone
= bone
.name
# Could be different from the FBX name!
1888 # get average distance to children
1891 for child
in self
.children
:
1893 bone_size
+= child
.get_bind_matrix().to_translation().magnitude
1896 bone_size
/= bone_count
1898 bone_size
= parent_bone_size
1900 # So that our bone gets its final length, but still Y-aligned in armature space.
1901 # 0-length bones are automatically collapsed into their parent when you leave edit mode,
1902 # so this enforces a minimum length.
1903 bone_tail
= Vector((0.0, 1.0, 0.0)) * max(0.01, bone_size
)
1904 bone
.tail
= bone_tail
1906 # And rotate/move it to its final "rest pose".
1907 bone_matrix
= parent_matrix
@ self
.get_bind_matrix().normalized()
1909 bone
.matrix
= bone_matrix
1911 # Correction for children attached to a bone. FBX expects to attach to the head of a bone,
1912 # while Blender attaches to the tail.
1913 self
.bone_child_matrix
= Matrix
.Translation(-bone_tail
)
1915 connect_ctx
= [force_connect_children
, ...]
1916 for child
in self
.children
:
1917 if child
.is_leaf
and force_connect_children
:
1918 # Arggggggggggggggggg! We do not want to create this bone, but we need its 'virtual head' location
1919 # to orient current one!!!
1920 child_head
= (bone_matrix
@ child
.get_bind_matrix().normalized()).translation
1921 child_connect(bone
, None, child_head
, connect_ctx
)
1922 elif child
.is_bone
and not child
.ignore
:
1923 child_bone
= child
.build_skeleton(arm
, bone_matrix
, bone_size
,
1924 force_connect_children
=force_connect_children
)
1925 # Connection to parent.
1926 child_connect(bone
, child_bone
, None, connect_ctx
)
1928 child_connect_finalize(bone
, connect_ctx
)
1931 def build_node_obj(self
, fbx_tmpl
, settings
):
1935 if self
.is_bone
or not self
.fbx_elem
:
1938 # create when linking since we need object data
1939 elem_name_utf8
= self
.fbx_name
1941 # Object data must be created already
1942 self
.bl_obj
= obj
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=self
.bl_data
)
1944 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
1945 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1950 obj
.color
[0:3] = elem_props_get_color_rgb(fbx_props
, b
'Color', (0.8, 0.8, 0.8))
1951 obj
.hide_viewport
= not bool(elem_props_get_visibility(fbx_props
, b
'Visibility', 1.0))
1953 obj
.matrix_basis
= self
.get_matrix()
1955 if settings
.use_custom_props
:
1956 blen_read_custom_properties(self
.fbx_elem
, obj
, settings
)
1960 def build_skeleton_children(self
, fbx_tmpl
, settings
, scene
, view_layer
):
1962 for child
in self
.children
:
1965 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
1968 # child is not a bone
1969 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
1974 for child
in self
.children
:
1977 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
1980 view_layer
.active_layer_collection
.collection
.objects
.link(obj
)
1981 obj
.select_set(True)
1985 def link_skeleton_children(self
, fbx_tmpl
, settings
, scene
):
1987 for child
in self
.children
:
1990 child_obj
= child
.bl_obj
1991 if child_obj
and child_obj
!= self
.bl_obj
:
1992 child_obj
.parent
= self
.bl_obj
# get the armature the bone belongs to
1993 child_obj
.parent_bone
= self
.bl_bone
1994 child_obj
.parent_type
= 'BONE'
1995 child_obj
.matrix_parent_inverse
= Matrix()
1997 # Blender attaches to the end of a bone, while FBX attaches to the start.
1998 # bone_child_matrix corrects for that.
1999 if child
.pre_matrix
:
2000 child
.pre_matrix
= self
.bone_child_matrix
@ child
.pre_matrix
2002 child
.pre_matrix
= self
.bone_child_matrix
2004 child_obj
.matrix_basis
= child
.get_matrix()
2005 child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2010 for child
in self
.children
:
2013 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2015 child_obj
.parent
= obj
2019 def set_pose_matrix(self
, arm
):
2020 pose_bone
= arm
.bl_obj
.pose
.bones
[self
.bl_bone
]
2021 pose_bone
.matrix_basis
= self
.get_bind_matrix().inverted_safe() @ self
.get_matrix()
2023 for child
in self
.children
:
2027 child
.set_pose_matrix(arm
)
2029 def merge_weights(self
, combined_weights
, fbx_cluster
):
2030 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
2031 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
2033 for index
, weight
in zip(indices
, weights
):
2034 w
= combined_weights
.get(index
)
2036 combined_weights
[index
] = [weight
]
2040 def set_bone_weights(self
):
2041 ignored_children
= tuple(child
for child
in self
.children
2042 if child
.is_bone
and child
.ignore
and len(child
.clusters
) > 0)
2044 if len(ignored_children
) > 0:
2045 # If we have an ignored child bone we need to merge their weights into the current bone weights.
2046 # This can happen both intentionally and accidentally when skinning a model. Either way, they
2047 # need to be moved into a parent bone or they cause animation glitches.
2048 for fbx_cluster
, meshes
in self
.clusters
:
2049 combined_weights
= {}
2050 self
.merge_weights(combined_weights
, fbx_cluster
)
2052 for child
in ignored_children
:
2053 for child_cluster
, child_meshes
in child
.clusters
:
2054 if not meshes
.isdisjoint(child_meshes
):
2055 self
.merge_weights(combined_weights
, child_cluster
)
2057 # combine child weights
2060 for i
, w
in combined_weights
.items():
2063 weights
.append(sum(w
) / len(w
))
2065 weights
.append(w
[0])
2067 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2069 # clusters that drive meshes not included in a parent don't need to be merged
2070 all_meshes
= set().union(*[meshes
for _
, meshes
in self
.clusters
])
2071 for child
in ignored_children
:
2072 for child_cluster
, child_meshes
in child
.clusters
:
2073 if all_meshes
.isdisjoint(child_meshes
):
2074 indices
= elem_prop_first(elem_find_first(child_cluster
, b
'Indexes', default
=None), default
=())
2075 weights
= elem_prop_first(elem_find_first(child_cluster
, b
'Weights', default
=None), default
=())
2076 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in child_meshes
])
2078 # set the vertex weights on meshes
2079 for fbx_cluster
, meshes
in self
.clusters
:
2080 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
2081 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
2082 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2084 for child
in self
.children
:
2085 if child
.is_bone
and not child
.ignore
:
2086 child
.set_bone_weights()
2088 def build_hierarchy(self
, fbx_tmpl
, settings
, scene
, view_layer
):
2089 if self
.is_armature
:
2090 # create when linking since we need object data
2091 elem_name_utf8
= self
.fbx_name
2093 self
.bl_data
= arm_data
= bpy
.data
.armatures
.new(name
=elem_name_utf8
)
2095 # Object data must be created already
2096 self
.bl_obj
= arm
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=arm_data
)
2098 arm
.matrix_basis
= self
.get_matrix()
2101 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
2102 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2104 if settings
.use_custom_props
:
2105 blen_read_custom_properties(self
.fbx_elem
, arm
, settings
)
2108 view_layer
.active_layer_collection
.collection
.objects
.link(arm
)
2109 arm
.select_set(True)
2113 # Switch to Edit mode.
2114 view_layer
.objects
.active
= arm
2115 is_hidden
= arm
.hide_viewport
2116 arm
.hide_viewport
= False # Can't switch to Edit mode hidden objects...
2117 bpy
.ops
.object.mode_set(mode
='EDIT')
2119 for child
in self
.children
:
2123 child
.build_skeleton(self
, Matrix(), force_connect_children
=settings
.force_connect_children
)
2125 bpy
.ops
.object.mode_set(mode
='OBJECT')
2127 arm
.hide_viewport
= is_hidden
2130 for child
in self
.children
:
2134 child
.set_pose_matrix(self
)
2136 # Add bone children:
2137 for child
in self
.children
:
2140 child_obj
= child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2143 elif self
.fbx_elem
and not self
.is_bone
:
2144 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
2146 # walk through children
2147 for child
in self
.children
:
2148 child
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2151 view_layer
.active_layer_collection
.collection
.objects
.link(obj
)
2152 obj
.select_set(True)
2156 for child
in self
.children
:
2157 child
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2161 def link_hierarchy(self
, fbx_tmpl
, settings
, scene
):
2162 if self
.is_armature
:
2165 # Link bone children:
2166 for child
in self
.children
:
2169 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2171 child_obj
.parent
= arm
2173 # Add armature modifiers to the meshes
2175 for mesh
in self
.meshes
:
2176 (mmat
, amat
) = mesh
.armature_setup
[self
]
2177 me_obj
= mesh
.bl_obj
2179 # bring global armature & mesh matrices into *Blender* global space.
2180 # Note: Usage of matrix_geom (local 'diff' transform) here is quite brittle.
2181 # Among other things, why in hell isn't it taken into account by bindpose & co???
2182 # Probably because org app (max) handles it completely aside from any parenting stuff,
2183 # which we obviously cannot do in Blender. :/
2185 amat
= self
.bind_matrix
2186 amat
= settings
.global_matrix
@ (Matrix() if amat
is None else amat
)
2187 if self
.matrix_geom
:
2188 amat
= amat
@ self
.matrix_geom
2189 mmat
= settings
.global_matrix
@ mmat
2190 if mesh
.matrix_geom
:
2191 mmat
= mmat
@ mesh
.matrix_geom
2193 # Now that we have armature and mesh in there (global) bind 'state' (matrix),
2194 # we can compute inverse parenting matrix of the mesh.
2195 me_obj
.matrix_parent_inverse
= amat
.inverted_safe() @ mmat
@ me_obj
.matrix_basis
.inverted_safe()
2197 mod
= mesh
.bl_obj
.modifiers
.new(arm
.name
, 'ARMATURE')
2200 # Add bone weights to the deformers
2201 for child
in self
.children
:
2205 child
.set_bone_weights()
2211 # walk through children
2212 for child
in self
.children
:
2213 child_obj
= child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2215 child_obj
.parent
= obj
2219 for child
in self
.children
:
2220 child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2225 def is_ascii(filepath
, size
):
2226 with
open(filepath
, 'r', encoding
="utf-8") as f
:
2230 except UnicodeDecodeError:
2236 def load(operator
, context
, filepath
="",
2237 use_manual_orientation
=False,
2241 bake_space_transform
=False,
2242 use_custom_normals
=True,
2243 use_image_search
=False,
2244 use_alpha_decals
=False,
2248 use_custom_props
=True,
2249 use_custom_props_enum_as_string
=True,
2250 ignore_leaf_bones
=False,
2251 force_connect_children
=False,
2252 automatic_bone_orientation
=False,
2253 primary_bone_axis
='Y',
2254 secondary_bone_axis
='X',
2255 use_prepost_rot
=True):
2258 fbx_elem_nil
= FBXElem('', (), (), ())
2262 from bpy_extras
.io_utils
import axis_conversion
2264 from . import parse_fbx
2265 from .fbx_utils
import RIGHT_HAND_AXES
, FBX_FRAMERATES
2267 start_time_proc
= time
.process_time()
2268 start_time_sys
= time
.time()
2272 perfmon
.step("FBX Import: start importing %s" % filepath
)
2275 # detect ascii files
2276 if is_ascii(filepath
, 24):
2277 operator
.report({'ERROR'}, "ASCII FBX files are not supported %r" % filepath
)
2278 return {'CANCELLED'}
2281 elem_root
, version
= parse_fbx
.parse(filepath
)
2282 except Exception as e
:
2284 traceback
.print_exc()
2286 operator
.report({'ERROR'}, "Couldn't open file %r (%s)" % (filepath
, e
))
2287 return {'CANCELLED'}
2290 operator
.report({'ERROR'}, "Version %r unsupported, must be %r or later" % (version
, 7100))
2291 return {'CANCELLED'}
2293 print("FBX version: %r" % version
)
2295 if bpy
.ops
.object.mode_set
.poll():
2296 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
2299 if bpy
.ops
.object.select_all
.poll():
2300 bpy
.ops
.object.select_all(action
='DESELECT')
2302 basedir
= os
.path
.dirname(filepath
)
2304 nodal_material_wrap_map
= {}
2307 # Tables: (FBX_byte_id -> [FBX_data, None or Blender_datablock])
2308 fbx_table_nodes
= {}
2310 if use_alpha_decals
:
2311 material_decals
= set()
2313 material_decals
= None
2315 scene
= context
.scene
2316 view_layer
= context
.view_layer
2318 # #### Get some info from GlobalSettings.
2320 perfmon
.step("FBX import: Prepare...")
2322 fbx_settings
= elem_find_first(elem_root
, b
'GlobalSettings')
2323 fbx_settings_props
= elem_find_first(fbx_settings
, b
'Properties70')
2324 if fbx_settings
is None or fbx_settings_props
is None:
2325 operator
.report({'ERROR'}, "No 'GlobalSettings' found in file %r" % filepath
)
2326 return {'CANCELLED'}
2328 # FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
2329 unit_scale
= elem_props_get_number(fbx_settings_props
, b
'UnitScaleFactor', 1.0)
2330 unit_scale_org
= elem_props_get_number(fbx_settings_props
, b
'OriginalUnitScaleFactor', 1.0)
2331 global_scale
*= (unit_scale
/ units_blender_to_fbx_factor(context
.scene
))
2332 # Compute global matrix and scale.
2333 if not use_manual_orientation
:
2334 axis_forward
= (elem_props_get_integer(fbx_settings_props
, b
'FrontAxis', 1),
2335 elem_props_get_integer(fbx_settings_props
, b
'FrontAxisSign', 1))
2336 axis_up
= (elem_props_get_integer(fbx_settings_props
, b
'UpAxis', 2),
2337 elem_props_get_integer(fbx_settings_props
, b
'UpAxisSign', 1))
2338 axis_coord
= (elem_props_get_integer(fbx_settings_props
, b
'CoordAxis', 0),
2339 elem_props_get_integer(fbx_settings_props
, b
'CoordAxisSign', 1))
2340 axis_key
= (axis_up
, axis_forward
, axis_coord
)
2341 axis_up
, axis_forward
= {v
: k
for k
, v
in RIGHT_HAND_AXES
.items()}.get(axis_key
, ('Z', 'Y'))
2342 global_matrix
= (Matrix
.Scale(global_scale
, 4) @
2343 axis_conversion(from_forward
=axis_forward
, from_up
=axis_up
).to_4x4())
2345 # To cancel out unwanted rotation/scale on nodes.
2346 global_matrix_inv
= global_matrix
.inverted()
2347 # For transforming mesh normals.
2348 global_matrix_inv_transposed
= global_matrix_inv
.transposed()
2350 # Compute bone correction matrix
2351 bone_correction_matrix
= None # None means no correction/identity
2352 if not automatic_bone_orientation
:
2353 if (primary_bone_axis
, secondary_bone_axis
) != ('Y', 'X'):
2354 bone_correction_matrix
= axis_conversion(from_forward
='X',
2356 to_forward
=secondary_bone_axis
,
2357 to_up
=primary_bone_axis
,
2360 # Compute framerate settings.
2361 custom_fps
= elem_props_get_number(fbx_settings_props
, b
'CustomFrameRate', 25.0)
2362 time_mode
= elem_props_get_enum(fbx_settings_props
, b
'TimeMode')
2363 real_fps
= {eid
: val
for val
, eid
in FBX_FRAMERATES
[1:]}.get(time_mode
, custom_fps
)
2366 scene
.render
.fps
= round(real_fps
)
2367 scene
.render
.fps_base
= scene
.render
.fps
/ real_fps
2369 # store global settings that need to be accessed during conversion
2370 settings
= FBXImportSettings(
2371 operator
.report
, (axis_up
, axis_forward
), global_matrix
, global_scale
,
2372 bake_space_transform
, global_matrix_inv
, global_matrix_inv_transposed
,
2373 use_custom_normals
, use_image_search
,
2374 use_alpha_decals
, decal_offset
,
2375 use_anim
, anim_offset
,
2376 use_custom_props
, use_custom_props_enum_as_string
,
2377 nodal_material_wrap_map
, image_cache
,
2378 ignore_leaf_bones
, force_connect_children
, automatic_bone_orientation
, bone_correction_matrix
,
2382 # #### And now, the "real" data.
2384 perfmon
.step("FBX import: Templates...")
2386 fbx_defs
= elem_find_first(elem_root
, b
'Definitions') # can be None
2387 fbx_nodes
= elem_find_first(elem_root
, b
'Objects')
2388 fbx_connections
= elem_find_first(elem_root
, b
'Connections')
2390 if fbx_nodes
is None:
2391 operator
.report({'ERROR'}, "No 'Objects' found in file %r" % filepath
)
2392 return {'CANCELLED'}
2393 if fbx_connections
is None:
2394 operator
.report({'ERROR'}, "No 'Connections' found in file %r" % filepath
)
2395 return {'CANCELLED'}
2398 # First load property templates
2399 # Load 'PropertyTemplate' values.
2400 # Key is a tuple, (ObjectType, FBXNodeType)
2401 # eg, (b'Texture', b'KFbxFileTexture')
2402 # (b'Geometry', b'KFbxMesh')
2406 if fbx_defs
is not None:
2407 for fbx_def
in fbx_defs
.elems
:
2408 if fbx_def
.id == b
'ObjectType':
2409 for fbx_subdef
in fbx_def
.elems
:
2410 if fbx_subdef
.id == b
'PropertyTemplate':
2411 assert(fbx_def
.props_type
== b
'S')
2412 assert(fbx_subdef
.props_type
== b
'S')
2413 # (b'Texture', b'KFbxFileTexture') - eg.
2414 key
= fbx_def
.props
[0], fbx_subdef
.props
[0]
2415 fbx_templates
[key
] = fbx_subdef
2418 def fbx_template_get(key
):
2419 ret
= fbx_templates
.get(key
, fbx_elem_nil
)
2420 if ret
is fbx_elem_nil
:
2421 # Newest FBX (7.4 and above) use no more 'K' in their type names...
2422 key
= (key
[0], key
[1][1:])
2423 return fbx_templates
.get(key
, fbx_elem_nil
)
2426 perfmon
.step("FBX import: Nodes...")
2429 # Build FBX node-table
2431 for fbx_obj
in fbx_nodes
.elems
:
2432 # TODO, investigate what other items after first 3 may be
2433 assert(fbx_obj
.props_type
[:3] == b
'LSS')
2434 fbx_uuid
= elem_uuid(fbx_obj
)
2435 fbx_table_nodes
[fbx_uuid
] = [fbx_obj
, None]
2440 # http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=
2441 # WS73099cc142f487551fea285e1221e4f9ff8-7fda.htm,topicNumber=d0e6388
2443 perfmon
.step("FBX import: Connections...")
2445 fbx_connection_map
= {}
2446 fbx_connection_map_reverse
= {}
2449 for fbx_link
in fbx_connections
.elems
:
2450 c_type
= fbx_link
.props
[0]
2451 if fbx_link
.props_type
[1:3] == b
'LL':
2452 c_src
, c_dst
= fbx_link
.props
[1:3]
2453 fbx_connection_map
.setdefault(c_src
, []).append((c_dst
, fbx_link
))
2454 fbx_connection_map_reverse
.setdefault(c_dst
, []).append((c_src
, fbx_link
))
2457 perfmon
.step("FBX import: Meshes...")
2462 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxMesh'))
2464 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2465 fbx_obj
, blen_data
= fbx_item
2466 if fbx_obj
.id != b
'Geometry':
2468 if fbx_obj
.props
[-1] == b
'Mesh':
2469 assert(blen_data
is None)
2470 fbx_item
[1] = blen_read_geom(fbx_tmpl
, fbx_obj
, settings
)
2473 perfmon
.step("FBX import: Materials & Textures...")
2476 # Load material data
2478 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
2479 # b'KFbxSurfaceLambert'
2481 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2482 fbx_obj
, blen_data
= fbx_item
2483 if fbx_obj
.id != b
'Material':
2485 assert(blen_data
is None)
2486 fbx_item
[1] = blen_read_material(fbx_tmpl
, fbx_obj
, settings
)
2490 # Load image & textures data
2492 fbx_tmpl_tex
= fbx_template_get((b
'Texture', b
'KFbxFileTexture'))
2493 fbx_tmpl_img
= fbx_template_get((b
'Video', b
'KFbxVideo'))
2495 # Important to run all 'Video' ones first, embedded images are stored in those nodes.
2496 # XXX Note we simplify things here, assuming both matching Video and Texture will use same file path,
2497 # this may be a bit weak, if issue arise we'll fallback to plain connection stuff...
2498 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2499 fbx_obj
, blen_data
= fbx_item
2500 if fbx_obj
.id != b
'Video':
2502 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_img
, fbx_obj
, basedir
, settings
)
2503 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2504 fbx_obj
, blen_data
= fbx_item
2505 if fbx_obj
.id != b
'Texture':
2507 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_tex
, fbx_obj
, basedir
, settings
)
2510 perfmon
.step("FBX import: Cameras & Lamps...")
2515 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxCamera'))
2517 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2518 fbx_obj
, blen_data
= fbx_item
2519 if fbx_obj
.id != b
'NodeAttribute':
2521 if fbx_obj
.props
[-1] == b
'Camera':
2522 assert(blen_data
is None)
2523 fbx_item
[1] = blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
)
2529 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxLight'))
2531 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2532 fbx_obj
, blen_data
= fbx_item
2533 if fbx_obj
.id != b
'NodeAttribute':
2535 if fbx_obj
.props
[-1] == b
'Light':
2536 assert(blen_data
is None)
2537 fbx_item
[1] = blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
)
2542 def connection_filter_ex(fbx_uuid
, fbx_id
, dct
):
2543 return [(c_found
[0], c_found
[1], c_type
)
2544 for (c_uuid
, c_type
) in dct
.get(fbx_uuid
, ())
2545 # 0 is used for the root node, which isnt in fbx_table_nodes
2546 for c_found
in (() if c_uuid
is 0 else (fbx_table_nodes
.get(c_uuid
, (None, None)),))
2547 if (fbx_id
is None) or (c_found
[0] and c_found
[0].id == fbx_id
)]
2549 def connection_filter_forward(fbx_uuid
, fbx_id
):
2550 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map
)
2552 def connection_filter_reverse(fbx_uuid
, fbx_id
):
2553 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map_reverse
)
2555 perfmon
.step("FBX import: Objects & Armatures...")
2557 # -- temporary helper hierarchy to build armatures and objects from
2558 # lookup from uuid to helper node. Used to build parent-child relations and later to look up animated nodes.
2559 fbx_helper_nodes
= {}
2562 # We build an intermediate hierarchy used to:
2563 # - Calculate and store bone orientation correction matrices. The same matrices will be reused for animation.
2564 # - Find/insert armature nodes.
2565 # - Filter leaf bones.
2568 fbx_helper_nodes
[0] = root_helper
= FbxImportHelperNode(None, None, None, False)
2569 root_helper
.is_root
= True
2572 fbx_tmpl
= fbx_template_get((b
'Model', b
'KFbxNode'))
2573 for a_uuid
, a_item
in fbx_table_nodes
.items():
2574 fbx_obj
, bl_data
= a_item
2575 if fbx_obj
is None or fbx_obj
.id != b
'Model':
2578 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
2579 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2581 transform_data
= blen_read_object_transform_preprocess(fbx_props
, fbx_obj
, Matrix(), use_prepost_rot
)
2582 # Note: 'Root' "bones" are handled as (armature) objects.
2583 # Note: See T46912 for first FBX file I ever saw with 'Limb' bones - thought those were totally deprecated.
2584 is_bone
= fbx_obj
.props
[2] in {b
'LimbNode', b
'Limb'}
2585 fbx_helper_nodes
[a_uuid
] = FbxImportHelperNode(fbx_obj
, bl_data
, transform_data
, is_bone
)
2587 # add parent-child relations and add blender data to the node
2588 for fbx_link
in fbx_connections
.elems
:
2589 if fbx_link
.props
[0] != b
'OO':
2591 if fbx_link
.props_type
[1:3] == b
'LL':
2592 c_src
, c_dst
= fbx_link
.props
[1:3]
2593 parent
= fbx_helper_nodes
.get(c_dst
)
2597 child
= fbx_helper_nodes
.get(c_src
)
2599 # add blender data (meshes, lights, cameras, etc.) to a helper node
2600 fbx_sdata
, bl_data
= p_item
= fbx_table_nodes
.get(c_src
, (None, None))
2601 if fbx_sdata
is None:
2603 if fbx_sdata
.id not in {b
'Geometry', b
'NodeAttribute'}:
2605 parent
.bl_data
= bl_data
2608 child
.parent
= parent
2610 # find armatures (either an empty below a bone or a new node inserted at the bone
2611 root_helper
.find_armatures()
2613 # mark nodes that have bone children
2614 root_helper
.find_bone_children()
2616 # mark nodes that need a bone to attach child-bones to
2617 root_helper
.find_fake_bones()
2619 # mark leaf nodes that are only required to mark the end of their parent bone
2620 if settings
.ignore_leaf_bones
:
2621 root_helper
.mark_leaf_bones()
2623 # What a mess! Some bones have several BindPoses, some have none, clusters contain a bind pose as well,
2624 # and you can have several clusters per bone!
2625 # Maybe some conversion can be applied to put them all into the same frame of reference?
2627 # get the bind pose from pose elements
2628 for a_uuid
, a_item
in fbx_table_nodes
.items():
2629 fbx_obj
, bl_data
= a_item
2632 if fbx_obj
.id != b
'Pose':
2634 if fbx_obj
.props
[2] != b
'BindPose':
2636 for fbx_pose_node
in fbx_obj
.elems
:
2637 if fbx_pose_node
.id != b
'PoseNode':
2639 node_elem
= elem_find_first(fbx_pose_node
, b
'Node')
2640 node
= elem_uuid(node_elem
)
2641 matrix_elem
= elem_find_first(fbx_pose_node
, b
'Matrix')
2642 matrix
= array_to_matrix4(matrix_elem
.props
[0]) if matrix_elem
else None
2643 bone
= fbx_helper_nodes
.get(node
)
2645 # Store the matrix in the helper node.
2646 # There may be several bind pose matrices for the same node, but in tests they seem to be identical.
2647 bone
.bind_matrix
= matrix
# global space
2649 # get clusters and bind pose
2650 for helper_uuid
, helper_node
in fbx_helper_nodes
.items():
2651 if not helper_node
.is_bone
:
2653 for cluster_uuid
, cluster_link
in fbx_connection_map
.get(helper_uuid
, ()):
2654 if cluster_link
.props
[0] != b
'OO':
2656 fbx_cluster
, _
= fbx_table_nodes
.get(cluster_uuid
, (None, None))
2657 if fbx_cluster
is None or fbx_cluster
.id != b
'Deformer' or fbx_cluster
.props
[2] != b
'Cluster':
2660 # Get the bind pose from the cluster:
2661 tx_mesh_elem
= elem_find_first(fbx_cluster
, b
'Transform', default
=None)
2662 tx_mesh
= array_to_matrix4(tx_mesh_elem
.props
[0]) if tx_mesh_elem
else Matrix()
2664 tx_bone_elem
= elem_find_first(fbx_cluster
, b
'TransformLink', default
=None)
2665 tx_bone
= array_to_matrix4(tx_bone_elem
.props
[0]) if tx_bone_elem
else None
2667 tx_arm_elem
= elem_find_first(fbx_cluster
, b
'TransformAssociateModel', default
=None)
2668 tx_arm
= array_to_matrix4(tx_arm_elem
.props
[0]) if tx_arm_elem
else None
2670 mesh_matrix
= tx_mesh
2671 armature_matrix
= tx_arm
2674 mesh_matrix
= tx_bone
@ mesh_matrix
2675 helper_node
.bind_matrix
= tx_bone
# overwrite the bind matrix
2677 # Get the meshes driven by this cluster: (Shouldn't that be only one?)
2679 for skin_uuid
, skin_link
in fbx_connection_map
.get(cluster_uuid
):
2680 if skin_link
.props
[0] != b
'OO':
2682 fbx_skin
, _
= fbx_table_nodes
.get(skin_uuid
, (None, None))
2683 if fbx_skin
is None or fbx_skin
.id != b
'Deformer' or fbx_skin
.props
[2] != b
'Skin':
2685 for mesh_uuid
, mesh_link
in fbx_connection_map
.get(skin_uuid
):
2686 if mesh_link
.props
[0] != b
'OO':
2688 fbx_mesh
, _
= fbx_table_nodes
.get(mesh_uuid
, (None, None))
2689 if fbx_mesh
is None or fbx_mesh
.id != b
'Geometry' or fbx_mesh
.props
[2] != b
'Mesh':
2691 for object_uuid
, object_link
in fbx_connection_map
.get(mesh_uuid
):
2692 if object_link
.props
[0] != b
'OO':
2694 mesh_node
= fbx_helper_nodes
[object_uuid
]
2697 # If we get a valid mesh matrix (in bone space), store armature and
2698 # mesh global matrices, we need them to compute mesh's matrix_parent_inverse
2699 # when actually binding them via the modifier.
2700 # Note we assume all bones were bound with the same mesh/armature (global) matrix,
2701 # we do not support otherwise in Blender anyway!
2702 mesh_node
.armature_setup
[helper_node
.armature
] = (mesh_matrix
, armature_matrix
)
2703 meshes
.add(mesh_node
)
2705 helper_node
.clusters
.append((fbx_cluster
, meshes
))
2707 # convert bind poses from global space into local space
2708 root_helper
.make_bind_pose_local()
2710 # collect armature meshes
2711 root_helper
.collect_armature_meshes()
2713 # find the correction matrices to align FBX objects with their Blender equivalent
2714 root_helper
.find_correction_matrix(settings
)
2716 # build the Object/Armature/Bone hierarchy
2717 root_helper
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2719 # Link the Object/Armature/Bone hierarchy
2720 root_helper
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2722 # root_helper.print_info(0)
2725 perfmon
.step("FBX import: ShapeKeys...")
2727 # We can handle shapes.
2728 blend_shape_channels
= {} # We do not need Shapes themselves, but keyblocks, for anim.
2731 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxShape'))
2733 for s_uuid
, s_item
in fbx_table_nodes
.items():
2734 fbx_sdata
, bl_sdata
= s_item
= fbx_table_nodes
.get(s_uuid
, (None, None))
2735 if fbx_sdata
is None or fbx_sdata
.id != b
'Geometry' or fbx_sdata
.props
[2] != b
'Shape':
2738 # shape -> blendshapechannel -> blendshape -> mesh.
2739 for bc_uuid
, bc_ctype
in fbx_connection_map
.get(s_uuid
, ()):
2740 if bc_ctype
.props
[0] != b
'OO':
2742 fbx_bcdata
, _bl_bcdata
= fbx_table_nodes
.get(bc_uuid
, (None, None))
2743 if fbx_bcdata
is None or fbx_bcdata
.id != b
'Deformer' or fbx_bcdata
.props
[2] != b
'BlendShapeChannel':
2747 for bs_uuid
, bs_ctype
in fbx_connection_map
.get(bc_uuid
, ()):
2748 if bs_ctype
.props
[0] != b
'OO':
2750 fbx_bsdata
, _bl_bsdata
= fbx_table_nodes
.get(bs_uuid
, (None, None))
2751 if fbx_bsdata
is None or fbx_bsdata
.id != b
'Deformer' or fbx_bsdata
.props
[2] != b
'BlendShape':
2753 for m_uuid
, m_ctype
in fbx_connection_map
.get(bs_uuid
, ()):
2754 if m_ctype
.props
[0] != b
'OO':
2756 fbx_mdata
, bl_mdata
= fbx_table_nodes
.get(m_uuid
, (None, None))
2757 if fbx_mdata
is None or fbx_mdata
.id != b
'Geometry' or fbx_mdata
.props
[2] != b
'Mesh':
2759 # Blenmeshes are assumed already created at that time!
2760 assert(isinstance(bl_mdata
, bpy
.types
.Mesh
))
2761 # And we have to find all objects using this mesh!
2763 for o_uuid
, o_ctype
in fbx_connection_map
.get(m_uuid
, ()):
2764 if o_ctype
.props
[0] != b
'OO':
2766 node
= fbx_helper_nodes
[o_uuid
]
2768 objects
.append(node
)
2769 meshes
.append((bl_mdata
, objects
))
2770 # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
2772 # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
2773 keyblocks
= blen_read_shape(fbx_tmpl
, fbx_sdata
, fbx_bcdata
, meshes
, scene
)
2774 blend_shape_channels
[bc_uuid
] = keyblocks
2778 perfmon
.step("FBX import: Animations...")
2782 fbx_tmpl_astack
= fbx_template_get((b
'AnimationStack', b
'FbxAnimStack'))
2783 fbx_tmpl_alayer
= fbx_template_get((b
'AnimationLayer', b
'FbxAnimLayer'))
2787 for as_uuid
, fbx_asitem
in fbx_table_nodes
.items():
2788 fbx_asdata
, _blen_data
= fbx_asitem
2789 if fbx_asdata
.id != b
'AnimationStack' or fbx_asdata
.props
[2] != b
'':
2791 stacks
[as_uuid
] = (fbx_asitem
, {})
2794 # (mixing is completely ignored for now, each layer results in an independent set of actions).
2795 def get_astacks_from_alayer(al_uuid
):
2796 for as_uuid
, as_ctype
in fbx_connection_map
.get(al_uuid
, ()):
2797 if as_ctype
.props
[0] != b
'OO':
2799 fbx_asdata
, _bl_asdata
= fbx_table_nodes
.get(as_uuid
, (None, None))
2800 if (fbx_asdata
is None or fbx_asdata
.id != b
'AnimationStack' or
2801 fbx_asdata
.props
[2] != b
'' or as_uuid
not in stacks
):
2804 for al_uuid
, fbx_alitem
in fbx_table_nodes
.items():
2805 fbx_aldata
, _blen_data
= fbx_alitem
2806 if fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
2808 for as_uuid
in get_astacks_from_alayer(al_uuid
):
2809 _fbx_asitem
, alayers
= stacks
[as_uuid
]
2810 alayers
[al_uuid
] = (fbx_alitem
, {})
2812 # AnimationCurveNodes (also the ones linked to actual animated data!).
2814 for acn_uuid
, fbx_acnitem
in fbx_table_nodes
.items():
2815 fbx_acndata
, _blen_data
= fbx_acnitem
2816 if fbx_acndata
.id != b
'AnimationCurveNode' or fbx_acndata
.props
[2] != b
'':
2818 cnode
= curvenodes
[acn_uuid
] = {}
2820 for n_uuid
, n_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
2821 if n_ctype
.props
[0] != b
'OP':
2823 lnk_prop
= n_ctype
.props
[3]
2824 if lnk_prop
in {b
'Lcl Translation', b
'Lcl Rotation', b
'Lcl Scaling'}:
2825 # n_uuid can (????) be linked to root '0' node, instead of a mere object node... See T41712.
2826 ob
= fbx_helper_nodes
.get(n_uuid
, None)
2827 if ob
is None or ob
.is_root
:
2829 items
.append((ob
, lnk_prop
))
2830 elif lnk_prop
== b
'DeformPercent': # Shape keys.
2831 keyblocks
= blend_shape_channels
.get(n_uuid
, None)
2832 if keyblocks
is None:
2834 items
+= [(kb
, lnk_prop
) for kb
in keyblocks
]
2835 elif lnk_prop
== b
'FocalLength': # Camera lens.
2836 from bpy
.types
import Camera
2837 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
2838 if fbx_item
is None or not isinstance(fbx_item
[1], Camera
):
2841 items
.append((cam
, lnk_prop
))
2842 elif lnk_prop
== b
'DiffuseColor':
2843 from bpy
.types
import Material
2844 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
2845 if fbx_item
is None or not isinstance(fbx_item
[1], Material
):
2848 items
.append((mat
, lnk_prop
))
2849 print("WARNING! Importing material's animation is not supported for Nodal materials...")
2850 for al_uuid
, al_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
2851 if al_ctype
.props
[0] != b
'OO':
2853 fbx_aldata
, _blen_aldata
= fbx_alitem
= fbx_table_nodes
.get(al_uuid
, (None, None))
2854 if fbx_aldata
is None or fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
2856 for as_uuid
in get_astacks_from_alayer(al_uuid
):
2857 _fbx_alitem
, anim_items
= stacks
[as_uuid
][1][al_uuid
]
2858 assert(_fbx_alitem
== fbx_alitem
)
2859 for item
, item_prop
in items
:
2860 # No need to keep curvenode FBX data here, contains nothing useful for us.
2861 anim_items
.setdefault(item
, {})[acn_uuid
] = (cnode
, item_prop
)
2863 # AnimationCurves (real animation data).
2864 for ac_uuid
, fbx_acitem
in fbx_table_nodes
.items():
2865 fbx_acdata
, _blen_data
= fbx_acitem
2866 if fbx_acdata
.id != b
'AnimationCurve' or fbx_acdata
.props
[2] != b
'':
2868 for acn_uuid
, acn_ctype
in fbx_connection_map
.get(ac_uuid
, ()):
2869 if acn_ctype
.props
[0] != b
'OP':
2871 fbx_acndata
, _bl_acndata
= fbx_table_nodes
.get(acn_uuid
, (None, None))
2872 if (fbx_acndata
is None or fbx_acndata
.id != b
'AnimationCurveNode' or
2873 fbx_acndata
.props
[2] != b
'' or acn_uuid
not in curvenodes
):
2875 # Note this is an infamous simplification of the compound props stuff,
2876 # seems to be standard naming but we'll probably have to be smarter to handle more exotic files?
2878 b
'd|X': 0, b
'd|Y': 1, b
'd|Z': 2,
2879 b
'd|DeformPercent': 0,
2881 }.get(acn_ctype
.props
[3], None)
2884 curvenodes
[acn_uuid
][ac_uuid
] = (fbx_acitem
, channel
)
2886 # And now that we have sorted all this, apply animations!
2887 blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, settings
.anim_offset
)
2891 perfmon
.step("FBX import: Assign materials...")
2894 # link Material's to Geometry (via Model's)
2895 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2896 fbx_obj
, blen_data
= fbx_item
2897 if fbx_obj
.id != b
'Geometry':
2900 mesh
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
2902 # can happen in rare cases
2906 # In Blender, we link materials to data, typically (meshes), while in FBX they are linked to objects...
2907 # So we have to be careful not to re-add endlessly the same material to a mesh!
2908 # This can easily happen with 'baked' dupliobjects, see T44386.
2909 # TODO: add an option to link materials to objects in Blender instead?
2910 done_materials
= set()
2912 for (fbx_lnk
, fbx_lnk_item
, fbx_lnk_type
) in connection_filter_forward(fbx_uuid
, b
'Model'):
2914 fbx_lnk_uuid
= elem_uuid(fbx_lnk
)
2915 for (fbx_lnk_material
, material
, fbx_lnk_material_type
) in connection_filter_reverse(fbx_lnk_uuid
, b
'Material'):
2916 if material
not in done_materials
:
2917 mesh
.materials
.append(material
)
2918 done_materials
.add(material
)
2920 # We have to validate mesh polygons' ma_idx, see T41015!
2921 # Some FBX seem to have an extra 'default' material which is not defined in FBX file.
2922 if mesh
.validate_material_indices():
2923 print("WARNING: mesh '%s' had invalid material indices, those were reset to first material" % mesh
.name
)
2926 perfmon
.step("FBX import: Assign textures...")
2929 material_images
= {}
2931 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
2932 # b'KFbxSurfaceLambert'
2934 def texture_mapping_set(fbx_obj
, node_texture
):
2935 assert(fbx_obj
.id == b
'Texture')
2937 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
2938 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2939 loc
= elem_props_get_vector_3d(fbx_props
, b
'Translation', (0.0, 0.0, 0.0))
2940 rot
= tuple(-r
for r
in elem_props_get_vector_3d(fbx_props
, b
'Rotation', (0.0, 0.0, 0.0)))
2941 scale
= tuple(((1.0 / s
) if s
!= 0.0 else 1.0)
2942 for s
in elem_props_get_vector_3d(fbx_props
, b
'Scaling', (1.0, 1.0, 1.0)))
2943 clamp_uv
= (bool(elem_props_get_enum(fbx_props
, b
'WrapModeU', 0)),
2944 bool(elem_props_get_enum(fbx_props
, b
'WrapModeV', 0)))
2946 if (loc
== (0.0, 0.0, 0.0) and
2947 rot
== (0.0, 0.0, 0.0) and
2948 scale
== (1.0, 1.0, 1.0) and
2949 clamp_uv
== (False, False)):
2952 node_texture
.translation
= loc
2953 node_texture
.rotation
= rot
2954 node_texture
.scale
= scale
2956 # awkward conversion UV clamping to min/max
2957 node_texture
.min = (0.0, 0.0, 0.0)
2958 node_texture
.max = (1.0, 1.0, 1.0)
2959 node_texture
.use_min
= node_texture
.use_max
= clamp_uv
[0] or clamp_uv
[1]
2960 if clamp_uv
[0] != clamp_uv
[1]:
2962 node_texture
.min[not clamp
[0]] = -1e9
2963 node_texture
.max[not clamp
[0]] = 1e9
2965 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2966 fbx_obj
, blen_data
= fbx_item
2967 if fbx_obj
.id != b
'Material':
2970 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
2973 fbx_lnk_type
) in connection_filter_reverse(fbx_uuid
, b
'Texture'):
2975 if fbx_lnk_type
.props
[0] == b
'OP':
2976 lnk_type
= fbx_lnk_type
.props
[3]
2978 ma_wrap
= nodal_material_wrap_map
[material
]
2980 if lnk_type
in {b
'DiffuseColor', b
'3dsMax|maps|texmap_diffuse'}:
2981 ma_wrap
.base_color_texture
.image
= image
2982 texture_mapping_set(fbx_lnk
, ma_wrap
.base_color_texture
)
2983 elif lnk_type
in {b
'SpecularColor', b
'SpecularFactor'}:
2984 # Intensity actually, not color...
2985 ma_wrap
.specular_texture
.image
= image
2986 texture_mapping_set(fbx_lnk
, ma_wrap
.specular_texture
)
2987 elif lnk_type
in {b
'ReflectionColor', b
'ReflectionFactor', b
'3dsMax|maps|texmap_reflection'}:
2988 # Intensity actually, not color...
2989 ma_wrap
.metallic_texture
.image
= image
2990 texture_mapping_set(fbx_lnk
, ma_wrap
.metallic_texture
)
2991 elif lnk_type
in {b
'TransparentColor', b
'TransparentFactor'}:
2992 # Transparency... sort of...
2993 ma_wrap
.transmission_texture
.image
= image
2994 texture_mapping_set(fbx_lnk
, ma_wrap
.transmission_texture
)
2995 if use_alpha_decals
:
2996 material_decals
.add(material
)
2997 elif lnk_type
== b
'ShininessExponent':
2998 # That is probably reversed compared to expected results? TODO...
2999 ma_wrap
.roughness_texture
.image
= image
3000 texture_mapping_set(fbx_lnk
, ma_wrap
.roughness_texture
)
3001 # XXX, applications abuse bump!
3002 elif lnk_type
in {b
'NormalMap', b
'Bump', b
'3dsMax|maps|texmap_bump'}:
3003 ma_wrap
.normalmap_texture
.image
= image
3004 texture_mapping_set(fbx_lnk
, ma_wrap
.normalmap_texture
)
3006 elif lnk_type == b'Bump':
3007 # TODO displacement...
3010 print("WARNING: material link %r ignored" % lnk_type
)
3012 material_images
.setdefault(material
, {})[lnk_type
] = image
3014 # Check if the diffuse image has an alpha channel,
3015 # if so, use the alpha channel.
3017 # Note: this could be made optional since images may have alpha but be entirely opaque
3018 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3019 fbx_obj
, blen_data
= fbx_item
3020 if fbx_obj
.id != b
'Material':
3022 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3023 image
= material_images
.get(material
, {}).get(b
'DiffuseColor', None)
3025 if image
and image
.depth
== 32:
3026 if use_alpha_decals
:
3027 material_decals
.add(material
)
3029 ma_wrap
= nodal_material_wrap_map
[material
]
3030 ma_wrap
.transmission_texture
.use_alpha
= True
3031 ma_wrap
.transmission_texture
.copy_from(ma_wrap
.base_color_texture
)
3033 # Propagate mapping from diffuse to all other channels which have none defined.
3034 # XXX Commenting for now, I do not really understand the logic here, why should diffuse mapping
3035 # be applied to all others if not defined for them???
3036 # ~ ma_wrap = nodal_material_wrap_map[material]
3037 # ~ ma_wrap.mapping_set_from_diffuse()
3041 perfmon
.step("FBX import: Cycles z-offset workaround...")
3044 # Annoying workaround for cycles having no z-offset
3045 if material_decals
and use_alpha_decals
:
3046 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3047 fbx_obj
, blen_data
= fbx_item
3048 if fbx_obj
.id != b
'Geometry':
3050 if fbx_obj
.props
[-1] == b
'Mesh':
3053 if decal_offset
!= 0.0:
3054 for material
in mesh
.materials
:
3055 if material
in material_decals
:
3056 for v
in mesh
.vertices
:
3057 v
.co
+= v
.normal
* decal_offset
3060 for obj
in (obj
for obj
in bpy
.data
.objects
if obj
.data
== mesh
):
3061 obj
.cycles_visibility
.shadow
= False
3064 perfmon
.level_down()
3066 perfmon
.level_down("Import finished.")