/// <summary>
            /// Export an AnimationCurve.
            /// NOTE: This is not used for rotations, because we need to convert from
            /// quaternion to euler and various other stuff.
            /// </summary>
            protected void ExportAnimCurve(UnityEngine.Object unityObj,
                                           AnimationCurve unityAnimCurve,
                                           string unityPropertyName,
                                           FbxScene fbxScene,
                                           FbxAnimLayer fbxAnimLayer)
            {
                FbxPropertyChannelPair fbxPropertyChannelPair;

                if (!FbxPropertyChannelPair.TryGetValue(unityPropertyName, out fbxPropertyChannelPair))
                {
                    Debug.LogWarning(string.Format("no mapping from Unity '{0}' to fbx property", unityPropertyName));
                    return;
                }

                GameObject unityGo = GetGameObject(unityObj);

                if (unityGo == null)
                {
                    Debug.LogError(string.Format("cannot find gameobject for {0}", unityObj.ToString()));
                    return;
                }

                FbxNode fbxNode;

                if (!MapUnityObjectToFbxNode.TryGetValue(unityGo, out fbxNode))
                {
                    Debug.LogError(string.Format("no fbx node for {0}", unityGo.ToString()));
                    return;
                }

                // map unity property name to fbx property
                var fbxProperty = fbxNode.FindProperty(fbxPropertyChannelPair.Property, false);

                if (!fbxProperty.IsValid())
                {
                    Debug.LogError(string.Format("no fbx property {0} found on {1} ", fbxPropertyChannelPair.Property, fbxNode.GetName()));
                    return;
                }

                if (Verbose)
                {
                    Debug.Log("Exporting animation for " + unityObj.ToString() + " (" + unityPropertyName + ")");
                }

                // Create the AnimCurve on the channel
                FbxAnimCurve fbxAnimCurve = fbxProperty.GetCurve(fbxAnimLayer, fbxPropertyChannelPair.Channel, true);

                // copy Unity AnimCurve to FBX AnimCurve.
                fbxAnimCurve.KeyModifyBegin();

                for (int keyIndex = 0, n = unityAnimCurve.length; keyIndex < n; ++keyIndex)
                {
                    var key     = unityAnimCurve [keyIndex];
                    var fbxTime = FbxTime.FromSecondDouble(key.time);
                    fbxAnimCurve.KeyAdd(fbxTime);
                    fbxAnimCurve.KeySet(keyIndex, fbxTime, key.value);
                }

                fbxAnimCurve.KeyModifyEnd();
            }
            /// <summary>
            /// Export an AnimationClip as a single take
            /// </summary>
            protected void ExportAnimationClip(AnimationClip unityAnimClip, GameObject unityRoot, FbxScene fbxScene)
            {
                if (Verbose)
                {
                    Debug.Log(string.Format("exporting clip {1} for {0}", unityRoot.name, unityAnimClip.name));
                }

                // setup anim stack
                FbxAnimStack fbxAnimStack = FbxAnimStack.Create(fbxScene, unityAnimClip.name);

                fbxAnimStack.Description.Set("Animation Take: " + unityAnimClip.name);

                // add one mandatory animation layer
                FbxAnimLayer fbxAnimLayer = FbxAnimLayer.Create(fbxScene, "Animation Base Layer");

                fbxAnimStack.AddMember(fbxAnimLayer);

                // Set up the FPS so our frame-relative math later works out
                // Custom frame rate isn't really supported in FBX SDK (there's
                // a bug), so try hard to find the nearest time mode.
                FbxTime.EMode timeMode  = FbxTime.EMode.eCustom;
                double        precision = 1e-6;

                while (timeMode == FbxTime.EMode.eCustom && precision < 1000)
                {
                    timeMode   = FbxTime.ConvertFrameRateToTimeMode(unityAnimClip.frameRate, precision);
                    precision *= 10;
                }
                if (timeMode == FbxTime.EMode.eCustom)
                {
                    timeMode = FbxTime.EMode.eFrames30;
                }
                FbxTime.SetGlobalTimeMode(timeMode);

                // set time correctly
                var fbxStartTime = FbxTime.FromSecondDouble(0);
                var fbxStopTime  = FbxTime.FromSecondDouble(unityAnimClip.length);

                fbxAnimStack.SetLocalTimeSpan(new FbxTimeSpan(fbxStartTime, fbxStopTime));

                /* The major difficulty: Unity uses quaternions for rotation
                 * (which is how it should be) but FBX uses euler angles. So we
                 * need to gather up the list of transform curves per object. */
                var quaternions = new Dictionary <UnityEngine.GameObject, QuaternionCurve> ();

                foreach (EditorCurveBinding unityCurveBinding in AnimationUtility.GetCurveBindings(unityAnimClip))
                {
                    Object unityObj = AnimationUtility.GetAnimatedObject(unityRoot, unityCurveBinding);
                    if (!unityObj)
                    {
                        continue;
                    }

                    AnimationCurve unityAnimCurve = AnimationUtility.GetEditorCurve(unityAnimClip, unityCurveBinding);
                    if (unityAnimCurve == null)
                    {
                        continue;
                    }

                    int index = QuaternionCurve.GetQuaternionIndex(unityCurveBinding.propertyName);
                    if (index == -1)
                    {
                        if (Verbose)
                        {
                            Debug.Log(string.Format("export binding {1} for {0}", unityCurveBinding.propertyName, unityObj.ToString()));
                        }

                        /* Some normal property (e.g. translation), export right away */
                        ExportAnimCurve(unityObj, unityAnimCurve, unityCurveBinding.propertyName,
                                        fbxScene, fbxAnimLayer);
                    }
                    else
                    {
                        /* Rotation property; save it to convert quaternion -> euler later. */

                        var unityGo = GetGameObject(unityObj);
                        if (!unityGo)
                        {
                            continue;
                        }

                        QuaternionCurve quat;
                        if (!quaternions.TryGetValue(unityGo, out quat))
                        {
                            quat = new QuaternionCurve();
                            quaternions.Add(unityGo, quat);
                        }
                        quat.SetCurve(index, unityAnimCurve);
                    }
                }

                /* now export all the quaternion curves */
                foreach (var kvp in quaternions)
                {
                    var unityGo = kvp.Key;
                    var quat    = kvp.Value;

                    FbxNode fbxNode;
                    if (!MapUnityObjectToFbxNode.TryGetValue(unityGo, out fbxNode))
                    {
                        Debug.LogError(string.Format("no fbxnode found for '0'", unityGo.name));
                        continue;
                    }
                    quat.Animate(unityGo.transform, fbxNode, fbxAnimLayer, Verbose);
                }
            }
            /// <summary>
            /// Export bones of skinned mesh, if this is a skinned mesh with
            /// bones and bind poses.
            /// </summary>
            private bool ExportSkeleton(GameObject unityGo, FbxScene fbxScene)
            {
                var unitySkinnedMeshRenderer = unityGo.GetComponentInChildren <SkinnedMeshRenderer> ();

                if (!unitySkinnedMeshRenderer)
                {
                    return(false);
                }
                var bones = unitySkinnedMeshRenderer.bones;

                if (bones == null || bones.Length == 0)
                {
                    return(false);
                }
                var mesh = unitySkinnedMeshRenderer.sharedMesh;

                if (!mesh)
                {
                    return(false);
                }
                var bindPoses = mesh.bindposes;

                if (bindPoses == null || bindPoses.Length != bones.Length)
                {
                    return(false);
                }

                // Three steps:
                // 0. Set up the map from bone to index.
                // 1. Create the bones, in arbitrary order.
                // 2. Connect up the hierarchy.
                // 3. Set the transforms.
                // Step 0 supports step 1 (finding which is the root bone) and step 3
                // (setting up transforms; the complication is the use of pivots).

                // Step 0: map transform to index so we can look up index by bone.
                Dictionary <Transform, int> index = new Dictionary <Transform, int>();

                for (int boneIndex = 0, n = bones.Length; boneIndex < n; boneIndex++)
                {
                    Transform unityBoneTransform = bones [boneIndex];
                    index[unityBoneTransform] = boneIndex;
                }

                // Step 1: create the bones.
                for (int boneIndex = 0, n = bones.Length; boneIndex < n; boneIndex++)
                {
                    Transform unityBoneTransform = bones [boneIndex];

                    // Create the bone node if we haven't already. Parent it to
                    // its corresponding parent, or to the scene if there is none.
                    FbxNode fbxBoneNode;
                    if (!MapUnityObjectToFbxNode.TryGetValue(unityBoneTransform.gameObject, out fbxBoneNode))
                    {
                        var     unityParent = unityBoneTransform.parent;
                        FbxNode fbxParent;
                        if (MapUnityObjectToFbxNode.TryGetValue(unityParent.gameObject, out fbxParent))
                        {
                            fbxBoneNode = FbxNode.Create(fbxParent, unityBoneTransform.name);
                        }
                        else
                        {
                            fbxBoneNode = FbxNode.Create(fbxScene, unityBoneTransform.name);
                        }
                        MapUnityObjectToFbxNode.Add(unityBoneTransform.gameObject, fbxBoneNode);
                    }

                    // Set it up as a skeleton node if we haven't already.
                    if (fbxBoneNode.GetSkeleton() == null)
                    {
                        FbxSkeleton fbxSkeleton     = FbxSkeleton.Create(fbxScene, unityBoneTransform.name + "_Skel");
                        var         fbxSkeletonType = index.ContainsKey(unityBoneTransform.parent)
                            ? FbxSkeleton.EType.eLimbNode : FbxSkeleton.EType.eRoot;
                        fbxSkeleton.SetSkeletonType(fbxSkeletonType);
                        fbxSkeleton.Size.Set(1.0f);
                        fbxBoneNode.SetNodeAttribute(fbxSkeleton);
                        if (Verbose)
                        {
                            Debug.Log("Converted " + unityBoneTransform.name + " to a " + fbxSkeletonType + " bone");
                        }
                    }
                }

                // Step 2: connect up the hierarchy.
                foreach (var unityBone in bones)
                {
                    var fbxBone   = MapUnityObjectToFbxNode[unityBone.gameObject];
                    var fbxParent = MapUnityObjectToFbxNode[unityBone.parent.gameObject];
                    fbxParent.AddChild(fbxBone);
                }

                // Step 3: set up the transforms.
                for (int boneIndex = 0, n = bones.Length; boneIndex < n; boneIndex++)
                {
                    var unityBone = bones[boneIndex];
                    var fbxBone   = MapUnityObjectToFbxNode[unityBone.gameObject];

                    Matrix4x4 pose;
                    if (fbxBone.GetSkeleton().GetSkeletonType() == FbxSkeleton.EType.eRoot)
                    {
                        // bind pose is local -> root. We want root -> local, so invert.
                        pose = bindPoses[boneIndex].inverse; // assuming parent is identity matrix
                    }
                    else
                    {
                        // Bind pose is local -> parent -> ... -> root.
                        // We want parent -> local.
                        // Invert our bind pose to get root -> local.
                        // The apply parent -> root to leave just parent -> local.
                        pose = bindPoses[index[unityBone.parent]] * bindPoses[boneIndex].inverse;
                    }

                    // FBX is transposed relative to Unity: transpose as we convert.
                    FbxMatrix matrix = new FbxMatrix();
                    matrix.SetColumn(0, new FbxVector4(pose.GetRow(0).x, pose.GetRow(0).y, pose.GetRow(0).z, pose.GetRow(0).w));
                    matrix.SetColumn(1, new FbxVector4(pose.GetRow(1).x, pose.GetRow(1).y, pose.GetRow(1).z, pose.GetRow(1).w));
                    matrix.SetColumn(2, new FbxVector4(pose.GetRow(2).x, pose.GetRow(2).y, pose.GetRow(2).z, pose.GetRow(2).w));
                    matrix.SetColumn(3, new FbxVector4(pose.GetRow(3).x, pose.GetRow(3).y, pose.GetRow(3).z, pose.GetRow(3).w));

                    // FBX wants translation, rotation (in euler angles) and scale.
                    // We assume there's no real shear, just rounding error.
                    FbxVector4 translation, rotation, shear, scale;
                    double     sign;
                    matrix.GetElements(out translation, out rotation, out shear, out scale, out sign);

                    // Bones should have zero rotation, and use a pivot instead.
                    fbxBone.LclTranslation.Set(new FbxDouble3(translation.X, translation.Y, translation.Z));
                    fbxBone.LclRotation.Set(new FbxDouble3(0, 0, 0));
                    fbxBone.LclScaling.Set(new FbxDouble3(scale.X, scale.Y, scale.Z));

                    fbxBone.SetRotationActive(true);
                    fbxBone.SetPivotState(FbxNode.EPivotSet.eSourcePivot, FbxNode.EPivotState.ePivotReference);
                    fbxBone.SetPreRotation(FbxNode.EPivotSet.eSourcePivot, new FbxVector4(rotation.X, rotation.Y, rotation.Z));
                }

                return(true);
            }
            /// <summary>
            /// Export an AnimationCurve.
            ///
            /// This is not used for rotations, because we need to convert from
            /// quaternion to euler and various other stuff.
            /// </summary>
            protected void ExportAnimCurve(UnityEngine.Object unityObj,
                                           AnimationCurve unityAnimCurve,
                                           string unityPropertyName,
                                           FbxAnimLayer fbxAnimLayer)
            {
                FbxPropertyChannelPair fbxPair;

                if (!MapUnityPropertyNameToFbx.TryGetValue(unityPropertyName, out fbxPair))
                {
                    Debug.LogWarning(string.Format("no property-channel mapping found for {0}", unityPropertyName));
                    return;
                }

                GameObject unityGo = GetGameObject(unityObj);

                if (unityGo == null)
                {
                    Debug.LogError(string.Format("cannot convert to GameObject from {0}", unityObj.ToString()));
                    return;
                }

                FbxNode fbxNode;

                if (!MapUnityObjectToFbxNode.TryGetValue(unityGo, out fbxNode))
                {
                    Debug.LogError(string.Format("cannot find fbxNode for {0}", unityGo.ToString()));
                    return;
                }

                FbxProperty fbxProperty = null;

                // try finding unity property name on node attribute
                FbxNodeAttribute fbxNodeAttribute = fbxNode.GetNodeAttribute();

                if (fbxNodeAttribute != null)
                {
                    fbxProperty = fbxNodeAttribute.FindProperty(fbxPair.Property, false);
                }

                // try finding unity property on the node
                if (fbxProperty == null || !fbxProperty.IsValid())
                {
                    fbxProperty = fbxNode.FindProperty(fbxPair.Property, false);
                }

                if (fbxProperty == null || !fbxProperty.IsValid())
                {
                    Debug.LogError(string.Format("cannot find fbxProperty {0} on {1}", fbxPair.Property, fbxNode.GetName()));
                    return;
                }

                if (Verbose)
                {
                    Debug.Log(string.Format("Exporting animation for {0} ({1})",
                                            unityObj.ToString(),
                                            fbxPair.Property));
                }

                // Create the AnimCurve on the channel
                FbxAnimCurve fbxAnimCurve = (fbxPair.Channel != null)
                    ? fbxProperty.GetCurve(fbxAnimLayer, fbxPair.Channel, true)
                                 : fbxProperty.GetCurve(fbxAnimLayer, true);

                // copy Unity AnimCurve to FBX AnimCurve.
                fbxAnimCurve.KeyModifyBegin();

                for (int keyIndex = 0, n = unityAnimCurve.length; keyIndex < n; ++keyIndex)
                {
                    var key     = unityAnimCurve [keyIndex];
                    var fbxTime = FbxTime.FromSecondDouble(key.time);
                    fbxAnimCurve.KeyAdd(fbxTime);
                    fbxAnimCurve.KeySet(keyIndex, fbxTime, key.value);
                }

                fbxAnimCurve.KeyModifyEnd();
            }