public override IOModel ImportFromFile(string filename) { string path = Path.GetDirectoryName(filename); // Use Assimp.NET for importing model AssimpContext context = new AssimpContext(); context.SetConfig(new NormalSmoothingAngleConfig(90.0f)); Scene scene = context.ImportFile(filename, PostProcessPreset.TargetRealTimeMaximumQuality); var newModel = new IOModel(); var textures = new Dictionary <int, Texture>(); if (_settings.ProcessGeometry) { var tmpList = new List <IOMesh>(); // Create the list of materials to load for (int i = 0; i < scene.Materials.Count; i++) { var mat = scene.Materials[i]; var material = new IOMaterial(mat.HasName ? mat.Name : "Material_" + i); var diffusePath = mat.HasTextureDiffuse ? mat.TextureDiffuse.FilePath : null; if (string.IsNullOrWhiteSpace(diffusePath)) { continue; } // Don't add materials with missing textures var texture = GetTexture(path, diffusePath); if (texture == null) { logger.Warn("Texture for material " + mat.Name + " is missing. Meshes referencing this material won't be imported."); continue; } else { textures.Add(i, texture); } // Create the new material material.Texture = textures[i]; material.AdditiveBlending = (mat.HasBlendMode && mat.BlendMode == global::Assimp.BlendMode.Additive) || mat.Opacity < 1.0f; material.DoubleSided = mat.HasTwoSided && mat.IsTwoSided; material.Shininess = mat.HasShininess ? (int)mat.Shininess : 0; newModel.Materials.Add(material); } var lastBaseVertex = 0; // Loop for each mesh loaded in scene foreach (var mesh in scene.Meshes) { // Discard nullmeshes if (!mesh.HasFaces || !mesh.HasVertices || mesh.VertexCount < 3 || mesh.TextureCoordinateChannelCount == 0 || !mesh.HasTextureCoords(0)) { logger.Warn("Mesh \"" + (mesh.Name ?? "") + "\" has no faces, no texture coordinates or wrong vertex count."); continue; } // Import only textured meshes with valid materials Texture faceTexture; if (!textures.TryGetValue(mesh.MaterialIndex, out faceTexture)) { logger.Warn("Mesh \"" + (mesh.Name ?? "") + "\" does have material index " + mesh.MaterialIndex + " which is unsupported or can't be found."); continue; } // Make sure we have appropriate material in list. If not, skip mesh and warn user. var material = newModel.Materials.FirstOrDefault(mat => mat.Name.Equals(scene.Materials[mesh.MaterialIndex].Name)); if (material == null) { logger.Warn("Can't find material with specified index (" + mesh.MaterialIndex + "). Probably you're missing textures or using non-diffuse materials only for this mesh."); continue; } // Assimp's mesh is our IOSubmesh so we import meshes with just one submesh var newMesh = new IOMesh(mesh.Name); var newSubmesh = new IOSubmesh(material); newMesh.Submeshes.Add(material, newSubmesh); bool hasColors = _settings.UseVertexColor && mesh.VertexColorChannelCount > 0 && mesh.HasVertexColors(0); bool hasNormals = mesh.HasNormals; // Additional integrity checks if ((mesh.VertexCount != mesh.TextureCoordinateChannels[0].Count) || (hasColors && mesh.VertexCount != mesh.VertexColorChannels[0].Count) || (hasNormals && mesh.VertexCount != mesh.Normals.Count)) { logger.Warn("Mesh \"" + (mesh.Name ?? "") + "\" data structure is inconsistent."); continue; } // Source data var positions = mesh.Vertices; var normals = mesh.Normals; var texCoords = mesh.TextureCoordinateChannels[0]; var colors = mesh.VertexColorChannels[0]; for (int i = 0; i < mesh.VertexCount; i++) { // Create position var position = new Vector3(positions[i].X, positions[i].Y, positions[i].Z); position = ApplyAxesTransforms(position); newMesh.Positions.Add(position); // Create normal if (hasNormals) { var normal = new Vector3(normals[i].X, normals[i].Y, normals[i].Z); normal = ApplyAxesTransforms(normal); newMesh.Normals.Add(normal); } else { newMesh.CalculateNormals(); } // Create UV var currentUV = new Vector2(texCoords[i].X, texCoords[i].Y); if (faceTexture != null) { currentUV = ApplyUVTransform(currentUV, faceTexture.Image.Width, faceTexture.Image.Height); } newMesh.UV.Add(currentUV); // Create colors if (hasColors) { var color = ApplyColorTransform(new Vector4(colors[i].R, colors[i].G, colors[i].B, colors[i].A)); newMesh.Colors.Add(color); } } // Add polygons foreach (var face in mesh.Faces) { if (face.IndexCount == 3) { var poly = new IOPolygon(IOPolygonShape.Triangle); poly.Indices.Add(lastBaseVertex + face.Indices[0]); poly.Indices.Add(lastBaseVertex + face.Indices[1]); poly.Indices.Add(lastBaseVertex + face.Indices[2]); if (_settings.InvertFaces) { poly.Indices.Reverse(); } newSubmesh.Polygons.Add(poly); } else if (face.IndexCount == 4) { var poly = new IOPolygon(IOPolygonShape.Quad); poly.Indices.Add(lastBaseVertex + face.Indices[0]); poly.Indices.Add(lastBaseVertex + face.Indices[1]); poly.Indices.Add(lastBaseVertex + face.Indices[2]); poly.Indices.Add(lastBaseVertex + face.Indices[3]); if (_settings.InvertFaces) { poly.Indices.Reverse(); } newSubmesh.Polygons.Add(poly); } } tmpList.Add(newMesh); } // Sort meshes by name, if specified if (_settings.SortByName) { tmpList = tmpList.OrderBy(m => m.Name, new CustomComparer <string>(NaturalComparer.Do)).ToList(); } foreach (var mesh in tmpList) { newModel.Meshes.Add(mesh); } } if (_settings.ProcessAnimations && scene.HasAnimations && scene.AnimationCount > 0) { // Find all mesh nodes to count against animation nodes var meshNameList = CollectMeshNodeNames(scene.RootNode); // Sort animations by name, if specified if (_settings.SortByName) { meshNameList = meshNameList.OrderBy(s => s, new CustomComparer <string>(NaturalComparer.Do)).ToList(); } // Loop through all animations and add appropriate ones. // Integrity check: there should be meshes and mesh count should be equal to unique mesh name count. if (scene.MeshCount <= 0 || scene.MeshCount != meshNameList.Count) { logger.Warn("Actual number of meshes doesn't correspond to mesh list. Animations won't be imported."); } else { for (int i = 0; i < scene.AnimationCount; i++) { var anim = scene.Animations[i]; // Integrity check: support only time-based node animations if (!anim.HasNodeAnimations || anim.DurationInTicks <= 0) { logger.Warn("Anim " + i + " isn't a valid type of animation for TR formats."); continue; } // Guess possible maximum frame and time var frameCount = 0; double maximumTime = 0; foreach (var node in anim.NodeAnimationChannels) { if (node.HasPositionKeys) { var maxNodeTime = node.PositionKeys.Max(key => key.Time); maximumTime = maximumTime >= maxNodeTime ? maximumTime : maxNodeTime; frameCount = frameCount >= node.PositionKeyCount ? frameCount : node.PositionKeyCount; } if (node.HasRotationKeys) { var maxNodeTime = node.RotationKeys.Max(key => key.Time); maximumTime = maximumTime >= maxNodeTime ? maximumTime : maxNodeTime; frameCount = frameCount >= node.RotationKeyCount ? frameCount : node.RotationKeyCount; } } // Calculate time multiplier var timeMult = (double)(frameCount - 1) / anim.DurationInTicks; // Integrity check: maximum frame time shouldn't excess duration if (timeMult * maximumTime >= frameCount) { logger.Warn("Anim " + i + " has frames outside of time limits and won't be imported."); continue; } IOAnimation ioAnim = new IOAnimation(string.IsNullOrEmpty(anim.Name) ? "Imported animation " + i : anim.Name, scene.MeshCount); // Precreate frames and set them to identity for (int j = 0; j < frameCount; j++) { ioAnim.Frames.Add(new IOFrame()); } // Precreate rotations and set them to identity // I am using generic foreach here instead of linq foreach because for some reason it // returns wrong amount of angles during enumeration with Enumerable.Repeat. foreach (var frame in ioAnim.Frames) { var angleList = Enumerable.Repeat(Vector3.Zero, scene.MeshCount); frame.Angles.AddRange(angleList); } // Search through all nodes and put data into corresponding frames. // It's not clear what should we do in case if multiple nodes refer to same mesh, but sometimes // it happens, e. g. in case of fbx format. In this case, we'll just add to existing values for now. foreach (var chan in anim.NodeAnimationChannels) { // Look if this channel belongs to any mesh in list. // If so, attribute it to appropriate frame. var chanIndex = meshNameList.IndexOf(item => chan.NodeName.Contains(item)); // Integrity check: no appropriate mesh found if (chanIndex < 0) { logger.Warn("Anim " + i + " channel " + chan.NodeName + " has no corresponding mesh in meshtree and will be ignored"); continue; } // Apply translation only if found channel belongs to root mesh. if (chanIndex == 0 && chan.HasPositionKeys && chan.PositionKeyCount > 0) { foreach (var key in chan.PositionKeys) { // Integrity check: frame shouldn't fall out of keyframe array bounds. var frameIndex = (int)Math.Round(key.Time * timeMult, MidpointRounding.AwayFromZero); if (frameIndex >= frameCount) { logger.Warn("Anim " + i + " channel " + chan.NodeName + " has a key outside of time limits and will be ignored."); continue; } float rX = key.Value.X; float rY = key.Value.Y; float rZ = key.Value.Z; if (_settings.SwapXY) { var temp = rX; rX = rY; rY = temp; } if (_settings.SwapXZ) { var temp = rX; rX = rZ; rZ = temp; } if (_settings.SwapYZ) { var temp = rY; rY = rZ; rZ = temp; } if (_settings.FlipX) { rX = -rX; } if (_settings.FlipY) { rY = -rY; } if (_settings.FlipZ) { rZ = -rZ; } ioAnim.Frames[frameIndex].Offset += new Vector3(rX, rY, rZ); } } if (chan.HasRotationKeys && chan.RotationKeyCount > 0) { foreach (var key in chan.RotationKeys) { // Integrity check: frame shouldn't fall out of keyframe array bounds. var frameIndex = (int)Math.Round(key.Time * timeMult, MidpointRounding.AwayFromZero); if (frameIndex >= frameCount) { logger.Warn("Anim " + i + " channel " + chan.NodeName + " has a key outside of time limits and will be ignored."); continue; } // Convert quaternions back to rotations. // This is similar to TRViewer's conversion routine. var quatI = System.Numerics.Quaternion.Identity; var quat = new System.Numerics.Quaternion(key.Value.X, key.Value.Z, key.Value.Y, -key.Value.W); quatI *= quat; var eulers = MathC.QuaternionToEuler(quatI); var rotation = new Vector3(eulers.X * 180.0f / (float)Math.PI, eulers.Y * 180.0f / (float)Math.PI, eulers.Z * 180.0f / (float)Math.PI); ioAnim.Frames[frameIndex].Angles[chanIndex] += MathC.NormalizeAngle(rotation); } } } newModel.Animations.Add(ioAnim); } } } return(newModel); }