Source code for rofunc.utils.datalab.poselib.poselib.skeleton.backend.fbx.fbx_backend

# Copyright (c) 2018-2023, NVIDIA Corporation
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


"""
This script reads an fbx file and returns the joint names, parents, and transforms.

NOTE: It requires the Python FBX package to be installed.
"""

import sys

import numpy as np

try:
    import fbx
    import FbxCommon
except ImportError as e:
    print(
        "Error: FBX library failed to load - importing FBX data will not succeed. Message: {}".format(
            e
        )
    )
    print("FBX Python SDK must be installed")


[docs]def fbx_to_npy(file_name_in, root_joint_name, fps): """ This function reads in an fbx file, and saves the relevant info to a numpy array Fbx files have a series of animation curves, each of which has animations at different times. This script assumes that for mocap data, there is only one animation curve that contains all the joints. Otherwise it is unclear how to read in the data. If this condition isn't met, then the method throws an error :param file_name_in: str, file path in. Should be .fbx file :return: nothing, it just writes a file. """ # Create the fbx scene object and load the .fbx file fbx_sdk_manager, fbx_scene = FbxCommon.InitializeSdkObjects() FbxCommon.LoadScene(fbx_sdk_manager, fbx_scene, file_name_in) """ To read in the animation, we must find the root node of the skeleton. Unfortunately fbx files can have "scene parents" and other parts of the tree that are not joints As a crude fix, this reader just takes and finds the first thing which has an animation curve attached """ search_root = root_joint_name is None or root_joint_name == "" # Get the root node of the skeleton, which is the child of the scene's root node possible_root_nodes = [fbx_scene.GetRootNode()] found_root_node = False max_key_count = 0 root_joint = None while len(possible_root_nodes) > 0: joint = possible_root_nodes.pop(0) if not search_root: if joint.GetName() == root_joint_name: root_joint = joint try: curve = _get_animation_curve(joint, fbx_scene) except RuntimeError: curve = None if curve is not None: key_count = curve.KeyGetCount() if key_count > max_key_count: found_root_node = True max_key_count = key_count root_curve = curve if search_root and not root_joint: root_joint = joint for child_index in range(joint.GetChildCount()): possible_root_nodes.append(joint.GetChild(child_index)) if not found_root_node: raise RuntimeError("No root joint found!! Exiting") joint_list, joint_names, parents = _get_skeleton(root_joint) """ Read in the transformation matrices of the animation, taking the scaling into account """ anim_range, frame_count, frame_rate = _get_frame_count(fbx_scene) local_transforms = [] # for frame in range(frame_count): time_sec = anim_range.GetStart().GetSecondDouble() time_range_sec = anim_range.GetStop().GetSecondDouble() - time_sec fbx_fps = frame_count / time_range_sec if fps != 120: fbx_fps = fps print("FPS: ", fbx_fps) while time_sec < anim_range.GetStop().GetSecondDouble(): fbx_time = fbx.FbxTime() fbx_time.SetSecondDouble(time_sec) fbx_time = fbx_time.GetFramedTime() transforms_current_frame = [] # Fbx has a unique time object which you need # fbx_time = root_curve.KeyGetTime(frame) for joint in joint_list: arr = np.array(_recursive_to_list(joint.EvaluateLocalTransform(fbx_time))) scales = np.array(_recursive_to_list(joint.EvaluateLocalScaling(fbx_time))) if not np.allclose(scales[0:3], scales[0]): raise ValueError( "Different X, Y and Z scaling. Unsure how this should be handled. " "To solve this, look at this link and try to upgrade the script " "http://help.autodesk.com/view/FBX/2017/ENU/?guid=__files_GUID_10CDD" "63C_79C1_4F2D_BB28_AD2BE65A02ED_htm" ) # Adjust the array for scaling arr /= scales[0] arr[3, 3] = 1.0 transforms_current_frame.append(arr) local_transforms.append(transforms_current_frame) time_sec += 1.0 / fbx_fps local_transforms = np.array(local_transforms) print("Frame Count: ", len(local_transforms)) return joint_names, parents, local_transforms, fbx_fps
def _get_frame_count(fbx_scene): # Get the animation stacks and layers, in order to pull off animation curves later num_anim_stacks = fbx_scene.GetSrcObjectCount( FbxCommon.FbxCriteria.ObjectType(FbxCommon.FbxAnimStack.ClassId) ) # if num_anim_stacks != 1: # raise RuntimeError( # "More than one animation stack was found. " # "This script must be modified to handle this case. Exiting" # ) if num_anim_stacks > 1: index = 1 else: index = 0 anim_stack = fbx_scene.GetSrcObject( FbxCommon.FbxCriteria.ObjectType(FbxCommon.FbxAnimStack.ClassId), index ) anim_range = anim_stack.GetLocalTimeSpan() duration = anim_range.GetDuration() fps = duration.GetFrameRate(duration.GetGlobalTimeMode()) frame_count = duration.GetFrameCount(True) return anim_range, frame_count, fps def _get_animation_curve(joint, fbx_scene): # Get the animation stacks and layers, in order to pull off animation curves later num_anim_stacks = fbx_scene.GetSrcObjectCount( FbxCommon.FbxCriteria.ObjectType(FbxCommon.FbxAnimStack.ClassId) ) # if num_anim_stacks != 1: # raise RuntimeError( # "More than one animation stack was found. " # "This script must be modified to handle this case. Exiting" # ) if num_anim_stacks > 1: index = 1 else: index = 0 anim_stack = fbx_scene.GetSrcObject( FbxCommon.FbxCriteria.ObjectType(FbxCommon.FbxAnimStack.ClassId), index ) num_anim_layers = anim_stack.GetSrcObjectCount( FbxCommon.FbxCriteria.ObjectType(FbxCommon.FbxAnimLayer.ClassId) ) if num_anim_layers != 1: raise RuntimeError( "More than one animation layer was found. " "This script must be modified to handle this case. Exiting" ) animation_layer = anim_stack.GetSrcObject( FbxCommon.FbxCriteria.ObjectType(FbxCommon.FbxAnimLayer.ClassId), 0 ) def _check_longest_curve(curve, max_curve_key_count): longest_curve = None if curve and curve.KeyGetCount() > max_curve_key_count[0]: max_curve_key_count[0] = curve.KeyGetCount() return True return False max_curve_key_count = [0] longest_curve = None for c in ["X", "Y", "Z"]: curve = joint.LclTranslation.GetCurve( animation_layer, c ) # sample curve for translation if _check_longest_curve(curve, max_curve_key_count): longest_curve = curve curve = joint.LclRotation.GetCurve(animation_layer, "X") if _check_longest_curve(curve, max_curve_key_count): longest_curve = curve return longest_curve def _get_skeleton(root_joint): # Do a depth first search of the skeleton to extract all the joints joint_list = [root_joint] joint_names = [root_joint.GetName()] parents = [-1] # -1 means no parent def append_children(joint, pos): """ Depth first search function :param joint: joint item in the fbx :param pos: position of current element (for parenting) :return: Nothing """ for child_index in range(joint.GetChildCount()): child = joint.GetChild(child_index) joint_list.append(child) joint_names.append(child.GetName()) parents.append(pos) append_children(child, len(parents) - 1) append_children(root_joint, 0) return joint_list, joint_names, parents def _recursive_to_list(array): """ Takes some iterable that might contain iterables and converts it to a list of lists [of lists... etc] Mainly used for converting the strange fbx wrappers for c++ arrays into python lists :param array: array to be converted :return: array converted to lists """ try: return float(array) except TypeError: return [_recursive_to_list(a) for a in array]
[docs]def parse_fbx(file_name_in, root_joint_name, fps): return fbx_to_npy(file_name_in, root_joint_name, fps)