/MonoGame.Framework.Content.Pipeline/OpenAssetImporter.cs

http://github.com/mono/MonoGame · C# · 1237 lines · 801 code · 146 blank · 290 comment · 215 complexity · 4435ab778a5e7db7440ba46ea7247e4c MD5 · raw file

Large files are truncated click here to view the full file

  1. // MonoGame - Copyright (C) The MonoGame Team
  2. // This file is subject to the terms and conditions defined in
  3. // file 'LICENSE.txt', which is part of this source code package.
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Diagnostics;
  7. using System.IO;
  8. using System.Linq;
  9. using System.Reflection;
  10. using Assimp;
  11. using Assimp.Unmanaged;
  12. using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
  13. using MonoGame.Utilities;
  14. namespace Microsoft.Xna.Framework.Content.Pipeline
  15. {
  16. [ContentImporter(
  17. ".dae", // Collada
  18. ".gltf", "glb", // glTF
  19. ".blend", // Blender 3D
  20. ".3ds", // 3ds Max 3DS
  21. ".ase", // 3ds Max ASE
  22. ".obj", // Wavefront Object
  23. ".ifc", // Industry Foundation Classes (IFC/Step)
  24. ".xgl", ".zgl", // XGL
  25. ".ply", // Stanford Polygon Library
  26. ".dxf", // AutoCAD DXF
  27. ".lwo", // LightWave
  28. ".lws", // LightWave Scene
  29. ".lxo", // Modo
  30. ".stl", // Stereolithography
  31. ".ac", // AC3D
  32. ".ms3d", // Milkshape 3D
  33. ".cob", ".scn", // TrueSpace
  34. ".bvh", // Biovision BVH
  35. ".csm", // CharacterStudio Motion
  36. ".irrmesh", // Irrlicht Mesh
  37. ".irr", // Irrlicht Scene
  38. ".mdl", // Quake I, 3D GameStudio (3DGS)
  39. ".md2", // Quake II
  40. ".md3", // Quake III Mesh
  41. ".pk3", // Quake III Map/BSP
  42. ".mdc", // Return to Castle Wolfenstein
  43. ".md5", // Doom 3
  44. ".smd", ".vta", // Valve Model
  45. ".ogex", // Open Game Engine Exchange
  46. ".3d", // Unreal
  47. ".b3d", // BlitzBasic 3D
  48. ".q3d", ".q3s", // Quick3D
  49. ".nff", // Neutral File Format, Sense8 WorldToolKit
  50. ".off", // Object File Format
  51. ".ter", // Terragen Terrain
  52. ".hmp", // 3D GameStudio (3DGS) Terrain
  53. ".ndo", // Izware Nendo
  54. DisplayName = "Open Asset Import Library - MonoGame", DefaultProcessor = "ModelProcessor")]
  55. public class OpenAssetImporter : ContentImporter<NodeContent>
  56. {
  57. // Assimp has a few limitations (not all FBX files are supported):
  58. // FBX files reference objects using IDs. Therefore, it is possible to resolve
  59. // bones even if multiple bones/nodes have the same name. But Assimp references
  60. // bones only by name!
  61. // --> Limitation #1: A model cannot have more than one skeleton!
  62. // --> Limitation #2: Bone names need to be unique!
  63. //
  64. // Bones are represented by regular nodes, but there is no flag indicating whether
  65. // a node is a bone. A mesh in Assimp references deformation bones (= bones that
  66. // affect vertices) by name. That means, we can identify the nodes that represent
  67. // deformation bones. But there is no way to identify helper bones (= bones that
  68. // belong to the skeleton, but do not affect vertices). As described in
  69. // http://assimp.sourceforge.net/lib_html/data.html and
  70. // http://gamedev.stackexchange.com/questions/26382/i-cant-figure-out-how-to-animate-my-loaded-model-with-assimp/26442#26442
  71. // we can only guess which nodes belong to a skeleton:
  72. // --> Limitation #3: The skeleton needs to be a direct child of the root node or
  73. // the mesh node!
  74. //
  75. // Node.Transform is irrelevant for bones. This transform is just the pose of the
  76. // bone at the time of the export. This could be one of the animation frames. It
  77. // is not necessarily the bind pose (rest pose)! For example, XNA's Dude.fbx does
  78. // NOT store the skeleton in bind pose.
  79. // The correct transform is stored in Mesh.Bones[i].OffsetMatrix. However, this
  80. // information is only available for deformation bones, not for helper bones.
  81. // --> Limitation #4: The skeleton either must not contain helper bones, or it must
  82. // be guaranteed that the skeleton is exported in bind pose!
  83. //
  84. // An FBX file does not directly store all animation values. In some FBX scene it
  85. // is insufficient to simply read the animation data from the file. Instead, the
  86. // animation properties of all relevant objects in the scene need to be evaluated.
  87. // For example, some animations are the result of the current skeleton pose + the
  88. // current animation. The current skeleton pose is not imported/processed by XNA.
  89. // Assimp does not include an "animation evaluater" that automatically bakes these
  90. // animations.
  91. // --> Limitation #5: All bones included in an animation need to be key framed.
  92. // (There is no automatic evaluation.)
  93. //
  94. // In FBX it is possible to define animations curves for some transform components
  95. // (e.g. translation X and Y) and leave other components (e.g. translation Z) undefined.
  96. // Assimp does not pick the right defaults for undefined components.
  97. // --> Limitation #6: When scale, rotation, or translation is animated, all components
  98. // X, Y, Z need to be key framed.
  99. #region Nested Types
  100. /// <summary>Defines the frame for local scale/rotation/translation of FBX nodes.</summary>
  101. /// <remarks>
  102. /// <para>
  103. /// The transformation pivot defines the frame for local scale/rotation/translation. The
  104. /// local transform of a node is:
  105. /// </para>
  106. /// <para>
  107. /// Local Transform = Translation * RotationOffset * RotationPivot * PreRotation
  108. /// * Rotation * PostRotation * RotationPivotInverse * ScalingOffset
  109. /// * ScalingPivot * Scaling * ScalingPivotInverse
  110. /// </para>
  111. /// <para>
  112. /// where the matrix multiplication order is right-to-left.
  113. /// </para>
  114. /// <para>
  115. /// 3ds max uses three additional transformations:
  116. /// </para>
  117. /// <para>
  118. /// Local Transform = Translation * Rotation * Scaling
  119. /// * GeometricTranslation * GeometricRotation * GeometricScaling
  120. /// </para>
  121. /// <para>
  122. /// Transformation pivots are stored per FBX node. When Assimp hits an FBX node with
  123. /// a transformation pivot it generates additional nodes named
  124. /// </para>
  125. /// <para>
  126. /// <i>OriginalName</i>_$AssimpFbx$_<i>TransformName</i>
  127. /// </para>
  128. /// <para>
  129. /// where <i>TransformName</i> is one of:
  130. /// </para>
  131. /// <para>
  132. /// Translation, RotationOffset, RotationPivot, PreRotation, Rotation, PostRotation,
  133. /// RotationPivotInverse, ScalingOffset, ScalingPivot, Scaling, ScalingPivotInverse,
  134. /// GeometricTranslation, GeometricRotation, GeometricScaling
  135. /// </para>
  136. /// </remarks>
  137. /// <seealso href="http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=WS1a9193826455f5ff1f92379812724681e696651.htm,topicNumber=d0e7429"/>
  138. /// <seealso href="http://area.autodesk.com/forum/autodesk-fbx/fbx-sdk/the-makeup-of-the-local-matrix-of-an-kfbxnode/"/>
  139. private class FbxPivot
  140. {
  141. public static readonly FbxPivot Default = new FbxPivot();
  142. public Matrix? Translation;
  143. public Matrix? RotationOffset;
  144. public Matrix? RotationPivot;
  145. public Matrix? PreRotation;
  146. public Matrix? Rotation;
  147. public Matrix? PostRotation;
  148. public Matrix? RotationPivotInverse;
  149. public Matrix? ScalingOffset;
  150. public Matrix? ScalingPivot;
  151. public Matrix? Scaling;
  152. public Matrix? ScalingPivotInverse;
  153. public Matrix? GeometricTranslation;
  154. public Matrix? GeometricRotation;
  155. public Matrix? GeometricScaling;
  156. public Matrix GetTransform(Vector3? scale, Quaternion? rotation, Vector3? translation)
  157. {
  158. var transform = Matrix.Identity;
  159. if (GeometricScaling.HasValue)
  160. transform *= GeometricScaling.Value;
  161. if (GeometricRotation.HasValue)
  162. transform *= GeometricRotation.Value;
  163. if (GeometricTranslation.HasValue)
  164. transform *= GeometricTranslation.Value;
  165. if (ScalingPivotInverse.HasValue)
  166. transform *= ScalingPivotInverse.Value;
  167. if (scale.HasValue)
  168. transform *= Matrix.CreateScale(scale.Value);
  169. else if (Scaling.HasValue)
  170. transform *= Scaling.Value;
  171. if (ScalingPivot.HasValue)
  172. transform *= ScalingPivot.Value;
  173. if (ScalingOffset.HasValue)
  174. transform *= ScalingOffset.Value;
  175. if (RotationPivotInverse.HasValue)
  176. transform *= RotationPivotInverse.Value;
  177. if (PostRotation.HasValue)
  178. transform *= PostRotation.Value;
  179. if (rotation.HasValue)
  180. transform *= Matrix.CreateFromQuaternion(rotation.Value);
  181. else if (Rotation.HasValue)
  182. transform *= Rotation.Value;
  183. if (PreRotation.HasValue)
  184. transform *= PreRotation.Value;
  185. if (RotationPivot.HasValue)
  186. transform *= RotationPivot.Value;
  187. if (RotationOffset.HasValue)
  188. transform *= RotationOffset.Value;
  189. if (translation.HasValue)
  190. transform *= Matrix.CreateTranslation(translation.Value);
  191. else if (Translation.HasValue)
  192. transform *= Translation.Value;
  193. return transform;
  194. }
  195. }
  196. #endregion
  197. private static readonly List<VectorKey> EmptyVectorKeys = new List<VectorKey>();
  198. private static readonly List<QuaternionKey> EmptyQuaternionKeys = new List<QuaternionKey>();
  199. // XNA Content importer
  200. private ContentImporterContext _context;
  201. private ContentIdentity _identity;
  202. // Assimp scene
  203. private Scene _scene;
  204. private Dictionary<string, Matrix> _deformationBones; // The names and offset matrices of all deformation bones.
  205. private Node _rootBone; // The node that represents the root bone.
  206. private List<Node> _bones = new List<Node>(); // All nodes attached to the root bone.
  207. private Dictionary<string, FbxPivot> _pivots; // The transformation pivots.
  208. // XNA content
  209. private NodeContent _rootNode;
  210. private List<MaterialContent> _materials;
  211. // This is used to enable backwards compatibility with
  212. // XNA providing a model as expected from the original
  213. // FbxImporter and XImporter.
  214. private readonly bool _xnaCompatible;
  215. private readonly string _importerName;
  216. /// <summary>
  217. /// Default constructor.
  218. /// </summary>
  219. public OpenAssetImporter()
  220. : this("OpenAssetImporter", false)
  221. {
  222. }
  223. internal OpenAssetImporter(string importerName, bool xnaCompatible)
  224. {
  225. _importerName = importerName;
  226. _xnaCompatible = xnaCompatible;
  227. }
  228. /// <summary>
  229. /// This disables some Assimp model loading features so that
  230. /// the resulting content is the same as what the XNA FbxImporter
  231. /// </summary>
  232. public bool XnaComptatible { get; set; }
  233. public override NodeContent Import(string filename, ContentImporterContext context)
  234. {
  235. if (filename == null)
  236. throw new ArgumentNullException("filename");
  237. if (context == null)
  238. throw new ArgumentNullException("context");
  239. _context = context;
  240. if (CurrentPlatform.OS == OS.Linux)
  241. {
  242. var targetDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName;
  243. try
  244. {
  245. AssimpLibrary.Instance.LoadLibrary(
  246. Path.Combine(targetDir, "libassimp.so"),
  247. Path.Combine(targetDir, "libassimp.so"));
  248. }
  249. catch { }
  250. }
  251. _identity = new ContentIdentity(filename, _importerName);
  252. using (var importer = new AssimpContext())
  253. {
  254. // FBXPreservePivotsConfig(false) can be set to remove transformation
  255. // pivots. However, Assimp does not automatically correct animations!
  256. // --> Leave default settings, handle transformation pivots explicitly.
  257. //importer.SetConfig(new Assimp.Configs.FBXPreservePivotsConfig(false));
  258. // Set flag to remove degenerate faces (points and lines).
  259. // This flag is very important when PostProcessSteps.FindDegenerates is used
  260. // because FindDegenerates converts degenerate triangles to points and lines!
  261. importer.SetConfig(new Assimp.Configs.RemoveDegeneratePrimitivesConfig(true));
  262. // Note about Assimp post-processing:
  263. // Keep post-processing to a minimum. The ModelImporter should import
  264. // the model as is. We don't want to lose any information, i.e. empty
  265. // nodes shoud not be thrown away, meshes/materials should not be merged,
  266. // etc. Custom model processors may depend on this information!
  267. _scene = importer.ImportFile(filename,
  268. PostProcessSteps.FindDegenerates |
  269. PostProcessSteps.FindInvalidData |
  270. PostProcessSteps.FlipUVs | // Required for Direct3D
  271. PostProcessSteps.FlipWindingOrder | // Required for Direct3D
  272. PostProcessSteps.JoinIdenticalVertices |
  273. PostProcessSteps.ImproveCacheLocality |
  274. PostProcessSteps.OptimizeMeshes |
  275. PostProcessSteps.Triangulate
  276. // Unused:
  277. //PostProcessSteps.CalculateTangentSpace
  278. //PostProcessSteps.Debone |
  279. //PostProcessSteps.FindInstances | // No effect + slow?
  280. //PostProcessSteps.FixInFacingNormals |
  281. //PostProcessSteps.GenerateNormals |
  282. //PostProcessSteps.GenerateSmoothNormals |
  283. //PostProcessSteps.GenerateUVCoords | // Might be needed... find test case
  284. //PostProcessSteps.LimitBoneWeights |
  285. //PostProcessSteps.MakeLeftHanded | // Not necessary, XNA is right-handed.
  286. //PostProcessSteps.OptimizeGraph | // Will eliminate helper nodes
  287. //PostProcessSteps.PreTransformVertices |
  288. //PostProcessSteps.RemoveComponent |
  289. //PostProcessSteps.RemoveRedundantMaterials |
  290. //PostProcessSteps.SortByPrimitiveType |
  291. //PostProcessSteps.SplitByBoneCount |
  292. //PostProcessSteps.SplitLargeMeshes |
  293. //PostProcessSteps.TransformUVCoords |
  294. //PostProcessSteps.ValidateDataStructure |
  295. );
  296. FindSkeleton(); // Find _rootBone, _bones, _deformationBones.
  297. // Create _materials.
  298. if (_xnaCompatible)
  299. ImportXnaMaterials();
  300. else
  301. ImportMaterials();
  302. ImportNodes(); // Create _pivots and _rootNode (incl. children).
  303. ImportSkeleton(); // Create skeleton (incl. animations) and add to _rootNode.
  304. // If we have a simple hierarchy with no bones and just the one
  305. // mesh, we can flatten it out so the mesh is the root node.
  306. if (_rootNode.Children.Count == 1 && _rootNode.Children[0] is MeshContent)
  307. {
  308. var absXform = _rootNode.Children[0].AbsoluteTransform;
  309. _rootNode = _rootNode.Children[0];
  310. _rootNode.Identity = _identity;
  311. _rootNode.Transform = absXform;
  312. }
  313. _scene.Clear();
  314. }
  315. return _rootNode;
  316. }
  317. /// <summary>
  318. /// Converts all Assimp <see cref="Material"/>s to standard XNA compatible <see cref="MaterialContent"/>s.
  319. /// </summary>
  320. private void ImportXnaMaterials()
  321. {
  322. _materials = new List<MaterialContent>();
  323. foreach (var aiMaterial in _scene.Materials)
  324. {
  325. // TODO: What about AlphaTestMaterialContent, DualTextureMaterialContent,
  326. // EffectMaterialContent, EnvironmentMapMaterialContent, and SkinnedMaterialContent?
  327. var material = new BasicMaterialContent
  328. {
  329. Name = aiMaterial.Name,
  330. Identity = _identity,
  331. };
  332. if (aiMaterial.HasTextureDiffuse)
  333. material.Texture = ImportTextureContentRef(aiMaterial.TextureDiffuse);
  334. if (aiMaterial.HasTextureOpacity)
  335. material.Textures.Add("Transparency", ImportTextureContentRef(aiMaterial.TextureOpacity));
  336. if (aiMaterial.HasTextureSpecular)
  337. material.Textures.Add("Specular", ImportTextureContentRef(aiMaterial.TextureSpecular));
  338. if (aiMaterial.HasTextureHeight)
  339. material.Textures.Add("Bump", ImportTextureContentRef(aiMaterial.TextureHeight));
  340. if (aiMaterial.HasColorDiffuse)
  341. material.DiffuseColor = ToXna(aiMaterial.ColorDiffuse);
  342. if (aiMaterial.HasColorEmissive)
  343. material.EmissiveColor = ToXna(aiMaterial.ColorEmissive);
  344. if (aiMaterial.HasOpacity)
  345. material.Alpha = aiMaterial.Opacity;
  346. if (aiMaterial.HasColorSpecular)
  347. material.SpecularColor = ToXna(aiMaterial.ColorSpecular);
  348. if (aiMaterial.HasShininessStrength)
  349. material.SpecularPower = aiMaterial.Shininess;
  350. _materials.Add(material);
  351. }
  352. }
  353. private ExternalReference<TextureContent> ImportTextureContentRef(TextureSlot textureSlot)
  354. {
  355. var texture = new ExternalReference<TextureContent>(textureSlot.FilePath, _identity);
  356. texture.OpaqueData.Add("TextureCoordinate", string.Format("TextureCoordinate{0}", textureSlot.UVIndex));
  357. if (!_xnaCompatible)
  358. {
  359. texture.OpaqueData.Add("Operation", textureSlot.Operation.ToString());
  360. texture.OpaqueData.Add("AddressU", textureSlot.WrapModeU.ToString());
  361. texture.OpaqueData.Add("AddressV", textureSlot.WrapModeU.ToString());
  362. texture.OpaqueData.Add("Mapping", textureSlot.Mapping.ToString());
  363. }
  364. return texture;
  365. }
  366. /// <summary>
  367. /// Returns all the Assimp <see cref="Material"/> features as a <see cref="MaterialContent"/>.
  368. /// </summary>
  369. private void ImportMaterials()
  370. {
  371. _materials = new List<MaterialContent>();
  372. foreach (var aiMaterial in _scene.Materials)
  373. {
  374. // TODO: Should we create a special AssImpMaterial?
  375. var material = new MaterialContent
  376. {
  377. Name = aiMaterial.Name,
  378. Identity = _identity,
  379. };
  380. var slots = aiMaterial.GetAllMaterialTextures();
  381. foreach (var tex in slots)
  382. {
  383. string name;
  384. // Force the XNA naming standard for diffuse textures
  385. // which allows the material to work with the stock
  386. // model processor.
  387. if (tex.TextureType == TextureType.Diffuse)
  388. name = BasicMaterialContent.TextureKey;
  389. else
  390. name = tex.TextureType.ToString();
  391. // We might have multiple textures of the same type so number
  392. // them starting with 2 like in DualTextureMaterialContent.
  393. if (tex.TextureIndex > 0)
  394. name += (tex.TextureIndex + 1);
  395. material.Textures.Add(name, ImportTextureContentRef(tex));
  396. }
  397. if (aiMaterial.HasBlendMode)
  398. material.OpaqueData.Add("BlendMode", aiMaterial.BlendMode.ToString());
  399. if (aiMaterial.HasBumpScaling)
  400. material.OpaqueData.Add("BumpScaling", aiMaterial.BumpScaling);
  401. if (aiMaterial.HasColorAmbient)
  402. material.OpaqueData.Add("AmbientColor", ToXna(aiMaterial.ColorAmbient));
  403. if (aiMaterial.HasColorDiffuse)
  404. material.OpaqueData.Add("DiffuseColor", ToXna(aiMaterial.ColorDiffuse));
  405. if (aiMaterial.HasColorEmissive)
  406. material.OpaqueData.Add("EmissiveColor", ToXna(aiMaterial.ColorEmissive));
  407. if (aiMaterial.HasColorReflective)
  408. material.OpaqueData.Add("ReflectiveColor", ToXna(aiMaterial.ColorReflective));
  409. if (aiMaterial.HasColorSpecular)
  410. material.OpaqueData.Add("SpecularColor", ToXna(aiMaterial.ColorSpecular));
  411. if (aiMaterial.HasColorTransparent)
  412. material.OpaqueData.Add("TransparentColor", ToXna(aiMaterial.ColorTransparent));
  413. if (aiMaterial.HasOpacity)
  414. material.OpaqueData.Add("Opacity", aiMaterial.Opacity);
  415. if (aiMaterial.HasReflectivity)
  416. material.OpaqueData.Add("Reflectivity", aiMaterial.Reflectivity);
  417. if (aiMaterial.HasShadingMode)
  418. material.OpaqueData.Add("ShadingMode", aiMaterial.ShadingMode.ToString());
  419. if (aiMaterial.HasShininess)
  420. material.OpaqueData.Add("Shininess", aiMaterial.Shininess);
  421. if (aiMaterial.HasShininessStrength)
  422. material.OpaqueData.Add("ShininessStrength", aiMaterial.ShininessStrength);
  423. if (aiMaterial.HasTwoSided)
  424. material.OpaqueData.Add("TwoSided", aiMaterial.IsTwoSided);
  425. if (aiMaterial.HasWireFrame)
  426. material.OpaqueData.Add("WireFrame", aiMaterial.IsWireFrameEnabled);
  427. _materials.Add(material);
  428. }
  429. }
  430. /// <summary>
  431. /// Converts all Assimp nodes to XNA nodes. (Nodes representing bones are excluded!)
  432. /// </summary>
  433. private void ImportNodes()
  434. {
  435. _pivots = new Dictionary<string, FbxPivot>();
  436. _rootNode = ImportNodes(_scene.RootNode, null, null);
  437. }
  438. /// <summary>
  439. /// Converts the specified node and all descendant nodes.
  440. /// </summary>
  441. /// <param name="aiNode">The node.</param>
  442. /// <param name="aiParent">The parent node. Can be <see langword="null"/>.</param>
  443. /// <param name="parent">The <paramref name="aiParent"/> node converted to XNA.</param>
  444. /// <returns>The XNA <see cref="NodeContent"/>.</returns>
  445. /// <remarks>
  446. /// It may be necessary to skip certain "preserve pivot" nodes in the hierarchy. The
  447. /// converted node needs to be relative to <paramref name="aiParent"/>, not <c>node.Parent</c>.
  448. /// </remarks>
  449. private NodeContent ImportNodes(Node aiNode, Node aiParent, NodeContent parent)
  450. {
  451. Debug.Assert(aiNode != null);
  452. NodeContent node = null;
  453. if (aiNode.HasMeshes)
  454. {
  455. var mesh = new MeshContent
  456. {
  457. Name = aiNode.Name,
  458. Identity = _identity,
  459. Transform = ToXna(GetRelativeTransform(aiNode, aiParent))
  460. };
  461. foreach (var meshIndex in aiNode.MeshIndices)
  462. {
  463. var aiMesh = _scene.Meshes[meshIndex];
  464. if (!aiMesh.HasVertices)
  465. continue;
  466. var geom = CreateGeometry(mesh, aiMesh);
  467. mesh.Geometry.Add(geom);
  468. }
  469. node = mesh;
  470. }
  471. else if (aiNode.Name.Contains("_$AssimpFbx$"))
  472. {
  473. // This is a transformation pivot.
  474. // <OriginalName>_$AssimpFbx$_<TransformName>
  475. // where <TransformName> is one of
  476. // Translation, RotationOffset, RotationPivot, PreRotation, Rotation,
  477. // PostRotation, RotationPivotInverse, ScalingOffset, ScalingPivot,
  478. // Scaling, ScalingPivotInverse
  479. string originalName = GetNodeName(aiNode.Name);
  480. FbxPivot pivot;
  481. if (!_pivots.TryGetValue(originalName, out pivot))
  482. {
  483. pivot = new FbxPivot();
  484. _pivots.Add(originalName, pivot);
  485. }
  486. Matrix transform = ToXna(aiNode.Transform);
  487. if (aiNode.Name.EndsWith("_Translation"))
  488. pivot.Translation = transform;
  489. else if (aiNode.Name.EndsWith("_RotationOffset"))
  490. pivot.RotationOffset = transform;
  491. else if (aiNode.Name.EndsWith("_RotationPivot"))
  492. pivot.RotationPivot = transform;
  493. else if (aiNode.Name.EndsWith("_PreRotation"))
  494. pivot.PreRotation = transform;
  495. else if (aiNode.Name.EndsWith("_Rotation"))
  496. pivot.Rotation = transform;
  497. else if (aiNode.Name.EndsWith("_PostRotation"))
  498. pivot.PostRotation = transform;
  499. else if (aiNode.Name.EndsWith("_RotationPivotInverse"))
  500. pivot.RotationPivotInverse = transform;
  501. else if (aiNode.Name.EndsWith("_ScalingOffset"))
  502. pivot.ScalingOffset = transform;
  503. else if (aiNode.Name.EndsWith("_ScalingPivot"))
  504. pivot.ScalingPivot = transform;
  505. else if (aiNode.Name.EndsWith("_Scaling"))
  506. pivot.Scaling = transform;
  507. else if (aiNode.Name.EndsWith("_ScalingPivotInverse"))
  508. pivot.ScalingPivotInverse = transform;
  509. else if (aiNode.Name.EndsWith("_GeometricTranslation"))
  510. pivot.GeometricTranslation = transform;
  511. else if (aiNode.Name.EndsWith("_GeometricRotation"))
  512. pivot.GeometricRotation = transform;
  513. else if (aiNode.Name.EndsWith("_GeometricScaling"))
  514. pivot.GeometricScaling = transform;
  515. else
  516. throw new InvalidContentException(string.Format("Unknown $AssimpFbx$ node: \"{0}\"", aiNode.Name), _identity);
  517. }
  518. else if (!_bones.Contains(aiNode)) // Ignore bones.
  519. {
  520. node = new NodeContent
  521. {
  522. Name = aiNode.Name,
  523. Identity = _identity,
  524. Transform = ToXna(GetRelativeTransform(aiNode, aiParent))
  525. };
  526. }
  527. if (node != null)
  528. {
  529. if (parent != null)
  530. parent.Children.Add(node);
  531. // For the children, this is the new parent.
  532. aiParent = aiNode;
  533. parent = node;
  534. if (_scene.HasAnimations)
  535. {
  536. foreach (var animation in _scene.Animations)
  537. {
  538. var animationContent = ImportAnimation(animation, node.Name);
  539. if (animationContent.Channels.Count > 0)
  540. node.Animations.Add(animationContent.Name, animationContent);
  541. }
  542. }
  543. }
  544. Debug.Assert(parent != null);
  545. foreach (var child in aiNode.Children)
  546. ImportNodes(child, aiParent, parent);
  547. return node;
  548. }
  549. private GeometryContent CreateGeometry(MeshContent mesh, Mesh aiMesh)
  550. {
  551. var geom = new GeometryContent
  552. {
  553. Identity = _identity,
  554. Material = _materials[aiMesh.MaterialIndex]
  555. };
  556. // Vertices
  557. var baseVertex = mesh.Positions.Count;
  558. foreach (var vert in aiMesh.Vertices)
  559. mesh.Positions.Add(ToXna(vert));
  560. geom.Vertices.AddRange(Enumerable.Range(baseVertex, aiMesh.VertexCount));
  561. geom.Indices.AddRange(aiMesh.GetIndices());
  562. if (aiMesh.HasBones)
  563. {
  564. var xnaWeights = new List<BoneWeightCollection>();
  565. var vertexCount = geom.Vertices.VertexCount;
  566. bool missingBoneWeights = false;
  567. for (var i = 0; i < vertexCount; i++)
  568. {
  569. var list = new BoneWeightCollection();
  570. for (var boneIndex = 0; boneIndex < aiMesh.BoneCount; boneIndex++)
  571. {
  572. var bone = aiMesh.Bones[boneIndex];
  573. foreach (var weight in bone.VertexWeights)
  574. {
  575. if (weight.VertexID != i)
  576. continue;
  577. list.Add(new BoneWeight(bone.Name, weight.Weight));
  578. }
  579. }
  580. if (list.Count == 0)
  581. {
  582. // No bone weights found for vertex. Use bone 0 as fallback.
  583. missingBoneWeights = true;
  584. list.Add(new BoneWeight(aiMesh.Bones[0].Name, 1));
  585. }
  586. xnaWeights.Add(list);
  587. }
  588. if (missingBoneWeights)
  589. {
  590. _context.Logger.LogWarning(
  591. string.Empty,
  592. _identity,
  593. "No bone weights found for one or more vertices of skinned mesh '{0}'.",
  594. aiMesh.Name);
  595. }
  596. geom.Vertices.Channels.Add(VertexChannelNames.Weights(0), xnaWeights);
  597. }
  598. // Individual channels go here
  599. if (aiMesh.HasNormals)
  600. geom.Vertices.Channels.Add(VertexChannelNames.Normal(), aiMesh.Normals.Select(ToXna));
  601. for (var i = 0; i < aiMesh.TextureCoordinateChannelCount; i++)
  602. geom.Vertices.Channels.Add(VertexChannelNames.TextureCoordinate(i), aiMesh.TextureCoordinateChannels[i].Select(ToXnaTexCoord));
  603. for (var i = 0; i < aiMesh.VertexColorChannelCount; i++)
  604. geom.Vertices.Channels.Add(VertexChannelNames.Color(i), aiMesh.VertexColorChannels[i].Select(ToXnaColor));
  605. return geom;
  606. }
  607. /// <summary>
  608. /// Identifies the nodes that represent bones and stores the bone offset matrices.
  609. /// </summary>
  610. private void FindSkeleton()
  611. {
  612. // See http://assimp.sourceforge.net/lib_html/data.html, section "Bones"
  613. // and notes above.
  614. // First, identify all deformation bones.
  615. _deformationBones = FindDeformationBones(_scene);
  616. if (_deformationBones.Count == 0)
  617. return;
  618. // Walk the tree upwards to find the root bones.
  619. var rootBones = new HashSet<Node>();
  620. foreach (var boneName in _deformationBones.Keys)
  621. rootBones.Add(FindRootBone(_scene, boneName));
  622. if (rootBones.Count > 1)
  623. throw new InvalidContentException("Multiple skeletons found. Please ensure that the model does not contain more that one skeleton.", _identity);
  624. _rootBone = rootBones.First();
  625. // Add all nodes below root bone to skeleton.
  626. GetSubtree(_rootBone, _bones);
  627. }
  628. /// <summary>
  629. /// Finds the deformation bones (= bones attached to meshes).
  630. /// </summary>
  631. /// <param name="scene">The scene.</param>
  632. /// <returns>A dictionary of all deformation bones and their offset matrices.</returns>
  633. private static Dictionary<string, Matrix> FindDeformationBones(Scene scene)
  634. {
  635. Debug.Assert(scene != null);
  636. var offsetMatrices = new Dictionary<string, Matrix>();
  637. if (scene.HasMeshes)
  638. foreach (var mesh in scene.Meshes)
  639. if (mesh.HasBones)
  640. foreach (var bone in mesh.Bones)
  641. if (!offsetMatrices.ContainsKey(bone.Name))
  642. offsetMatrices[bone.Name] = ToXna(bone.OffsetMatrix);
  643. return offsetMatrices;
  644. }
  645. /// <summary>
  646. /// Finds the root bone of a specific bone in the skeleton.
  647. /// </summary>
  648. /// <param name="scene">The scene.</param>
  649. /// <param name="boneName">The name of a bone in the skeleton.</param>
  650. /// <returns>The root bone.</returns>
  651. private static Node FindRootBone(Scene scene, string boneName)
  652. {
  653. Debug.Assert(scene != null);
  654. Debug.Assert(!string.IsNullOrEmpty(boneName));
  655. // Start with the specified bone.
  656. Node node = scene.RootNode.FindNode(boneName);
  657. Debug.Assert(node != null, "Node referenced by mesh not found in model.");
  658. // Walk all the way up to the scene root or the mesh node.
  659. Node rootBone = node;
  660. while (node != scene.RootNode && !node.HasMeshes)
  661. {
  662. // Only when FBXPreservePivotsConfig(true):
  663. // The FBX path likes to put these extra preserve pivot nodes in here.
  664. if (!node.Name.Contains("$AssimpFbx$"))
  665. rootBone = node;
  666. node = node.Parent;
  667. }
  668. return rootBone;
  669. }
  670. /// <summary>
  671. /// Imports the skeleton including all skeletal animations.
  672. /// </summary>
  673. private void ImportSkeleton()
  674. {
  675. if (_rootBone == null)
  676. return;
  677. // Convert nodes to bones and attach to root node.
  678. var rootBoneContent = (BoneContent)ImportBones(_rootBone, _rootBone.Parent, null);
  679. _rootNode.Children.Add(rootBoneContent);
  680. if (!_scene.HasAnimations)
  681. return;
  682. // Convert animations and add to root bone.
  683. foreach (var animation in _scene.Animations)
  684. {
  685. var animationContent = ImportAnimation(animation);
  686. rootBoneContent.Animations.Add(animationContent.Name, animationContent);
  687. }
  688. }
  689. /// <summary>
  690. /// Converts the specified node and all descendant nodes to XNA bones.
  691. /// </summary>
  692. /// <param name="aiNode">The node.</param>
  693. /// <param name="aiParent">The parent node.</param>
  694. /// <param name="parent">The <paramref name="aiParent"/> node converted to XNA.</param>
  695. /// <returns>The XNA <see cref="NodeContent"/>.</returns>
  696. private NodeContent ImportBones(Node aiNode, Node aiParent, NodeContent parent)
  697. {
  698. Debug.Assert(aiNode != null);
  699. Debug.Assert(aiParent != null);
  700. NodeContent node = null;
  701. if (!aiNode.Name.Contains("_$AssimpFbx$")) // Ignore pivot nodes
  702. {
  703. const string mangling = "_$AssimpFbxNull$"; // Null leaf nodes are helpers
  704. if (aiNode.Name.Contains(mangling))
  705. {
  706. // Null leaf node
  707. node = new NodeContent
  708. {
  709. Name = aiNode.Name.Replace(mangling, string.Empty),
  710. Identity = _identity,
  711. Transform = ToXna(GetRelativeTransform(aiNode, aiParent))
  712. };
  713. }
  714. else if (_bones.Contains(aiNode))
  715. {
  716. // Bone
  717. node = new BoneContent
  718. {
  719. Name = aiNode.Name,
  720. Identity = _identity
  721. };
  722. // node.Transform is irrelevant for bones. This transform is just the
  723. // pose of the node at the time of the export. This could, for example,
  724. // be one of the animation frames. It is not necessarily the bind pose
  725. // (rest pose)!
  726. // In XNA BoneContent.Transform needs to be set to the relative bind pose
  727. // matrix. The relative bind pose matrix can be derived from the OffsetMatrix
  728. // which is stored in aiMesh.Bones.
  729. //
  730. // offsetMatrix ... Offset matrix. Transforms the mesh from local space to bone space in bind pose.
  731. // bindPoseRel ... Relative bind pose matrix. Defines the transform of a bone relative to its parent bone.
  732. // bindPoseAbs ... Absolute bind pose matrix. Defines the transform of a bone relative to world space.
  733. //
  734. // The offset matrix is the inverse of the absolute bind pose matrix.
  735. // offsetMatrix = inverse(bindPoseAbs)
  736. //
  737. // bindPoseAbs = bindPoseRel * parentBindPoseAbs
  738. // => bindPoseRel = bindPoseAbs * inverse(parentBindPoseAbs)
  739. // = inverse(offsetMatrix) * parentOffsetMatrix
  740. Matrix offsetMatrix;
  741. Matrix parentOffsetMatrix;
  742. bool isOffsetMatrixValid = _deformationBones.TryGetValue(aiNode.Name, out offsetMatrix);
  743. bool isParentOffsetMatrixValid = _deformationBones.TryGetValue(aiParent.Name, out parentOffsetMatrix);
  744. if (isOffsetMatrixValid && isParentOffsetMatrixValid)
  745. {
  746. node.Transform = Matrix.Invert(offsetMatrix) * parentOffsetMatrix;
  747. }
  748. else if (isOffsetMatrixValid && aiNode == _rootBone)
  749. {
  750. // The current bone is the first in the chain.
  751. // The parent offset matrix is missing. :(
  752. FbxPivot pivot;
  753. if (_pivots.TryGetValue(node.Name, out pivot))
  754. {
  755. // --> Use transformation pivot.
  756. node.Transform = pivot.GetTransform(null, null, null);
  757. }
  758. else
  759. {
  760. // --> Let's assume that parent's transform is Identity.
  761. node.Transform = Matrix.Invert(offsetMatrix);
  762. }
  763. }
  764. else if (isOffsetMatrixValid && aiParent == _rootBone)
  765. {
  766. // The current bone is the second bone in the chain.
  767. // The parent offset matrix is missing. :(
  768. // --> Derive matrix from parent bone, which is the root bone.
  769. parentOffsetMatrix = Matrix.Invert(parent.Transform);
  770. node.Transform = Matrix.Invert(offsetMatrix) * parentOffsetMatrix;
  771. }
  772. else
  773. {
  774. // Offset matrices are not provided by Assimp. :(
  775. // Let's hope that the skeleton was exported in bind pose.
  776. // (Otherwise we are just importing garbage.)
  777. node.Transform = ToXna(GetRelativeTransform(aiNode, aiParent));
  778. }
  779. }
  780. }
  781. if (node != null)
  782. {
  783. if (parent != null)
  784. parent.Children.Add(node);
  785. // For the children, this is the new parent.
  786. aiParent = aiNode;
  787. parent = node;
  788. }
  789. foreach (var child in aiNode.Children)
  790. ImportBones(child, aiParent, parent);
  791. return node;
  792. }
  793. /// <summary>
  794. /// Converts the specified animation to XNA.
  795. /// </summary>
  796. /// <param name="aiAnimation">The animation.</param>
  797. /// <param name="nodeName">An optional filter.</param>
  798. /// <returns>The animation converted to XNA.</returns>
  799. private AnimationContent ImportAnimation(Animation aiAnimation, string nodeName = null)
  800. {
  801. var animation = new AnimationContent
  802. {
  803. Name = GetAnimationName(aiAnimation.Name),
  804. Identity = _identity,
  805. Duration = TimeSpan.FromSeconds(aiAnimation.DurationInTicks / aiAnimation.TicksPerSecond)
  806. };
  807. // In Assimp animation channels may be split into separate channels.
  808. // "nodeXyz" --> "nodeXyz_$AssimpFbx$_Translation",
  809. // "nodeXyz_$AssimpFbx$_Rotation",
  810. // "nodeXyz_$AssimpFbx$_Scaling"
  811. // Group animation channels by name (strip the "_$AssimpFbx$" part).
  812. IEnumerable < IGrouping < string,NodeAnimationChannel >> channelGroups;
  813. if (nodeName != null)
  814. {
  815. channelGroups = aiAnimation.NodeAnimationChannels
  816. .Where(channel => nodeName == GetNodeName(channel.NodeName))
  817. .GroupBy(channel => GetNodeName(channel.NodeName));
  818. }
  819. else
  820. {
  821. channelGroups = aiAnimation.NodeAnimationChannels
  822. .GroupBy(channel => GetNodeName(channel.NodeName));
  823. }
  824. foreach (var channelGroup in channelGroups)
  825. {
  826. var boneName = channelGroup.Key;
  827. var channel = new AnimationChannel();
  828. // Get transformation pivot for current bone.
  829. FbxPivot pivot;
  830. if (!_pivots.TryGetValue(boneName, out pivot))
  831. pivot = FbxPivot.Default;
  832. var scaleKeys = EmptyVectorKeys;
  833. var rotationKeys = EmptyQuaternionKeys;
  834. var translationKeys = EmptyVectorKeys;
  835. foreach (var aiChannel in channelGroup)
  836. {
  837. if (aiChannel.NodeName.EndsWith("_$AssimpFbx$_Scaling"))
  838. {
  839. scaleKeys = aiChannel.ScalingKeys;
  840. Debug.Assert(pivot.Scaling.HasValue);
  841. 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))));
  842. Debug.Assert(!aiChannel.HasPositionKeys || (aiChannel.PositionKeyCount == 1 && aiChannel.PositionKeys[0].Value == new Vector3D(0, 0, 0)));
  843. }
  844. else if (aiChannel.NodeName.EndsWith("_$AssimpFbx$_Rotation"))
  845. {
  846. rotationKeys = aiChannel.RotationKeys;
  847. Debug.Assert(pivot.Rotation.HasValue);
  848. Debug.Assert(!aiChannel.HasScalingKeys || (aiChannel.ScalingKeyCount == 1 && aiChannel.ScalingKeys[0].Value == new Vector3D(1, 1, 1)));
  849. Debug.Assert(!aiChannel.HasPositionKeys || (aiChannel.PositionKeyCount == 1 && aiChannel.PositionKeys[0].Value == new Vector3D(0, 0, 0)));
  850. }
  851. else if (aiChannel.NodeName.EndsWith("_$AssimpFbx$_Translation"))
  852. {
  853. translationKeys = aiChannel.PositionKeys;
  854. Debug.Assert(pivot.Translation.HasValue);
  855. Debug.Assert(!aiChannel.HasScalingKeys || (aiChannel.ScalingKeyCount == 1 && aiChannel.ScalingKeys[0].Value == new Vector3D(1, 1, 1)));
  856. 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))));
  857. }
  858. else
  859. {
  860. scaleKeys = aiChannel.ScalingKeys;
  861. rotationKeys = aiChannel.RotationKeys;
  862. translationKeys = aiChannel.PositionKeys;
  863. }
  864. }
  865. // Get all unique keyframe times. (Assuming that no two key frames
  866. // have the same time, which is usually a safe assumption.)
  867. var times = scaleKeys.Select(k => k.Time)
  868. .Union(rotationKeys.Select(k => k.Time))
  869. .Union(translationKeys.Select(k => k.Time))
  870. .OrderBy(t => t)
  871. .ToList();
  872. Debug.Assert(times.Count == times.Distinct().Count(), "Sequences combined with Union() should not have duplicates.");
  873. int prevScaleIndex = -1;
  874. int prevRotationIndex = -1;
  875. int prevTranslationIndex = -1;
  876. double prevScaleTime = 0.0;
  877. double prevRotationTime = 0.0;
  878. double prevTranslationTime = 0.0;
  879. Vector3? prevScale = null;
  880. Quaternion? prevRotation = null;
  881. Vector3? prevTranslation = null;
  882. foreach (var time in times)
  883. {
  884. // Get scaling.
  885. Vector3? scale;
  886. int scaleIndex = scaleKeys.FindIndex(k => k.Time == time);
  887. if (scaleIndex != -1)
  888. {
  889. // Scaling key found.
  890. scale = ToXna(scaleKeys[scaleIndex].Value);
  891. prevScaleIndex = scaleIndex;
  892. prevScaleTime = time;
  893. prevScale = scale;
  894. }
  895. else
  896. {
  897. // No scaling key found.
  898. if (prevScaleIndex != -1 && prevScaleIndex + 1 < scaleKeys.Count)
  899. {
  900. // Lerp between previous and next scaling key.
  901. var nextScaleKey = scaleKeys[prevScaleIndex + 1];
  902. var nextScaleTime = nextScaleKey.Time;
  903. var nextScale = ToXna(nextScaleKey.Value);
  904. var amount = (float)((time - prevScaleTime) / (nextScaleTime - prevScaleTime));
  905. scale = Vector3.Lerp(prevScale.Value, nextScale, amount);
  906. }
  907. else
  908. {
  909. // Hold previous scaling value.
  910. scale = prevScale;
  911. }
  912. }
  913. // Get rotation.
  914. Quaternion? rotation;
  915. int rotationIndex = rotationKeys.FindIndex(k => k.Time == time);
  916. if (rotationIndex != -1)
  917. {
  918. // Rotation key found.
  919. rotation = ToXna(rotationKeys[rotationIndex].Value);
  920. prevRotationIndex = rotationIndex;
  921. prevRotationTime = time;
  922. prevRotation = rotation;
  923. }
  924. else
  925. {
  926. // No rotation key found.
  927. if (prevRotationIndex != -1 && prevRotationIndex + 1 < rotationKeys.Count)
  928. {
  929. // Lerp between previous and next rotation key.
  930. var nextRotationKey = rotationKeys[prevRotationIndex + 1];
  931. var nextRotationTime = nextRotationKey.Time;
  932. var nextRotation = ToXna(nextRotationKey.Value);
  933. var amount = (float)((time - prevRotationTime) / (nextRotationTime - prevRotationTime));
  934. rotation = Quaternion.Slerp(prevRotation.Value, nextRotation, amount);
  935. }
  936. else
  937. {
  938. // Hold previous rotation value.
  939. rotation = prevRotation;
  940. }
  941. }
  942. // Get translation.
  943. Vector3? translation;
  944. int translationIndex = translationKeys.FindIndex(k => k.Time == time);
  945. if (translationIndex != -1)
  946. {
  947. // Translation key found.
  948. translation = ToXna(translationKeys[translationIndex].Value);
  949. prevTranslationIndex = translationIndex;
  950. prevTranslationTime = time;
  951. prevTranslation = translation;