// MonoGame - Copyright (C) The MonoGame Team // This file is subject to the terms and conditions defined in // file 'LICENSE.txt', which is part of this source code package. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using Assimp; using Assimp.Unmanaged; using Microsoft.Xna.Framework.Content.Pipeline.Graphics; using MonoGame.Utilities; namespace Microsoft.Xna.Framework.Content.Pipeline { [ContentImporter( ".dae", // Collada ".gltf", "glb", // glTF ".blend", // Blender 3D ".3ds", // 3ds Max 3DS ".ase", // 3ds Max ASE ".obj", // Wavefront Object ".ifc", // Industry Foundation Classes (IFC/Step) ".xgl", ".zgl", // XGL ".ply", // Stanford Polygon Library ".dxf", // AutoCAD DXF ".lwo", // LightWave ".lws", // LightWave Scene ".lxo", // Modo ".stl", // Stereolithography ".ac", // AC3D ".ms3d", // Milkshape 3D ".cob", ".scn", // TrueSpace ".bvh", // Biovision BVH ".csm", // CharacterStudio Motion ".irrmesh", // Irrlicht Mesh ".irr", // Irrlicht Scene ".mdl", // Quake I, 3D GameStudio (3DGS) ".md2", // Quake II ".md3", // Quake III Mesh ".pk3", // Quake III Map/BSP ".mdc", // Return to Castle Wolfenstein ".md5", // Doom 3 ".smd", ".vta", // Valve Model ".ogex", // Open Game Engine Exchange ".3d", // Unreal ".b3d", // BlitzBasic 3D ".q3d", ".q3s", // Quick3D ".nff", // Neutral File Format, Sense8 WorldToolKit ".off", // Object File Format ".ter", // Terragen Terrain ".hmp", // 3D GameStudio (3DGS) Terrain ".ndo", // Izware Nendo DisplayName = "Open Asset Import Library - MonoGame", DefaultProcessor = "ModelProcessor")] public class OpenAssetImporter : ContentImporter { // Assimp has a few limitations (not all FBX files are supported): // FBX files reference objects using IDs. Therefore, it is possible to resolve // bones even if multiple bones/nodes have the same name. But Assimp references // bones only by name! // --> Limitation #1: A model cannot have more than one skeleton! // --> Limitation #2: Bone names need to be unique! // // Bones are represented by regular nodes, but there is no flag indicating whether // a node is a bone. A mesh in Assimp references deformation bones (= bones that // affect vertices) by name. That means, we can identify the nodes that represent // deformation bones. But there is no way to identify helper bones (= bones that // belong to the skeleton, but do not affect vertices). As described in // http://assimp.sourceforge.net/lib_html/data.html and // http://gamedev.stackexchange.com/questions/26382/i-cant-figure-out-how-to-animate-my-loaded-model-with-assimp/26442#26442 // we can only guess which nodes belong to a skeleton: // --> Limitation #3: The skeleton needs to be a direct child of the root node or // the mesh node! // // Node.Transform is irrelevant for bones. This transform is just the pose of the // bone at the time of the export. This could be one of the animation frames. It // is not necessarily the bind pose (rest pose)! For example, XNA's Dude.fbx does // NOT store the skeleton in bind pose. // The correct transform is stored in Mesh.Bones[i].OffsetMatrix. However, this // information is only available for deformation bones, not for helper bones. // --> Limitation #4: The skeleton either must not contain helper bones, or it must // be guaranteed that the skeleton is exported in bind pose! // // An FBX file does not directly store all animation values. In some FBX scene it // is insufficient to simply read the animation data from the file. Instead, the // animation properties of all relevant objects in the scene need to be evaluated. // For example, some animations are the result of the current skeleton pose + the // current animation. The current skeleton pose is not imported/processed by XNA. // Assimp does not include an "animation evaluater" that automatically bakes these // animations. // --> Limitation #5: All bones included in an animation need to be key framed. // (There is no automatic evaluation.) // // In FBX it is possible to define animations curves for some transform components // (e.g. translation X and Y) and leave other components (e.g. translation Z) undefined. // Assimp does not pick the right defaults for undefined components. // --> Limitation #6: When scale, rotation, or translation is animated, all components // X, Y, Z need to be key framed. #region Nested Types /// Defines the frame for local scale/rotation/translation of FBX nodes. /// /// /// The transformation pivot defines the frame for local scale/rotation/translation. The /// local transform of a node is: /// /// /// Local Transform = Translation * RotationOffset * RotationPivot * PreRotation /// * Rotation * PostRotation * RotationPivotInverse * ScalingOffset /// * ScalingPivot * Scaling * ScalingPivotInverse /// /// /// where the matrix multiplication order is right-to-left. /// /// /// 3ds max uses three additional transformations: /// /// /// Local Transform = Translation * Rotation * Scaling /// * GeometricTranslation * GeometricRotation * GeometricScaling /// /// /// Transformation pivots are stored per FBX node. When Assimp hits an FBX node with /// a transformation pivot it generates additional nodes named /// /// /// OriginalName_$AssimpFbx$_TransformName /// /// /// where TransformName is one of: /// /// /// Translation, RotationOffset, RotationPivot, PreRotation, Rotation, PostRotation, /// RotationPivotInverse, ScalingOffset, ScalingPivot, Scaling, ScalingPivotInverse, /// GeometricTranslation, GeometricRotation, GeometricScaling /// /// /// /// private class FbxPivot { public static readonly FbxPivot Default = new FbxPivot(); public Matrix? Translation; public Matrix? RotationOffset; public Matrix? RotationPivot; public Matrix? PreRotation; public Matrix? Rotation; public Matrix? PostRotation; public Matrix? RotationPivotInverse; public Matrix? ScalingOffset; public Matrix? ScalingPivot; public Matrix? Scaling; public Matrix? ScalingPivotInverse; public Matrix? GeometricTranslation; public Matrix? GeometricRotation; public Matrix? GeometricScaling; public Matrix GetTransform(Vector3? scale, Quaternion? rotation, Vector3? translation) { var transform = Matrix.Identity; if (GeometricScaling.HasValue) transform *= GeometricScaling.Value; if (GeometricRotation.HasValue) transform *= GeometricRotation.Value; if (GeometricTranslation.HasValue) transform *= GeometricTranslation.Value; if (ScalingPivotInverse.HasValue) transform *= ScalingPivotInverse.Value; if (scale.HasValue) transform *= Matrix.CreateScale(scale.Value); else if (Scaling.HasValue) transform *= Scaling.Value; if (ScalingPivot.HasValue) transform *= ScalingPivot.Value; if (ScalingOffset.HasValue) transform *= ScalingOffset.Value; if (RotationPivotInverse.HasValue) transform *= RotationPivotInverse.Value; if (PostRotation.HasValue) transform *= PostRotation.Value; if (rotation.HasValue) transform *= Matrix.CreateFromQuaternion(rotation.Value); else if (Rotation.HasValue) transform *= Rotation.Value; if (PreRotation.HasValue) transform *= PreRotation.Value; if (RotationPivot.HasValue) transform *= RotationPivot.Value; if (RotationOffset.HasValue) transform *= RotationOffset.Value; if (translation.HasValue) transform *= Matrix.CreateTranslation(translation.Value); else if (Translation.HasValue) transform *= Translation.Value; return transform; } } #endregion private static readonly List EmptyVectorKeys = new List(); private static readonly List EmptyQuaternionKeys = new List(); // XNA Content importer private ContentImporterContext _context; private ContentIdentity _identity; // Assimp scene private Scene _scene; private Dictionary _deformationBones; // The names and offset matrices of all deformation bones. private Node _rootBone; // The node that represents the root bone. private List _bones = new List(); // All nodes attached to the root bone. private Dictionary _pivots; // The transformation pivots. // XNA content private NodeContent _rootNode; private List _materials; // This is used to enable backwards compatibility with // XNA providing a model as expected from the original // FbxImporter and XImporter. private readonly bool _xnaCompatible; private readonly string _importerName; /// /// Default constructor. /// public OpenAssetImporter() : this("OpenAssetImporter", false) { } internal OpenAssetImporter(string importerName, bool xnaCompatible) { _importerName = importerName; _xnaCompatible = xnaCompatible; } /// /// This disables some Assimp model loading features so that /// the resulting content is the same as what the XNA FbxImporter /// public bool XnaComptatible { get; set; } public override NodeContent Import(string filename, ContentImporterContext context) { if (filename == null) throw new ArgumentNullException("filename"); if (context == null) throw new ArgumentNullException("context"); _context = context; if (CurrentPlatform.OS == OS.Linux) { var targetDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName; try { AssimpLibrary.Instance.LoadLibrary( Path.Combine(targetDir, "libassimp.so"), Path.Combine(targetDir, "libassimp.so")); } catch { } } _identity = new ContentIdentity(filename, _importerName); using (var importer = new AssimpContext()) { // FBXPreservePivotsConfig(false) can be set to remove transformation // pivots. However, Assimp does not automatically correct animations! // --> Leave default settings, handle transformation pivots explicitly. //importer.SetConfig(new Assimp.Configs.FBXPreservePivotsConfig(false)); // Set flag to remove degenerate faces (points and lines). // This flag is very important when PostProcessSteps.FindDegenerates is used // because FindDegenerates converts degenerate triangles to points and lines! importer.SetConfig(new Assimp.Configs.RemoveDegeneratePrimitivesConfig(true)); // Note about Assimp post-processing: // Keep post-processing to a minimum. The ModelImporter should import // the model as is. We don't want to lose any information, i.e. empty // nodes shoud not be thrown away, meshes/materials should not be merged, // etc. Custom model processors may depend on this information! _scene = importer.ImportFile(filename, PostProcessSteps.FindDegenerates | PostProcessSteps.FindInvalidData | PostProcessSteps.FlipUVs | // Required for Direct3D PostProcessSteps.FlipWindingOrder | // Required for Direct3D PostProcessSteps.JoinIdenticalVertices | PostProcessSteps.ImproveCacheLocality | PostProcessSteps.OptimizeMeshes | PostProcessSteps.Triangulate // Unused: //PostProcessSteps.CalculateTangentSpace //PostProcessSteps.Debone | //PostProcessSteps.FindInstances | // No effect + slow? //PostProcessSteps.FixInFacingNormals | //PostProcessSteps.GenerateNormals | //PostProcessSteps.GenerateSmoothNormals | //PostProcessSteps.GenerateUVCoords | // Might be needed... find test case //PostProcessSteps.LimitBoneWeights | //PostProcessSteps.MakeLeftHanded | // Not necessary, XNA is right-handed. //PostProcessSteps.OptimizeGraph | // Will eliminate helper nodes //PostProcessSteps.PreTransformVertices | //PostProcessSteps.RemoveComponent | //PostProcessSteps.RemoveRedundantMaterials | //PostProcessSteps.SortByPrimitiveType | //PostProcessSteps.SplitByBoneCount | //PostProcessSteps.SplitLargeMeshes | //PostProcessSteps.TransformUVCoords | //PostProcessSteps.ValidateDataStructure | ); FindSkeleton(); // Find _rootBone, _bones, _deformationBones. // Create _materials. if (_xnaCompatible) ImportXnaMaterials(); else ImportMaterials(); ImportNodes(); // Create _pivots and _rootNode (incl. children). ImportSkeleton(); // Create skeleton (incl. animations) and add to _rootNode. // If we have a simple hierarchy with no bones and just the one // mesh, we can flatten it out so the mesh is the root node. if (_rootNode.Children.Count == 1 && _rootNode.Children[0] is MeshContent) { var absXform = _rootNode.Children[0].AbsoluteTransform; _rootNode = _rootNode.Children[0]; _rootNode.Identity = _identity; _rootNode.Transform = absXform; } _scene.Clear(); } return _rootNode; } /// /// Converts all Assimp s to standard XNA compatible s. /// private void ImportXnaMaterials() { _materials = new List(); foreach (var aiMaterial in _scene.Materials) { // TODO: What about AlphaTestMaterialContent, DualTextureMaterialContent, // EffectMaterialContent, EnvironmentMapMaterialContent, and SkinnedMaterialContent? var material = new BasicMaterialContent { Name = aiMaterial.Name, Identity = _identity, }; if (aiMaterial.HasTextureDiffuse) material.Texture = ImportTextureContentRef(aiMaterial.TextureDiffuse); if (aiMaterial.HasTextureOpacity) material.Textures.Add("Transparency", ImportTextureContentRef(aiMaterial.TextureOpacity)); if (aiMaterial.HasTextureSpecular) material.Textures.Add("Specular", ImportTextureContentRef(aiMaterial.TextureSpecular)); if (aiMaterial.HasTextureHeight) material.Textures.Add("Bump", ImportTextureContentRef(aiMaterial.TextureHeight)); if (aiMaterial.HasColorDiffuse) material.DiffuseColor = ToXna(aiMaterial.ColorDiffuse); if (aiMaterial.HasColorEmissive) material.EmissiveColor = ToXna(aiMaterial.ColorEmissive); if (aiMaterial.HasOpacity) material.Alpha = aiMaterial.Opacity; if (aiMaterial.HasColorSpecular) material.SpecularColor = ToXna(aiMaterial.ColorSpecular); if (aiMaterial.HasShininessStrength) material.SpecularPower = aiMaterial.Shininess; _materials.Add(material); } } private ExternalReference ImportTextureContentRef(TextureSlot textureSlot) { var texture = new ExternalReference(textureSlot.FilePath, _identity); texture.OpaqueData.Add("TextureCoordinate", string.Format("TextureCoordinate{0}", textureSlot.UVIndex)); if (!_xnaCompatible) { texture.OpaqueData.Add("Operation", textureSlot.Operation.ToString()); texture.OpaqueData.Add("AddressU", textureSlot.WrapModeU.ToString()); texture.OpaqueData.Add("AddressV", textureSlot.WrapModeU.ToString()); texture.OpaqueData.Add("Mapping", textureSlot.Mapping.ToString()); } return texture; } /// /// Returns all the Assimp features as a . /// private void ImportMaterials() { _materials = new List(); foreach (var aiMaterial in _scene.Materials) { // TODO: Should we create a special AssImpMaterial? var material = new MaterialContent { Name = aiMaterial.Name, Identity = _identity, }; var slots = aiMaterial.GetAllMaterialTextures(); foreach (var tex in slots) { string name; // Force the XNA naming standard for diffuse textures // which allows the material to work with the stock // model processor. if (tex.TextureType == TextureType.Diffuse) name = BasicMaterialContent.TextureKey; else name = tex.TextureType.ToString(); // We might have multiple textures of the same type so number // them starting with 2 like in DualTextureMaterialContent. if (tex.TextureIndex > 0) name += (tex.TextureIndex + 1); material.Textures.Add(name, ImportTextureContentRef(tex)); } if (aiMaterial.HasBlendMode) material.OpaqueData.Add("BlendMode", aiMaterial.BlendMode.ToString()); if (aiMaterial.HasBumpScaling) material.OpaqueData.Add("BumpScaling", aiMaterial.BumpScaling); if (aiMaterial.HasColorAmbient) material.OpaqueData.Add("AmbientColor", ToXna(aiMaterial.ColorAmbient)); if (aiMaterial.HasColorDiffuse) material.OpaqueData.Add("DiffuseColor", ToXna(aiMaterial.ColorDiffuse)); if (aiMaterial.HasColorEmissive) material.OpaqueData.Add("EmissiveColor", ToXna(aiMaterial.ColorEmissive)); if (aiMaterial.HasColorReflective) material.OpaqueData.Add("ReflectiveColor", ToXna(aiMaterial.ColorReflective)); if (aiMaterial.HasColorSpecular) material.OpaqueData.Add("SpecularColor", ToXna(aiMaterial.ColorSpecular)); if (aiMaterial.HasColorTransparent) material.OpaqueData.Add("TransparentColor", ToXna(aiMaterial.ColorTransparent)); if (aiMaterial.HasOpacity) material.OpaqueData.Add("Opacity", aiMaterial.Opacity); if (aiMaterial.HasReflectivity) material.OpaqueData.Add("Reflectivity", aiMaterial.Reflectivity); if (aiMaterial.HasShadingMode) material.OpaqueData.Add("ShadingMode", aiMaterial.ShadingMode.ToString()); if (aiMaterial.HasShininess) material.OpaqueData.Add("Shininess", aiMaterial.Shininess); if (aiMaterial.HasShininessStrength) material.OpaqueData.Add("ShininessStrength", aiMaterial.ShininessStrength); if (aiMaterial.HasTwoSided) material.OpaqueData.Add("TwoSided", aiMaterial.IsTwoSided); if (aiMaterial.HasWireFrame) material.OpaqueData.Add("WireFrame", aiMaterial.IsWireFrameEnabled); _materials.Add(material); } } /// /// Converts all Assimp nodes to XNA nodes. (Nodes representing bones are excluded!) /// private void ImportNodes() { _pivots = new Dictionary(); _rootNode = ImportNodes(_scene.RootNode, null, null); } /// /// Converts the specified node and all descendant nodes. /// /// The node. /// The parent node. Can be . /// The node converted to XNA. /// The XNA . /// /// It may be necessary to skip certain "preserve pivot" nodes in the hierarchy. The /// converted node needs to be relative to , not node.Parent. /// private NodeContent ImportNodes(Node aiNode, Node aiParent, NodeContent parent) { Debug.Assert(aiNode != null); NodeContent node = null; if (aiNode.HasMeshes) { var mesh = new MeshContent { Name = aiNode.Name, Identity = _identity, Transform = ToXna(GetRelativeTransform(aiNode, aiParent)) }; foreach (var meshIndex in aiNode.MeshIndices) { var aiMesh = _scene.Meshes[meshIndex]; if (!aiMesh.HasVertices) continue; var geom = CreateGeometry(mesh, aiMesh); mesh.Geometry.Add(geom); } node = mesh; } else if (aiNode.Name.Contains("_$AssimpFbx$")) { // This is a transformation pivot. // _$AssimpFbx$_ // where is one of // Translation, RotationOffset, RotationPivot, PreRotation, Rotation, // PostRotation, RotationPivotInverse, ScalingOffset, ScalingPivot, // Scaling, ScalingPivotInverse string originalName = GetNodeName(aiNode.Name); FbxPivot pivot; if (!_pivots.TryGetValue(originalName, out pivot)) { pivot = new FbxPivot(); _pivots.Add(originalName, pivot); } Matrix transform = ToXna(aiNode.Transform); if (aiNode.Name.EndsWith("_Translation")) pivot.Translation = transform; else if (aiNode.Name.EndsWith("_RotationOffset")) pivot.RotationOffset = transform; else if (aiNode.Name.EndsWith("_RotationPivot")) pivot.RotationPivot = transform; else if (aiNode.Name.EndsWith("_PreRotation")) pivot.PreRotation = transform; else if (aiNode.Name.EndsWith("_Rotation")) pivot.Rotation = transform; else if (aiNode.Name.EndsWith("_PostRotation")) pivot.PostRotation = transform; else if (aiNode.Name.EndsWith("_RotationPivotInverse")) pivot.RotationPivotInverse = transform; else if (aiNode.Name.EndsWith("_ScalingOffset")) pivot.ScalingOffset = transform; else if (aiNode.Name.EndsWith("_ScalingPivot")) pivot.ScalingPivot = transform; else if (aiNode.Name.EndsWith("_Scaling")) pivot.Scaling = transform; else if (aiNode.Name.EndsWith("_ScalingPivotInverse")) pivot.ScalingPivotInverse = transform; else if (aiNode.Name.EndsWith("_GeometricTranslation")) pivot.GeometricTranslation = transform; else if (aiNode.Name.EndsWith("_GeometricRotation")) pivot.GeometricRotation = transform; else if (aiNode.Name.EndsWith("_GeometricScaling")) pivot.GeometricScaling = transform; else throw new InvalidContentException(string.Format("Unknown $AssimpFbx$ node: \"{0}\"", aiNode.Name), _identity); } else if (!_bones.Contains(aiNode)) // Ignore bones. { node = new NodeContent { Name = aiNode.Name, Identity = _identity, Transform = ToXna(GetRelativeTransform(aiNode, aiParent)) }; } if (node != null) { if (parent != null) parent.Children.Add(node); // For the children, this is the new parent. aiParent = aiNode; parent = node; if (_scene.HasAnimations) { foreach (var animation in _scene.Animations) { var animationContent = ImportAnimation(animation, node.Name); if (animationContent.Channels.Count > 0) node.Animations.Add(animationContent.Name, animationContent); } } } Debug.Assert(parent != null); foreach (var child in aiNode.Children) ImportNodes(child, aiParent, parent); return node; } private GeometryContent CreateGeometry(MeshContent mesh, Mesh aiMesh) { var geom = new GeometryContent { Identity = _identity, Material = _materials[aiMesh.MaterialIndex] }; // Vertices var baseVertex = mesh.Positions.Count; foreach (var vert in aiMesh.Vertices) mesh.Positions.Add(ToXna(vert)); geom.Vertices.AddRange(Enumerable.Range(baseVertex, aiMesh.VertexCount)); geom.Indices.AddRange(aiMesh.GetIndices()); if (aiMesh.HasBones) { var xnaWeights = new List(); var vertexCount = geom.Vertices.VertexCount; bool missingBoneWeights = false; for (var i = 0; i < vertexCount; i++) { var list = new BoneWeightCollection(); for (var boneIndex = 0; boneIndex < aiMesh.BoneCount; boneIndex++) { var bone = aiMesh.Bones[boneIndex]; foreach (var weight in bone.VertexWeights) { if (weight.VertexID != i) continue; list.Add(new BoneWeight(bone.Name, weight.Weight)); } } if (list.Count == 0) { // No bone weights found for vertex. Use bone 0 as fallback. missingBoneWeights = true; list.Add(new BoneWeight(aiMesh.Bones[0].Name, 1)); } xnaWeights.Add(list); } if (missingBoneWeights) { _context.Logger.LogWarning( string.Empty, _identity, "No bone weights found for one or more vertices of skinned mesh '{0}'.", aiMesh.Name); } geom.Vertices.Channels.Add(VertexChannelNames.Weights(0), xnaWeights); } // Individual channels go here if (aiMesh.HasNormals) geom.Vertices.Channels.Add(VertexChannelNames.Normal(), aiMesh.Normals.Select(ToXna)); for (var i = 0; i < aiMesh.TextureCoordinateChannelCount; i++) geom.Vertices.Channels.Add(VertexChannelNames.TextureCoordinate(i), aiMesh.TextureCoordinateChannels[i].Select(ToXnaTexCoord)); for (var i = 0; i < aiMesh.VertexColorChannelCount; i++) geom.Vertices.Channels.Add(VertexChannelNames.Color(i), aiMesh.VertexColorChannels[i].Select(ToXnaColor)); return geom; } /// /// Identifies the nodes that represent bones and stores the bone offset matrices. /// private void FindSkeleton() { // See http://assimp.sourceforge.net/lib_html/data.html, section "Bones" // and notes above. // First, identify all deformation bones. _deformationBones = FindDeformationBones(_scene); if (_deformationBones.Count == 0) return; // Walk the tree upwards to find the root bones. var rootBones = new HashSet(); foreach (var boneName in _deformationBones.Keys) rootBones.Add(FindRootBone(_scene, boneName)); if (rootBones.Count > 1) throw new InvalidContentException("Multiple skeletons found. Please ensure that the model does not contain more that one skeleton.", _identity); _rootBone = rootBones.First(); // Add all nodes below root bone to skeleton. GetSubtree(_rootBone, _bones); } /// /// Finds the deformation bones (= bones attached to meshes). /// /// The scene. /// A dictionary of all deformation bones and their offset matrices. private static Dictionary FindDeformationBones(Scene scene) { Debug.Assert(scene != null); var offsetMatrices = new Dictionary(); if (scene.HasMeshes) foreach (var mesh in scene.Meshes) if (mesh.HasBones) foreach (var bone in mesh.Bones) if (!offsetMatrices.ContainsKey(bone.Name)) offsetMatrices[bone.Name] = ToXna(bone.OffsetMatrix); return offsetMatrices; } /// /// Finds the root bone of a specific bone in the skeleton. /// /// The scene. /// The name of a bone in the skeleton. /// The root bone. private static Node FindRootBone(Scene scene, string boneName) { Debug.Assert(scene != null); Debug.Assert(!string.IsNullOrEmpty(boneName)); // Start with the specified bone. Node node = scene.RootNode.FindNode(boneName); Debug.Assert(node != null, "Node referenced by mesh not found in model."); // Walk all the way up to the scene root or the mesh node. Node rootBone = node; while (node != scene.RootNode && !node.HasMeshes) { // Only when FBXPreservePivotsConfig(true): // The FBX path likes to put these extra preserve pivot nodes in here. if (!node.Name.Contains("$AssimpFbx$")) rootBone = node; node = node.Parent; } return rootBone; } /// /// Imports the skeleton including all skeletal animations. /// private void ImportSkeleton() { if (_rootBone == null) return; // Convert nodes to bones and attach to root node. var rootBoneContent = (BoneContent)ImportBones(_rootBone, _rootBone.Parent, null); _rootNode.Children.Add(rootBoneContent); if (!_scene.HasAnimations) return; // Convert animations and add to root bone. foreach (var animation in _scene.Animations) { var animationContent = ImportAnimation(animation); rootBoneContent.Animations.Add(animationContent.Name, animationContent); } } /// /// Converts the specified node and all descendant nodes to XNA bones. /// /// The node. /// The parent node. /// The node converted to XNA. /// The XNA . private NodeContent ImportBones(Node aiNode, Node aiParent, NodeContent parent) { Debug.Assert(aiNode != null); Debug.Assert(aiParent != null); NodeContent node = null; if (!aiNode.Name.Contains("_$AssimpFbx$")) // Ignore pivot nodes { const string mangling = "_$AssimpFbxNull$"; // Null leaf nodes are helpers if (aiNode.Name.Contains(mangling)) { // Null leaf node node = new NodeContent { Name = aiNode.Name.Replace(mangling, string.Empty), Identity = _identity, Transform = ToXna(GetRelativeTransform(aiNode, aiParent)) }; } else if (_bones.Contains(aiNode)) { // Bone node = new BoneContent { Name = aiNode.Name, Identity = _identity }; // node.Transform is irrelevant for bones. This transform is just the // pose of the node at the time of the export. This could, for example, // be one of the animation frames. It is not necessarily the bind pose // (rest pose)! // In XNA BoneContent.Transform needs to be set to the relative bind pose // matrix. The relative bind pose matrix can be derived from the OffsetMatrix // which is stored in aiMesh.Bones. // // offsetMatrix ... Offset matrix. Transforms the mesh from local space to bone space in bind pose. // bindPoseRel ... Relative bind pose matrix. Defines the transform of a bone relative to its parent bone. // bindPoseAbs ... Absolute bind pose matrix. Defines the transform of a bone relative to world space. // // The offset matrix is the inverse of the absolute bind pose matrix. // offsetMatrix = inverse(bindPoseAbs) // // bindPoseAbs = bindPoseRel * parentBindPoseAbs // => bindPoseRel = bindPoseAbs * inverse(parentBindPoseAbs) // = inverse(offsetMatrix) * parentOffsetMatrix Matrix offsetMatrix; Matrix parentOffsetMatrix; bool isOffsetMatrixValid = _deformationBones.TryGetValue(aiNode.Name, out offsetMatrix); bool isParentOffsetMatrixValid = _deformationBones.TryGetValue(aiParent.Name, out parentOffsetMatrix); if (isOffsetMatrixValid && isParentOffsetMatrixValid) { node.Transform = Matrix.Invert(offsetMatrix) * parentOffsetMatrix; } else if (isOffsetMatrixValid && aiNode == _rootBone) { // The current bone is the first in the chain. // The parent offset matrix is missing. :( FbxPivot pivot; if (_pivots.TryGetValue(node.Name, out pivot)) { // --> Use transformation pivot. node.Transform = pivot.GetTransform(null, null, null); } else { // --> Let's assume that parent's transform is Identity. node.Transform = Matrix.Invert(offsetMatrix); } } else if (isOffsetMatrixValid && aiParent == _rootBone) { // The current bone is the second bone in the chain. // The parent offset matrix is missing. :( // --> Derive matrix from parent bone, which is the root bone. parentOffsetMatrix = Matrix.Invert(parent.Transform); node.Transform = Matrix.Invert(offsetMatrix) * parentOffsetMatrix; } else { // Offset matrices are not provided by Assimp. :( // Let's hope that the skeleton was exported in bind pose. // (Otherwise we are just importing garbage.) node.Transform = ToXna(GetRelativeTransform(aiNode, aiParent)); } } } if (node != null) { if (parent != null) parent.Children.Add(node); // For the children, this is the new parent. aiParent = aiNode; parent = node; } foreach (var child in aiNode.Children) ImportBones(child, aiParent, parent); return node; } /// /// Converts the specified animation to XNA. /// /// The animation. /// An optional filter. /// The animation converted to XNA. private AnimationContent ImportAnimation(Animation aiAnimation, string nodeName = null) { var animation = new AnimationContent { Name = GetAnimationName(aiAnimation.Name), Identity = _identity, Duration = TimeSpan.FromSeconds(aiAnimation.DurationInTicks / aiAnimation.TicksPerSecond) }; // In Assimp animation channels may be split into separate channels. // "nodeXyz" --> "nodeXyz_$AssimpFbx$_Translation", // "nodeXyz_$AssimpFbx$_Rotation", // "nodeXyz_$AssimpFbx$_Scaling" // Group animation channels by name (strip the "_$AssimpFbx$" part). IEnumerable < IGrouping < string,NodeAnimationChannel >> channelGroups; if (nodeName != null) { channelGroups = aiAnimation.NodeAnimationChannels .Where(channel => nodeName == GetNodeName(channel.NodeName)) .GroupBy(channel => GetNodeName(channel.NodeName)); } else { channelGroups = aiAnimation.NodeAnimationChannels .GroupBy(channel => GetNodeName(channel.NodeName)); } foreach (var channelGroup in channelGroups) { var boneName = channelGroup.Key; var channel = new AnimationChannel(); // Get transformation pivot for current bone. FbxPivot pivot; if (!_pivots.TryGetValue(boneName, out pivot)) pivot = FbxPivot.Default; var scaleKeys = EmptyVectorKeys; var rotationKeys = EmptyQuaternionKeys; var translationKeys = EmptyVectorKeys; foreach (var aiChannel in channelGroup) { if (aiChannel.NodeName.EndsWith("_$AssimpFbx$_Scaling")) { scaleKeys = aiChannel.ScalingKeys; Debug.Assert(pivot.Scaling.HasValue); Debug.Assert(!aiChannel.HasRotationKeys || (aiChannel.RotationKeyCount == 1 && (aiChannel.RotationKeys[0].Value == new Assimp.Quaternion(1, 0, 0, 0) || aiChannel.RotationKeys[0].Value == new Assimp.Quaternion(0, 0, 0, 0)))); Debug.Assert(!aiChannel.HasPositionKeys || (aiChannel.PositionKeyCount == 1 && aiChannel.PositionKeys[0].Value == new Vector3D(0, 0, 0))); } else if (aiChannel.NodeName.EndsWith("_$AssimpFbx$_Rotation")) { rotationKeys = aiChannel.RotationKeys; Debug.Assert(pivot.Rotation.HasValue); Debug.Assert(!aiChannel.HasScalingKeys || (aiChannel.ScalingKeyCount == 1 && aiChannel.ScalingKeys[0].Value == new Vector3D(1, 1, 1))); Debug.Assert(!aiChannel.HasPositionKeys || (aiChannel.PositionKeyCount == 1 && aiChannel.PositionKeys[0].Value == new Vector3D(0, 0, 0))); } else if (aiChannel.NodeName.EndsWith("_$AssimpFbx$_Translation")) { translationKeys = aiChannel.PositionKeys; Debug.Assert(pivot.Translation.HasValue); Debug.Assert(!aiChannel.HasScalingKeys || (aiChannel.ScalingKeyCount == 1 && aiChannel.ScalingKeys[0].Value == new Vector3D(1, 1, 1))); Debug.Assert(!aiChannel.HasRotationKeys || (aiChannel.RotationKeyCount == 1 && (aiChannel.RotationKeys[0].Value == new Assimp.Quaternion(1, 0, 0, 0) || aiChannel.RotationKeys[0].Value == new Assimp.Quaternion(0, 0, 0, 0)))); } else { scaleKeys = aiChannel.ScalingKeys; rotationKeys = aiChannel.RotationKeys; translationKeys = aiChannel.PositionKeys; } } // Get all unique keyframe times. (Assuming that no two key frames // have the same time, which is usually a safe assumption.) var times = scaleKeys.Select(k => k.Time) .Union(rotationKeys.Select(k => k.Time)) .Union(translationKeys.Select(k => k.Time)) .OrderBy(t => t) .ToList(); Debug.Assert(times.Count == times.Distinct().Count(), "Sequences combined with Union() should not have duplicates."); int prevScaleIndex = -1; int prevRotationIndex = -1; int prevTranslationIndex = -1; double prevScaleTime = 0.0; double prevRotationTime = 0.0; double prevTranslationTime = 0.0; Vector3? prevScale = null; Quaternion? prevRotation = null; Vector3? prevTranslation = null; foreach (var time in times) { // Get scaling. Vector3? scale; int scaleIndex = scaleKeys.FindIndex(k => k.Time == time); if (scaleIndex != -1) { // Scaling key found. scale = ToXna(scaleKeys[scaleIndex].Value); prevScaleIndex = scaleIndex; prevScaleTime = time; prevScale = scale; } else { // No scaling key found. if (prevScaleIndex != -1 && prevScaleIndex + 1 < scaleKeys.Count) { // Lerp between previous and next scaling key. var nextScaleKey = scaleKeys[prevScaleIndex + 1]; var nextScaleTime = nextScaleKey.Time; var nextScale = ToXna(nextScaleKey.Value); var amount = (float)((time - prevScaleTime) / (nextScaleTime - prevScaleTime)); scale = Vector3.Lerp(prevScale.Value, nextScale, amount); } else { // Hold previous scaling value. scale = prevScale; } } // Get rotation. Quaternion? rotation; int rotationIndex = rotationKeys.FindIndex(k => k.Time == time); if (rotationIndex != -1) { // Rotation key found. rotation = ToXna(rotationKeys[rotationIndex].Value); prevRotationIndex = rotationIndex; prevRotationTime = time; prevRotation = rotation; } else { // No rotation key found. if (prevRotationIndex != -1 && prevRotationIndex + 1 < rotationKeys.Count) { // Lerp between previous and next rotation key. var nextRotationKey = rotationKeys[prevRotationIndex + 1]; var nextRotationTime = nextRotationKey.Time; var nextRotation = ToXna(nextRotationKey.Value); var amount = (float)((time - prevRotationTime) / (nextRotationTime - prevRotationTime)); rotation = Quaternion.Slerp(prevRotation.Value, nextRotation, amount); } else { // Hold previous rotation value. rotation = prevRotation; } } // Get translation. Vector3? translation; int translationIndex = translationKeys.FindIndex(k => k.Time == time); if (translationIndex != -1) { // Translation key found. translation = ToXna(translationKeys[translationIndex].Value); prevTranslationIndex = translationIndex; prevTranslationTime = time; prevTranslation = translation; } else { // No translation key found. if (prevTranslationIndex != -1 && prevTranslationIndex + 1 < translationKeys.Count) { // Lerp between previous and next translation key. var nextTranslationKey = translationKeys[prevTranslationIndex + 1]; var nextTranslationTime = nextTranslationKey.Time; var nextTranslation = ToXna(nextTranslationKey.Value); var amount = (float)((time - prevTranslationTime) / (nextTranslationTime - prevTranslationTime)); translation = Vector3.Lerp(prevTranslation.Value, nextTranslation, amount); } else { // Hold previous translation value. translation = prevTranslation; } } // Apply transformation pivot. var transform = pivot.GetTransform(scale, rotation, translation); long ticks = (long)(time * (TimeSpan.TicksPerSecond / aiAnimation.TicksPerSecond)); channel.Add(new AnimationKeyframe(TimeSpan.FromTicks(ticks), transform)); } animation.Channels[channelGroup.Key] = channel; } return animation; } /// /// Copies the current node and all descendant nodes into a list. /// /// The current node. /// The list. private static void GetSubtree(Node node, List list) { Debug.Assert(node != null); Debug.Assert(list != null); list.Add(node); foreach (var child in node.Children) GetSubtree(child, list); } /// /// Gets the transform of node relative to a specific ancestor node. /// /// The node. /// The ancestor node. Can be . /// /// The relative transform. If is the /// absolute transform of is returned. /// private static Matrix4x4 GetRelativeTransform(Node node, Node ancestor) { Debug.Assert(node != null); // Get transform of node relative to ancestor. Matrix4x4 transform = node.Transform; Node parent = node.Parent; while (parent != null && parent != ancestor) { transform *= parent.Transform; parent = parent.Parent; } if (parent == null && ancestor != null) throw new ArgumentException(string.Format("Node \"{0}\" is not an ancestor of \"{1}\".", ancestor.Name, node.Name)); return transform; } /// /// Gets the animation name without the "AnimStack::" part. /// /// The mangled animation name. /// The original animation name. private static string GetAnimationName(string name) { return name.Replace("AnimStack::", string.Empty); } /// /// Gets the node name without the "_$AssimpFbx$" part. /// /// The mangled node name. /// The original node name. private static string GetNodeName(string name) { int index = name.IndexOf("_$AssimpFbx$", StringComparison.Ordinal); return (index >= 0) ? name.Remove(index) : name; } #region Conversion Helpers [DebuggerStepThrough] public static Matrix ToXna(Matrix4x4 matrix) { var result = Matrix.Identity; result.M11 = matrix.A1; result.M12 = matrix.B1; result.M13 = matrix.C1; result.M14 = matrix.D1; result.M21 = matrix.A2; result.M22 = matrix.B2; result.M23 = matrix.C2; result.M24 = matrix.D2; result.M31 = matrix.A3; result.M32 = matrix.B3; result.M33 = matrix.C3; result.M34 = matrix.D3; result.M41 = matrix.A4; result.M42 = matrix.B4; result.M43 = matrix.C4; result.M44 = matrix.D4; return result; } [DebuggerStepThrough] public static Vector2 ToXna(Vector2D vector) { return new Vector2(vector.X, vector.Y); } [DebuggerStepThrough] public static Vector3 ToXna(Vector3D vector) { return new Vector3(vector.X, vector.Y, vector.Z); } [DebuggerStepThrough] public static Quaternion ToXna(Assimp.Quaternion quaternion) { return new Quaternion(quaternion.X, quaternion.Y, quaternion.Z, quaternion.W); } [DebuggerStepThrough] public static Vector3 ToXna(Color4D color) { return new Vector3(color.R, color.G, color.B); } [DebuggerStepThrough] public static Vector2 ToXnaTexCoord(Vector3D vector) { return new Vector2(vector.X, vector.Y); } [DebuggerStepThrough] public static Color ToXnaColor(Color4D color) { return new Color(color.R, color.G, color.B, color.A); } #endregion } }