/// <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(); }