Skip to content
Snippets Groups Projects
import_fbx.py 109 KiB
Newer Older
  • Learn to ignore specific revisions
  •         mesh.edges.add(tot_edges)
            mesh.edges.foreach_set("vertices", edges_conv)
    
    
        # must be after edge, face loading.
        ok_smooth = blen_read_geom_layer_smooth(fbx_obj, mesh)
    
    
        if geom_mat_no is None:
            ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh)
        else:
            def nortrans(v):
                return geom_mat_no * Vector(v)
            ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh, nortrans)
    
    
        if not ok_normals:
            mesh.calc_normals()
    
        if not ok_smooth:
            for p in mesh.polygons:
                p.use_smooth = True
    
    
        if settings.use_custom_props:
            blen_read_custom_properties(fbx_obj, mesh, settings)
    
    
    def blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene):
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        from mathutils import Vector
    
        elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry')
        indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes'), default=())
        dvcos = tuple(co for co in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata, b'Vertices'), default=()))] * 3))
        # We completely ignore normals here!
        weight = elem_prop_first(elem_find_first(fbx_bcdata, b'DeformPercent'), default=100.0) / 100.0
        vgweights = tuple(vgw / 100.0 for vgw in elem_prop_first(elem_find_first(fbx_bcdata, b'FullWeights'), default=()))
    
    
        # Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
        nbr_indices = len(indices)
        if len(vgweights) == 1 and nbr_indices > 1:
            vgweights = (vgweights[0],) * nbr_indices
    
        assert(len(vgweights) == nbr_indices == len(dvcos))
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        create_vg = bool(set(vgweights) - {1.0})
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        for me, objects in meshes:
            vcos = tuple((idx, me.vertices[idx].co + Vector(dvco)) for idx, dvco in zip(indices, dvcos))
    
            objects = list({node.bl_obj for node in objects})
    
    Bastien Montagne's avatar
    Bastien Montagne committed
            assert(objects)
    
            if me.shape_keys is None:
                objects[0].shape_key_add(name="Basis", from_mix=False)
            objects[0].shape_key_add(name=elem_name_utf8, from_mix=False)
            me.shape_keys.use_relative = True  # Should already be set as such.
    
            kb = me.shape_keys.key_blocks[elem_name_utf8]
            for idx, co in vcos:
                kb.data[idx].co[:] = co
            kb.value = weight
    
            # Add vgroup if necessary.
            if create_vg:
                add_vgroup_to_objects(indices, vgweights, elem_name_utf8, objects)
                kb.vertex_group = elem_name_utf8
    
    
    def blen_read_material(fbx_tmpl, fbx_obj, settings):
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Material')
    
        cycles_material_wrap_map = settings.cycles_material_wrap_map
    
        ma = bpy.data.materials.new(name=elem_name_utf8)
    
        const_color_white = 1.0, 1.0, 1.0
    
    
        fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
                     elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
        assert(fbx_props[0] is not None)
    
    
        ma_diff = elem_props_get_color_rgb(fbx_props, b'DiffuseColor', const_color_white)
        ma_spec = elem_props_get_color_rgb(fbx_props, b'SpecularColor', const_color_white)
        ma_alpha = elem_props_get_number(fbx_props, b'Opacity', 1.0)
        ma_spec_intensity = ma.specular_intensity = elem_props_get_number(fbx_props, b'SpecularFactor', 0.25) * 2.0
        ma_spec_hardness = elem_props_get_number(fbx_props, b'Shininess', 9.6)
        ma_refl_factor = elem_props_get_number(fbx_props, b'ReflectionFactor', 0.0)
        ma_refl_color = elem_props_get_color_rgb(fbx_props, b'ReflectionColor', const_color_white)
    
    
            from . import cycles_shader_compat
            # viewport color
            ma.diffuse_color = ma_diff
    
            ma_wrap = cycles_shader_compat.CyclesShaderWrapper(ma)
            ma_wrap.diffuse_color_set(ma_diff)
            ma_wrap.specular_color_set([c * ma_spec_intensity for c in ma_spec])
    
            ma_wrap.hardness_value_set(((ma_spec_hardness + 3.0) / 5.0) - 0.65)
    
            ma_wrap.alpha_value_set(ma_alpha)
            ma_wrap.reflect_factor_set(ma_refl_factor)
            ma_wrap.reflect_color_set(ma_refl_color)
    
            cycles_material_wrap_map[ma] = ma_wrap
        else:
            # TODO, number BumpFactor isnt used yet
            ma.diffuse_color = ma_diff
            ma.specular_color = ma_spec
            ma.alpha = ma_alpha
            ma.specular_intensity = ma_spec_intensity
            ma.specular_hardness = ma_spec_hardness * 5.10 + 1.0
    
            if ma_refl_factor != 0.0:
                ma.raytrace_mirror.use = True
                ma.raytrace_mirror.reflect_factor = ma_refl_factor
                ma.mirror_color = ma_refl_color
    
    
        if settings.use_custom_props:
            blen_read_custom_properties(fbx_obj, ma, settings)
    
    
    def blen_read_texture(fbx_tmpl, fbx_obj, basedir, settings):
    
        import os
        from bpy_extras import image_utils
    
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Texture')
    
        filepath = elem_find_first_string(fbx_obj, b'FileName')
        if os.sep == '/':
            filepath = filepath.replace('\\', '/')
        else:
            filepath = filepath.replace('/', '\\')
    
        image = image_cache.get(filepath)
        if image is not None:
            return image
    
    
        image = image_utils.load_image(
            filepath,
            dirname=basedir,
            place_holder=True,
    
        image_cache[filepath] = image
        # name can be ../a/b/c
        image.name = os.path.basename(elem_name_utf8)
    
        if settings.use_custom_props:
            blen_read_custom_properties(fbx_obj, image, settings)
    
    
    def blen_read_camera(fbx_tmpl, fbx_obj, global_scale):
    
        # meters to inches
        M2I = 0.0393700787
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'NodeAttribute')
    
        fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
                     elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
        assert(fbx_props[0] is not None)
    
    
        camera = bpy.data.cameras.new(name=elem_name_utf8)
    
    
        camera.type = 'ORTHO' if elem_props_get_enum(fbx_props, b'CameraProjectionType', 0) == 1 else 'PERSP'
    
    
        camera.lens = elem_props_get_number(fbx_props, b'FocalLength', 35.0)
        camera.sensor_width = elem_props_get_number(fbx_props, b'FilmWidth', 32.0 * M2I) / M2I
        camera.sensor_height = elem_props_get_number(fbx_props, b'FilmHeight', 32.0 * M2I) / M2I
    
    
        camera.ortho_scale = elem_props_get_number(fbx_props, b'OrthoZoom', 1.0)
    
    
        filmaspect = camera.sensor_width / camera.sensor_height
        # film offset
        camera.shift_x = elem_props_get_number(fbx_props, b'FilmOffsetX', 0.0) / (M2I * camera.sensor_width)
        camera.shift_y = elem_props_get_number(fbx_props, b'FilmOffsetY', 0.0) / (M2I * camera.sensor_height * filmaspect)
    
    
        camera.clip_start = elem_props_get_number(fbx_props, b'NearPlane', 0.01) * global_scale
        camera.clip_end = elem_props_get_number(fbx_props, b'FarPlane', 100.0) * global_scale
    
    def blen_read_light(fbx_tmpl, fbx_obj, global_scale):
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'NodeAttribute')
    
        fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
                     elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
    
        # rare
        if fbx_props[0] is None:
            lamp = bpy.data.lamps.new(name=elem_name_utf8, type='POINT')
            return lamp
    
    
        light_type = {
            0: 'POINT',
            1: 'SUN',
            2: 'SPOT'}.get(elem_props_get_enum(fbx_props, b'LightType', 0), 'POINT')
    
        lamp = bpy.data.lamps.new(name=elem_name_utf8, type=light_type)
    
    
        if light_type == 'SPOT':
    
            spot_size = elem_props_get_number(fbx_props, b'OuterAngle', None)
            if spot_size is None:
                # Deprecated.
                spot_size = elem_props_get_number(fbx_props, b'Cone angle', 45.0)
            lamp.spot_size = math.radians(spot_size)
    
            spot_blend = elem_props_get_number(fbx_props, b'InnerAngle', None)
            if spot_blend is None:
                # Deprecated.
                spot_blend = elem_props_get_number(fbx_props, b'HotSpot', 45.0)
            lamp.spot_blend = 1.0 - (spot_blend / spot_size)
    
        lamp.color = elem_props_get_color_rgb(fbx_props, b'Color', (1.0, 1.0, 1.0))
    
        lamp.energy = elem_props_get_number(fbx_props, b'Intensity', 100.0) / 100.0
        lamp.distance = elem_props_get_number(fbx_props, b'DecayStart', 25.0) * global_scale
        lamp.shadow_method = ('RAY_SHADOW' if elem_props_get_bool(fbx_props, b'CastShadow', True) else 'NOSHADOW')
        lamp.shadow_color = elem_props_get_color_rgb(fbx_props, b'ShadowColor', (0.0, 0.0, 0.0))
    
    1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831
    # ### Import Utility class
    class FbxImportHelperNode:
        """
        Temporary helper node to store a hierarchy of fbxNode objects before building
        Objects, Armatures and Bones. It tries to keep the correction data in one place so it can be applied consistently to the imported data.
        """
    
        __slots__ = ('_parent', 'anim_compensation_matrix', 'armature_setup', 'bind_matrix', 'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix',
                     'children', 'clusters', 'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type', 'has_bone_children', 'ignore', 'is_armature',
                     'is_bone', 'is_root', 'matrix', 'meshes', 'post_matrix', 'pre_matrix')
    
        def __init__(self, fbx_elem, bl_data, fbx_transform_data, is_bone):
            self.fbx_name = elem_name_ensure_class(fbx_elem, b'Model') if fbx_elem else 'Unknown'
            self.fbx_type = fbx_elem.props[2] if fbx_elem else None
            self.fbx_elem = fbx_elem
            self.bl_obj = None
            self.bl_data = bl_data
            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!)
            self.fbx_transform_data = fbx_transform_data
            self.is_root = False
            self.is_bone = is_bone
            self.is_armature = False
            self.has_bone_children = False          # True if the hierarchy below this node contains bones, important to support mixed hierarchies.
            self.ignore = False                     # True for leaf-bones added to the end of some bone chains to set the lengths.
            self.pre_matrix = None                  # correction matrix that needs to be applied before the FBX transform
            self.bind_matrix = None                 # for bones this is the matrix used to bind to the skin
            self.matrix = blen_read_object_transform_do(fbx_transform_data) if fbx_transform_data else None
            self.post_matrix = None                 # correction matrix that needs to be applied after the FBX transform
            self.bone_child_matrix = None           # Objects attached to a bone end not the beginning, this matrix corrects for that
            self.anim_compensation_matrix = None    # a mesh moved in the hierarchy may have a different local matrix. This compensates animations for this.
    
            self.meshes = None                      # List of meshes influenced by this bone.
            self.clusters = []                      # Deformer Cluster nodes
            self.armature_setup = None              # mesh and armature matrix when the mesh was bound
    
            self._parent = None
            self.children = []
    
        @property
        def parent(self):
            return self._parent
    
        @parent.setter
        def parent(self, value):
            if self._parent is not None:
                self._parent.children.remove(self)
            self._parent = value
            if self._parent is not None:
                self._parent.children.append(self)
    
        def print_info(self, indent=0):
            print(" " * indent + (self.fbx_name if self.fbx_name else "(Null)")
                  + ("[root]" if self.is_root else "")
                  + ("[ignore]" if self.ignore else "")
                  + ("[armature]" if self.is_armature else "")
                  + ("[bone]" if self.is_bone else "")
                  + ("[HBC]" if self.has_bone_children else "")
                  )
            for c in self.children:
                c.print_info(indent + 1)
    
        def mark_leaf_bones(self):
            if self.is_bone and len(self.children) == 1:
                child = self.children[0]
                if child.is_bone and len(child.children) == 0:
                    child.ignore = True  # Ignore leaf bone at end of chain
            for child in self.children:
                child.mark_leaf_bones()
    
        def do_bake_transform(self, settings):
            return (settings.bake_space_transform and self.fbx_type in (b'Mesh', b'Null') and
                    not self.is_armature and not self.is_bone)
    
        def find_correction_matrix(self, settings, parent_correction_inv=None):
            from bpy_extras.io_utils import axis_conversion
            from mathutils import Matrix, Vector
    
            if self.parent and (self.parent.is_root or self.parent.do_bake_transform(settings)):
                self.pre_matrix = settings.global_matrix
    
            if parent_correction_inv:
                self.pre_matrix = parent_correction_inv * (self.pre_matrix if self.pre_matrix else Matrix())
    
            correction_matrix = None
    
            if self.is_bone:
                if settings.automatic_bone_orientation:
                    # find best orientation to align bone with
                    bone_children = [child for child in self.children if child.is_bone]
                    if len(bone_children) == 0:
                        # no children, inherit the correction from parent (if possible)
                        if self.parent and self.parent.is_bone:
                            correction_matrix = parent_correction_inv.inverted() if parent_correction_inv else None
                    else:
                        # else find how best to rotate the bone to align the Y axis with the children
                        best_axis = (1, 0, 0)
                        if len(bone_children) == 1:
                            vec = bone_children[0].bind_matrix.to_translation()
                            best_axis = Vector((0, 0, 1 if vec[2] >= 0 else -1))
                            if abs(vec[0]) > abs(vec[1]):
                                if abs(vec[0]) > abs(vec[2]):
                                    best_axis = Vector((1 if vec[0] >= 0 else -1, 0, 0))
                            elif abs(vec[1]) > abs(vec[2]):
                                best_axis = Vector((0, 1 if vec[1] >= 0 else -1, 0))
                        else:
                            # get the child directions once because they may be checked several times
                            child_locs = [loc.normalized() for loc in [bind_matrix.to_translation() for bind_matrix in [child.bind_matrix for child in bone_children]] if loc.magnitude > 0.0]
    
                            # I'm not sure which one I like better...
                            if False:
                                best_angle = -1.0
                                for i in range(6):
                                    a = i // 2
                                    s = -1 if i % 2 == 1 else 1
                                    test_axis = Vector((s if a == 0 else 0, s if a == 1 else 0, s if a == 2 else 0))
    
                                    # find max angle to children
                                    max_angle = 1.0
                                    for loc in child_locs:
                                        max_angle = min(max_angle, test_axis.dot(loc))
    
                                    # is it better than the last one?
                                    if best_angle < max_angle:
                                        best_angle = max_angle
                                        best_axis = test_axis
                            else:
                                best_angle = -1.0
                                for vec in child_locs:
                                    test_axis = Vector((0, 0, 1 if vec[2] >= 0 else -1))
                                    if abs(vec[0]) > abs(vec[1]):
                                        if abs(vec[0]) > abs(vec[2]):
                                            test_axis = Vector((1 if vec[0] >= 0 else -1, 0, 0))
                                    elif abs(vec[1]) > abs(vec[2]):
                                        test_axis = Vector((0, 1 if vec[1] >= 0 else -1, 0))
    
                                    # find max angle to children
                                    max_angle = 1.0
                                    for loc in child_locs:
                                        max_angle = min(max_angle, test_axis.dot(loc))
    
                                    # is it better than the last one?
                                    if best_angle < max_angle:
                                        best_angle = max_angle
                                        best_axis = test_axis
    
                        # convert best_axis to axis string
                        to_up = 'Z' if best_axis[2] >= 0 else '-Z'
                        if abs(best_axis[0]) > abs(best_axis[1]):
                            if abs(best_axis[0]) > abs(best_axis[2]):
                                to_up = 'X' if best_axis[0] >= 0 else '-X'
                        elif abs(best_axis[1]) > abs(best_axis[2]):
                            to_up = 'Y' if best_axis[1] >= 0 else '-Y'
                        to_forward = 'X' if to_up not in {'X', '-X'} else 'Y'
    
                        # Build correction matrix
                        if (to_up, to_forward) != ('Y', 'X'):
                            correction_matrix = axis_conversion(from_forward='X',
                                                                from_up='Y',
                                                                to_forward=to_forward,
                                                                to_up=to_up,
                                                                ).to_4x4()
                else:
                    correction_matrix = settings.bone_correction_matrix
            else:
                # camera and light can be hard wired
                if self.fbx_type == b'Camera':
                    correction_matrix = MAT_CONVERT_CAMERA
                elif self.fbx_type == b'Light':
                    correction_matrix = MAT_CONVERT_LAMP
    
            self.post_matrix = correction_matrix
    
            if self.do_bake_transform(settings):
                self.post_matrix = settings.global_matrix_inv * (self.post_matrix if self.post_matrix else Matrix())
    
            # process children
            correction_matrix_inv = correction_matrix.inverted() if correction_matrix else None
            for child in self.children:
                child.find_correction_matrix(settings, correction_matrix_inv)
    
        def find_armatures(self):
            needs_armature = False
            for child in self.children:
                if child.is_bone:
                    needs_armature = True
                    break
            if needs_armature:
                if self.fbx_type == b'Null':
                    # if empty then convert into armature
                    self.is_armature = True
                else:
                    # otherwise insert a new node
                    armature = FbxImportHelperNode(None, None, None, False)
                    armature.fbx_name = "Armature"
                    armature.is_armature = True
    
                    for child in self.children[:]:
                        if child.is_bone:
                            child.parent = armature
    
                    armature.parent = self
    
            for child in self.children:
                if child.is_armature:
                    continue
                if child.is_bone:
                    continue
                child.find_armatures()
    
        def find_bone_children(self):
            has_bone_children = False
            for child in self.children:
                has_bone_children |= child.find_bone_children()
            self.has_bone_children = has_bone_children
            return self.is_bone or has_bone_children
    
        def find_fake_bones(self, in_armature=False):
            if in_armature and not self.is_bone and self.has_bone_children:
                self.is_bone = True
                # if we are not a null node we need an intermediate node for the data
                if self.fbx_type != b'Null':
                    node = FbxImportHelperNode(self.fbx_elem, self.bl_data, None, False)
                    self.fbx_elem = None
                    self.bl_data = None
    
                    # transfer children
                    for child in self.children:
                        if child.is_bone or child.has_bone_children:
                            continue
                        child.parent = node
    
                    # attach to parent
                    node.parent = self
    
            if self.is_armature:
                in_armature = True
            for child in self.children:
                child.find_fake_bones(in_armature)
    
        def get_world_matrix(self):
            from mathutils import Matrix
    
            matrix = self.parent.get_world_matrix() if self.parent else Matrix()
            if self.matrix:
                matrix = matrix * self.matrix
            return matrix
    
        def get_matrix(self):
            from mathutils import Matrix
    
            matrix = self.matrix if self.matrix else Matrix()
            if self.pre_matrix:
                matrix = self.pre_matrix * matrix
            if self.post_matrix:
                matrix = matrix * self.post_matrix
            return matrix
    
        def get_bind_matrix(self):
            from mathutils import Matrix
    
            matrix = self.bind_matrix if self.bind_matrix else Matrix()
            if self.pre_matrix:
                matrix = self.pre_matrix * matrix
            if self.post_matrix:
                matrix = matrix * self.post_matrix
            return matrix
    
        def make_bind_pose_local(self, parent_matrix=None):
            from mathutils import Matrix
    
            if parent_matrix is None:
                parent_matrix = Matrix()
    
            if self.bind_matrix:
                bind_matrix = parent_matrix.inverted() * self.bind_matrix
            else:
                bind_matrix = self.matrix.copy() if self.matrix else None
    
            self.bind_matrix = bind_matrix
            if bind_matrix:
                parent_matrix = parent_matrix * bind_matrix
    
            for child in self.children:
                child.make_bind_pose_local(parent_matrix)
    
        def collect_skeleton_meshes(self, meshes):
            for _, m in self.clusters:
                meshes.update(m)
            for child in self.children:
                child.collect_skeleton_meshes(meshes)
    
        def collect_armature_meshes(self):
            if self.is_armature:
                armature_matrix_inv = self.get_world_matrix().inverted()
    
                meshes = set()
                for child in self.children:
                    child.collect_skeleton_meshes(meshes)
                for m in meshes:
                    old_matrix = m.matrix
                    m.matrix = armature_matrix_inv * m.get_world_matrix()
                    m.anim_compensation_matrix = old_matrix.inverted() * m.matrix
                    m.parent = self
                self.meshes = meshes
            else:
                for child in self.children:
                    child.collect_armature_meshes()
    
        def build_skeleton(self, arm, parent_matrix, parent_bone_size=1):
            from mathutils import Vector, Matrix
    
            # ----
            # Now, create the (edit)bone.
            bone = arm.bl_data.edit_bones.new(name=self.fbx_name)
            bone.select = True
            self.bl_obj = arm.bl_obj
            self.bl_data = arm.bl_data
            self.bl_bone = bone.name  # Could be different from the FBX name!
    
            # get average distance to children
            bone_size = 0.0
            bone_count = 0
            for child in self.children:
                if child.is_bone:
                    bone_size += child.bind_matrix.to_translation().magnitude
                    bone_count += 1
            if bone_count > 0:
                bone_size /= bone_count
            else:
                bone_size = parent_bone_size
    
            # So that our bone gets its final length, but still Y-aligned in armature space.
            # 0-length bones are automatically collapsed into their parent when you leave edit mode, so this enforces a minimum length
            bone_tail = Vector((0.0, 1.0, 0.0)) * max(0.01, bone_size)
            bone.tail = bone_tail
    
            # And rotate/move it to its final "rest pose".
            bone_matrix = parent_matrix * self.get_bind_matrix().normalized()
    
            bone.matrix = bone_matrix
    
            # correction for children attached to a bone. Fbx expects to attach to the head of a bone, while blender attaches to the tail.
            self.bone_child_matrix = Matrix.Translation(-bone_tail)
    
            for child in self.children:
                if child.ignore:
                    continue
                if child.is_bone:
                    child_bone = child.build_skeleton(arm, bone_matrix, bone_size)
                    # Connection to parent.
                    child_bone.parent = bone
                    if similar_values_iter(bone.tail, child_bone.head):
                        child_bone.use_connect = True
    
            return bone
    
        def build_node(self, fbx_tmpl, settings):
            # create when linking since we need object data
            elem_name_utf8 = self.fbx_name
    
            # Object data must be created already
            self.bl_obj = obj = bpy.data.objects.new(name=elem_name_utf8, object_data=self.bl_data)
    
            fbx_props = (elem_find_first(self.fbx_elem, b'Properties70'),
                         elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
            assert(fbx_props[0] is not None)
    
            # ----
            # Misc Attributes
    
            obj.color[0:3] = elem_props_get_color_rgb(fbx_props, b'Color', (0.8, 0.8, 0.8))
            obj.hide = not bool(elem_props_get_visibility(fbx_props, b'Visibility', 1.0))
    
            obj.matrix_basis = self.get_matrix()
    
            if settings.use_custom_props:
                blen_read_custom_properties(fbx_props[0], obj, settings)
    
            return obj
    
        def build_skeleton_children(self, fbx_tmpl, settings, scene):
            if self.is_bone:
                for child in self.children:
                    if child.ignore:
                        continue
                    child_obj = child.build_skeleton_children(fbx_tmpl, settings, scene)
                    if child_obj:
                        child_obj.parent = self.bl_obj  # get the armature the bone belongs to
                        child_obj.parent_bone = self.bl_bone
                        child_obj.parent_type = 'BONE'
    
                        # Blender attaches to the end of a bone, while FBX attaches to the start. bone_child_matrix corrects for that.
                        if child.pre_matrix:
                            child.pre_matrix = self.bone_child_matrix * child.pre_matrix
                        else:
                            child.pre_matrix = self.bone_child_matrix
    
                        child_obj.matrix_basis = child.get_matrix()
            else:
                # child is not a bone
                obj = self.build_node(fbx_tmpl, settings)
    
                for child in self.children:
                    if child.ignore:
                        continue
                    child_obj = child.build_skeleton_children(fbx_tmpl, settings, scene)
                    if child_obj:
                        child_obj.parent = obj
    
                # instance in scene
                obj_base = scene.objects.link(obj)
                obj_base.select = True
    
                return obj
    
        def set_pose_matrix(self, arm):
            pose_bone = arm.bl_obj.pose.bones[self.bl_bone]
            pose_bone.matrix_basis = self.get_bind_matrix().inverted() * self.get_matrix()
    
            for child in self.children:
                if child.ignore:
                    continue
                if child.is_bone:
                    child.set_pose_matrix(arm)
    
        def merge_weights(self, combined_weights, fbx_cluster):
            indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=())
            weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=())
    
            for index, weight in zip(indices, weights):
                w = combined_weights.get(index)
                if w is None:
                    combined_weights[index] = [weight]
                else:
                    w.append(weight)
    
        def set_bone_weights(self):
            ignored_children = [child for child in self.children if child.is_bone and child.ignore and len(child.clusters) > 0]
    
            if len(ignored_children) > 0:
                # If we have an ignored child bone we need to merge their weights into the current bone weights.
                # (This can happen both intentionally and accidentally when skinning a model. Either way, they
                # need to be moved into a parent bone or they cause animation glitches.)
    
                for fbx_cluster, meshes in self.clusters:
                    combined_weights = {}
                    self.merge_weights(combined_weights, fbx_cluster)
    
                    for child in ignored_children:
                        for child_cluster, child_meshes in child.clusters:
                            if not meshes.isdisjoint(child_meshes):
                                self.merge_weights(combined_weights, child_cluster)
    
                    # combine child weights
                    indices = []
                    weights = []
                    for i, w in combined_weights.items():
                        indices.append(i)
                        if len(w) > 1:
                            weights.append(sum(w) / len(w))
                        else:
                            weights.append(w[0])
    
                    add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes])
    
                # clusters that drive meshes not included in a parent don't need to be merged
                all_meshes = set().union(*[meshes for _, meshes in self.clusters])
                for child in ignored_children:
                    for child_cluster, child_meshes in child.clusters:
                        if all_meshes.isdisjoint(child_meshes):
                            indices = elem_prop_first(elem_find_first(child_cluster, b'Indexes', default=None), default=())
                            weights = elem_prop_first(elem_find_first(child_cluster, b'Weights', default=None), default=())
                            add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in child_meshes])
            else:
                # set the vertex weights on meshes
                for fbx_cluster, meshes in self.clusters:
                    indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=())
                    weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=())
                    add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes])
    
            for child in self.children:
                if child.ignore:
                    continue
                if child.is_bone:
                    child.set_bone_weights()
    
        def build_hierarchy(self, fbx_tmpl, settings, scene):
            from mathutils import Matrix
    
            if self.is_armature:
                # create when linking since we need object data
                elem_name_utf8 = self.fbx_name
    
                self.bl_data = arm_data = bpy.data.armatures.new(name=elem_name_utf8)
    
                # Object data must be created already
                self.bl_obj = arm = bpy.data.objects.new(name=elem_name_utf8, object_data=arm_data)
    
                arm.matrix_basis = self.get_matrix()
    
                if self.fbx_elem:
                    fbx_props = (elem_find_first(self.fbx_elem, b'Properties70'),
                                 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
                    assert(fbx_props[0] is not None)
    
                    if settings.use_custom_props:
                        blen_read_custom_properties(fbx_props[0], arm, settings)
    
                # instance in scene
                obj_base = scene.objects.link(arm)
                obj_base.select = True
    
                # Add bones:
    
                # Switch to Edit mode.
                scene.objects.active = arm
                is_hidden = arm.hide
                arm.hide = False  # Can't switch to Edit mode hidden objects...
                bpy.ops.object.mode_set(mode='EDIT')
    
                for child in self.children:
                    if child.ignore:
                        continue
                    if child.is_bone:
                        child_obj = child.build_skeleton(self, Matrix())
    
                bpy.ops.object.mode_set(mode='OBJECT')
    
                arm.hide = is_hidden
    
                # Set pose matrix
                for child in self.children:
                    if child.ignore:
                        continue
                    if child.is_bone:
                        child.set_pose_matrix(self)
    
                # Add bone children:
                for child in self.children:
                    if child.ignore:
                        continue
                    child_obj = child.build_skeleton_children(fbx_tmpl, settings, scene)
                    if child_obj:
                        child_obj.parent = arm
    
                # Add armature modifiers to the meshes
                if self.meshes:
                    arm_mat_back = arm.matrix_basis.copy()
                    for mesh in self.meshes:
                        (amat, mmat) = mesh.armature_setup
    
                        # bring global armature & mesh matrices into *Blender* global space.
                        amat = settings.global_matrix * amat
                        mmat = settings.global_matrix * mmat
    
                        arm.matrix_basis = amat
                        me_mat_back = mesh.bl_obj.matrix_basis.copy()
                        mesh.bl_obj.matrix_basis = mmat
    
                        mod = mesh.bl_obj.modifiers.new(elem_name_utf8, 'ARMATURE')
                        mod.object = arm
    
                        mesh.bl_obj.matrix_basis = me_mat_back
                    arm.matrix_basis = arm_mat_back
    
                # Add bone weights to the deformers
                for child in self.children:
                    if child.ignore:
                        continue
                    if child.is_bone:
                        child.set_bone_weights()
    
                return arm
            elif self.fbx_elem:
                obj = self.build_node(fbx_tmpl, settings)
    
                # walk through children
                for child in self.children:
                    child_obj = child.build_hierarchy(fbx_tmpl, settings, scene)
                    child_obj.parent = obj
    
                # instance in scene
                obj_base = scene.objects.link(obj)
                obj_base.select = True
    
                return obj
            else:
                for child in self.children:
                    child_obj = child.build_hierarchy(fbx_tmpl, settings, scene)
    
    
    
    def is_ascii(filepath, size):
        with open(filepath, 'r', encoding="utf-8") as f:
            try:
                f.read(size)
                return True
            except UnicodeDecodeError:
                pass
    
        return False
    
    
    
    def load(operator, context, filepath="",
    
             use_manual_orientation=False,
             axis_forward='-Z',
             axis_up='Y',
             global_scale=1.0,
    
             use_image_search=False,
             use_alpha_decals=False,
    
             decal_offset=0.0,
             use_custom_props=True,
    
             use_custom_props_enum_as_string=True,
             ignore_leaf_bones=False,
             automatic_bone_orientation=False,
             primary_bone_axis='Y',
             secondary_bone_axis='X'):
    
        global fbx_elem_nil
        fbx_elem_nil = FBXElem('', (), (), ())
    
    
        import os
        import time
    
        from bpy_extras.io_utils import axis_conversion
        from mathutils import Matrix
    
    
        from .fbx_utils import RIGHT_HAND_AXES, FBX_FRAMERATES
    
    Bastien Montagne's avatar
    Bastien Montagne committed
        start_time = time.process_time()
    
    
        # detect ascii files
        if is_ascii(filepath, 24):
            operator.report({'ERROR'}, "ASCII FBX files are not supported %r" % filepath)
            return {'CANCELLED'}
    
    
        try:
            elem_root, version = parse_fbx.parse(filepath)
        except:
            import traceback
            traceback.print_exc()
    
            operator.report({'ERROR'}, "Couldn't open file %r" % filepath)
            return {'CANCELLED'}
    
        if version < 7100:
            operator.report({'ERROR'}, "Version %r unsupported, must be %r or later" % (version, 7100))
            return {'CANCELLED'}
    
    
        if bpy.ops.object.mode_set.poll():
            bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
    
    
        # deselect all
        if bpy.ops.object.select_all.poll():
            bpy.ops.object.select_all(action='DESELECT')
    
        basedir = os.path.dirname(filepath)
    
        cycles_material_wrap_map = {}
        image_cache = {}
        if not use_cycles:
            texture_cache = {}
    
        # Tables: (FBX_byte_id -> [FBX_data, None or Blender_datablock])
        fbx_table_nodes = {}
    
    
        if use_alpha_decals:
            material_decals = set()
        else:
            material_decals = None
    
    
        # #### Get some info from GlobalSettings.
    
    
        fbx_settings = elem_find_first(elem_root, b'GlobalSettings')
        fbx_settings_props = elem_find_first(fbx_settings, b'Properties70')
        if fbx_settings is None or fbx_settings_props is None:
            operator.report({'ERROR'}, "No 'GlobalSettings' found in file %r" % filepath)
            return {'CANCELLED'}
    
    
        # FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
        global_scale *= elem_props_get_number(fbx_settings_props, b'UnitScaleFactor', 100.0) / 100.0
    
        # Compute global matrix and scale.
        if not use_manual_orientation:
            axis_forward = (elem_props_get_integer(fbx_settings_props, b'FrontAxis', 1),
                            elem_props_get_integer(fbx_settings_props, b'FrontAxisSign', 1))
            axis_up = (elem_props_get_integer(fbx_settings_props, b'UpAxis', 2),
                       elem_props_get_integer(fbx_settings_props, b'UpAxisSign', 1))
            axis_coord = (elem_props_get_integer(fbx_settings_props, b'CoordAxis', 0),
                          elem_props_get_integer(fbx_settings_props, b'CoordAxisSign', 1))
    
            axis_key = (axis_up, axis_forward, axis_coord)
            axis_up, axis_forward = {v: k for k, v in RIGHT_HAND_AXES.items()}.get(axis_key, ('Z', 'Y'))
    
        global_matrix = (Matrix.Scale(global_scale, 4) *
                         axis_conversion(from_forward=axis_forward, from_up=axis_up).to_4x4())
    
    
        # To cancel out unwanted rotation/scale on nodes.
        global_matrix_inv = global_matrix.inverted()
        # For transforming mesh normals.
        global_matrix_inv_transposed = global_matrix_inv.transposed()
    
        # Compute bone correction matrix
        bone_correction_matrix = None  # None means no correction/identity
        if not automatic_bone_orientation:
            if (primary_bone_axis, secondary_bone_axis) != ('Y', 'X'):
                bone_correction_matrix = axis_conversion(from_forward='X',
                                                         from_up='Y',
                                                         to_forward=secondary_bone_axis,
                                                         to_up=primary_bone_axis,
                                                         ).to_4x4()
    
    
        # Compute framerate settings.
        custom_fps = elem_props_get_number(fbx_settings_props, b'CustomFrameRate', 25.0)
        time_mode = elem_props_get_enum(fbx_settings_props, b'TimeMode')
        real_fps = {eid: val for val, eid in FBX_FRAMERATES[1:]}.get(time_mode, custom_fps)
        if real_fps < 0.0:
            real_fps = 25.0
        scene.render.fps = round(real_fps)
        scene.render.fps_base = scene.render.fps / real_fps
    
    
        # store global settings that need to be accessed during conversion
        settings = FBXImportSettings(
            operator.report, (axis_up, axis_forward), global_matrix, global_scale,
    
            bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
    
            use_cycles, use_image_search,
            use_alpha_decals, decal_offset,
    
            use_custom_props, use_custom_props_enum_as_string,
    
            cycles_material_wrap_map, image_cache,
            ignore_leaf_bones, automatic_bone_orientation, bone_correction_matrix,
    
        # #### And now, the "real" data.
    
        fbx_defs = elem_find_first(elem_root, b'Definitions')  # can be None
    
        fbx_nodes = elem_find_first(elem_root, b'Objects')
        fbx_connections = elem_find_first(elem_root, b'Connections')
    
        if fbx_nodes is None:
    
            operator.report({'ERROR'}, "No 'Objects' found in file %r" % filepath)
            return {'CANCELLED'}
    
        if fbx_connections is None:
    
            operator.report({'ERROR'}, "No 'Connections' found in file %r" % filepath)
            return {'CANCELLED'}
    
        # ----
        # First load property templates
        # Load 'PropertyTemplate' values.
        # Key is a tuple, (ObjectType, FBXNodeType)
        # eg, (b'Texture', b'KFbxFileTexture')
        #     (b'Geometry', b'KFbxMesh')
        fbx_templates = {}
    
        def _():
            if fbx_defs is not None:
                for fbx_def in fbx_defs.elems:
                    if fbx_def.id == b'ObjectType':
                        for fbx_subdef in fbx_def.elems:
                            if fbx_subdef.id == b'PropertyTemplate':
                                assert(fbx_def.props_type == b'S')