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_LAMP
= fbx_utils
.MAT_CONVERT_LAMP
.inverted()
64 MAT_CONVERT_CAMERA
= fbx_utils
.MAT_CONVERT_CAMERA
.inverted()
67 def elem_find_first(elem
, id_search
, default
=None):
68 for fbx_item
in elem
.elems
:
69 if fbx_item
.id == id_search
:
74 def elem_find_iter(elem
, id_search
):
75 for fbx_item
in elem
.elems
:
76 if fbx_item
.id == id_search
:
80 def elem_find_first_string(elem
, id_search
):
81 fbx_item
= elem_find_first(elem
, id_search
)
82 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
83 assert(len(fbx_item
.props
) == 1)
84 assert(fbx_item
.props_type
[0] == data_types
.STRING
)
85 return fbx_item
.props
[0].decode('utf-8')
89 def elem_find_first_string_as_bytes(elem
, id_search
):
90 fbx_item
= elem_find_first(elem
, id_search
)
91 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
92 assert(len(fbx_item
.props
) == 1)
93 assert(fbx_item
.props_type
[0] == data_types
.STRING
)
94 return fbx_item
.props
[0] # Keep it as bytes as requested...
98 def elem_find_first_bytes(elem
, id_search
, decode
=True):
99 fbx_item
= elem_find_first(elem
, id_search
)
100 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
101 assert(len(fbx_item
.props
) == 1)
102 assert(fbx_item
.props_type
[0] == data_types
.BYTES
)
103 return fbx_item
.props
[0]
108 return "%s: props[%d=%r], elems=(%r)" % (
111 ", ".join([repr(p
) for p
in elem
.props
]),
113 b
", ".join([e
.id for e
in elem
.elems
]),
117 def elem_split_name_class(elem
):
118 assert(elem
.props_type
[-2] == data_types
.STRING
)
119 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
120 return elem_name
, elem_class
123 def elem_name_ensure_class(elem
, clss
=...):
124 elem_name
, elem_class
= elem_split_name_class(elem
)
126 assert(elem_class
== clss
)
127 return elem_name
.decode('utf-8')
130 def elem_name_ensure_classes(elem
, clss
=...):
131 elem_name
, elem_class
= elem_split_name_class(elem
)
133 assert(elem_class
in clss
)
134 return elem_name
.decode('utf-8')
137 def elem_split_name_class_nodeattr(elem
):
138 assert(elem
.props_type
[-2] == data_types
.STRING
)
139 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
140 assert(elem_class
== b
'NodeAttribute')
141 assert(elem
.props_type
[-1] == data_types
.STRING
)
142 elem_class
= elem
.props
[-1]
143 return elem_name
, elem_class
147 assert(elem
.props_type
[0] == data_types
.INT64
)
151 def elem_prop_first(elem
, default
=None):
152 return elem
.props
[0] if (elem
is not None) and elem
.props
else default
157 # Properties70: { ... P:
158 def elem_props_find_first(elem
, elem_prop_id
):
160 # When properties are not found... Should never happen, but happens - as usual.
162 # support for templates (tuple of elems)
163 if type(elem
) is not FBXElem
:
164 assert(type(elem
) is tuple)
166 result
= elem_props_find_first(e
, elem_prop_id
)
167 if result
is not None:
169 assert(len(elem
) > 0)
172 for subelem
in elem
.elems
:
173 assert(subelem
.id == b
'P')
174 if subelem
.props
[0] == elem_prop_id
:
179 def elem_props_get_color_rgb(elem
, elem_prop_id
, default
=None):
180 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
181 if elem_prop
is not None:
182 assert(elem_prop
.props
[0] == elem_prop_id
)
183 if elem_prop
.props
[1] == b
'Color':
185 assert(elem_prop
.props
[1] == b
'Color')
186 assert(elem_prop
.props
[2] == b
'')
188 assert(elem_prop
.props
[1] == b
'ColorRGB')
189 assert(elem_prop
.props
[2] == b
'Color')
190 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
191 return elem_prop
.props
[4:7]
195 def elem_props_get_vector_3d(elem
, elem_prop_id
, default
=None):
196 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
197 if elem_prop
is not None:
198 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
199 return elem_prop
.props
[4:7]
203 def elem_props_get_number(elem
, elem_prop_id
, default
=None):
204 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
205 if elem_prop
is not None:
206 assert(elem_prop
.props
[0] == elem_prop_id
)
207 if elem_prop
.props
[1] == b
'double':
208 assert(elem_prop
.props
[1] == b
'double')
209 assert(elem_prop
.props
[2] == b
'Number')
211 assert(elem_prop
.props
[1] == b
'Number')
212 assert(elem_prop
.props
[2] == b
'')
214 # we could allow other number types
215 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
217 return elem_prop
.props
[4]
221 def elem_props_get_integer(elem
, elem_prop_id
, default
=None):
222 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
223 if elem_prop
is not None:
224 assert(elem_prop
.props
[0] == elem_prop_id
)
225 if elem_prop
.props
[1] == b
'int':
226 assert(elem_prop
.props
[1] == b
'int')
227 assert(elem_prop
.props
[2] == b
'Integer')
228 elif elem_prop
.props
[1] == b
'ULongLong':
229 assert(elem_prop
.props
[1] == b
'ULongLong')
230 assert(elem_prop
.props
[2] == b
'')
232 # we could allow other number types
233 assert(elem_prop
.props_type
[4] in {data_types
.INT32
, data_types
.INT64
})
235 return elem_prop
.props
[4]
239 def elem_props_get_bool(elem
, elem_prop_id
, default
=None):
240 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
241 if elem_prop
is not None:
242 assert(elem_prop
.props
[0] == elem_prop_id
)
243 assert(elem_prop
.props
[1] == b
'bool')
244 assert(elem_prop
.props
[2] == b
'')
245 assert(elem_prop
.props
[3] == b
'')
247 # we could allow other number types
248 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
249 assert(elem_prop
.props
[4] in {0, 1})
251 return bool(elem_prop
.props
[4])
255 def elem_props_get_enum(elem
, elem_prop_id
, default
=None):
256 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
257 if elem_prop
is not None:
258 assert(elem_prop
.props
[0] == elem_prop_id
)
259 assert(elem_prop
.props
[1] == b
'enum')
260 assert(elem_prop
.props
[2] == b
'')
261 assert(elem_prop
.props
[3] == b
'')
263 # we could allow other number types
264 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
266 return elem_prop
.props
[4]
270 def elem_props_get_visibility(elem
, elem_prop_id
, default
=None):
271 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
272 if elem_prop
is not None:
273 assert(elem_prop
.props
[0] == elem_prop_id
)
274 assert(elem_prop
.props
[1] == b
'Visibility')
275 assert(elem_prop
.props
[2] == b
'')
277 # we could allow other number types
278 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
280 return elem_prop
.props
[4]
284 # ----------------------------------------------------------------------------
289 from collections
import namedtuple
292 FBXTransformData
= namedtuple("FBXTransformData", (
294 "rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat", "geom_rot",
295 "sca", "sca_ofs", "sca_piv", "geom_sca",
299 def blen_read_custom_properties(fbx_obj
, blen_obj
, settings
):
300 # There doesn't seem to be a way to put user properties into templates, so this only get the object properties:
301 fbx_obj_props
= elem_find_first(fbx_obj
, b
'Properties70')
303 for fbx_prop
in fbx_obj_props
.elems
:
304 assert(fbx_prop
.id == b
'P')
306 if b
'U' in fbx_prop
.props
[3]:
307 if fbx_prop
.props
[0] == b
'UDP3DSMAX':
308 # Special case for 3DS Max user properties:
309 assert(fbx_prop
.props
[1] == b
'KString')
310 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
311 items
= fbx_prop
.props
[4].decode('utf-8')
312 for item
in items
.split('\r\n'):
314 prop_name
, prop_value
= item
.split('=', 1)
315 blen_obj
[prop_name
.strip()] = prop_value
.strip()
317 prop_name
= fbx_prop
.props
[0].decode('utf-8')
318 prop_type
= fbx_prop
.props
[1]
319 if prop_type
in {b
'Vector', b
'Vector3D', b
'Color', b
'ColorRGB'}:
320 assert(fbx_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
321 blen_obj
[prop_name
] = fbx_prop
.props
[4:7]
322 elif prop_type
in {b
'Vector4', b
'ColorRGBA'}:
323 assert(fbx_prop
.props_type
[4:8] == bytes((data_types
.FLOAT64
,)) * 4)
324 blen_obj
[prop_name
] = fbx_prop
.props
[4:8]
325 elif prop_type
== b
'Vector2D':
326 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.FLOAT64
,)) * 2)
327 blen_obj
[prop_name
] = fbx_prop
.props
[4:6]
328 elif prop_type
in {b
'Integer', b
'int'}:
329 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
330 blen_obj
[prop_name
] = fbx_prop
.props
[4]
331 elif prop_type
== b
'KString':
332 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
333 blen_obj
[prop_name
] = fbx_prop
.props
[4].decode('utf-8')
334 elif prop_type
in {b
'Number', b
'double', b
'Double'}:
335 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT64
)
336 blen_obj
[prop_name
] = fbx_prop
.props
[4]
337 elif prop_type
in {b
'Float', b
'float'}:
338 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT32
)
339 blen_obj
[prop_name
] = fbx_prop
.props
[4]
340 elif prop_type
in {b
'Bool', b
'bool'}:
341 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
342 blen_obj
[prop_name
] = fbx_prop
.props
[4] != 0
343 elif prop_type
in {b
'Enum', b
'enum'}:
344 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.INT32
, data_types
.STRING
)))
345 val
= fbx_prop
.props
[4]
346 if settings
.use_custom_props_enum_as_string
:
347 enum_items
= fbx_prop
.props
[5].decode('utf-8').split('~')
348 assert(val
>= 0 and val
< len(enum_items
))
349 blen_obj
[prop_name
] = enum_items
[val
]
351 blen_obj
[prop_name
] = val
353 print ("WARNING: User property type '%s' is not supported" % prop_type
.decode('utf-8'))
356 def blen_read_object_transform_do(transform_data
):
357 # This is a nightmare. FBX SDK uses Maya way to compute the transformation matrix of a node - utterly simple:
359 # WorldTransform = ParentWorldTransform * T * Roff * Rp * Rpre * R * Rpost * Rp-1 * Soff * Sp * S * Sp-1
361 # Where all those terms are 4 x 4 matrices that contain:
362 # WorldTransform: Transformation matrix of the node in global space.
363 # ParentWorldTransform: Transformation matrix of the parent node in global space.
365 # Roff: Rotation offset
369 # Rpost: Post-rotation
370 # Rp-1: Inverse of the rotation pivot
371 # Soff: Scaling offset
374 # Sp-1: Inverse of the scaling pivot
376 # But it was still too simple, and FBX notion of compatibility is... quite specific. So we also have to
377 # support 3DSMax way:
379 # WorldTransform = ParentWorldTransform * T * R * S * OT * OR * OS
381 # Where all those terms are 4 x 4 matrices that contain:
382 # WorldTransform: Transformation matrix of the node in global space
383 # ParentWorldTransform: Transformation matrix of the parent node in global space
387 # OT: Geometric transform translation
388 # OR: Geometric transform rotation
389 # OS: Geometric transform translation
392 # Geometric transformations ***are not inherited***: ParentWorldTransform does not contain the OT, OR, OS
393 # of WorldTransform's parent node.
395 # Taken from http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/
396 # index.html?url=WS1a9193826455f5ff1f92379812724681e696651.htm,topicNumber=d0e7429
399 lcl_translation
= Matrix
.Translation(transform_data
.loc
)
400 geom_loc
= Matrix
.Translation(transform_data
.geom_loc
)
403 to_rot
= lambda rot
, rot_ord
: Euler(convert_deg_to_rad_iter(rot
), rot_ord
).to_matrix().to_4x4()
404 lcl_rot
= to_rot(transform_data
.rot
, transform_data
.rot_ord
) * transform_data
.rot_alt_mat
405 pre_rot
= to_rot(transform_data
.pre_rot
, transform_data
.rot_ord
)
406 pst_rot
= to_rot(transform_data
.pst_rot
, transform_data
.rot_ord
)
407 geom_rot
= to_rot(transform_data
.geom_rot
, transform_data
.rot_ord
)
409 rot_ofs
= Matrix
.Translation(transform_data
.rot_ofs
)
410 rot_piv
= Matrix
.Translation(transform_data
.rot_piv
)
411 sca_ofs
= Matrix
.Translation(transform_data
.sca_ofs
)
412 sca_piv
= Matrix
.Translation(transform_data
.sca_piv
)
416 lcl_scale
[0][0], lcl_scale
[1][1], lcl_scale
[2][2] = transform_data
.sca
417 geom_scale
= Matrix();
418 geom_scale
[0][0], geom_scale
[1][1], geom_scale
[2][2] = transform_data
.geom_sca
427 rot_piv
.inverted_safe() *
431 sca_piv
.inverted_safe()
433 geom_mat
= geom_loc
* geom_rot
* geom_scale
434 # We return mat without 'geometric transforms' too, because it is to be used for children, sigh...
435 return (base_mat
* geom_mat
, base_mat
, geom_mat
)
438 # XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become
439 # more likely, will have to make this more robust!!!
440 def add_vgroup_to_objects(vg_indices
, vg_weights
, vg_name
, objects
):
441 assert(len(vg_indices
) == len(vg_weights
))
444 # We replace/override here...
445 vg
= obj
.vertex_groups
.get(vg_name
)
447 vg
= obj
.vertex_groups
.new(vg_name
)
448 for i
, w
in zip(vg_indices
, vg_weights
):
449 vg
.add((i
,), w
, 'REPLACE')
452 def blen_read_object_transform_preprocess(fbx_props
, fbx_obj
, rot_alt_mat
, use_prepost_rot
):
453 # This is quite involved, 'fbxRNode.cpp' from openscenegraph used as a reference
454 const_vector_zero_3d
= 0.0, 0.0, 0.0
455 const_vector_one_3d
= 1.0, 1.0, 1.0
457 loc
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Translation', const_vector_zero_3d
))
458 rot
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Rotation', const_vector_zero_3d
))
459 sca
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Scaling', const_vector_one_3d
))
461 geom_loc
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricTranslation', const_vector_zero_3d
))
462 geom_rot
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricRotation', const_vector_zero_3d
))
463 geom_sca
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricScaling', const_vector_one_3d
))
465 rot_ofs
= elem_props_get_vector_3d(fbx_props
, b
'RotationOffset', const_vector_zero_3d
)
466 rot_piv
= elem_props_get_vector_3d(fbx_props
, b
'RotationPivot', const_vector_zero_3d
)
467 sca_ofs
= elem_props_get_vector_3d(fbx_props
, b
'ScalingOffset', const_vector_zero_3d
)
468 sca_piv
= elem_props_get_vector_3d(fbx_props
, b
'ScalingPivot', const_vector_zero_3d
)
470 is_rot_act
= elem_props_get_bool(fbx_props
, b
'RotationActive', False)
474 pre_rot
= elem_props_get_vector_3d(fbx_props
, b
'PreRotation', const_vector_zero_3d
)
475 pst_rot
= elem_props_get_vector_3d(fbx_props
, b
'PostRotation', const_vector_zero_3d
)
477 pre_rot
= const_vector_zero_3d
478 pst_rot
= const_vector_zero_3d
486 6: 'XYZ', # XXX eSphericXYZ, not really supported...
487 }.get(elem_props_get_enum(fbx_props
, b
'RotationOrder', 0))
489 pre_rot
= const_vector_zero_3d
490 pst_rot
= const_vector_zero_3d
493 return FBXTransformData(loc
, geom_loc
,
494 rot
, rot_ofs
, rot_piv
, pre_rot
, pst_rot
, rot_ord
, rot_alt_mat
, geom_rot
,
495 sca
, sca_ofs
, sca_piv
, geom_sca
)
500 def blen_read_animations_curves_iter(fbx_curves
, blen_start_offset
, fbx_start_offset
, fps
):
502 Get raw FBX AnimCurve list, and yield values for all curves at each singular curves' keyframes,
503 together with (blender) timing, in frames.
504 blen_start_offset is expected in frames, while fbx_start_offset is expected in FBX ktime.
506 # As a first step, assume linear interpolation between key frames, we'll (try to!) handle more
507 # of FBX curves later.
508 from .fbx_utils
import FBX_KTIME
509 timefac
= fps
/ FBX_KTIME
512 elem_prop_first(elem_find_first(c
[2], b
'KeyTime')),
513 elem_prop_first(elem_find_first(c
[2], b
'KeyValueFloat')),
517 allkeys
= sorted({item
for sublist
in curves
for item
in sublist
[1]})
518 for curr_fbxktime
in allkeys
:
521 idx
, times
, values
, fbx_curve
= item
523 if times
[idx
] < curr_fbxktime
:
526 if idx
>= len(times
):
527 # We have reached our last element for this curve, stay on it from now on...
531 if times
[idx
] >= curr_fbxktime
:
533 curr_values
.append((values
[idx
], fbx_curve
))
535 # Interpolate between this key and the previous one.
536 ifac
= (curr_fbxktime
- times
[idx
- 1]) / (times
[idx
] - times
[idx
- 1])
537 curr_values
.append(((values
[idx
] - values
[idx
- 1]) * ifac
+ values
[idx
- 1], fbx_curve
))
538 curr_blenkframe
= (curr_fbxktime
- fbx_start_offset
) * timefac
+ blen_start_offset
539 yield (curr_blenkframe
, curr_values
)
542 def blen_read_animations_action_item(action
, item
, cnodes
, fps
, anim_offset
):
544 'Bake' loc/rot/scale into the action,
545 taking any pre_ and post_ matrix into account to transform from fbx into blender space.
547 from bpy
.types
import Object
, PoseBone
, ShapeKey
548 from itertools
import chain
551 for curves
, fbxprop
in cnodes
.values():
552 for (fbx_acdata
, _blen_data
), channel
in curves
.values():
553 fbx_curves
.append((fbxprop
, channel
, fbx_acdata
))
555 # Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
556 if len(fbx_curves
) == 0:
562 if isinstance(item
, ShapeKey
):
563 props
= [(item
.path_from_id("value"), 1, "Key")]
564 else: # Object or PoseBone:
566 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
570 # We want to create actions for objects, but for bones we 'reuse' armatures' actions!
571 grpname
= item
.bl_obj
.name
573 # Since we might get other channels animated in the end, due to all FBX transform magic,
574 # we need to add curves for whole loc/rot/scale in any case.
575 props
= [(bl_obj
.path_from_id("location"), 3, grpname
or "Location"),
577 (bl_obj
.path_from_id("scale"), 3, grpname
or "Scale")]
578 rot_mode
= bl_obj
.rotation_mode
579 if rot_mode
== 'QUATERNION':
580 props
[1] = (bl_obj
.path_from_id("rotation_quaternion"), 4, grpname
or "Quaternion Rotation")
581 elif rot_mode
== 'AXIS_ANGLE':
582 props
[1] = (bl_obj
.path_from_id("rotation_axis_angle"), 4, grpname
or "Axis Angle Rotation")
584 props
[1] = (bl_obj
.path_from_id("rotation_euler"), 3, grpname
or "Euler Rotation")
586 blen_curves
= [action
.fcurves
.new(prop
, channel
, grpname
)
587 for prop
, nbr_channels
, grpname
in props
for channel
in range(nbr_channels
)]
589 if isinstance(item
, ShapeKey
):
590 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
592 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
593 assert(fbxprop
== b
'DeformPercent')
597 for fc
, v
in zip(blen_curves
, (value
,)):
598 fc
.keyframe_points
.insert(frame
, v
, {'NEEDED', 'FAST'}).interpolation
= 'LINEAR'
600 else: # Object or PoseBone:
602 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
606 transform_data
= item
.fbx_transform_data
607 rot_prev
= bl_obj
.rotation_euler
.copy()
609 # Pre-compute inverted local rest matrix of the bone, if relevant.
610 restmat_inv
= item
.get_bind_matrix().inverted_safe() if item
.is_bone
else None
612 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
613 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
614 if fbxprop
== b
'Lcl Translation':
615 transform_data
.loc
[channel
] = v
616 elif fbxprop
== b
'Lcl Rotation':
617 transform_data
.rot
[channel
] = v
618 elif fbxprop
== b
'Lcl Scaling':
619 transform_data
.sca
[channel
] = v
620 mat
, _
, _
= blen_read_object_transform_do(transform_data
)
622 # compensate for changes in the local matrix during processing
623 if item
.anim_compensation_matrix
:
624 mat
= mat
* item
.anim_compensation_matrix
626 # apply pre- and post matrix
627 # post-matrix will contain any correction for lights, camera and bone orientation
628 # pre-matrix will contain any correction for a parent's correction matrix or the global matrix
630 mat
= item
.pre_matrix
* mat
632 mat
= mat
* item
.post_matrix
634 # And now, remove that rest pose matrix from current mat (also in parent space).
636 mat
= restmat_inv
* mat
638 # Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
639 loc
, rot
, sca
= mat
.decompose()
640 if rot_mode
== 'QUATERNION':
641 pass # nothing to do!
642 elif rot_mode
== 'AXIS_ANGLE':
643 vec
, ang
= rot
.to_axis_angle()
644 rot
= ang
, vec
.x
, vec
.y
, vec
.z
646 rot
= rot
.to_euler(rot_mode
, rot_prev
)
648 for fc
, value
in zip(blen_curves
, chain(loc
, rot
, sca
)):
649 fc
.keyframe_points
.insert(frame
, value
, {'NEEDED', 'FAST'}).interpolation
= 'LINEAR'
651 # Since we inserted our keyframes in 'FAST' mode, we have to update the fcurves now.
652 for fc
in blen_curves
:
656 def blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, anim_offset
):
658 Recreate an action per stack/layer/object combinations.
659 Only the first found action is linked to objects, more complex setups are not handled,
660 it's up to user to reproduce them!
662 from bpy
.types
import ShapeKey
665 for as_uuid
, ((fbx_asdata
, _blen_data
), alayers
) in stacks
.items():
666 stack_name
= elem_name_ensure_class(fbx_asdata
, b
'AnimStack')
667 for al_uuid
, ((fbx_aldata
, _blen_data
), items
) in alayers
.items():
668 layer_name
= elem_name_ensure_class(fbx_aldata
, b
'AnimLayer')
669 for item
, cnodes
in items
.items():
670 if isinstance(item
, ShapeKey
):
671 id_data
= item
.id_data
673 id_data
= item
.bl_obj
676 # Create new action if needed (should always be needed!
677 key
= (as_uuid
, al_uuid
, id_data
)
678 action
= actions
.get(key
)
680 action_name
= "|".join((id_data
.name
, stack_name
, layer_name
))
681 actions
[key
] = action
= bpy
.data
.actions
.new(action_name
)
682 action
.use_fake_user
= True
683 # If none yet assigned, assign this action to id_data.
684 if not id_data
.animation_data
:
685 id_data
.animation_data_create()
686 if not id_data
.animation_data
.action
:
687 id_data
.animation_data
.action
= action
688 # And actually populate the action!
689 blen_read_animations_action_item(action
, item
, cnodes
, scene
.render
.fps
, anim_offset
)
695 def blen_read_geom_layerinfo(fbx_layer
):
697 elem_find_first_string(fbx_layer
, b
'Name'),
698 elem_find_first_string_as_bytes(fbx_layer
, b
'MappingInformationType'),
699 elem_find_first_string_as_bytes(fbx_layer
, b
'ReferenceInformationType'),
703 def blen_read_geom_array_setattr(generator
, blen_data
, blen_attr
, fbx_data
, stride
, item_size
, descr
, xform
):
704 """Generic fbx_layer to blen_data setter, generator is expected to yield tuples (ble_idx, fbx_idx)."""
705 max_idx
= len(blen_data
) - 1
708 def check_skip(blen_idx
, fbx_idx
):
710 if fbx_idx
< 0: # Negative values mean 'skip'.
712 if blen_idx
> max_idx
:
714 print("ERROR: too much data in this layer, compared to elements in mesh, skipping!")
719 if xform
is not None:
720 if isinstance(blen_data
, list):
722 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
723 blen_data
[blen_idx
] = xform(fbx_data
[fbx_idx
])
725 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
726 blen_data
[blen_idx
] = xform(fbx_data
[fbx_idx
:fbx_idx
+ item_size
])
729 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
730 setattr(blen_data
[blen_idx
], blen_attr
, xform(fbx_data
[fbx_idx
]))
732 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
733 setattr(blen_data
[blen_idx
], blen_attr
, xform(fbx_data
[fbx_idx
:fbx_idx
+ item_size
]))
735 if isinstance(blen_data
, list):
737 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
738 blen_data
[blen_idx
] = fbx_data
[fbx_idx
]
740 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
741 blen_data
[blen_idx
] = fbx_data
[fbx_idx
:fbx_idx
+ item_size
]
744 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
745 setattr(blen_data
[blen_idx
], blen_attr
, fbx_data
[fbx_idx
])
747 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
748 setattr(blen_data
[blen_idx
], blen_attr
, fbx_data
[fbx_idx
:fbx_idx
+ item_size
])
750 for blen_idx
, fbx_idx
in generator
:
751 if check_skip(blen_idx
, fbx_idx
):
753 _process(blen_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
)
756 # generic generators.
757 def blen_read_geom_array_gen_allsame(data_len
):
758 return zip(*(range(data_len
), (0,) * data_len
))
761 def blen_read_geom_array_gen_direct(fbx_data
, stride
):
762 fbx_data_len
= len(fbx_data
)
763 return zip(*(range(fbx_data_len
// stride
), range(0, fbx_data_len
, stride
)))
766 def blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
):
767 return ((bi
, fi
* stride
) for bi
, fi
in enumerate(fbx_layer_index
))
770 def blen_read_geom_array_gen_direct_looptovert(mesh
, fbx_data
, stride
):
771 fbx_data_len
= len(fbx_data
) // stride
773 for p
in mesh
.polygons
:
774 for lidx
in p
.loop_indices
:
775 vidx
= loops
[lidx
].vertex_index
776 if vidx
< fbx_data_len
:
777 yield lidx
, vidx
* stride
780 # generic error printers.
781 def blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
):
782 print("warning layer %r mapping type unsupported: %r" % (descr
, fbx_layer_mapping
))
785 def blen_read_geom_array_error_ref(descr
, fbx_layer_ref
):
786 print("warning layer %r ref type unsupported: %r" % (descr
, fbx_layer_ref
))
789 def blen_read_geom_array_mapped_vert(
790 mesh
, blen_data
, blen_attr
,
791 fbx_layer_data
, fbx_layer_index
,
792 fbx_layer_mapping
, fbx_layer_ref
,
793 stride
, item_size
, descr
,
796 if fbx_layer_mapping
== b
'ByVertice':
797 if fbx_layer_ref
== b
'Direct':
798 assert(fbx_layer_index
is None)
799 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
800 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
802 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
)
803 elif fbx_layer_mapping
== b
'AllSame':
804 if fbx_layer_ref
== b
'IndexToDirect':
805 assert(fbx_layer_index
is None)
806 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
807 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
809 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
)
811 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
)
816 def blen_read_geom_array_mapped_edge(
817 mesh
, blen_data
, blen_attr
,
818 fbx_layer_data
, fbx_layer_index
,
819 fbx_layer_mapping
, fbx_layer_ref
,
820 stride
, item_size
, descr
,
823 if fbx_layer_mapping
== b
'ByEdge':
824 if fbx_layer_ref
== b
'Direct':
825 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
826 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
828 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
)
829 elif fbx_layer_mapping
== b
'AllSame':
830 if fbx_layer_ref
== b
'IndexToDirect':
831 assert(fbx_layer_index
is None)
832 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
833 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
835 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
)
837 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
)
842 def blen_read_geom_array_mapped_polygon(
843 mesh
, blen_data
, blen_attr
,
844 fbx_layer_data
, fbx_layer_index
,
845 fbx_layer_mapping
, fbx_layer_ref
,
846 stride
, item_size
, descr
,
849 if fbx_layer_mapping
== b
'ByPolygon':
850 if fbx_layer_ref
== b
'IndexToDirect':
851 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
852 # We fallback to 'Direct' mapping in this case.
853 #~ assert(fbx_layer_index is not None)
854 if fbx_layer_index
is None:
855 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
856 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
858 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
),
859 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
861 elif fbx_layer_ref
== b
'Direct':
862 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
863 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
865 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
)
866 elif fbx_layer_mapping
== b
'AllSame':
867 if fbx_layer_ref
== b
'IndexToDirect':
868 assert(fbx_layer_index
is None)
869 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
870 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
872 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
)
874 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
)
879 def blen_read_geom_array_mapped_polyloop(
880 mesh
, blen_data
, blen_attr
,
881 fbx_layer_data
, fbx_layer_index
,
882 fbx_layer_mapping
, fbx_layer_ref
,
883 stride
, item_size
, descr
,
886 if fbx_layer_mapping
== b
'ByPolygonVertex':
887 if fbx_layer_ref
== b
'IndexToDirect':
888 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
889 # We fallback to 'Direct' mapping in this case.
890 #~ assert(fbx_layer_index is not None)
891 if fbx_layer_index
is None:
892 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
893 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
895 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
),
896 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
898 elif fbx_layer_ref
== b
'Direct':
899 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
900 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
902 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
)
903 elif fbx_layer_mapping
== b
'ByVertice':
904 if fbx_layer_ref
== b
'Direct':
905 assert(fbx_layer_index
is None)
906 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct_looptovert(mesh
, fbx_layer_data
, stride
),
907 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
909 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
)
910 elif fbx_layer_mapping
== b
'AllSame':
911 if fbx_layer_ref
== b
'IndexToDirect':
912 assert(fbx_layer_index
is None)
913 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
914 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
916 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
)
918 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
)
923 def blen_read_geom_layer_material(fbx_obj
, mesh
):
924 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementMaterial')
926 if fbx_layer
is None:
932 ) = blen_read_geom_layerinfo(fbx_layer
)
934 layer_id
= b
'Materials'
935 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
937 blen_data
= mesh
.polygons
938 blen_read_geom_array_mapped_polygon(
939 mesh
, blen_data
, "material_index",
940 fbx_layer_data
, None,
941 fbx_layer_mapping
, fbx_layer_ref
,
946 def blen_read_geom_layer_uv(fbx_obj
, mesh
):
947 for layer_id
in (b
'LayerElementUV',):
948 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
949 # all should be valid
953 ) = blen_read_geom_layerinfo(fbx_layer
)
955 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'UV'))
956 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'UVIndex'))
958 uv_tex
= mesh
.uv_textures
.new(name
=fbx_layer_name
)
959 uv_lay
= mesh
.uv_layers
[-1]
960 blen_data
= uv_lay
.data
962 # some valid files omit this data
963 if fbx_layer_data
is None:
964 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
967 blen_read_geom_array_mapped_polyloop(
968 mesh
, blen_data
, "uv",
969 fbx_layer_data
, fbx_layer_index
,
970 fbx_layer_mapping
, fbx_layer_ref
,
975 def blen_read_geom_layer_color(fbx_obj
, mesh
):
976 # almost same as UV's
977 for layer_id
in (b
'LayerElementColor',):
978 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
979 # all should be valid
983 ) = blen_read_geom_layerinfo(fbx_layer
)
985 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'Colors'))
986 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'ColorIndex'))
988 color_lay
= mesh
.vertex_colors
.new(name
=fbx_layer_name
)
989 blen_data
= color_lay
.data
991 # some valid files omit this data
992 if fbx_layer_data
is None:
993 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
996 # ignore alpha layer (read 4 items into 3)
997 blen_read_geom_array_mapped_polyloop(
998 mesh
, blen_data
, "color",
999 fbx_layer_data
, fbx_layer_index
,
1000 fbx_layer_mapping
, fbx_layer_ref
,
1005 def blen_read_geom_layer_smooth(fbx_obj
, mesh
):
1006 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementSmoothing')
1008 if fbx_layer
is None:
1011 # all should be valid
1015 ) = blen_read_geom_layerinfo(fbx_layer
)
1017 layer_id
= b
'Smoothing'
1018 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1020 # udk has 'Direct' mapped, with no Smoothing, not sure why, but ignore these
1021 if fbx_layer_data
is None:
1024 if fbx_layer_mapping
== b
'ByEdge':
1025 # some models have bad edge data, we cant use this info...
1027 print("warning skipping sharp edges data, no valid edges...")
1030 blen_data
= mesh
.edges
1031 blen_read_geom_array_mapped_edge(
1032 mesh
, blen_data
, "use_edge_sharp",
1033 fbx_layer_data
, None,
1034 fbx_layer_mapping
, fbx_layer_ref
,
1036 xform
=lambda s
: not s
,
1038 # We only set sharp edges here, not face smoothing itself...
1039 mesh
.use_auto_smooth
= True
1040 mesh
.show_edge_sharp
= True
1042 elif fbx_layer_mapping
== b
'ByPolygon':
1043 blen_data
= mesh
.polygons
1044 return blen_read_geom_array_mapped_polygon(
1045 mesh
, blen_data
, "use_smooth",
1046 fbx_layer_data
, None,
1047 fbx_layer_mapping
, fbx_layer_ref
,
1049 xform
=lambda s
: (s
!= 0), # smoothgroup bitflags, treat as booleans for now
1052 print("warning layer %r mapping type unsupported: %r" % (fbx_layer
.id, fbx_layer_mapping
))
1056 def blen_read_geom_layer_normal(fbx_obj
, mesh
, xform
=None):
1057 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementNormal')
1059 if fbx_layer
is None:
1065 ) = blen_read_geom_layerinfo(fbx_layer
)
1067 layer_id
= b
'Normals'
1068 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1069 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'NormalsIndex'))
1071 # try loops, then vertices.
1072 tries
= ((mesh
.loops
, False, blen_read_geom_array_mapped_polyloop
),
1073 (mesh
.polygons
, True, blen_read_geom_array_mapped_polygon
),
1074 (mesh
.vertices
, True, blen_read_geom_array_mapped_vert
))
1075 for blen_data
, is_fake
, func
in tries
:
1076 bdata
= [None] * len(blen_data
) if is_fake
else blen_data
1077 if func(mesh
, bdata
, "normal",
1078 fbx_layer_data
, fbx_layer_index
, fbx_layer_mapping
, fbx_layer_ref
, 3, 3, layer_id
, xform
):
1079 if blen_data
is mesh
.polygons
:
1080 for pidx
, p
in enumerate(mesh
.polygons
):
1081 for lidx
in range(p
.loop_start
, p
.loop_start
+ p
.loop_total
):
1082 mesh
.loops
[lidx
].normal
[:] = bdata
[pidx
]
1083 elif blen_data
is mesh
.vertices
:
1084 # We have to copy vnors to lnors! Far from elegant, but simple.
1085 for l
in mesh
.loops
:
1086 l
.normal
[:] = bdata
[l
.vertex_index
]
1091 def blen_read_geom(fbx_tmpl
, fbx_obj
, settings
):
1092 from itertools
import chain
1095 # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
1096 # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
1097 geom_mat_co
= settings
.global_matrix
if settings
.bake_space_transform
else None
1098 # We need to apply the inverse transpose of the global matrix when transforming normals.
1099 geom_mat_no
= Matrix(settings
.global_matrix_inv_transposed
) if settings
.bake_space_transform
else None
1100 if geom_mat_no
is not None:
1101 # Remove translation & scaling!
1102 geom_mat_no
.translation
= Vector()
1103 geom_mat_no
.normalize()
1105 # TODO, use 'fbx_tmpl'
1106 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Geometry')
1108 fbx_verts
= elem_prop_first(elem_find_first(fbx_obj
, b
'Vertices'))
1109 fbx_polys
= elem_prop_first(elem_find_first(fbx_obj
, b
'PolygonVertexIndex'))
1110 fbx_edges
= elem_prop_first(elem_find_first(fbx_obj
, b
'Edges'))
1112 if geom_mat_co
is not None:
1113 def _vcos_transformed_gen(raw_cos
, m
=None):
1114 # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
1115 return chain(*(m
* Vector(v
) for v
in zip(*(iter(raw_cos
),) * 3)))
1116 fbx_verts
= array
.array(fbx_verts
.typecode
, _vcos_transformed_gen(fbx_verts
, geom_mat_co
))
1118 if fbx_verts
is None:
1120 if fbx_polys
is None:
1123 mesh
= bpy
.data
.meshes
.new(name
=elem_name_utf8
)
1124 mesh
.vertices
.add(len(fbx_verts
) // 3)
1125 mesh
.vertices
.foreach_set("co", fbx_verts
)
1128 mesh
.loops
.add(len(fbx_polys
))
1129 poly_loop_starts
= []
1130 poly_loop_totals
= []
1132 for i
, l
in enumerate(mesh
.loops
):
1133 index
= fbx_polys
[i
]
1135 poly_loop_starts
.append(poly_loop_prev
)
1136 poly_loop_totals
.append((i
- poly_loop_prev
) + 1)
1137 poly_loop_prev
= i
+ 1
1139 l
.vertex_index
= index
1141 mesh
.polygons
.add(len(poly_loop_starts
))
1142 mesh
.polygons
.foreach_set("loop_start", poly_loop_starts
)
1143 mesh
.polygons
.foreach_set("loop_total", poly_loop_totals
)
1145 blen_read_geom_layer_material(fbx_obj
, mesh
)
1146 blen_read_geom_layer_uv(fbx_obj
, mesh
)
1147 blen_read_geom_layer_color(fbx_obj
, mesh
)
1150 # edges in fact index the polygons (NOT the vertices)
1152 tot_edges
= len(fbx_edges
)
1153 edges_conv
= array
.array('i', [0]) * (tot_edges
* 2)
1159 e_b
= fbx_polys
[i
+ 1]
1163 # Last index of polygon, wrap back to the start.
1165 # ideally we wouldn't have to search back,
1166 # but it should only be 2-3 iterations.
1168 while j
>= 0 and fbx_polys
[j
] >= 0:
1171 e_b
= fbx_polys
[j
+ 1]
1173 edges_conv
[edge_index
] = e_a
1174 edges_conv
[edge_index
+ 1] = e_b
1177 mesh
.edges
.add(tot_edges
)
1178 mesh
.edges
.foreach_set("vertices", edges_conv
)
1180 # must be after edge, face loading.
1181 ok_smooth
= blen_read_geom_layer_smooth(fbx_obj
, mesh
)
1184 if settings
.use_custom_normals
:
1185 # Note: we store 'temp' normals in loops, since validate() may alter final mesh,
1186 # we can only set custom lnors *after* calling it.
1187 mesh
.create_normals_split()
1188 if geom_mat_no
is None:
1189 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
)
1192 return geom_mat_no
* Vector(v
)
1193 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
, nortrans
)
1195 mesh
.validate(clean_customdata
=False) # *Very* important to not remove lnors here!
1198 clnors
= array
.array('f', [0.0] * (len(mesh
.loops
) * 3))
1199 mesh
.loops
.foreach_get("normal", clnors
)
1202 mesh
.polygons
.foreach_set("use_smooth", [True] * len(mesh
.polygons
))
1205 mesh
.normals_split_custom_set(tuple(zip(*(iter(clnors
),) * 3)))
1206 mesh
.use_auto_smooth
= True
1207 mesh
.show_edge_sharp
= True
1211 if settings
.use_custom_normals
:
1212 mesh
.free_normals_split()
1215 mesh
.polygons
.foreach_set("use_smooth", [True] * len(mesh
.polygons
))
1217 if settings
.use_custom_props
:
1218 blen_read_custom_properties(fbx_obj
, mesh
, settings
)
1223 def blen_read_shape(fbx_tmpl
, fbx_sdata
, fbx_bcdata
, meshes
, scene
):
1224 elem_name_utf8
= elem_name_ensure_class(fbx_sdata
, b
'Geometry')
1225 indices
= elem_prop_first(elem_find_first(fbx_sdata
, b
'Indexes'), default
=())
1226 dvcos
= tuple(co
for co
in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata
, b
'Vertices'), default
=()))] * 3))
1227 # We completely ignore normals here!
1228 weight
= elem_prop_first(elem_find_first(fbx_bcdata
, b
'DeformPercent'), default
=100.0) / 100.0
1229 vgweights
= tuple(vgw
/ 100.0 for vgw
in elem_prop_first(elem_find_first(fbx_bcdata
, b
'FullWeights'), default
=()))
1231 # Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
1232 nbr_indices
= len(indices
)
1233 if len(vgweights
) == 1 and nbr_indices
> 1:
1234 vgweights
= (vgweights
[0],) * nbr_indices
1236 assert(len(vgweights
) == nbr_indices
== len(dvcos
))
1237 create_vg
= bool(set(vgweights
) - {1.0})
1241 for me
, objects
in meshes
:
1242 vcos
= tuple((idx
, me
.vertices
[idx
].co
+ Vector(dvco
)) for idx
, dvco
in zip(indices
, dvcos
))
1243 objects
= list({node
.bl_obj
for node
in objects
})
1246 if me
.shape_keys
is None:
1247 objects
[0].shape_key_add(name
="Basis", from_mix
=False)
1248 objects
[0].shape_key_add(name
=elem_name_utf8
, from_mix
=False)
1249 me
.shape_keys
.use_relative
= True # Should already be set as such.
1251 kb
= me
.shape_keys
.key_blocks
[elem_name_utf8
]
1252 for idx
, co
in vcos
:
1253 kb
.data
[idx
].co
[:] = co
1256 # Add vgroup if necessary.
1258 add_vgroup_to_objects(indices
, vgweights
, elem_name_utf8
, objects
)
1259 kb
.vertex_group
= elem_name_utf8
1261 keyblocks
.append(kb
)
1269 def blen_read_material(fbx_tmpl
, fbx_obj
, settings
):
1270 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Material')
1272 cycles_material_wrap_map
= settings
.cycles_material_wrap_map
1273 ma
= bpy
.data
.materials
.new(name
=elem_name_utf8
)
1275 const_color_white
= 1.0, 1.0, 1.0
1277 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1278 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1279 assert(fbx_props
[0] is not None)
1281 ma_diff
= elem_props_get_color_rgb(fbx_props
, b
'DiffuseColor', const_color_white
)
1282 ma_spec
= elem_props_get_color_rgb(fbx_props
, b
'SpecularColor', const_color_white
)
1283 ma_alpha
= elem_props_get_number(fbx_props
, b
'Opacity', 1.0)
1284 ma_spec_intensity
= ma
.specular_intensity
= elem_props_get_number(fbx_props
, b
'SpecularFactor', 0.25) * 2.0
1285 ma_spec_hardness
= elem_props_get_number(fbx_props
, b
'Shininess', 9.6)
1286 ma_refl_factor
= elem_props_get_number(fbx_props
, b
'ReflectionFactor', 0.0)
1287 ma_refl_color
= elem_props_get_color_rgb(fbx_props
, b
'ReflectionColor', const_color_white
)
1289 if settings
.use_cycles
:
1290 from . import cycles_shader_compat
1292 ma
.diffuse_color
= ma_diff
1294 ma_wrap
= cycles_shader_compat
.CyclesShaderWrapper(ma
)
1295 ma_wrap
.diffuse_color_set(ma_diff
)
1296 ma_wrap
.specular_color_set([c
* ma_spec_intensity
for c
in ma_spec
])
1297 ma_wrap
.hardness_value_set(((ma_spec_hardness
+ 3.0) / 5.0) - 0.65)
1298 ma_wrap
.alpha_value_set(ma_alpha
)
1299 ma_wrap
.reflect_factor_set(ma_refl_factor
)
1300 ma_wrap
.reflect_color_set(ma_refl_color
)
1302 cycles_material_wrap_map
[ma
] = ma_wrap
1304 # TODO, number BumpFactor isnt used yet
1305 ma
.diffuse_color
= ma_diff
1306 ma
.specular_color
= ma_spec
1308 ma
.specular_intensity
= ma_spec_intensity
1309 ma
.specular_hardness
= ma_spec_hardness
* 5.10 + 1.0
1311 if ma_refl_factor
!= 0.0:
1312 ma
.raytrace_mirror
.use
= True
1313 ma
.raytrace_mirror
.reflect_factor
= ma_refl_factor
1314 ma
.mirror_color
= ma_refl_color
1316 if settings
.use_custom_props
:
1317 blen_read_custom_properties(fbx_obj
, ma
, settings
)
1325 def blen_read_texture_image(fbx_tmpl
, fbx_obj
, basedir
, settings
):
1327 from bpy_extras
import image_utils
1329 def pack_data_from_content(image
, fbx_obj
):
1330 data
= elem_find_first_bytes(fbx_obj
, b
'Content')
1332 data_len
= len(data
)
1334 image
.pack(data
=data
, data_len
=data_len
)
1336 elem_name_utf8
= elem_name_ensure_classes(fbx_obj
, {b
'Texture', b
'Video'})
1338 image_cache
= settings
.image_cache
1340 # Yet another beautiful logic demonstration by Master FBX:
1341 # * RelativeFilename in both Video and Texture nodes.
1342 # * FileName in texture nodes.
1343 # * Filename in video nodes.
1344 # Aaaaaaaarrrrrrrrgggggggggggg!!!!!!!!!!!!!!
1345 filepath
= elem_find_first_string(fbx_obj
, b
'RelativeFilename')
1347 filepath
= os
.path
.join(basedir
, filepath
)
1349 filepath
= elem_find_first_string(fbx_obj
, b
'FileName')
1351 filepath
= elem_find_first_string(fbx_obj
, b
'Filename')
1353 print("Error, could not find any file path in ", fbx_obj
)
1354 print(" Falling back to: ", elem_name_utf8
)
1355 filepath
= elem_name_utf8
1357 filepath
= filepath
.replace('\\', '/') if (os
.sep
== '/') else filepath
.replace('/', '\\')
1359 image
= image_cache
.get(filepath
)
1360 if image
is not None:
1361 # Data is only embedded once, we may have already created the image but still be missing its data!
1362 if not image
.has_data
:
1363 pack_data_from_content(image
, fbx_obj
)
1366 image
= image_utils
.load_image(
1370 recursive
=settings
.use_image_search
,
1373 # Try to use embedded data, if available!
1374 pack_data_from_content(image
, fbx_obj
)
1376 image_cache
[filepath
] = image
1377 # name can be ../a/b/c
1378 image
.name
= os
.path
.basename(elem_name_utf8
)
1380 if settings
.use_custom_props
:
1381 blen_read_custom_properties(fbx_obj
, image
, settings
)
1386 def blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
):
1390 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1392 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1393 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1394 assert(fbx_props
[0] is not None)
1396 camera
= bpy
.data
.cameras
.new(name
=elem_name_utf8
)
1398 camera
.type = 'ORTHO' if elem_props_get_enum(fbx_props
, b
'CameraProjectionType', 0) == 1 else 'PERSP'
1400 camera
.lens
= elem_props_get_number(fbx_props
, b
'FocalLength', 35.0)
1401 camera
.sensor_width
= elem_props_get_number(fbx_props
, b
'FilmWidth', 32.0 * M2I
) / M2I
1402 camera
.sensor_height
= elem_props_get_number(fbx_props
, b
'FilmHeight', 32.0 * M2I
) / M2I
1404 camera
.ortho_scale
= elem_props_get_number(fbx_props
, b
'OrthoZoom', 1.0)
1406 filmaspect
= camera
.sensor_width
/ camera
.sensor_height
1408 camera
.shift_x
= elem_props_get_number(fbx_props
, b
'FilmOffsetX', 0.0) / (M2I
* camera
.sensor_width
)
1409 camera
.shift_y
= elem_props_get_number(fbx_props
, b
'FilmOffsetY', 0.0) / (M2I
* camera
.sensor_height
* filmaspect
)
1411 camera
.clip_start
= elem_props_get_number(fbx_props
, b
'NearPlane', 0.01) * global_scale
1412 camera
.clip_end
= elem_props_get_number(fbx_props
, b
'FarPlane', 100.0) * global_scale
1417 def blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
):
1419 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1421 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1422 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1424 if fbx_props
[0] is None:
1425 lamp
= bpy
.data
.lamps
.new(name
=elem_name_utf8
, type='POINT')
1431 2: 'SPOT'}.get(elem_props_get_enum(fbx_props
, b
'LightType', 0), 'POINT')
1433 lamp
= bpy
.data
.lamps
.new(name
=elem_name_utf8
, type=light_type
)
1435 if light_type
== 'SPOT':
1436 spot_size
= elem_props_get_number(fbx_props
, b
'OuterAngle', None)
1437 if spot_size
is None:
1439 spot_size
= elem_props_get_number(fbx_props
, b
'Cone angle', 45.0)
1440 lamp
.spot_size
= math
.radians(spot_size
)
1442 spot_blend
= elem_props_get_number(fbx_props
, b
'InnerAngle', None)
1443 if spot_blend
is None:
1445 spot_blend
= elem_props_get_number(fbx_props
, b
'HotSpot', 45.0)
1446 lamp
.spot_blend
= 1.0 - (spot_blend
/ spot_size
)
1449 lamp
.color
= elem_props_get_color_rgb(fbx_props
, b
'Color', (1.0, 1.0, 1.0))
1450 lamp
.energy
= elem_props_get_number(fbx_props
, b
'Intensity', 100.0) / 100.0
1451 lamp
.distance
= elem_props_get_number(fbx_props
, b
'DecayStart', 25.0) * global_scale
1452 lamp
.shadow_method
= ('RAY_SHADOW' if elem_props_get_bool(fbx_props
, b
'CastShadow', True) else 'NOSHADOW')
1453 lamp
.shadow_color
= elem_props_get_color_rgb(fbx_props
, b
'ShadowColor', (0.0, 0.0, 0.0))
1458 # ### Import Utility class
1459 class FbxImportHelperNode
:
1461 Temporary helper node to store a hierarchy of fbxNode objects before building Objects, Armatures and Bones.
1462 It tries to keep the correction data in one place so it can be applied consistently to the imported data.
1466 '_parent', 'anim_compensation_matrix', 'armature_setup', 'armature', 'bind_matrix',
1467 'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix', 'children', 'clusters',
1468 'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type',
1469 'is_armature', 'has_bone_children', 'is_bone', 'is_root', 'is_leaf',
1470 'matrix', 'matrix_as_parent', 'matrix_geom', 'meshes', 'post_matrix', 'pre_matrix')
1472 def __init__(self
, fbx_elem
, bl_data
, fbx_transform_data
, is_bone
):
1473 self
.fbx_name
= elem_name_ensure_class(fbx_elem
, b
'Model') if fbx_elem
else 'Unknown'
1474 self
.fbx_type
= fbx_elem
.props
[2] if fbx_elem
else None
1475 self
.fbx_elem
= fbx_elem
1477 self
.bl_data
= bl_data
1478 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!)
1479 self
.fbx_transform_data
= fbx_transform_data
1480 self
.is_root
= False
1481 self
.is_bone
= is_bone
1482 self
.is_armature
= False
1483 self
.armature
= None # For bones only, relevant armature node.
1484 self
.has_bone_children
= False # True if the hierarchy below this node contains bones, important to support mixed hierarchies.
1485 self
.is_leaf
= False # True for leaf-bones added to the end of some bone chains to set the lengths.
1486 self
.pre_matrix
= None # correction matrix that needs to be applied before the FBX transform
1487 self
.bind_matrix
= None # for bones this is the matrix used to bind to the skin
1488 if fbx_transform_data
:
1489 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= blen_read_object_transform_do(fbx_transform_data
)
1491 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= (None, None, None)
1492 self
.post_matrix
= None # correction matrix that needs to be applied after the FBX transform
1493 self
.bone_child_matrix
= None # Objects attached to a bone end not the beginning, this matrix corrects for that
1494 self
.anim_compensation_matrix
= None # a mesh moved in the hierarchy may have a different local matrix. This compensates animations for this.
1496 self
.meshes
= None # List of meshes influenced by this bone.
1497 self
.clusters
= [] # Deformer Cluster nodes
1498 self
.armature_setup
= {} # mesh and armature matrix when the mesh was bound
1508 def parent(self
, value
):
1509 if self
._parent
is not None:
1510 self
._parent
.children
.remove(self
)
1511 self
._parent
= value
1512 if self
._parent
is not None:
1513 self
._parent
.children
.append(self
)
1517 # Separating leaf status from ignore status itself.
1518 # Currently they are equivalent, but this may change in future.
1523 return self
.fbx_elem
.props
[1].decode()
1527 def print_info(self
, indent
=0):
1528 print(" " * indent
+ (self
.fbx_name
if self
.fbx_name
else "(Null)")
1529 + ("[root]" if self
.is_root
else "")
1530 + ("[leaf]" if self
.is_leaf
else "")
1531 + ("[ignore]" if self
.ignore
else "")
1532 + ("[armature]" if self
.is_armature
else "")
1533 + ("[bone]" if self
.is_bone
else "")
1534 + ("[HBC]" if self
.has_bone_children
else "")
1536 for c
in self
.children
:
1537 c
.print_info(indent
+ 1)
1539 def mark_leaf_bones(self
):
1540 if self
.is_bone
and len(self
.children
) == 1:
1541 child
= self
.children
[0]
1542 if child
.is_bone
and len(child
.children
) == 0:
1543 child
.is_leaf
= True
1544 for child
in self
.children
:
1545 child
.mark_leaf_bones()
1547 def do_bake_transform(self
, settings
):
1548 return (settings
.bake_space_transform
and self
.fbx_type
in (b
'Mesh', b
'Null') and
1549 not self
.is_armature
and not self
.is_bone
)
1551 def find_correction_matrix(self
, settings
, parent_correction_inv
=None):
1552 from bpy_extras
.io_utils
import axis_conversion
1554 if self
.parent
and (self
.parent
.is_root
or self
.parent
.do_bake_transform(settings
)):
1555 self
.pre_matrix
= settings
.global_matrix
1557 if parent_correction_inv
:
1558 self
.pre_matrix
= parent_correction_inv
* (self
.pre_matrix
if self
.pre_matrix
else Matrix())
1560 correction_matrix
= None
1563 if settings
.automatic_bone_orientation
:
1564 # find best orientation to align bone with
1565 bone_children
= tuple(child
for child
in self
.children
if child
.is_bone
)
1566 if len(bone_children
) == 0:
1567 # no children, inherit the correction from parent (if possible)
1568 if self
.parent
and self
.parent
.is_bone
:
1569 correction_matrix
= parent_correction_inv
.inverted() if parent_correction_inv
else None
1571 # else find how best to rotate the bone to align the Y axis with the children
1572 best_axis
= (1, 0, 0)
1573 if len(bone_children
) == 1:
1574 vec
= bone_children
[0].get_bind_matrix().to_translation()
1575 best_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
1576 if abs(vec
[0]) > abs(vec
[1]):
1577 if abs(vec
[0]) > abs(vec
[2]):
1578 best_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
1579 elif abs(vec
[1]) > abs(vec
[2]):
1580 best_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
1582 # get the child directions once because they may be checked several times
1583 child_locs
= (child
.get_bind_matrix().to_translation() for child
in bone_children
)
1584 child_locs
= tuple(loc
.normalized() for loc
in child_locs
if loc
.magnitude
> 0.0)
1586 # I'm not sure which one I like better...
1591 s
= -1 if i
% 2 == 1 else 1
1592 test_axis
= Vector((s
if a
== 0 else 0, s
if a
== 1 else 0, s
if a
== 2 else 0))
1594 # find max angle to children
1596 for loc
in child_locs
:
1597 max_angle
= min(max_angle
, test_axis
.dot(loc
))
1599 # is it better than the last one?
1600 if best_angle
< max_angle
:
1601 best_angle
= max_angle
1602 best_axis
= test_axis
1605 for vec
in child_locs
:
1606 test_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
1607 if abs(vec
[0]) > abs(vec
[1]):
1608 if abs(vec
[0]) > abs(vec
[2]):
1609 test_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
1610 elif abs(vec
[1]) > abs(vec
[2]):
1611 test_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
1613 # find max angle to children
1615 for loc
in child_locs
:
1616 max_angle
= min(max_angle
, test_axis
.dot(loc
))
1618 # is it better than the last one?
1619 if best_angle
< max_angle
:
1620 best_angle
= max_angle
1621 best_axis
= test_axis
1623 # convert best_axis to axis string
1624 to_up
= 'Z' if best_axis
[2] >= 0 else '-Z'
1625 if abs(best_axis
[0]) > abs(best_axis
[1]):
1626 if abs(best_axis
[0]) > abs(best_axis
[2]):
1627 to_up
= 'X' if best_axis
[0] >= 0 else '-X'
1628 elif abs(best_axis
[1]) > abs(best_axis
[2]):
1629 to_up
= 'Y' if best_axis
[1] >= 0 else '-Y'
1630 to_forward
= 'X' if to_up
not in {'X', '-X'} else 'Y'
1632 # Build correction matrix
1633 if (to_up
, to_forward
) != ('Y', 'X'):
1634 correction_matrix
= axis_conversion(from_forward
='X',
1636 to_forward
=to_forward
,
1640 correction_matrix
= settings
.bone_correction_matrix
1642 # camera and light can be hard wired
1643 if self
.fbx_type
== b
'Camera':
1644 correction_matrix
= MAT_CONVERT_CAMERA
1645 elif self
.fbx_type
== b
'Light':
1646 correction_matrix
= MAT_CONVERT_LAMP
1648 self
.post_matrix
= correction_matrix
1650 if self
.do_bake_transform(settings
):
1651 self
.post_matrix
= settings
.global_matrix_inv
* (self
.post_matrix
if self
.post_matrix
else Matrix())
1654 correction_matrix_inv
= correction_matrix
.inverted_safe() if correction_matrix
else None
1655 for child
in self
.children
:
1656 child
.find_correction_matrix(settings
, correction_matrix_inv
)
1658 def find_armature_bones(self
, armature
):
1659 for child
in self
.children
:
1661 child
.armature
= armature
1662 child
.find_armature_bones(armature
)
1664 def find_armatures(self
):
1665 needs_armature
= False
1666 for child
in self
.children
:
1668 needs_armature
= True
1671 if self
.fbx_type
in {b
'Null', b
'Root'}:
1672 # if empty then convert into armature
1673 self
.is_armature
= True
1676 # otherwise insert a new node
1677 armature
= FbxImportHelperNode(None, None, None, False)
1678 armature
.fbx_name
= "Armature"
1679 armature
.is_armature
= True
1681 for child
in self
.children
:
1683 child
.parent
= armature
1685 armature
.parent
= self
1687 armature
.find_armature_bones(armature
)
1689 for child
in self
.children
:
1690 if child
.is_armature
or child
.is_bone
:
1692 child
.find_armatures()
1694 def find_bone_children(self
):
1695 has_bone_children
= False
1696 for child
in self
.children
:
1697 has_bone_children |
= child
.find_bone_children()
1698 self
.has_bone_children
= has_bone_children
1699 return self
.is_bone
or has_bone_children
1701 def find_fake_bones(self
, in_armature
=False):
1702 if in_armature
and not self
.is_bone
and self
.has_bone_children
:
1704 # if we are not a null node we need an intermediate node for the data
1705 if self
.fbx_type
not in {b
'Null', b
'Root'}:
1706 node
= FbxImportHelperNode(self
.fbx_elem
, self
.bl_data
, None, False)
1707 self
.fbx_elem
= None
1711 for child
in self
.children
:
1712 if child
.is_bone
or child
.has_bone_children
:
1719 if self
.is_armature
:
1721 for child
in self
.children
:
1722 child
.find_fake_bones(in_armature
)
1724 def get_world_matrix_as_parent(self
):
1725 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
1726 if self
.matrix_as_parent
:
1727 matrix
= matrix
* self
.matrix_as_parent
1730 def get_world_matrix(self
):
1731 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
1733 matrix
= matrix
* self
.matrix
1736 def get_matrix(self
):
1737 matrix
= self
.matrix
if self
.matrix
else Matrix()
1739 matrix
= self
.pre_matrix
* matrix
1740 if self
.post_matrix
:
1741 matrix
= matrix
* self
.post_matrix
1744 def get_bind_matrix(self
):
1745 matrix
= self
.bind_matrix
if self
.bind_matrix
else Matrix()
1747 matrix
= self
.pre_matrix
* matrix
1748 if self
.post_matrix
:
1749 matrix
= matrix
* self
.post_matrix
1752 def make_bind_pose_local(self
, parent_matrix
=None):
1753 if parent_matrix
is None:
1754 parent_matrix
= Matrix()
1756 if self
.bind_matrix
:
1757 bind_matrix
= parent_matrix
.inverted_safe() * self
.bind_matrix
1759 bind_matrix
= self
.matrix
.copy() if self
.matrix
else None
1761 self
.bind_matrix
= bind_matrix
1763 parent_matrix
= parent_matrix
* bind_matrix
1765 for child
in self
.children
:
1766 child
.make_bind_pose_local(parent_matrix
)
1768 def collect_skeleton_meshes(self
, meshes
):
1769 for _
, m
in self
.clusters
:
1771 for child
in self
.children
:
1772 child
.collect_skeleton_meshes(meshes
)
1774 def collect_armature_meshes(self
):
1775 if self
.is_armature
:
1776 armature_matrix_inv
= self
.get_world_matrix().inverted_safe()
1779 for child
in self
.children
:
1780 child
.collect_skeleton_meshes(meshes
)
1782 old_matrix
= m
.matrix
1783 m
.matrix
= armature_matrix_inv
* m
.get_world_matrix()
1784 m
.anim_compensation_matrix
= old_matrix
.inverted_safe() * m
.matrix
1786 self
.meshes
= meshes
1788 for child
in self
.children
:
1789 child
.collect_armature_meshes()
1791 def build_skeleton(self
, arm
, parent_matrix
, parent_bone_size
=1, force_connect_children
=False):
1792 def child_connect(par_bone
, child_bone
, child_head
, connect_ctx
):
1793 # child_bone or child_head may be None.
1794 force_connect_children
, connected
= connect_ctx
1795 if child_bone
is not None:
1796 child_bone
.parent
= par_bone
1797 child_head
= child_bone
.head
1799 if similar_values_iter(par_bone
.tail
, child_head
):
1800 if child_bone
is not None:
1801 child_bone
.use_connect
= True
1802 # Disallow any force-connection at this level from now on, since that child was 'really'
1803 # connected, we do not want to move current bone's tail anymore!
1805 elif force_connect_children
and connected
is not None:
1806 # We only store position where tail of par_bone should be in the end.
1807 # Actual tail moving and force connection of compatible child bones will happen
1808 # once all have been checked.
1809 if connected
is ...:
1810 connected
= ([child_head
.copy(), 1], [child_bone
] if child_bone
is not None else [])
1812 connected
[0][0] += child_head
1813 connected
[0][1] += 1
1814 if child_bone
is not None:
1815 connected
[1].append(child_bone
)
1816 connect_ctx
[1] = connected
1818 def child_connect_finalize(par_bone
, connect_ctx
):
1819 force_connect_children
, connected
= connect_ctx
1820 # Do nothing if force connection is not enabled!
1821 if force_connect_children
and connected
is not None and connected
is not ...:
1822 # Here again we have to be wary about zero-length bones!!!
1823 par_tail
= connected
[0][0] / connected
[0][1]
1824 if (par_tail
- par_bone
.head
).magnitude
< 1e-2:
1825 par_bone_vec
= (par_bone
.tail
- par_bone
.head
).normalized()
1826 par_tail
= par_bone
.head
+ par_bone_vec
* 0.01
1827 par_bone
.tail
= par_tail
1828 for child_bone
in connected
[1]:
1829 if similar_values_iter(par_tail
, child_bone
.head
):
1830 child_bone
.use_connect
= True
1832 # Create the (edit)bone.
1833 bone
= arm
.bl_data
.edit_bones
.new(name
=self
.fbx_name
)
1835 self
.bl_obj
= arm
.bl_obj
1836 self
.bl_data
= arm
.bl_data
1837 self
.bl_bone
= bone
.name
# Could be different from the FBX name!
1839 # get average distance to children
1842 for child
in self
.children
:
1844 bone_size
+= child
.get_bind_matrix().to_translation().magnitude
1847 bone_size
/= bone_count
1849 bone_size
= parent_bone_size
1851 # So that our bone gets its final length, but still Y-aligned in armature space.
1852 # 0-length bones are automatically collapsed into their parent when you leave edit mode,
1853 # so this enforces a minimum length.
1854 bone_tail
= Vector((0.0, 1.0, 0.0)) * max(0.01, bone_size
)
1855 bone
.tail
= bone_tail
1857 # And rotate/move it to its final "rest pose".
1858 bone_matrix
= parent_matrix
* self
.get_bind_matrix().normalized()
1860 bone
.matrix
= bone_matrix
1862 # Correction for children attached to a bone. FBX expects to attach to the head of a bone,
1863 # while Blender attaches to the tail.
1864 self
.bone_child_matrix
= Matrix
.Translation(-bone_tail
)
1866 connect_ctx
= [force_connect_children
, ...]
1867 for child
in self
.children
:
1868 if child
.is_leaf
and force_connect_children
:
1869 # Arggggggggggggggggg! We do not want to create this bone, but we need its 'virtual head' location
1870 # to orient current one!!!
1871 child_head
= (bone_matrix
* child
.get_bind_matrix().normalized()).translation
1872 child_connect(bone
, None, child_head
, connect_ctx
)
1873 elif child
.is_bone
and not child
.ignore
:
1874 child_bone
= child
.build_skeleton(arm
, bone_matrix
, bone_size
,
1875 force_connect_children
=force_connect_children
)
1876 # Connection to parent.
1877 child_connect(bone
, child_bone
, None, connect_ctx
)
1879 child_connect_finalize(bone
, connect_ctx
)
1882 def build_node_obj(self
, fbx_tmpl
, settings
):
1886 if self
.is_bone
or not self
.fbx_elem
:
1889 # create when linking since we need object data
1890 elem_name_utf8
= self
.fbx_name
1892 # Object data must be created already
1893 self
.bl_obj
= obj
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=self
.bl_data
)
1895 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
1896 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1897 assert(fbx_props
[0] is not None)
1902 obj
.color
[0:3] = elem_props_get_color_rgb(fbx_props
, b
'Color', (0.8, 0.8, 0.8))
1903 obj
.hide
= not bool(elem_props_get_visibility(fbx_props
, b
'Visibility', 1.0))
1905 obj
.matrix_basis
= self
.get_matrix()
1907 if settings
.use_custom_props
:
1908 blen_read_custom_properties(fbx_props
[0], obj
, settings
)
1912 def build_skeleton_children(self
, fbx_tmpl
, settings
, scene
):
1914 for child
in self
.children
:
1917 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
)
1920 # child is not a bone
1921 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
1926 for child
in self
.children
:
1929 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
)
1932 obj_base
= scene
.objects
.link(obj
)
1933 obj_base
.select
= True
1937 def link_skeleton_children(self
, fbx_tmpl
, settings
, scene
):
1939 for child
in self
.children
:
1942 child_obj
= child
.bl_obj
1943 if child_obj
and child_obj
!= self
.bl_obj
:
1944 child_obj
.parent
= self
.bl_obj
# get the armature the bone belongs to
1945 child_obj
.parent_bone
= self
.bl_bone
1946 child_obj
.parent_type
= 'BONE'
1947 child_obj
.matrix_parent_inverse
= Matrix()
1949 # Blender attaches to the end of a bone, while FBX attaches to the start.
1950 # bone_child_matrix corrects for that.
1951 if child
.pre_matrix
:
1952 child
.pre_matrix
= self
.bone_child_matrix
* child
.pre_matrix
1954 child
.pre_matrix
= self
.bone_child_matrix
1956 child_obj
.matrix_basis
= child
.get_matrix()
1961 for child
in self
.children
:
1964 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
1966 child_obj
.parent
= obj
1970 def set_pose_matrix(self
, arm
):
1971 pose_bone
= arm
.bl_obj
.pose
.bones
[self
.bl_bone
]
1972 pose_bone
.matrix_basis
= self
.get_bind_matrix().inverted_safe() * self
.get_matrix()
1974 for child
in self
.children
:
1978 child
.set_pose_matrix(arm
)
1980 def merge_weights(self
, combined_weights
, fbx_cluster
):
1981 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
1982 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
1984 for index
, weight
in zip(indices
, weights
):
1985 w
= combined_weights
.get(index
)
1987 combined_weights
[index
] = [weight
]
1991 def set_bone_weights(self
):
1992 ignored_children
= tuple(child
for child
in self
.children
1993 if child
.is_bone
and child
.ignore
and len(child
.clusters
) > 0)
1995 if len(ignored_children
) > 0:
1996 # If we have an ignored child bone we need to merge their weights into the current bone weights.
1997 # This can happen both intentionally and accidentally when skinning a model. Either way, they
1998 # need to be moved into a parent bone or they cause animation glitches.
1999 for fbx_cluster
, meshes
in self
.clusters
:
2000 combined_weights
= {}
2001 self
.merge_weights(combined_weights
, fbx_cluster
)
2003 for child
in ignored_children
:
2004 for child_cluster
, child_meshes
in child
.clusters
:
2005 if not meshes
.isdisjoint(child_meshes
):
2006 self
.merge_weights(combined_weights
, child_cluster
)
2008 # combine child weights
2011 for i
, w
in combined_weights
.items():
2014 weights
.append(sum(w
) / len(w
))
2016 weights
.append(w
[0])
2018 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2020 # clusters that drive meshes not included in a parent don't need to be merged
2021 all_meshes
= set().union(*[meshes
for _
, meshes
in self
.clusters
])
2022 for child
in ignored_children
:
2023 for child_cluster
, child_meshes
in child
.clusters
:
2024 if all_meshes
.isdisjoint(child_meshes
):
2025 indices
= elem_prop_first(elem_find_first(child_cluster
, b
'Indexes', default
=None), default
=())
2026 weights
= elem_prop_first(elem_find_first(child_cluster
, b
'Weights', default
=None), default
=())
2027 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in child_meshes
])
2029 # set the vertex weights on meshes
2030 for fbx_cluster
, meshes
in self
.clusters
:
2031 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
2032 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
2033 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2035 for child
in self
.children
:
2036 if child
.is_bone
and not child
.ignore
:
2037 child
.set_bone_weights()
2039 def build_hierarchy(self
, fbx_tmpl
, settings
, scene
):
2040 if self
.is_armature
:
2041 # create when linking since we need object data
2042 elem_name_utf8
= self
.fbx_name
2044 self
.bl_data
= arm_data
= bpy
.data
.armatures
.new(name
=elem_name_utf8
)
2046 # Object data must be created already
2047 self
.bl_obj
= arm
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=arm_data
)
2049 arm
.matrix_basis
= self
.get_matrix()
2052 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
2053 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2054 assert(fbx_props
[0] is not None)
2056 if settings
.use_custom_props
:
2057 blen_read_custom_properties(fbx_props
[0], arm
, settings
)
2060 obj_base
= scene
.objects
.link(arm
)
2061 obj_base
.select
= True
2065 # Switch to Edit mode.
2066 scene
.objects
.active
= arm
2067 is_hidden
= arm
.hide
2068 arm
.hide
= False # Can't switch to Edit mode hidden objects...
2069 bpy
.ops
.object.mode_set(mode
='EDIT')
2071 for child
in self
.children
:
2075 child
.build_skeleton(self
, Matrix(), force_connect_children
=settings
.force_connect_children
)
2077 bpy
.ops
.object.mode_set(mode
='OBJECT')
2079 arm
.hide
= is_hidden
2082 for child
in self
.children
:
2086 child
.set_pose_matrix(self
)
2088 # Add bone children:
2089 for child
in self
.children
:
2092 child_obj
= child
.build_skeleton_children(fbx_tmpl
, settings
, scene
)
2095 elif self
.fbx_elem
and not self
.is_bone
:
2096 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
2098 # walk through children
2099 for child
in self
.children
:
2100 child
.build_hierarchy(fbx_tmpl
, settings
, scene
)
2103 obj_base
= scene
.objects
.link(obj
)
2104 obj_base
.select
= True
2108 for child
in self
.children
:
2109 child
.build_hierarchy(fbx_tmpl
, settings
, scene
)
2113 def link_hierarchy(self
, fbx_tmpl
, settings
, scene
):
2114 if self
.is_armature
:
2117 # Link bone children:
2118 for child
in self
.children
:
2121 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2123 child_obj
.parent
= arm
2125 # Add armature modifiers to the meshes
2127 for mesh
in self
.meshes
:
2128 (mmat
, amat
) = mesh
.armature_setup
[self
]
2129 me_obj
= mesh
.bl_obj
2131 # bring global armature & mesh matrices into *Blender* global space.
2132 # Note: Usage of matrix_geom (local 'diff' transform) here is quite brittle.
2133 # Among other things, why in hell isn't it taken into account by bindpose & co???
2134 # Probably because org app (max) handles it completely aside from any parenting stuff,
2135 # which we obviously cannot do in Blender. :/
2137 amat
= self
.bind_matrix
2138 amat
= settings
.global_matrix
* (Matrix() if amat
is None else amat
)
2139 if self
.matrix_geom
:
2140 amat
= amat
* self
.matrix_geom
2141 mmat
= settings
.global_matrix
* mmat
2142 if mesh
.matrix_geom
:
2143 mmat
= mmat
* mesh
.matrix_geom
2145 # Now that we have armature and mesh in there (global) bind 'state' (matrix),
2146 # we can compute inverse parenting matrix of the mesh.
2147 me_obj
.matrix_parent_inverse
= amat
.inverted_safe() * mmat
* me_obj
.matrix_basis
.inverted_safe()
2149 mod
= mesh
.bl_obj
.modifiers
.new(arm
.name
, 'ARMATURE')
2152 # Add bone weights to the deformers
2153 for child
in self
.children
:
2157 child
.set_bone_weights()
2163 # walk through children
2164 for child
in self
.children
:
2165 child_obj
= child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2167 child_obj
.parent
= obj
2171 for child
in self
.children
:
2172 child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2177 def is_ascii(filepath
, size
):
2178 with
open(filepath
, 'r', encoding
="utf-8") as f
:
2182 except UnicodeDecodeError:
2188 def load(operator
, context
, filepath
="",
2189 use_manual_orientation
=False,
2193 bake_space_transform
=False,
2194 use_custom_normals
=True,
2196 use_image_search
=False,
2197 use_alpha_decals
=False,
2201 use_custom_props
=True,
2202 use_custom_props_enum_as_string
=True,
2203 ignore_leaf_bones
=False,
2204 force_connect_children
=False,
2205 automatic_bone_orientation
=False,
2206 primary_bone_axis
='Y',
2207 secondary_bone_axis
='X',
2208 use_prepost_rot
=True):
2211 fbx_elem_nil
= FBXElem('', (), (), ())
2215 from bpy_extras
.io_utils
import axis_conversion
2217 from . import parse_fbx
2218 from .fbx_utils
import RIGHT_HAND_AXES
, FBX_FRAMERATES
2220 start_time_proc
= time
.process_time()
2221 start_time_sys
= time
.time()
2225 perfmon
.step("FBX Import: start importing %s" % filepath
)
2228 # detect ascii files
2229 if is_ascii(filepath
, 24):
2230 operator
.report({'ERROR'}, "ASCII FBX files are not supported %r" % filepath
)
2231 return {'CANCELLED'}
2234 elem_root
, version
= parse_fbx
.parse(filepath
)
2237 traceback
.print_exc()
2239 operator
.report({'ERROR'}, "Couldn't open file %r" % filepath
)
2240 return {'CANCELLED'}
2243 operator
.report({'ERROR'}, "Version %r unsupported, must be %r or later" % (version
, 7100))
2244 return {'CANCELLED'}
2246 print("FBX version: %r" % version
)
2248 if bpy
.ops
.object.mode_set
.poll():
2249 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
2252 if bpy
.ops
.object.select_all
.poll():
2253 bpy
.ops
.object.select_all(action
='DESELECT')
2255 basedir
= os
.path
.dirname(filepath
)
2257 cycles_material_wrap_map
= {}
2262 # Tables: (FBX_byte_id -> [FBX_data, None or Blender_datablock])
2263 fbx_table_nodes
= {}
2265 if use_alpha_decals
:
2266 material_decals
= set()
2268 material_decals
= None
2270 scene
= context
.scene
2272 # #### Get some info from GlobalSettings.
2274 perfmon
.step("FBX import: Prepare...")
2276 fbx_settings
= elem_find_first(elem_root
, b
'GlobalSettings')
2277 fbx_settings_props
= elem_find_first(fbx_settings
, b
'Properties70')
2278 if fbx_settings
is None or fbx_settings_props
is None:
2279 operator
.report({'ERROR'}, "No 'GlobalSettings' found in file %r" % filepath
)
2280 return {'CANCELLED'}
2282 # FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
2283 unit_scale
= elem_props_get_number(fbx_settings_props
, b
'UnitScaleFactor', 1.0)
2284 unit_scale_org
= elem_props_get_number(fbx_settings_props
, b
'OriginalUnitScaleFactor', 1.0)
2285 global_scale
*= (unit_scale
/ units_blender_to_fbx_factor(context
.scene
))
2286 # Compute global matrix and scale.
2287 if not use_manual_orientation
:
2288 axis_forward
= (elem_props_get_integer(fbx_settings_props
, b
'FrontAxis', 1),
2289 elem_props_get_integer(fbx_settings_props
, b
'FrontAxisSign', 1))
2290 axis_up
= (elem_props_get_integer(fbx_settings_props
, b
'UpAxis', 2),
2291 elem_props_get_integer(fbx_settings_props
, b
'UpAxisSign', 1))
2292 axis_coord
= (elem_props_get_integer(fbx_settings_props
, b
'CoordAxis', 0),
2293 elem_props_get_integer(fbx_settings_props
, b
'CoordAxisSign', 1))
2294 axis_key
= (axis_up
, axis_forward
, axis_coord
)
2295 axis_up
, axis_forward
= {v
: k
for k
, v
in RIGHT_HAND_AXES
.items()}.get(axis_key
, ('Z', 'Y'))
2296 global_matrix
= (Matrix
.Scale(global_scale
, 4) *
2297 axis_conversion(from_forward
=axis_forward
, from_up
=axis_up
).to_4x4())
2299 # To cancel out unwanted rotation/scale on nodes.
2300 global_matrix_inv
= global_matrix
.inverted()
2301 # For transforming mesh normals.
2302 global_matrix_inv_transposed
= global_matrix_inv
.transposed()
2304 # Compute bone correction matrix
2305 bone_correction_matrix
= None # None means no correction/identity
2306 if not automatic_bone_orientation
:
2307 if (primary_bone_axis
, secondary_bone_axis
) != ('Y', 'X'):
2308 bone_correction_matrix
= axis_conversion(from_forward
='X',
2310 to_forward
=secondary_bone_axis
,
2311 to_up
=primary_bone_axis
,
2314 # Compute framerate settings.
2315 custom_fps
= elem_props_get_number(fbx_settings_props
, b
'CustomFrameRate', 25.0)
2316 time_mode
= elem_props_get_enum(fbx_settings_props
, b
'TimeMode')
2317 real_fps
= {eid
: val
for val
, eid
in FBX_FRAMERATES
[1:]}.get(time_mode
, custom_fps
)
2320 scene
.render
.fps
= round(real_fps
)
2321 scene
.render
.fps_base
= scene
.render
.fps
/ real_fps
2323 # store global settings that need to be accessed during conversion
2324 settings
= FBXImportSettings(
2325 operator
.report
, (axis_up
, axis_forward
), global_matrix
, global_scale
,
2326 bake_space_transform
, global_matrix_inv
, global_matrix_inv_transposed
,
2327 use_custom_normals
, use_cycles
, use_image_search
,
2328 use_alpha_decals
, decal_offset
,
2329 use_anim
, anim_offset
,
2330 use_custom_props
, use_custom_props_enum_as_string
,
2331 cycles_material_wrap_map
, image_cache
,
2332 ignore_leaf_bones
, force_connect_children
, automatic_bone_orientation
, bone_correction_matrix
,
2336 # #### And now, the "real" data.
2338 perfmon
.step("FBX import: Templates...")
2340 fbx_defs
= elem_find_first(elem_root
, b
'Definitions') # can be None
2341 fbx_nodes
= elem_find_first(elem_root
, b
'Objects')
2342 fbx_connections
= elem_find_first(elem_root
, b
'Connections')
2344 if fbx_nodes
is None:
2345 operator
.report({'ERROR'}, "No 'Objects' found in file %r" % filepath
)
2346 return {'CANCELLED'}
2347 if fbx_connections
is None:
2348 operator
.report({'ERROR'}, "No 'Connections' found in file %r" % filepath
)
2349 return {'CANCELLED'}
2352 # First load property templates
2353 # Load 'PropertyTemplate' values.
2354 # Key is a tuple, (ObjectType, FBXNodeType)
2355 # eg, (b'Texture', b'KFbxFileTexture')
2356 # (b'Geometry', b'KFbxMesh')
2360 if fbx_defs
is not None:
2361 for fbx_def
in fbx_defs
.elems
:
2362 if fbx_def
.id == b
'ObjectType':
2363 for fbx_subdef
in fbx_def
.elems
:
2364 if fbx_subdef
.id == b
'PropertyTemplate':
2365 assert(fbx_def
.props_type
== b
'S')
2366 assert(fbx_subdef
.props_type
== b
'S')
2367 # (b'Texture', b'KFbxFileTexture') - eg.
2368 key
= fbx_def
.props
[0], fbx_subdef
.props
[0]
2369 fbx_templates
[key
] = fbx_subdef
2372 def fbx_template_get(key
):
2373 ret
= fbx_templates
.get(key
, fbx_elem_nil
)
2375 # Newest FBX (7.4 and above) use no more 'K' in their type names...
2376 key
= (key
[0], key
[1][1:])
2377 return fbx_templates
.get(key
, fbx_elem_nil
)
2380 perfmon
.step("FBX import: Nodes...")
2383 # Build FBX node-table
2385 for fbx_obj
in fbx_nodes
.elems
:
2386 # TODO, investigate what other items after first 3 may be
2387 assert(fbx_obj
.props_type
[:3] == b
'LSS')
2388 fbx_uuid
= elem_uuid(fbx_obj
)
2389 fbx_table_nodes
[fbx_uuid
] = [fbx_obj
, None]
2394 # http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=
2395 # WS73099cc142f487551fea285e1221e4f9ff8-7fda.htm,topicNumber=d0e6388
2397 perfmon
.step("FBX import: Connections...")
2399 fbx_connection_map
= {}
2400 fbx_connection_map_reverse
= {}
2403 for fbx_link
in fbx_connections
.elems
:
2404 c_type
= fbx_link
.props
[0]
2405 if fbx_link
.props_type
[1:3] == b
'LL':
2406 c_src
, c_dst
= fbx_link
.props
[1:3]
2407 fbx_connection_map
.setdefault(c_src
, []).append((c_dst
, fbx_link
))
2408 fbx_connection_map_reverse
.setdefault(c_dst
, []).append((c_src
, fbx_link
))
2411 perfmon
.step("FBX import: Meshes...")
2416 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxMesh'))
2418 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2419 fbx_obj
, blen_data
= fbx_item
2420 if fbx_obj
.id != b
'Geometry':
2422 if fbx_obj
.props
[-1] == b
'Mesh':
2423 assert(blen_data
is None)
2424 fbx_item
[1] = blen_read_geom(fbx_tmpl
, fbx_obj
, settings
)
2427 perfmon
.step("FBX import: Materials & Textures...")
2430 # Load material data
2432 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
2433 # b'KFbxSurfaceLambert'
2435 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2436 fbx_obj
, blen_data
= fbx_item
2437 if fbx_obj
.id != b
'Material':
2439 assert(blen_data
is None)
2440 fbx_item
[1] = blen_read_material(fbx_tmpl
, fbx_obj
, settings
)
2444 # Load image & textures data
2446 fbx_tmpl_tex
= fbx_template_get((b
'Texture', b
'KFbxFileTexture'))
2447 fbx_tmpl_img
= fbx_template_get((b
'Video', b
'KFbxVideo'))
2449 # Important to run all 'Video' ones first, embedded images are stored in those nodes.
2450 # XXX Note we simplify things here, assuming both matching Video and Texture will use same file path,
2451 # this may be a bit weak, if issue arise we'll fallback to plain connection stuff...
2452 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2453 fbx_obj
, blen_data
= fbx_item
2454 if fbx_obj
.id != b
'Video':
2456 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_img
, fbx_obj
, basedir
, settings
)
2457 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2458 fbx_obj
, blen_data
= fbx_item
2459 if fbx_obj
.id != b
'Texture':
2461 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_tex
, fbx_obj
, basedir
, settings
)
2464 perfmon
.step("FBX import: Cameras & Lamps...")
2469 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxCamera'))
2471 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2472 fbx_obj
, blen_data
= fbx_item
2473 if fbx_obj
.id != b
'NodeAttribute':
2475 if fbx_obj
.props
[-1] == b
'Camera':
2476 assert(blen_data
is None)
2477 fbx_item
[1] = blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
)
2483 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxLight'))
2485 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2486 fbx_obj
, blen_data
= fbx_item
2487 if fbx_obj
.id != b
'NodeAttribute':
2489 if fbx_obj
.props
[-1] == b
'Light':
2490 assert(blen_data
is None)
2491 fbx_item
[1] = blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
)
2496 def connection_filter_ex(fbx_uuid
, fbx_id
, dct
):
2497 return [(c_found
[0], c_found
[1], c_type
)
2498 for (c_uuid
, c_type
) in dct
.get(fbx_uuid
, ())
2499 # 0 is used for the root node, which isnt in fbx_table_nodes
2500 for c_found
in (() if c_uuid
is 0 else (fbx_table_nodes
.get(c_uuid
, (None, None)),))
2501 if (fbx_id
is None) or (c_found
[0] and c_found
[0].id == fbx_id
)]
2503 def connection_filter_forward(fbx_uuid
, fbx_id
):
2504 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map
)
2506 def connection_filter_reverse(fbx_uuid
, fbx_id
):
2507 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map_reverse
)
2509 perfmon
.step("FBX import: Objects & Armatures...")
2511 # -- temporary helper hierarchy to build armatures and objects from
2512 # lookup from uuid to helper node. Used to build parent-child relations and later to look up animated nodes.
2513 fbx_helper_nodes
= {}
2516 # We build an intermediate hierarchy used to:
2517 # - Calculate and store bone orientation correction matrices. The same matrices will be reused for animation.
2518 # - Find/insert armature nodes.
2519 # - Filter leaf bones.
2522 fbx_helper_nodes
[0] = root_helper
= FbxImportHelperNode(None, None, None, False)
2523 root_helper
.is_root
= True
2526 fbx_tmpl
= fbx_template_get((b
'Model', b
'KFbxNode'))
2527 for a_uuid
, a_item
in fbx_table_nodes
.items():
2528 fbx_obj
, bl_data
= a_item
2529 if fbx_obj
is None or fbx_obj
.id != b
'Model':
2532 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
2533 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2534 assert(fbx_props
[0] is not None)
2536 transform_data
= blen_read_object_transform_preprocess(fbx_props
, fbx_obj
, Matrix(), use_prepost_rot
)
2537 # Note: 'Root' "bones" are handled as (armature) objects.
2538 # Note: See T46912 for first FBX file I ever saw with 'Limb' bones - thought those were totally deprecated.
2539 is_bone
= fbx_obj
.props
[2] in {b
'LimbNode', b
'Limb'}
2540 fbx_helper_nodes
[a_uuid
] = FbxImportHelperNode(fbx_obj
, bl_data
, transform_data
, is_bone
)
2542 # add parent-child relations and add blender data to the node
2543 for fbx_link
in fbx_connections
.elems
:
2544 if fbx_link
.props
[0] != b
'OO':
2546 if fbx_link
.props_type
[1:3] == b
'LL':
2547 c_src
, c_dst
= fbx_link
.props
[1:3]
2548 parent
= fbx_helper_nodes
.get(c_dst
)
2552 child
= fbx_helper_nodes
.get(c_src
)
2554 # add blender data (meshes, lights, cameras, etc.) to a helper node
2555 fbx_sdata
, bl_data
= p_item
= fbx_table_nodes
.get(c_src
, (None, None))
2556 if fbx_sdata
is None:
2558 if fbx_sdata
.id not in {b
'Geometry', b
'NodeAttribute'}:
2560 parent
.bl_data
= bl_data
2563 child
.parent
= parent
2565 # find armatures (either an empty below a bone or a new node inserted at the bone
2566 root_helper
.find_armatures()
2568 # mark nodes that have bone children
2569 root_helper
.find_bone_children()
2571 # mark nodes that need a bone to attach child-bones to
2572 root_helper
.find_fake_bones()
2574 # mark leaf nodes that are only required to mark the end of their parent bone
2575 if settings
.ignore_leaf_bones
:
2576 root_helper
.mark_leaf_bones()
2578 # What a mess! Some bones have several BindPoses, some have none, clusters contain a bind pose as well,
2579 # and you can have several clusters per bone!
2580 # Maybe some conversion can be applied to put them all into the same frame of reference?
2582 # get the bind pose from pose elements
2583 for a_uuid
, a_item
in fbx_table_nodes
.items():
2584 fbx_obj
, bl_data
= a_item
2587 if fbx_obj
.id != b
'Pose':
2589 if fbx_obj
.props
[2] != b
'BindPose':
2591 for fbx_pose_node
in fbx_obj
.elems
:
2592 if fbx_pose_node
.id != b
'PoseNode':
2594 node_elem
= elem_find_first(fbx_pose_node
, b
'Node')
2595 node
= elem_uuid(node_elem
)
2596 matrix_elem
= elem_find_first(fbx_pose_node
, b
'Matrix')
2597 matrix
= array_to_matrix4(matrix_elem
.props
[0]) if matrix_elem
else None
2598 bone
= fbx_helper_nodes
.get(node
)
2600 # Store the matrix in the helper node.
2601 # There may be several bind pose matrices for the same node, but in tests they seem to be identical.
2602 bone
.bind_matrix
= matrix
# global space
2604 # get clusters and bind pose
2605 for helper_uuid
, helper_node
in fbx_helper_nodes
.items():
2606 if not helper_node
.is_bone
:
2608 for cluster_uuid
, cluster_link
in fbx_connection_map
.get(helper_uuid
, ()):
2609 if cluster_link
.props
[0] != b
'OO':
2611 fbx_cluster
, _
= fbx_table_nodes
.get(cluster_uuid
, (None, None))
2612 if fbx_cluster
is None or fbx_cluster
.id != b
'Deformer' or fbx_cluster
.props
[2] != b
'Cluster':
2615 # Get the bind pose from the cluster:
2616 tx_mesh_elem
= elem_find_first(fbx_cluster
, b
'Transform', default
=None)
2617 tx_mesh
= array_to_matrix4(tx_mesh_elem
.props
[0]) if tx_mesh_elem
else Matrix()
2619 tx_bone_elem
= elem_find_first(fbx_cluster
, b
'TransformLink', default
=None)
2620 tx_bone
= array_to_matrix4(tx_bone_elem
.props
[0]) if tx_bone_elem
else None
2622 tx_arm_elem
= elem_find_first(fbx_cluster
, b
'TransformAssociateModel', default
=None)
2623 tx_arm
= array_to_matrix4(tx_arm_elem
.props
[0]) if tx_arm_elem
else None
2625 mesh_matrix
= tx_mesh
2626 armature_matrix
= tx_arm
2629 mesh_matrix
= tx_bone
* mesh_matrix
2630 helper_node
.bind_matrix
= tx_bone
# overwrite the bind matrix
2632 # Get the meshes driven by this cluster: (Shouldn't that be only one?)
2634 for skin_uuid
, skin_link
in fbx_connection_map
.get(cluster_uuid
):
2635 if skin_link
.props
[0] != b
'OO':
2637 fbx_skin
, _
= fbx_table_nodes
.get(skin_uuid
, (None, None))
2638 if fbx_skin
is None or fbx_skin
.id != b
'Deformer' or fbx_skin
.props
[2] != b
'Skin':
2640 for mesh_uuid
, mesh_link
in fbx_connection_map
.get(skin_uuid
):
2641 if mesh_link
.props
[0] != b
'OO':
2643 fbx_mesh
, _
= fbx_table_nodes
.get(mesh_uuid
, (None, None))
2644 if fbx_mesh
is None or fbx_mesh
.id != b
'Geometry' or fbx_mesh
.props
[2] != b
'Mesh':
2646 for object_uuid
, object_link
in fbx_connection_map
.get(mesh_uuid
):
2647 if object_link
.props
[0] != b
'OO':
2649 mesh_node
= fbx_helper_nodes
[object_uuid
]
2652 # If we get a valid mesh matrix (in bone space), store armature and
2653 # mesh global matrices, we need them to compute mesh's matrix_parent_inverse
2654 # when actually binding them via the modifier.
2655 # Note we assume all bones were bound with the same mesh/armature (global) matrix,
2656 # we do not support otherwise in Blender anyway!
2657 mesh_node
.armature_setup
[helper_node
.armature
] = (mesh_matrix
, armature_matrix
)
2658 meshes
.add(mesh_node
)
2660 helper_node
.clusters
.append((fbx_cluster
, meshes
))
2662 # convert bind poses from global space into local space
2663 root_helper
.make_bind_pose_local()
2665 # collect armature meshes
2666 root_helper
.collect_armature_meshes()
2668 # find the correction matrices to align FBX objects with their Blender equivalent
2669 root_helper
.find_correction_matrix(settings
)
2671 # build the Object/Armature/Bone hierarchy
2672 root_helper
.build_hierarchy(fbx_tmpl
, settings
, scene
)
2674 # Link the Object/Armature/Bone hierarchy
2675 root_helper
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2677 # root_helper.print_info(0)
2680 perfmon
.step("FBX import: ShapeKeys...")
2682 # We can handle shapes.
2683 blend_shape_channels
= {} # We do not need Shapes themselves, but keyblocks, for anim.
2686 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxShape'))
2688 for s_uuid
, s_item
in fbx_table_nodes
.items():
2689 fbx_sdata
, bl_sdata
= s_item
= fbx_table_nodes
.get(s_uuid
, (None, None))
2690 if fbx_sdata
is None or fbx_sdata
.id != b
'Geometry' or fbx_sdata
.props
[2] != b
'Shape':
2693 # shape -> blendshapechannel -> blendshape -> mesh.
2694 for bc_uuid
, bc_ctype
in fbx_connection_map
.get(s_uuid
, ()):
2695 if bc_ctype
.props
[0] != b
'OO':
2697 fbx_bcdata
, _bl_bcdata
= fbx_table_nodes
.get(bc_uuid
, (None, None))
2698 if fbx_bcdata
is None or fbx_bcdata
.id != b
'Deformer' or fbx_bcdata
.props
[2] != b
'BlendShapeChannel':
2702 for bs_uuid
, bs_ctype
in fbx_connection_map
.get(bc_uuid
, ()):
2703 if bs_ctype
.props
[0] != b
'OO':
2705 fbx_bsdata
, _bl_bsdata
= fbx_table_nodes
.get(bs_uuid
, (None, None))
2706 if fbx_bsdata
is None or fbx_bsdata
.id != b
'Deformer' or fbx_bsdata
.props
[2] != b
'BlendShape':
2708 for m_uuid
, m_ctype
in fbx_connection_map
.get(bs_uuid
, ()):
2709 if m_ctype
.props
[0] != b
'OO':
2711 fbx_mdata
, bl_mdata
= fbx_table_nodes
.get(m_uuid
, (None, None))
2712 if fbx_mdata
is None or fbx_mdata
.id != b
'Geometry' or fbx_mdata
.props
[2] != b
'Mesh':
2714 # Blenmeshes are assumed already created at that time!
2715 assert(isinstance(bl_mdata
, bpy
.types
.Mesh
))
2716 # And we have to find all objects using this mesh!
2718 for o_uuid
, o_ctype
in fbx_connection_map
.get(m_uuid
, ()):
2719 if o_ctype
.props
[0] != b
'OO':
2721 node
= fbx_helper_nodes
[o_uuid
]
2723 objects
.append(node
)
2724 meshes
.append((bl_mdata
, objects
))
2725 # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
2727 # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
2728 keyblocks
= blen_read_shape(fbx_tmpl
, fbx_sdata
, fbx_bcdata
, meshes
, scene
)
2729 blend_shape_channels
[bc_uuid
] = keyblocks
2733 perfmon
.step("FBX import: Animations...")
2737 fbx_tmpl_astack
= fbx_template_get((b
'AnimationStack', b
'FbxAnimStack'))
2738 fbx_tmpl_alayer
= fbx_template_get((b
'AnimationLayer', b
'FbxAnimLayer'))
2742 for as_uuid
, fbx_asitem
in fbx_table_nodes
.items():
2743 fbx_asdata
, _blen_data
= fbx_asitem
2744 if fbx_asdata
.id != b
'AnimationStack' or fbx_asdata
.props
[2] != b
'':
2746 stacks
[as_uuid
] = (fbx_asitem
, {})
2749 # (mixing is completely ignored for now, each layer results in an independent set of actions).
2750 def get_astacks_from_alayer(al_uuid
):
2751 for as_uuid
, as_ctype
in fbx_connection_map
.get(al_uuid
, ()):
2752 if as_ctype
.props
[0] != b
'OO':
2754 fbx_asdata
, _bl_asdata
= fbx_table_nodes
.get(as_uuid
, (None, None))
2755 if (fbx_asdata
is None or fbx_asdata
.id != b
'AnimationStack' or
2756 fbx_asdata
.props
[2] != b
'' or as_uuid
not in stacks
):
2759 for al_uuid
, fbx_alitem
in fbx_table_nodes
.items():
2760 fbx_aldata
, _blen_data
= fbx_alitem
2761 if fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
2763 for as_uuid
in get_astacks_from_alayer(al_uuid
):
2764 _fbx_asitem
, alayers
= stacks
[as_uuid
]
2765 alayers
[al_uuid
] = (fbx_alitem
, {})
2767 # AnimationCurveNodes (also the ones linked to actual animated data!).
2769 for acn_uuid
, fbx_acnitem
in fbx_table_nodes
.items():
2770 fbx_acndata
, _blen_data
= fbx_acnitem
2771 if fbx_acndata
.id != b
'AnimationCurveNode' or fbx_acndata
.props
[2] != b
'':
2773 cnode
= curvenodes
[acn_uuid
] = {}
2775 for n_uuid
, n_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
2776 if n_ctype
.props
[0] != b
'OP':
2778 lnk_prop
= n_ctype
.props
[3]
2779 if lnk_prop
in {b
'Lcl Translation', b
'Lcl Rotation', b
'Lcl Scaling'}:
2780 # n_uuid can (????) be linked to root '0' node, instead of a mere object node... See T41712.
2781 ob
= fbx_helper_nodes
.get(n_uuid
, None)
2784 items
.append((ob
, lnk_prop
))
2785 elif lnk_prop
== b
'DeformPercent': # Shape keys.
2786 keyblocks
= blend_shape_channels
.get(n_uuid
)
2787 if keyblocks
is None:
2789 items
+= [(kb
, lnk_prop
) for kb
in keyblocks
]
2790 for al_uuid
, al_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
2791 if al_ctype
.props
[0] != b
'OO':
2793 fbx_aldata
, _blen_aldata
= fbx_alitem
= fbx_table_nodes
.get(al_uuid
, (None, None))
2794 if fbx_aldata
is None or fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
2796 for as_uuid
in get_astacks_from_alayer(al_uuid
):
2797 _fbx_alitem
, anim_items
= stacks
[as_uuid
][1][al_uuid
]
2798 assert(_fbx_alitem
== fbx_alitem
)
2799 for item
, item_prop
in items
:
2800 # No need to keep curvenode FBX data here, contains nothing useful for us.
2801 anim_items
.setdefault(item
, {})[acn_uuid
] = (cnode
, item_prop
)
2803 # AnimationCurves (real animation data).
2804 for ac_uuid
, fbx_acitem
in fbx_table_nodes
.items():
2805 fbx_acdata
, _blen_data
= fbx_acitem
2806 if fbx_acdata
.id != b
'AnimationCurve' or fbx_acdata
.props
[2] != b
'':
2808 for acn_uuid
, acn_ctype
in fbx_connection_map
.get(ac_uuid
, ()):
2809 if acn_ctype
.props
[0] != b
'OP':
2811 fbx_acndata
, _bl_acndata
= fbx_table_nodes
.get(acn_uuid
, (None, None))
2812 if (fbx_acndata
is None or fbx_acndata
.id != b
'AnimationCurveNode' or
2813 fbx_acndata
.props
[2] != b
'' or acn_uuid
not in curvenodes
):
2815 # Note this is an infamous simplification of the compound props stuff,
2816 # seems to be standard naming but we'll probably have to be smarter to handle more exotic files?
2817 channel
= {b
'd|X': 0, b
'd|Y': 1, b
'd|Z': 2, b
'd|DeformPercent': 0}.get(acn_ctype
.props
[3], None)
2820 curvenodes
[acn_uuid
][ac_uuid
] = (fbx_acitem
, channel
)
2822 # And now that we have sorted all this, apply animations!
2823 blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, settings
.anim_offset
)
2827 perfmon
.step("FBX import: Assign materials...")
2830 # link Material's to Geometry (via Model's)
2831 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2832 fbx_obj
, blen_data
= fbx_item
2833 if fbx_obj
.id != b
'Geometry':
2836 mesh
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
2838 # can happen in rare cases
2842 # In Blender, we link materials to data, typically (meshes), while in FBX they are linked to objects...
2843 # So we have to be careful not to re-add endlessly the same material to a mesh!
2844 # This can easily happen with 'baked' dupliobjects, see T44386.
2845 # TODO: add an option to link materials to objects in Blender instead?
2848 for (fbx_lnk
, fbx_lnk_item
, fbx_lnk_type
) in connection_filter_forward(fbx_uuid
, b
'Model'):
2850 fbx_lnk_uuid
= elem_uuid(fbx_lnk
)
2851 for (fbx_lnk_material
, material
, fbx_lnk_material_type
) in connection_filter_reverse(fbx_lnk_uuid
, b
'Material'):
2852 if material
not in done_mats
:
2853 mesh
.materials
.append(material
)
2854 done_mats
.add(material
)
2856 # We have to validate mesh polygons' mat_idx, see T41015!
2857 # Some FBX seem to have an extra 'default' material which is not defined in FBX file.
2858 if mesh
.validate_material_indices():
2859 print("WARNING: mesh '%s' had invalid material indices, those were reset to first material" % mesh
.name
)
2862 perfmon
.step("FBX import: Assign textures...")
2865 material_images
= {}
2867 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
2868 # b'KFbxSurfaceLambert'
2870 # textures that use this material
2871 def texture_bumpfac_get(fbx_obj
):
2872 assert(fbx_obj
.id == b
'Material')
2873 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
2874 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2875 # Do not assert, it can be None actually, sigh...
2876 #~ assert(fbx_props[0] is not None)
2877 # (x / 7.142) is only a guess, cycles usable range is (0.0 -> 0.5)
2878 return elem_props_get_number(fbx_props
, b
'BumpFactor', 2.5) / 7.142
2880 def texture_mapping_get(fbx_obj
):
2881 assert(fbx_obj
.id == b
'Texture')
2883 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
2884 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2885 # Do not assert, it can be None actually, sigh...
2886 #~ assert(fbx_props[0] is not None)
2887 return (elem_props_get_vector_3d(fbx_props
, b
'Translation', (0.0, 0.0, 0.0)),
2888 elem_props_get_vector_3d(fbx_props
, b
'Rotation', (0.0, 0.0, 0.0)),
2889 elem_props_get_vector_3d(fbx_props
, b
'Scaling', (1.0, 1.0, 1.0)),
2890 (bool(elem_props_get_enum(fbx_props
, b
'WrapModeU', 0)),
2891 bool(elem_props_get_enum(fbx_props
, b
'WrapModeV', 0))))
2894 # Simple function to make a new mtex and set defaults
2895 def material_mtex_new(material
, image
, tex_map
):
2896 tex
= texture_cache
.get(image
)
2898 tex
= bpy
.data
.textures
.new(name
=image
.name
, type='IMAGE')
2900 texture_cache
[image
] = tex
2902 # copy custom properties from image object to texture
2903 for key
, value
in image
.items():
2906 # delete custom properties on the image object
2907 for key
in image
.keys():
2910 mtex
= material
.texture_slots
.add()
2912 mtex
.texture_coords
= 'UV'
2913 mtex
.use_map_color_diffuse
= False
2915 # No rotation here...
2916 mtex
.offset
[:] = tex_map
[0]
2917 mtex
.scale
[:] = tex_map
[2]
2920 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2921 fbx_obj
, blen_data
= fbx_item
2922 if fbx_obj
.id != b
'Material':
2925 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
2928 fbx_lnk_type
) in connection_filter_reverse(fbx_uuid
, b
'Texture'):
2931 if fbx_lnk_type
.props
[0] == b
'OP':
2932 lnk_type
= fbx_lnk_type
.props
[3]
2934 ma_wrap
= cycles_material_wrap_map
[material
]
2937 tex_map
= texture_mapping_get(fbx_lnk
)
2938 if (tex_map
[0] == (0.0, 0.0, 0.0) and
2939 tex_map
[1] == (0.0, 0.0, 0.0) and
2940 tex_map
[2] == (1.0, 1.0, 1.0) and
2941 tex_map
[3] == (False, False)):
2946 "translation": tex_map
[0],
2947 "rotation": [-i
for i
in tex_map
[1]],
2948 "scale": [((1.0 / i
) if i
!= 0.0 else 1.0) for i
in tex_map
[2]],
2949 "clamp": tex_map
[3],
2952 if lnk_type
in {b
'DiffuseColor', b
'3dsMax|maps|texmap_diffuse'}:
2953 ma_wrap
.diffuse_image_set(image
)
2955 ma_wrap
.diffuse_mapping_set(**tex_map_kw
)
2956 elif lnk_type
== b
'SpecularColor':
2957 ma_wrap
.specular_image_set(image
)
2959 ma_wrap
.specular_mapping_set(**tex_map_kw
)
2960 elif lnk_type
in {b
'ReflectionColor', b
'3dsMax|maps|texmap_reflection'}:
2961 ma_wrap
.reflect_image_set(image
)
2963 ma_wrap
.reflect_mapping_set(**tex_map_kw
)
2964 elif lnk_type
== b
'TransparentColor': # alpha
2965 ma_wrap
.alpha_image_set(image
)
2967 ma_wrap
.alpha_mapping_set(**tex_map_kw
)
2968 if use_alpha_decals
:
2969 material_decals
.add(material
)
2970 elif lnk_type
== b
'DiffuseFactor':
2972 elif lnk_type
== b
'ShininessExponent':
2973 ma_wrap
.hardness_image_set(image
)
2975 ma_wrap
.hardness_mapping_set(**tex_map_kw
)
2976 # XXX, applications abuse bump!
2977 elif lnk_type
in {b
'NormalMap', b
'Bump', b
'3dsMax|maps|texmap_bump'}:
2978 ma_wrap
.normal_image_set(image
)
2979 ma_wrap
.normal_factor_set(texture_bumpfac_get(fbx_obj
))
2981 ma_wrap
.normal_mapping_set(**tex_map_kw
)
2983 elif lnk_type == b'Bump':
2984 ma_wrap.bump_image_set(image)
2985 ma_wrap.bump_factor_set(texture_bumpfac_get(fbx_obj))
2987 ma_wrap.bump_mapping_set(**tex_map_kw)
2990 print("WARNING: material link %r ignored" % lnk_type
)
2992 material_images
.setdefault(material
, {})[lnk_type
] = (image
, tex_map
)
2994 if fbx_lnk_type
.props
[0] == b
'OP':
2995 lnk_type
= fbx_lnk_type
.props
[3]
2997 # tx/rot/scale (rot is ignored here!).
2998 tex_map
= texture_mapping_get(fbx_lnk
)
3000 mtex
= material_mtex_new(material
, image
, tex_map
)
3002 if lnk_type
in {b
'DiffuseColor', b
'3dsMax|maps|texmap_diffuse'}:
3003 mtex
.use_map_color_diffuse
= True
3004 mtex
.blend_type
= 'MULTIPLY'
3005 elif lnk_type
== b
'SpecularColor':
3006 mtex
.use_map_color_spec
= True
3007 mtex
.blend_type
= 'MULTIPLY'
3008 elif lnk_type
in {b
'ReflectionColor', b
'3dsMax|maps|texmap_reflection'}:
3009 mtex
.use_map_raymir
= True
3010 elif lnk_type
== b
'TransparentColor': # alpha
3011 material
.use_transparency
= True
3012 material
.transparency_method
= 'RAYTRACE'
3013 material
.alpha
= 0.0
3014 mtex
.use_map_alpha
= True
3015 mtex
.alpha_factor
= 1.0
3016 if use_alpha_decals
:
3017 material_decals
.add(material
)
3018 elif lnk_type
== b
'DiffuseFactor':
3019 mtex
.use_map_diffuse
= True
3020 elif lnk_type
== b
'ShininessExponent':
3021 mtex
.use_map_hardness
= True
3022 # XXX, applications abuse bump!
3023 elif lnk_type
in {b
'NormalMap', b
'Bump', b
'3dsMax|maps|texmap_bump'}:
3024 mtex
.texture
.use_normal_map
= True # not ideal!
3025 mtex
.use_map_normal
= True
3026 mtex
.normal_factor
= texture_bumpfac_get(fbx_obj
)
3028 elif lnk_type == b'Bump':
3029 mtex.use_map_normal = True
3030 mtex.normal_factor = texture_bumpfac_get(fbx_obj)
3033 print("WARNING: material link %r ignored" % lnk_type
)
3035 material_images
.setdefault(material
, {})[lnk_type
] = (image
, tex_map
)
3037 # Check if the diffuse image has an alpha channel,
3038 # if so, use the alpha channel.
3040 # Note: this could be made optional since images may have alpha but be entirely opaque
3041 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3042 fbx_obj
, blen_data
= fbx_item
3043 if fbx_obj
.id != b
'Material':
3045 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3046 image
, tex_map
= material_images
.get(material
, {}).get(b
'DiffuseColor', (None, None))
3048 if image
and image
.depth
== 32:
3049 if use_alpha_decals
:
3050 material_decals
.add(material
)
3053 ma_wrap
= cycles_material_wrap_map
[material
]
3054 if ma_wrap
.node_bsdf_alpha
.mute
:
3055 ma_wrap
.alpha_image_set_from_diffuse()
3057 if not any((True for mtex
in material
.texture_slots
if mtex
and mtex
.use_map_alpha
)):
3058 mtex
= material_mtex_new(material
, image
, tex_map
)
3060 material
.use_transparency
= True
3061 material
.transparency_method
= 'RAYTRACE'
3062 material
.alpha
= 0.0
3063 mtex
.use_map_alpha
= True
3064 mtex
.alpha_factor
= 1.0
3066 # propagate mapping from diffuse to all other channels which have none defined.
3068 ma_wrap
= cycles_material_wrap_map
[material
]
3069 ma_wrap
.mapping_set_from_diffuse()
3073 perfmon
.step("FBX import: Cycles z-offset workaround...")
3076 # Annoying workaround for cycles having no z-offset
3077 if material_decals
and use_alpha_decals
:
3078 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3079 fbx_obj
, blen_data
= fbx_item
3080 if fbx_obj
.id != b
'Geometry':
3082 if fbx_obj
.props
[-1] == b
'Mesh':
3085 if decal_offset
!= 0.0:
3086 for material
in mesh
.materials
:
3087 if material
in material_decals
:
3088 for v
in mesh
.vertices
:
3089 v
.co
+= v
.normal
* decal_offset
3093 for obj
in (obj
for obj
in bpy
.data
.objects
if obj
.data
== mesh
):
3094 obj
.cycles_visibility
.shadow
= False
3096 for material
in mesh
.materials
:
3097 if material
in material_decals
:
3098 # recieve but dont cast shadows
3099 material
.use_raytrace
= False
3102 perfmon
.level_down()
3104 perfmon
.level_down("Import finished.")