private static void MergeAnimation(string animationFilePath, AnimationContentDictionary animationDictionary, ContentIdentity contentIdentity, ContentProcessorContext context) { // Use content pipeline to build the asset. NodeContent mergeModel = context.BuildAndLoadAsset<NodeContent, NodeContent>(new ExternalReference<NodeContent>(animationFilePath), null); // Find the skeleton. BoneContent mergeRoot = MeshHelper.FindSkeleton(mergeModel); if (mergeRoot == null) { context.Logger.LogWarning(null, contentIdentity, "Animation model file '{0}' has no root bone. Cannot merge animations.", animationFilePath); return; } // Merge all animations of the skeleton root node. foreach (string animationName in mergeRoot.Animations.Keys) { if (animationDictionary.ContainsKey(animationName)) { context.Logger.LogWarning(null, contentIdentity, "Replacing animation '{0}' from '{1}' with merged animation.", animationName, animationFilePath); animationDictionary[animationName] = mergeRoot.Animations[animationName]; } else { context.Logger.LogImportantMessage("Merging animation '{0}' from '{1}'.", animationName, animationFilePath); animationDictionary.Add(animationName, mergeRoot.Animations[animationName]); } } }
protected FragmentContent LoadFragmentContent(ContentProcessorContext context, string fileName, ContentIdentity relativeToContent = null) { ExternalReference<FragmentContent> externalReference = (relativeToContent != null) ? new ExternalReference<FragmentContent>(fileName, relativeToContent) : new ExternalReference<FragmentContent>(fileName); return context.BuildAndLoadAsset<FragmentContent, FragmentContent>(externalReference, null); }
/// <summary> /// Initializes a new instance of the InvalidContentException class with the specified error message and the identity of the content throwing the exception. /// </summary> /// <param name="message">A message that describes the error.</param> /// <param name="contentIdentity">Information about the content item that caused this error, including the file name. In some cases, a location within the file (of the problem) is specified.</param> public InvalidContentException( string message, ContentIdentity contentIdentity ) : this(message, contentIdentity, null) { }
/// <summary> /// Initializes a new instance of the InvalidContentException class with the specified error message and the identity of the content throwing the exception. /// </summary> /// <param name="message">A message that describes the error.</param> /// <param name="contentIdentity">Information about the content item that caused this error, including the file name. In some cases, a location within the file (of the problem) is specified.</param> public InvalidContentException( string message, ContentIdentity contentIdentity ) : this(message, contentIdentity, null) { }
public override void LogWarning(string helpLink, ContentIdentity contentIdentity, string message, params object[] messageArgs) { Debug.LogWarning(message, messageArgs); Debug.LogWarning(contentIdentity.SourceFilename); Debug.LogWarning(contentIdentity.SourceTool); Debug.LogWarning(contentIdentity.FragmentIdentifier); Debug.LogWarning(helpLink); }
/// <summary> /// Initializes a new instance of the InvalidContentException class with the specified error message, the identity of the content throwing the exception, and a reference to the inner exception that is the cause of this exception. /// </summary> /// <param name="message">A message that describes the error.</param> /// <param name="contentIdentity">Information about the content item that caused this error, including the file name. In some cases, a location within the file (of the problem) is specified.</param> /// <param name="innerException">The exception that is the cause of the current exception. If innerException is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param> public InvalidContentException( string message, ContentIdentity contentIdentity, Exception innerException ) : base(message, innerException) { ContentIdentity = contentIdentity; }
/// <summary> /// Initializes a new instance of the InvalidContentException class with the specified error message, the identity of the content throwing the exception, and a reference to the inner exception that is the cause of this exception. /// </summary> /// <param name="message">A message that describes the error.</param> /// <param name="contentIdentity">Information about the content item that caused this error, including the file name. In some cases, a location within the file (of the problem) is specified.</param> /// <param name="innerException">The exception that is the cause of the current exception. If innerException is not a null reference, the current exception is raised in a catch block that handles the inner exception.</param> public InvalidContentException( string message, ContentIdentity contentIdentity, Exception innerException ) : base(message, innerException) { ContentIdentity = contentIdentity; }
private static List <MaterialContent> ImportMaterials(ContentIdentity identity, Scene scene) { var materials = new List <MaterialContent>(); foreach (var sceneMaterial in scene.Materials) { var material = new BasicMaterialContent { Name = sceneMaterial.Name, Identity = identity, }; if (sceneMaterial.HasTextureDiffuse) { var texture = new ExternalReference <TextureContent>(sceneMaterial.TextureDiffuse.FilePath, identity); texture.OpaqueData.Add("TextureCoordinate", string.Format("TextureCoordinate{0}", sceneMaterial.TextureDiffuse.UVIndex)); material.Textures.Add("Texture", texture); } if (sceneMaterial.HasTextureOpacity) { var texture = new ExternalReference <TextureContent>(sceneMaterial.TextureOpacity.FilePath, identity); texture.OpaqueData.Add("TextureCoordinate", string.Format("TextureCoordinate{0}", sceneMaterial.TextureOpacity.UVIndex)); material.Textures.Add("Transparency", texture); } if (sceneMaterial.HasColorDiffuse) { material.DiffuseColor = ToXna(sceneMaterial.ColorDiffuse); } if (sceneMaterial.HasColorEmissive) { material.EmissiveColor = ToXna(sceneMaterial.ColorEmissive); } if (sceneMaterial.HasOpacity) { material.Alpha = sceneMaterial.Opacity; } if (sceneMaterial.HasColorSpecular) { material.SpecularColor = ToXna(sceneMaterial.ColorSpecular); } if (sceneMaterial.HasShininessStrength) { material.SpecularPower = sceneMaterial.ShininessStrength; } materials.Add(material); } return(materials); }
/// <summary> /// Gets the filename currently being processed, for use in warning and error messages. /// </summary> /// <param name="contentIdentity">Identity of a content item. If specified, GetCurrentFilename uses this value to refine the search. If no value is specified, the current PushFile state is used.</param> /// <returns>Name of the file being processed.</returns> protected string GetCurrentFilename( ContentIdentity contentIdentity ) { if ((contentIdentity != null) && !string.IsNullOrEmpty(contentIdentity.SourceFilename)) return GetRelativePath(contentIdentity.SourceFilename, LoggerRootDirectory); if (filenames.Count > 0) return GetRelativePath(filenames.Peek(), LoggerRootDirectory); return null; }
public override NodeContent Import(string filename, ContentImporterContext context) { var identity = new ContentIdentity(filename, GetType().Name); using (var importer = new AssimpContext()) { _scene = importer.ImportFile(filename, //PostProcessSteps.FindInstances | // No effect + slow? PostProcessSteps.FindInvalidData | PostProcessSteps.FlipUVs | PostProcessSteps.FlipWindingOrder | //PostProcessSteps.MakeLeftHanded | // Appears to just mess things up PostProcessSteps.JoinIdenticalVertices | PostProcessSteps.ImproveCacheLocality | PostProcessSteps.OptimizeMeshes | //PostProcessSteps.OptimizeGraph | // Will eliminate helper nodes PostProcessSteps.RemoveRedundantMaterials | PostProcessSteps.Triangulate ); _globalInverseXform = _scene.RootNode.Transform; _globalInverseXform.Inverse(); _rootNode = new NodeContent { Name = _scene.RootNode.Name, Identity = identity, Transform = ToXna(_scene.RootNode.Transform) }; _materials = ImportMaterials(identity, _scene); FindMeshes(_scene.RootNode, _scene.RootNode.Transform); if (_scene.HasAnimations) { var skeleton = CreateSkeleton(); CreateAnimation(skeleton); } // 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); }
internal static Exception CreateInvalidContentException(this IntermediateReader reader, string message, params object[] messageArgs) { ContentIdentity contentIdentity = new ContentIdentity(); contentIdentity.SourceFilename = reader.Xml.BaseURI; IXmlLineInfo xml = reader.Xml as IXmlLineInfo; if (xml != null) { contentIdentity.FragmentIdentifier = string.Format(CultureInfo.InvariantCulture, "{0},{1}", new object[] { xml.LineNumber, xml.LinePosition }); } return new InvalidContentException(string.Format(CultureInfo.CurrentCulture, message, messageArgs), contentIdentity); }
public override void LogWarning(string helpLink, Microsoft.Xna.Framework.Content.Pipeline.ContentIdentity contentIdentity, string message, params object[] messageArgs) { var msg = string.Format(message, messageArgs); var fileName = GetCurrentFilename(contentIdentity); monitor.Log.WriteLine(string.Format("{0}: {1}", fileName, msg)); if (result != null) { result.AddWarning(string.Format("{0}: {1}", fileName, msg)); } }
protected string GetCurrentFilename(ContentIdentity contentIdentity) { if ((contentIdentity != null) && !string.IsNullOrEmpty(contentIdentity.SourceFilename)) { return(GetFilename(contentIdentity.SourceFilename, loggerRootDirectory)); } if (this.fileStack.Count > 0) { return(GetFilename(fileStack.Peek(), loggerRootDirectory)); } return(null); }
public override void LogWarning(string helpLink, ContentIdentity contentIdentity, string message, params object[] messageArgs) { var warning = string.Empty; if (contentIdentity != null && !string.IsNullOrEmpty(contentIdentity.SourceFilename)) { warning = contentIdentity.SourceFilename; if (!string.IsNullOrEmpty(contentIdentity.FragmentIdentifier)) warning += "(" + contentIdentity.FragmentIdentifier + ")"; warning += ": "; } warning += string.Format(message, messageArgs); Console.WriteLine(warning); }
/// <summary> /// The main Process method converts an intermediate format content pipeline /// NodeContent tree to a ModelContent object with embedded animation data. /// </summary> public override ModelContent Process(NodeContent input, ContentProcessorContext context) { rootIdentity = input.Identity; ValidateMesh(input, context, null); // Find the skeleton. BoneContent skeleton = MeshHelper.FindSkeleton(input); if (skeleton == null) throw new InvalidContentException("Input skeleton not found."); // We don't want to have to worry about different parts of the model being // in different local coordinate systems, so let's just bake everything. FlattenTransforms(input, skeleton); // Read the bind pose and skeleton hierarchy data. IList<BoneContent> bones = MeshHelper.FlattenSkeleton(skeleton); if (bones.Count > SkinnedEffect.MaxBones) { throw new InvalidContentException(string.Format( "Skeleton has {0} bones, but the maximum supported is {1}.", bones.Count, SkinnedEffect.MaxBones)); } List<Matrix> bindPose = new List<Matrix>(); List<Matrix> inverseBindPose = new List<Matrix>(); List<int> skeletonHierarchy = new List<int>(); foreach (BoneContent bone in bones) { bindPose.Add(bone.Transform); inverseBindPose.Add(Matrix.Invert(bone.AbsoluteTransform)); skeletonHierarchy.Add(bones.IndexOf(bone.Parent as BoneContent)); } // Convert animation data to our runtime format. Dictionary<string, AnimationClip> animationClips; animationClips = ProcessAnimations(skeleton.Animations, bones); // Chain to the base ModelProcessor class so it can convert the model data. ModelContent model = base.Process(input, context); // Store our custom animation data in the Tag property of the model. model.Tag = new SkinningData(animationClips, bindPose, inverseBindPose, skeletonHierarchy); return model; }
/// <summary> /// Converts a DigitalRune <see cref="Texture"/> to an XNA <see cref="TextureContent"/>. /// </summary> /// <param name="texture">The <see cref="Texture"/>.</param> /// <param name="identity">The content identity.</param> /// <returns>The <see cref="TextureContent"/>.</returns> public static TextureContent ToContent(Texture texture, ContentIdentity identity) { var description = texture.Description; switch (description.Dimension) { case TextureDimension.Texture1D: case TextureDimension.Texture2D: { var textureContent = new Texture2DContent { Identity = identity }; for (int mipIndex = 0; mipIndex < description.MipLevels; mipIndex++) { var image = texture.Images[texture.GetImageIndex(mipIndex, 0, 0)]; textureContent.Mipmaps.Add(ToContent(image)); } return textureContent; } case TextureDimension.TextureCube: { var textureContent = new TextureCubeContent { Identity = identity }; for (int faceIndex = 0; faceIndex < 6; faceIndex++) { for (int mipIndex = 0; mipIndex < description.MipLevels; mipIndex++) { var image = texture.Images[texture.GetImageIndex(mipIndex, faceIndex, 0)]; textureContent.Faces[faceIndex].Add(ToContent(image)); } } return textureContent; } case TextureDimension.Texture3D: { var textureContent = new Texture3DContent { Identity = identity }; for (int zIndex = 0; zIndex < description.Depth; zIndex++) { textureContent.Faces.Add(new MipmapChain()); for (int mipIndex = 0; mipIndex < description.MipLevels; mipIndex++) { var image = texture.Images[texture.GetImageIndex(mipIndex, 0, zIndex)]; textureContent.Faces[zIndex].Add(ToContent(image)); } } return textureContent; } } throw new InvalidOperationException("Invalid texture dimension."); }
public static void Split(AnimationContentDictionary animationDictionary, string splitFile, ContentIdentity contentIdentity, ContentProcessorContext context) { if (animationDictionary == null) return; if (string.IsNullOrEmpty(splitFile)) return; if (contentIdentity == null) throw new ArgumentNullException("contentIdentity"); if (context == null) throw new ArgumentNullException("context"); if (animationDictionary.Count == 0) { context.Logger.LogWarning(null, contentIdentity, "The model does not have an animation. Animation splitting is skipped."); return; } if (animationDictionary.Count > 1) context.Logger.LogWarning(null, contentIdentity, "The model contains more than 1 animation. The animation splitting is performed on the first animation. Other animations are deleted!"); // Load XML file. splitFile = ContentHelper.FindFile(splitFile, contentIdentity); XDocument document = XDocument.Load(splitFile, LoadOptions.SetLineInfo); // Let the content pipeline know that we depend on this file and we need to // rebuild the content if the file is modified. context.AddDependency(splitFile); // Parse XML. var animationsElement = document.Element("Animations"); if (animationsElement == null) { context.Logger.LogWarning(null, contentIdentity, "The animation split file \"{0}\" does not contain an <Animations> root node.", splitFile); return; } var wrappedContext = new ContentPipelineContext(context); var splits = ParseAnimationSplitDefinitions(animationsElement, contentIdentity, wrappedContext); if (splits == null || splits.Count == 0) { context.Logger.LogWarning(null, contentIdentity, "The XML file with the animation split definitions is invalid or empty. Animation is not split."); return; } // Split animations. Split(animationDictionary, splits, contentIdentity, context); }
/// <summary> /// Gets the filename currently being processed, for use in warning and error messages. /// </summary> /// <param name="contentIdentity">Identity of a content item. If specified, GetCurrentFilename uses this value to refine the search. If no value is specified, the current PushFile state is used.</param> /// <returns>Name of the file being processed.</returns> protected string GetCurrentFilename( ContentIdentity contentIdentity ) { if ((contentIdentity != null) && !string.IsNullOrEmpty(contentIdentity.SourceFilename)) { return(GetRelativePath(contentIdentity.SourceFilename, LoggerRootDirectory)); } if (filenames.Count > 0) { return(GetRelativePath(filenames.Peek(), LoggerRootDirectory)); } return(null); }
/// <summary> /// Override the Process method to store the ContentIdentity of the model root node. /// </summary> public override ModelContent Process(NodeContent input, ContentProcessorContext context) { rootIdentity = input.Identity; string modelName = input.Identity.SourceFilename.Substring(input.Identity.SourceFilename.LastIndexOf("\\") + 1); this.context = context; Dictionary<string, object> ModelData = new Dictionary<string, object>(); ModelContent baseModel = base.Process(input, context); GenerateData(input); ModelData.Add("BBox", boxs); ModelData.Add("BSphere", spheres); baseModel.Tag = ModelData; return baseModel; }
/// <summary> /// Merges the specified animation files to the specified animation dictionary. /// </summary> /// <param name="animationFiles"> /// The animation files as a string separated by semicolon (relative to the folder of the model /// file). For example: "run.fbx;jump.fbx;turn.fbx". /// </param> /// <param name="animationDictionary">The animation dictionary.</param> /// <param name="contentIdentity">The content identity.</param> /// <param name="context">The content processor context.</param> public static void Merge(string animationFiles, AnimationContentDictionary animationDictionary, ContentIdentity contentIdentity, ContentProcessorContext context) { if (string.IsNullOrEmpty(animationFiles)) return; // Get path of the model file. var files = animationFiles.Split(';', ',') .Select(s => s.Trim()) .Where(s => !string.IsNullOrEmpty(s)); foreach (string file in files) { MergeAnimation(file, animationDictionary, contentIdentity, context); } }
/// <summary> /// Initializes a new instance of ExternalReference, specifying the file path relative to another content item. /// </summary> /// <param name="filename">The name of the referenced file.</param> /// <param name="relativeToContent">The content that the path specified in filename is relative to.</param> public ExternalReference(string filename, ContentIdentity relativeToContent) { if (string.IsNullOrEmpty(filename)) { throw new ArgumentNullException("filename"); } if (relativeToContent == null) { throw new ArgumentNullException("relativeToContent"); } if (string.IsNullOrEmpty(relativeToContent.SourceFilename)) { throw new ArgumentNullException("relativeToContent.SourceFilename"); } Filename = Path.Combine(relativeToContent.SourceFilename, filename); }
public override NodeContent Import(string filename, ContentImporterContext context) { var identity = new ContentIdentity(filename, GetType().Name); using (var importer = new AssimpContext()) { _scene = importer.ImportFile(filename, //PostProcessSteps.FindInstances | // No effect + slow? PostProcessSteps.FindInvalidData | PostProcessSteps.FlipUVs | PostProcessSteps.FlipWindingOrder | //PostProcessSteps.MakeLeftHanded | // Appears to just mess things up PostProcessSteps.JoinIdenticalVertices | PostProcessSteps.ImproveCacheLocality | PostProcessSteps.OptimizeMeshes | //PostProcessSteps.OptimizeGraph | // Will eliminate helper nodes PostProcessSteps.RemoveRedundantMaterials | PostProcessSteps.Triangulate ); _globalInverseXform = _scene.RootNode.Transform; _globalInverseXform.Inverse(); _rootNode = new NodeContent { Name = _scene.RootNode.Name, Identity = identity, Transform = ToXna(_scene.RootNode.Transform) }; _materials = ImportMaterials(identity, _scene); FindMeshes(_scene.RootNode, _scene.RootNode.Transform); if (_scene.HasAnimations) { var skeleton = CreateSkeleton(); CreateAnimation(skeleton); } _scene.Clear(); } return(_rootNode); }
/// <summary> /// Merges the specified animation files to the specified animation dictionary. /// </summary> /// <param name="animationFiles"> /// The animation files as a string separated by semicolon (relative to the folder of the model /// file). For example: "run.fbx;jump.fbx;turn.fbx". /// </param> /// <param name="animationDictionary">The animation dictionary.</param> /// <param name="contentIdentity">The content identity.</param> /// <param name="context">The content processor context.</param> public static void Merge(string animationFiles, AnimationContentDictionary animationDictionary, ContentIdentity contentIdentity, ContentProcessorContext context) { if (string.IsNullOrEmpty(animationFiles)) return; // Get path of the model file. string sourcePath = Path.GetDirectoryName(contentIdentity.SourceFilename); var files = animationFiles.Split(';') .Select(s => s.Trim()) .Where(s => !string.IsNullOrEmpty(s)); foreach (string file in files) { string filePath = Path.GetFullPath(Path.Combine(sourcePath, file)); MergeAnimation(filePath, animationDictionary, contentIdentity, context); } }
public ExternalReference(string filename, ContentIdentity relativeToContent) { if (filename != null) { if (filename.Length == 0) { throw new ArgumentNullException("filename"); } if (relativeToContent == null) { throw new ArgumentNullException("relativeToContent"); } if (string.IsNullOrEmpty(relativeToContent.SourceFilename)) { throw new ArgumentNullException("relativeToContent.SourceFilename"); } this.filename = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(relativeToContent.SourceFilename), filename)); } }
/// <summary> /// Called by the XNA Framework when importing an texture file to be used as a game asset. This /// is the method called by the XNA Framework when an asset is to be imported into an object /// that can be recognized by the Content Pipeline. /// </summary> /// <param name="filename">Name of a game asset file.</param> /// <param name="context"> /// Contains information for importing a game asset, such as a logger interface. /// </param> /// <returns>Resulting game asset.</returns> public override TextureContent Import(string filename, ContentImporterContext context) { string extension = Path.GetExtension(filename); if (extension != null) { Texture texture = null; if (extension.Equals(".DDS", StringComparison.OrdinalIgnoreCase)) { using (var stream = File.OpenRead(filename)) texture = DdsHelper.Load(stream, DdsFlags.ForceRgb | DdsFlags.ExpandLuminance); } else if (extension.Equals(".TGA", StringComparison.OrdinalIgnoreCase)) { using (var stream = File.OpenRead(filename)) texture = TgaHelper.Load(stream); } if (texture != null) { #if !MONOGAME // When using the XNA content pipeline, check for MonoGame content. if (!string.IsNullOrEmpty(ContentHelper.GetMonoGamePlatform())) #endif { // These formats are not (yet) available in MonoGame. switch (texture.Description.Format) { case DataFormat.B5G5R5A1_UNORM: // (16-bit TGA files.) case DataFormat.R8_UNORM: case DataFormat.A8_UNORM: texture = texture.ConvertTo(DataFormat.R8G8B8A8_UNORM); break; } } // Convert DigitalRune Texture to XNA TextureContent. var identity = new ContentIdentity(filename, "DigitalRune"); return TextureHelper.ToContent(texture, identity); } } return base.Import(filename, context); }
/// <summary> /// Initializes a new instance of ExternalReference, specifying the file path relative to another content item. /// </summary> /// <param name="filename">The name of the referenced file.</param> /// <param name="relativeToContent">The content that the path specified in filename is relative to.</param> public ExternalReference(string filename, ContentIdentity relativeToContent) { if (string.IsNullOrEmpty(filename)) { throw new ArgumentNullException("filename"); } if (relativeToContent == null) { throw new ArgumentNullException("relativeToContent"); } if (string.IsNullOrEmpty(relativeToContent.SourceFilename)) { throw new ArgumentNullException("relativeToContent.SourceFilename"); } // The intermediate serializer from XNA has the external reference // path walking up to the content project directory and then back // down to the asset path. We don't appear to have any way to do // that from here, so we'll work with the absolute path and let the // higher level process sort out any relative paths they need. var basePath = Path.GetDirectoryName(relativeToContent.SourceFilename); Filename = PathHelper.NormalizeOS(Path.GetFullPath(Path.Combine(basePath, filename))); }
public override void LogWarning(string helpLink, ContentIdentity contentIdentity, string message, params object[] messageArgs) { var warning = new StringBuilder(); if (!string.IsNullOrEmpty(contentIdentity?.SourceFilename)) { warning.Append(contentIdentity.SourceFilename); if (!string.IsNullOrEmpty(contentIdentity.FragmentIdentifier)) { warning.Append(" ("); warning.Append(contentIdentity.FragmentIdentifier); warning.Append(")"); } warning.Append(": "); } if (messageArgs != null && messageArgs.Length > 0) warning.AppendFormat(CultureInfo.InvariantCulture, message, messageArgs); else warning.Append(message); _outputService.WriteLine(warning.ToString()); }
public InvalidContentException(string message, ContentIdentity contentIdentity) : base(message) { ContentIdentity = contentIdentity; }
public override NodeContent Import(string filename, ContentImporterContext context) { var identity = new ContentIdentity(filename, GetType().Name); var importer = new AssimpImporter(); importer.AttachLogStream(new LogStream((msg, userData) => context.Logger.LogMessage(msg))); var scene = importer.ImportFile(filename, PostProcessSteps.FlipUVs | // So far appears necessary PostProcessSteps.JoinIdenticalVertices | PostProcessSteps.Triangulate | PostProcessSteps.SortByPrimitiveType | PostProcessSteps.FindInvalidData ); var rootNode = new NodeContent { Name = scene.RootNode.Name, Identity = identity, Transform = ToXna(scene.RootNode.Transform) }; // TODO: Materials var materials = new List <MaterialContent>(); foreach (var sceneMaterial in scene.Materials) { var diffuse = sceneMaterial.GetTexture(TextureType.Diffuse, 0); materials.Add(new BasicMaterialContent() { Name = sceneMaterial.Name, Identity = identity, Texture = new ExternalReference <TextureContent>(diffuse.FilePath, identity) }); } // Meshes var meshes = new Dictionary <Mesh, MeshContent>(); foreach (var sceneMesh in scene.Meshes) { if (!sceneMesh.HasVertices) { continue; } var mesh = new MeshContent { Name = sceneMesh.Name }; // Position vertices are shared at the mesh level foreach (var vert in sceneMesh.Vertices) { mesh.Positions.Add(new Vector3(vert.X, vert.Y, vert.Z)); } var geom = new GeometryContent { Name = string.Empty, //Material = materials[sceneMesh.MaterialIndex] }; // Geometry vertices reference 1:1 with the MeshContent parent, // no indirection is necessary. geom.Vertices.Positions.AddRange(mesh.Positions); geom.Vertices.AddRange(Enumerable.Range(0, sceneMesh.VertexCount)); geom.Indices.AddRange(sceneMesh.GetIntIndices()); // Individual channels go here if (sceneMesh.HasNormals) { geom.Vertices.Channels.Add(VertexChannelNames.Normal(), ToXna(sceneMesh.Normals)); } for (var i = 0; i < sceneMesh.TextureCoordsChannelCount; i++) { geom.Vertices.Channels.Add(VertexChannelNames.TextureCoordinate(i), ToXnaVector2(sceneMesh.GetTextureCoords(i))); } mesh.Geometry.Add(geom); rootNode.Children.Add(mesh); meshes.Add(sceneMesh, mesh); } // Bones var bones = new Dictionary <Node, BoneContent>(); var hierarchyNodes = scene.RootNode.Children.SelectDeep(n => n.Children).ToList(); foreach (var node in hierarchyNodes) { var bone = new BoneContent { Name = node.Name, Transform = Matrix.Transpose(ToXna(node.Transform)) }; if (node.Parent == scene.RootNode) { rootNode.Children.Add(bone); } else { var parent = bones[node.Parent]; parent.Children.Add(bone); } // Copy the bone's name to the MeshContent - this appears to be // the way it comes out of XNA's FBXImporter. foreach (var meshIndex in node.MeshIndices) { meshes[scene.Meshes[meshIndex]].Name = node.Name; } bones.Add(node, bone); } return(rootNode); }
/// <summary> /// Entry point for animation processing. /// </summary> /// <param name="model"></param> /// <param name="input"></param> /// <param name="context"></param> private void ProcessAnimations(ModelContent model, NodeContent input, ContentProcessorContext context, ContentIdentity sourceIdentity) { // First build a lookup table so we can determine the // index into the list of bones from a bone name. for (int i = 0; i < model.Bones.Count; i++) { bones[model.Bones[i].Name] = i; } // For saving the bone transforms boneTransforms = new Matrix[model.Bones.Count]; // // Collect up all of the animation data // ProcessAnimationsRecursive(input); // Check to see if there's an animation clip definition // Here, we're checking for a file with the _Anims suffix. // So, if your model is named dude.fbx, we'd add dude_Anims.xml in the same folder // and the pipeline will see the file and use it to override the animations in the // original model file. string SourceModelFile = sourceIdentity.SourceFilename; string SourcePath = Path.GetDirectoryName(SourceModelFile); string AnimFilename = Path.GetFileNameWithoutExtension(SourceModelFile); AnimFilename += "_Anims.xml"; string AnimPath = Path.Combine(SourcePath, AnimFilename); if (File.Exists(AnimPath)) { // Add the filename as a dependency, so if it changes, the model is rebuilt context.AddDependency(AnimPath); // Load the animation definition from the XML file AnimationDefinition AnimDef = context.BuildAndLoadAsset<XmlImporter, AnimationDefinition>(new ExternalReference<XmlImporter>(AnimPath), null); if (modelExtra.Clips.Count > 0) //if there are some animations in our model { foreach (AnimationDefinition.ClipPart Part in AnimDef.ClipParts) { // Grab the main clip that we are using and copy to MainClip AnimationClip MainClip = new AnimationClip(); float StartTime = GetTimeSpanForFrame(Part.StartFrame, AnimDef.OriginalFrameCount, modelExtra.Clips[AnimDef.OriginalClipName].Duration); float EndTime = GetTimeSpanForFrame(Part.EndFrame, AnimDef.OriginalFrameCount, modelExtra.Clips[AnimDef.OriginalClipName].Duration); MainClip.Duration = EndTime-StartTime; MainClip.Name = modelExtra.Clips[AnimDef.OriginalClipName].Name; // Process each of our new animation clip parts for (int i = 0; i < modelExtra.Clips[AnimDef.OriginalClipName].Bones.Count; i++) { AnimationClip.Bone clipBone = new AnimationClip.Bone(); clipBone.Name = modelExtra.Clips[AnimDef.OriginalClipName].Bones[i].Name; LinkedList<AnimationClip.Keyframe> keyframes = new LinkedList<AnimationClip.Keyframe>(); if (modelExtra.Clips[AnimDef.OriginalClipName].Bones[i].Keyframes.Count != 0) { for (int j = 0; j < modelExtra.Clips[AnimDef.OriginalClipName].Bones[i].Keyframes.Count; j++) { if ((modelExtra.Clips[AnimDef.OriginalClipName].Bones[i].Keyframes[j].Time >= StartTime) && (modelExtra.Clips[AnimDef.OriginalClipName].Bones[i].Keyframes[j].Time <= EndTime)) { AnimationClip.Keyframe frame = new AnimationClip.Keyframe(); frame.Rotation = modelExtra.Clips[AnimDef.OriginalClipName].Bones[i].Keyframes[j].Rotation; frame.Time = modelExtra.Clips[AnimDef.OriginalClipName].Bones[i].Keyframes[j].Time - StartTime; frame.Translation = modelExtra.Clips[AnimDef.OriginalClipName].Bones[i].Keyframes[j].Translation; keyframes.AddLast(frame); //clipBone.Keyframes.Add(frame); } } } // LinearKeyframeReduction(keyframes); clipBone.Keyframes = keyframes.ToList<AnimationClip.Keyframe>(); MainClip.Bones.Add(clipBone); } modelExtra.Clips.Add(Part.ClipName, MainClip); } } } // Ensure there is always a clip, even if none is included in the FBX // That way we can create poses using FBX files as one-frame // animation clips if (modelExtra.Clips.Count == 0) { AnimationClip clip = new AnimationClip(); modelExtra.Clips.Add("Take 001",clip); string clipName = "Take 001"; // Retain by name clips[clipName] = clip; clip.Name = clipName; foreach (ModelBoneContent bone in model.Bones) { AnimationClip.Bone clipBone = new AnimationClip.Bone(); clipBone.Name = bone.Name; clip.Bones.Add(clipBone); } } //Ensure all animations have a first key frame for every bone foreach (KeyValuePair<string,AnimationClip> clip in modelExtra.Clips) { for (int b = 0; b < bones.Count; b++) { List<AnimationClip.Keyframe> keyframes = clip.Value.Bones[b].Keyframes; if (keyframes.Count == 0 || keyframes[0].Time > 0) { AnimationClip.Keyframe keyframe = new AnimationClip.Keyframe(); keyframe.Time = 0; keyframe.Transform = boneTransforms[b]; keyframes.Insert(0, keyframe); } } } }
/// <summary> /// Parses the animation split definitions defined by the specified XML element. /// </summary> /// <param name="animationsElement">The XML element that defines the animation splits.</param> /// <param name="contentIdentity">The content identity.</param> /// <param name="context">The context.</param> /// <returns>The list of animation split definitions.</returns> /// <exception cref="ArgumentNullException"> /// <paramref name="animationsElement"/>, <paramref name="contentIdentity"/>, or /// <paramref name="context"/> is <see langword="null"/>. /// </exception> internal static List<AnimationSplitDefinition> ParseAnimationSplitDefinitions(XElement animationsElement, ContentIdentity contentIdentity, ContentPipelineContext context) { if (animationsElement == null) throw new ArgumentNullException("animationsElement"); if (contentIdentity == null) throw new ArgumentNullException("contentIdentity"); if (context == null) throw new ArgumentNullException("context"); // The frame rate needs to be set, when the splits are defined in frames. double? framerate = (double?)animationsElement.Attribute("Framerate"); // Read all animation splits. var splits = new List<AnimationSplitDefinition>(); foreach (var animationElement in animationsElement.Elements("Animation")) { var name = animationElement.GetMandatoryAttribute("Name", contentIdentity); double? startTime = (double?)animationElement.Attribute("StartTime"); double? endTime = (double?)animationElement.Attribute("EndTime"); int? startFrame = (int?)animationElement.Attribute("StartFrame"); int? endFrame = (int?)animationElement.Attribute("EndFrame"); bool? addLoopFrame = (bool?)animationsElement.Attribute("AddLoopFrame"); if (startTime == null && startFrame == null) { string message = XmlHelper.GetExceptionMessage(animationElement, "The animation element does not contain a valid \"StartTime\" or \"StartFrame\" attribute."); throw new InvalidContentException(message, contentIdentity); } if (endTime == null && endFrame == null) { string message = XmlHelper.GetExceptionMessage(animationElement, "The animation element does not contain a valid \"EndTime\" or \"EndFrame\" attribute."); throw new InvalidContentException(message, contentIdentity); } if (framerate == null && (startTime == null || endTime == null)) { string message = XmlHelper.GetExceptionMessage(animationsElement, "The animations element must have a <Framerate> element if start and end are specified in frames."); throw new InvalidContentException(message, contentIdentity); } startTime = startTime ?? startFrame.Value / framerate.Value; endTime = endTime ?? endFrame.Value / framerate.Value; TimeSpan start = new TimeSpan((long)(startTime.Value * TimeSpan.TicksPerSecond)); TimeSpan end = new TimeSpan((long)(endTime.Value * TimeSpan.TicksPerSecond)); if (start > end) { string message = XmlHelper.GetExceptionMessage(animationElement, "Invalid animation element: The start time is larger than the end time."); throw new InvalidContentException(message, contentIdentity); } splits.Add(new AnimationSplitDefinition { Name = name, StartTime = start, EndTime = end, AddLoopFrame = addLoopFrame }); } return splits; }
public override void LogWarning(string helpLink, ContentIdentity contentIdentity, string message, object[] messageArgs) { log.LogWarning(message, messageArgs); }
static internal TextureContent Import(string filename, ContentImporterContext context) { var identity = new ContentIdentity(filename); TextureContent output = null; using (var reader = new BinaryReader(new FileStream(filename, FileMode.Open, FileAccess.Read))) { // Read signature ("DDS ") var valid = reader.ReadByte() == 0x44; valid = valid && reader.ReadByte() == 0x44; valid = valid && reader.ReadByte() == 0x53; valid = valid && reader.ReadByte() == 0x20; if (!valid) { throw new ContentLoadException("Invalid file signature"); } var header = new DdsHeader(); // Read DDS_HEADER header.dwSize = reader.ReadUInt32(); if (header.dwSize != 124) { throw new ContentLoadException("Invalid DDS_HEADER dwSize value"); } header.dwFlags = (Ddsd)reader.ReadUInt32(); header.dwHeight = reader.ReadUInt32(); header.dwWidth = reader.ReadUInt32(); header.dwPitchOrLinearSize = reader.ReadUInt32(); header.dwDepth = reader.ReadUInt32(); header.dwMipMapCount = reader.ReadUInt32(); // The next 11 DWORDs are reserved and unused for (int i = 0; i < 11; ++i) { reader.ReadUInt32(); } // Read DDS_PIXELFORMAT header.ddspf.dwSize = reader.ReadUInt32(); if (header.ddspf.dwSize != 32) { throw new ContentLoadException("Invalid DDS_PIXELFORMAT dwSize value"); } header.ddspf.dwFlags = (Ddpf)reader.ReadUInt32(); header.ddspf.dwFourCC = (FourCC)reader.ReadUInt32(); header.ddspf.dwRgbBitCount = reader.ReadUInt32(); header.ddspf.dwRBitMask = reader.ReadUInt32(); header.ddspf.dwGBitMask = reader.ReadUInt32(); header.ddspf.dwBBitMask = reader.ReadUInt32(); header.ddspf.dwABitMask = reader.ReadUInt32(); // Continue reading DDS_HEADER header.dwCaps = (DdsCaps)reader.ReadUInt32(); header.dwCaps2 = (DdsCaps2)reader.ReadUInt32(); // dwCaps3 unused reader.ReadUInt32(); // dwCaps4 unused reader.ReadUInt32(); // dwReserved2 unused reader.ReadUInt32(); // Check for the existence of the DDS_HEADER_DXT10 struct next if (header.ddspf.dwFlags == Ddpf.FourCC && header.ddspf.dwFourCC == FourCC.Dx10) { throw new ContentLoadException("Unsupported DDS_HEADER_DXT10 struct found"); } int faceCount = 1; int mipMapCount = (int)(header.dwCaps.HasFlag(DdsCaps.MipMap) ? header.dwMipMapCount : 1); if (header.dwCaps.HasFlag(DdsCaps.Complex)) { if (header.dwCaps2.HasFlag(DdsCaps2.Cubemap)) { if (!header.dwCaps2.HasFlag(DdsCaps2.CubemapAllFaces)) { throw new ContentLoadException("Incomplete cubemap in DDS file"); } faceCount = 6; output = new TextureCubeContent() { Identity = identity }; } else { output = new Texture2DContent() { Identity = identity }; } } else { output = new Texture2DContent() { Identity = identity }; } bool rbSwap; var format = GetSurfaceFormat(ref header.ddspf, out rbSwap); for (int f = 0; f < faceCount; ++f) { var w = (int)header.dwWidth; var h = (int)header.dwHeight; var mipMaps = new MipmapChain(); for (int m = 0; m < mipMapCount; ++m) { var content = CreateBitmapContent(format, w, h); var byteCount = GetBitmapSize(format, w, h); var bytes = reader.ReadBytes(byteCount); content.SetPixelData(bytes); mipMaps.Add(content); w = MathHelper.Max(1, w / 2); h = MathHelper.Max(1, h / 2); } output.Faces[f] = mipMaps; } } return(output); }
/*static*/ private void OptimizeForCache(IList<Vector3F> positions, // In: Original positions. IList<int> indices, // In: Original indices. Out: Optimized indices. out int[] vertexRemap, // Maps original vertex location to optimized vertex location. out int[] duplicateVertices, // Original locations of duplicate vertices. ContentIdentity identity) { Debug.Assert(positions != null); Debug.Assert(indices != null); #if COMPUTE_VERTEX_CACHE_MISS_RATE float acmrOld; float atvrOld; DirectXMesh.ComputeVertexCacheMissRate(indices, positions.Count, DirectXMesh.OPTFACES_V_DEFAULT, out acmrOld, out atvrOld); #endif int numberOfVertices = positions.Count; int[] pointRep; int[] adjacency; DirectXMesh.GenerateAdjacencyAndPointReps(indices, positions, Numeric.EpsilonF, out pointRep, out adjacency); if (!DirectXMesh.Clean(indices, numberOfVertices, adjacency, null, false, out duplicateVertices)) { List<string> validationMessages = new List<string>(); DirectXMesh.Validate(indices, numberOfVertices, adjacency, MeshValidationOptions.Default, validationMessages); string message; if (validationMessages.Count == 0) { message = "Mesh cleaning failed."; } else { var messageBuilder = new StringBuilder(); messageBuilder.AppendLine("Mesh cleaning failed:"); foreach (var validationMessage in validationMessages) messageBuilder.AppendLine(validationMessage); message = messageBuilder.ToString(); } throw new InvalidContentException(message, identity); } // Skip DirectXMesh.AttributeSort and DirectXMesh.ReorderIBAndAdjacency. // (GeometryContent already sorted.) int[] faceRemap; DirectXMesh.OptimizeFaces(indices, adjacency, null, out faceRemap); DirectXMesh.ReorderIB(indices, faceRemap); DirectXMesh.OptimizeVertices(indices, numberOfVertices, out vertexRemap); DirectXMesh.FinalizeIB(indices, vertexRemap); // Skip DirectXMesh.FinalizeVB. // (Needs to be handled by caller.) Debug.Assert(vertexRemap.Length == numberOfVertices + duplicateVertices.Length); #if COMPUTE_VERTEX_CACHE_MISS_RATE int newNumberOfVertices = vertexRemap.Count(i => i != -1); float acmrNew; float atvrNew; DirectXMesh.ComputeVertexCacheMissRate(indices, newNumberOfVertices, DirectXMesh.OPTFACES_V_DEFAULT, out acmrNew, out atvrNew); _context.Logger.LogMessage( "Mesh optimization: Vertices before {0}, after {1}; ACMR before {2}, after {3}; ATVR before {4}, after {5}", numberOfVertices, newNumberOfVertices, acmrOld, acmrNew, atvrOld, atvrNew); #endif }
/// <summary> /// Converts to the specified <see cref="XAttribute"/> to a <see cref="Vector3"/>. /// </summary> /// <param name="attribute">The XML attribute to parse. Can be <see langword="null"/>.</param> /// <param name="defaultValue"> /// The default value, used if <paramref name="attribute"/> is null or empty. /// </param> /// <param name="identity">The content identity.</param> /// <returns>The 3D vector.</returns> /// <exception cref="InvalidContentException"> /// Error parsing <paramref name="attribute"/>. /// </exception> public static Vector3 ToVector3(this XAttribute attribute, Vector3 defaultValue, ContentIdentity identity) { string value = (string)attribute; if (value == null) return defaultValue; try { var values = value.Split(ListSeparators, StringSplitOptions.RemoveEmptyEntries); if (values.Length == 3) { Vector3 result; result.X = float.Parse(values[0], CultureInfo.InvariantCulture); result.Y = float.Parse(values[1], CultureInfo.InvariantCulture); result.Z = float.Parse(values[2], CultureInfo.InvariantCulture); return result; } else { var message = GetExceptionMessage(attribute, "Could not parse 3-dimensional vector: '{0}'", value); throw new InvalidContentException(message, identity); } } catch (Exception exception) { var message = GetExceptionMessage(attribute, "Could not parse 3-dimensional vector: '{0}'", value); throw new InvalidContentException(message, identity, exception); } }
/// <summary> /// Converts the specified <see cref="XAttribute"/> to a <see cref="DRTextureFormat"/> value. /// </summary> /// <param name="attribute">The XML attribute to parse. Can be <see langword="null"/>.</param> /// <param name="defaultValue"> /// The default value, used if <paramref name="attribute"/> is null or empty. /// </param> /// <param name="identity">The content identity.</param> /// <returns>The texture format.</returns> /// <exception cref="InvalidContentException"> /// Error parsing <paramref name="attribute"/>. /// </exception> public static DRTextureFormat ToTextureFormat(this XAttribute attribute, DRTextureFormat defaultValue, ContentIdentity identity) { string value = (string)attribute; if (string.IsNullOrWhiteSpace(value)) return defaultValue; switch (value.Trim().ToUpperInvariant()) { case "NOCHANGE": return DRTextureFormat.NoChange; case "COLOR": return DRTextureFormat.Color; case "DXT": return DRTextureFormat.Dxt; case "NORMAL": return DRTextureFormat.Normal; case "NORMALINVERTY": return DRTextureFormat.NormalInvertY; default: var message = GetExceptionMessage(attribute, "Could not parse texture format: '{0}'", value); throw new InvalidContentException(message, identity); } }
/// <summary> /// Converts the specified <see cref="XAttribute"/> to an effect parameter value. /// </summary> /// <param name="attribute">The XML attribute to parse. Can be <see langword="null"/>.</param> /// <param name="defaultValue"> /// The default value, used if <paramref name="attribute"/> is null or empty. /// </param> /// <param name="identity">The content identity.</param> /// <returns>The effect parameter value.</returns> /// <exception cref="InvalidContentException"> /// Error parsing <paramref name="attribute"/>. /// </exception> public static object ToParameterValue(this XAttribute attribute, object defaultValue, ContentIdentity identity) { // TODO: Add support for Int32, Quaternion, Matrix. string value = (string)attribute; if (value == null) return defaultValue; try { var values = value.Split(ListSeparators, StringSplitOptions.RemoveEmptyEntries); // Empty value if (values.Length == 0) return defaultValue; if (values.Any(IsBoolean)) { // Boolean if (values.Length == 1) return bool.Parse(values[0]); // Boolean[] bool[] array = new bool[values.Length]; for (int i = 0; i < values.Length; i++) array[i] = bool.Parse(values[i]); return array; } else { // Single if (values.Length == 1) return float.Parse(values[0], CultureInfo.InvariantCulture); // Vector2 if (values.Length == 2) { Vector2 result; result.X = float.Parse(values[0], CultureInfo.InvariantCulture); result.Y = float.Parse(values[1], CultureInfo.InvariantCulture); return result; } // Vector3 if (values.Length == 3) { Vector3 result; result.X = float.Parse(values[0], CultureInfo.InvariantCulture); result.Y = float.Parse(values[1], CultureInfo.InvariantCulture); result.Z = float.Parse(values[2], CultureInfo.InvariantCulture); return result; } // Vector4 if (values.Length == 4) { Vector4 result; result.X = float.Parse(values[0], CultureInfo.InvariantCulture); result.Y = float.Parse(values[1], CultureInfo.InvariantCulture); result.Z = float.Parse(values[2], CultureInfo.InvariantCulture); result.W = float.Parse(values[3], CultureInfo.InvariantCulture); return result; } else { // Single[] float[] array = new float[values.Length]; for (int i = 0; i < values.Length; i++) array[i] = float.Parse(values[i], CultureInfo.InvariantCulture); return array; } } } catch (Exception exception) { var message = GetExceptionMessage(attribute, "Could not parse parameter value: '{0}'", value); throw new InvalidContentException(message, identity, exception); } }
/// <summary> /// Converts the specified <see cref="XAttribute"/> to a color value. /// </summary> /// <param name="attribute">The XML attribute to parse. Can be <see langword="null"/>.</param> /// <param name="defaultValue"> /// The default value, used if <paramref name="attribute"/> is null or empty. /// </param> /// <param name="identity">The content identity.</param> /// <returns>The color value.</returns> /// <exception cref="InvalidContentException"> /// Error parsing <paramref name="attribute"/>. /// </exception> public static Color ToColor(this XAttribute attribute, Color defaultValue, ContentIdentity identity) { string value = (string)attribute; if (value == null) return defaultValue; try { var values = value.Split(ListSeparators, StringSplitOptions.RemoveEmptyEntries); if (values.Length == 3) { Color color = new Color(); color.R = byte.Parse(values[0], CultureInfo.InvariantCulture); color.G = byte.Parse(values[1], CultureInfo.InvariantCulture); color.B = byte.Parse(values[2], CultureInfo.InvariantCulture); color.A = byte.MaxValue; return color; } else if (values.Length == 4) { Color color = new Color(); color.R = byte.Parse(values[0], CultureInfo.InvariantCulture); color.G = byte.Parse(values[1], CultureInfo.InvariantCulture); color.B = byte.Parse(values[2], CultureInfo.InvariantCulture); color.A = byte.Parse(values[3], CultureInfo.InvariantCulture); return color; } else { var message = GetExceptionMessage(attribute, "Could not parse color value: '{0}'", value); throw new InvalidContentException(message, identity); } } catch (Exception exception) { var message = GetExceptionMessage(attribute, "Could not parse color value: '{0}'", value); throw new InvalidContentException(message, identity, exception); } }
/*static*/ private void OptimizeForCache(IList<Vector3F> positions, IList<int> indices, ContentIdentity identity) { Debug.Assert(positions != null); Debug.Assert(indices != null); var originalPositions = positions.ToArray(); int numberOfVertices = originalPositions.Length; int[] vertexRemap; int[] duplicateVertices; OptimizeForCache(positions, indices, out vertexRemap, out duplicateVertices, identity); positions.Clear(); for (int i = 0; i < vertexRemap.Length; i++) { if (vertexRemap[i] != -1) positions.Add(new Vector3F()); } // Add reordered vertices. for (int oldIndex = 0; oldIndex < numberOfVertices; oldIndex++) { int newIndex = vertexRemap[oldIndex]; if (newIndex != -1) positions[newIndex] = originalPositions[oldIndex]; } Debug.Assert(vertexRemap.Length == numberOfVertices + duplicateVertices.Length); // Add duplicate vertices. for (int i = 0; i < duplicateVertices.Length; i++) { int newIndex = vertexRemap[numberOfVertices + i]; if (newIndex != -1) positions[newIndex] = originalPositions[duplicateVertices[i]]; } }
public static void Split(AnimationContentDictionary animationDictionary, IList<AnimationSplitDefinition> splits, ContentIdentity contentIdentity, ContentProcessorContext context) { if (splits == null || splits.Count == 0) return; if (animationDictionary == null) return; if (contentIdentity == null) throw new ArgumentNullException("contentIdentity"); if (context == null) throw new ArgumentNullException("context"); if (animationDictionary.Count == 0) { context.Logger.LogWarning(null, contentIdentity, "The model does not have an animation. Animation splitting is skipped."); return; } if (animationDictionary.Count > 1) context.Logger.LogWarning(null, contentIdentity, "The model contains more than 1 animation. The animation splitting is performed on the first animation. Other animations are deleted!"); // Get first animation. var originalAnimation = animationDictionary.First().Value; // Clear animation dictionary. - We do not keep the original animations! animationDictionary.Clear(); // Add an animation to animationDictionary for each split. foreach (var split in splits) { TimeSpan startTime = split.StartTime; TimeSpan endTime = split.EndTime; var newAnimation = new AnimationContent { Name = split.Name, Duration = endTime - startTime }; // Process all channels. foreach (var item in originalAnimation.Channels) { string channelName = item.Key; AnimationChannel originalChannel = item.Value; if (originalChannel.Count == 0) return; AnimationChannel newChannel = new AnimationChannel(); // Add all key frames to the channel that are in the split interval. foreach (AnimationKeyframe keyFrame in originalChannel) { TimeSpan time = keyFrame.Time; if (startTime <= time && time <= endTime) { newChannel.Add(new AnimationKeyframe(keyFrame.Time - startTime, keyFrame.Transform)); } } // Add channel if it contains key frames. if (newChannel.Count > 0) newAnimation.Channels.Add(channelName, newChannel); } if (newAnimation.Channels.Count == 0) { var message = string.Format(CultureInfo.InvariantCulture, "The split animation '{0}' is empty.", split.Name); throw new InvalidContentException(message, contentIdentity); } if (animationDictionary.ContainsKey(split.Name)) { var message = string.Format(CultureInfo.InvariantCulture, "Cannot add split animation '{0}' because an animation with the same name already exits.", split.Name); throw new InvalidContentException(message, contentIdentity); } animationDictionary.Add(split.Name, newAnimation); } }
/// <summary> /// Converts an intermediate format content pipeline AnimationContentDictionary /// object to our runtime AnimationClip format. /// </summary> static Dictionary<string, AnimationClip> ProcessAnimations( AnimationContentDictionary animations, IList<BoneContent> bones, ContentProcessorContext context, ContentIdentity sourceIdentity) { // Build up a table mapping bone names to indices. Dictionary<string, int> boneMap = new Dictionary<string, int>(); for (int i = 0; i < bones.Count; i++) { string boneName = bones[i].Name; if (!string.IsNullOrEmpty(boneName)) boneMap.Add(boneName, i); } // Convert each animation in turn. Dictionary<string, AnimationClip> animationClips; animationClips = new Dictionary<string, AnimationClip>(); foreach (KeyValuePair<string, AnimationContent> animation in animations) { AnimationClip processed = ProcessAnimation(animation.Value, boneMap, animation.Key); animationClips.Add(animation.Key, processed); } // Check to see if there's an animation clip definition // Here, we're checking for a file with the _Anims suffix. // So, if your model is named dude.fbx, we'd add dude_Anims.xml in the same folder // and the pipeline will see the file and use it to override the animations in the // original model file. string SourceModelFile = sourceIdentity.SourceFilename; string SourcePath = Path.GetDirectoryName(SourceModelFile); string AnimFilename = Path.GetFileNameWithoutExtension(SourceModelFile); AnimFilename += "_Anims.xml"; string AnimPath = Path.Combine(SourcePath, AnimFilename); if (File.Exists(AnimPath)) { context.AddDependency(AnimPath); AnimationDefinition AnimDef = context.BuildAndLoadAsset<XmlImporter, AnimationDefinition>(new ExternalReference<XmlImporter>(AnimPath), null); //breaks up original animation clips into new clips if (animationClips.ContainsKey(AnimDef.originalClipName)) { //graps main clip AnimationClip MainClip = animationClips[AnimDef.originalClipName]; //remove original clip from animations animationClips.Remove(AnimDef.originalClipName); foreach (AnimationDefinition.clipPart Part in AnimDef.ClipParts) { //calculate frame times TimeSpan StartTime = GetTimeSpanForFrame(Part.StartFrame, AnimDef.originalFrameCount, MainClip.Duration.Ticks); TimeSpan EndTime = GetTimeSpanForFrame(Part.EndFrame, AnimDef.originalFrameCount, MainClip.Duration.Ticks); //get keyframes for animation clip thats in start and end time List<Keyframe> Keyframes = new List<Keyframe>(); foreach (Keyframe AnimFrame in MainClip.Keyframes) { if ((AnimFrame.Time >= StartTime) && (AnimFrame.Time <= EndTime)) { Keyframe NewFrame = new Keyframe(AnimFrame.Bone, AnimFrame.Time - StartTime, AnimFrame.Transform); Keyframes.Add(NewFrame); } } //process events /*List<AnimationEvent> Events = new List<AnimationEvent>(); if (Part.Events != null) { foreach (AnimationDefinition.clipPart.Event Event in Part.Events) { TimeSpan EventTime = GetTimeSpanForFrame(Event.Keyframe, AnimDef.originalFrameCount, MainClip.Duration.Ticks); EventTime -= StartTime; AnimationEvent newEvent = new AnimationEvent(); newEvent.EventTime = EventTime; newEvent.EventName = Event.Name; Events.Add(newEvent); } }*/ AnimationClip newClip = new AnimationClip(EndTime - StartTime, Keyframes, Part.ClipName); animationClips[Part.ClipName] = newClip; } } } /* if (animationClips.Count == 0) { throw new InvalidContentException( "Input file does not contain any animations."); }*/ return animationClips; }
/// <summary> /// Recursive function adds vegetation billboards to all meshes. /// </summary> void GenerateVegetation(NodeContent node, ContentIdentity identity) { // First, recurse over any child nodes. foreach (NodeContent child in node.Children) { GenerateVegetation(child, identity); } // Check whether this node is in fact a mesh. MeshContent mesh = node as MeshContent; if (mesh != null) { // Create three new geometry objects, one for each type // of billboard that we are going to create. Set different // effect parameters to control the size and wind sensitivity // for each type of billboard. GeometryContent grass = CreateVegetationGeometry("grass.tga", 5, 5, 1, identity); GeometryContent trees = CreateVegetationGeometry("tree.tga", 12, 12, 0.5f, identity); GeometryContent cats = CreateVegetationGeometry("cat.tga", 5, 5, 0, identity); MeshContent vegetationMesh = new MeshContent { Name = "Billboards" }; // Loop over all the existing geometry in this mesh. foreach (GeometryContent geometry in mesh.Geometry) { IList<int> indices = geometry.Indices; IList<Vector3> positions = geometry.Vertices.Positions; IList<Vector3> normals = geometry.Vertices.Channels.Get<Vector3>(VertexChannelNames.Normal()); // Loop over all the triangles in this piece of geometry. for (int triangle = 0; triangle < indices.Count; triangle += 3) { // Look up the three indices for this triangle. int i1 = indices[triangle]; int i2 = indices[triangle + 1]; int i3 = indices[triangle + 2]; // Create vegetation billboards to cover this triangle. // A more sophisticated implementation would measure the // size of the triangle to work out how many to create, // but we don't bother since we happen to know that all // our triangles are roughly the same size. for (int count = 0; count < BillboardsPerTriangle; count++) { Vector3 position, normal; // Choose a random location on the triangle. PickRandomPoint(positions[i1], positions[i2], positions[i3], normals[i1], normals[i2], normals[i3], out position, out normal); // Randomly choose what type of billboard to create. GeometryContent billboardType; if (random.NextDouble() < TreeProbability) { billboardType = trees; // As a special case, force trees to point straight // upward, even if they are growing on a slope. // That's what trees do in real life, after all! normal = Vector3.Up; } else if (random.NextDouble() < CatProbability) { billboardType = cats; } else { billboardType = grass; } // Add a new billboard to the output geometry. GenerateBillboard(vegetationMesh, billboardType, position, normal); } } } // Add our new billboard geometry to the main mesh. vegetationMesh.Geometry.Add(grass); vegetationMesh.Geometry.Add(trees); vegetationMesh.Geometry.Add(cats); mesh.Children.Add(vegetationMesh); } }
public override NodeContent Import(string filename, ContentImporterContext context) { #if 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 { } #endif _identity = new ContentIdentity(filename, string.IsNullOrEmpty(ImporterName) ? GetType().Name : 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)); // 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 | //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. ImportMaterials(); // Create _materials. 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); }
private Exception ProcessErrorsAndWarnings(string errorsAndWarnings, EffectContent input, ContentProcessorContext context) { // Split the errors by lines. var errors = errorsAndWarnings.Split('\n'); // Process each error line extracting the location and message information. for (var i = 0; i < errors.Length; i++) { // Skip blank lines. if (errors[i].StartsWith(Environment.NewLine)) break; // find some unique characters in the error string var openIndex = errors[i].IndexOf('('); var closeIndex = errors[i].IndexOf(')'); // can't process the message if it has no line counter if (openIndex == -1 || closeIndex == -1) continue; // find the error number, then move forward into the message var errorIndex = errors[i].IndexOf('X', closeIndex); if (errorIndex < 0) return new InvalidContentException(errors[i], input.Identity); // trim out the data we need to feed the logger var fileName = errors[i].Remove(openIndex); var lineAndColumn = errors[i].Substring(openIndex + 1, closeIndex - openIndex - 1); var description = errors[i].Substring(errorIndex); // when the file name is not present, the error can be found in the root file if (string.IsNullOrEmpty(fileName)) fileName = input.Identity.SourceFilename; // ensure that the file data points toward the correct file var fileInfo = new FileInfo(fileName); if (!fileInfo.Exists) { var parentFile = new FileInfo(input.Identity.SourceFilename); fileInfo = new FileInfo(Path.Combine(parentFile.Directory.FullName, fileName)); } fileName = fileInfo.FullName; // construct the temporary content identity and file the error or warning var identity = new ContentIdentity(fileName, input.Identity.SourceTool, lineAndColumn); if (errors[i].Contains("warning")) { description = "A warning was generated when compiling the effect.\n" + description; context.Logger.LogWarning(string.Empty, identity, description, string.Empty); } else if (errors[i].Contains("error")) { description = "Unable to compile the effect.\n" + description; return new InvalidContentException(description, identity); } } // if no exceptions were created in the above loop, generate a generic one here return new InvalidContentException(errorsAndWarnings, input.Identity); }
public ExternalReference(string filename, ContentIdentity relativeToContent) { Filename = filename; }
public abstract void LogWarning(string helpLink, ContentIdentity contentIdentity, string message, Object[] messageArgs);
protected string GetCurrentFilename(ContentIdentity contentIdentity) { throw new NotImplementedException(); }
/// <summary> /// Gets the mandatory attribute from the specified XML element. /// </summary> /// <param name="element">The XML element.</param> /// <param name="name">The name of the attribute.</param> /// <param name="identity">The content identity.</param> /// <returns>The attribute value.</returns> /// <exception cref="ArgumentNullException"> /// <paramref name="element"/> or <paramref name="name"/> is <see langword="null"/>. /// </exception> /// <exception cref="ArgumentException"> /// <paramref name="name"/> is empty. /// </exception> public static string GetMandatoryAttribute(this XElement element, string name, ContentIdentity identity) { if (element == null) throw new ArgumentNullException("element"); if (name == null) throw new ArgumentNullException("name"); if (name.Length == 0) throw new ArgumentException("The attribute name must not be empty.", "name"); var attribute = element.Attribute(name); if (attribute == null) { string message = GetExceptionMessage(element, "\"{0}\" attribute is missing.", name); throw new InvalidContentException(message, identity); } string s = (string)attribute; if (s.Length == 0) { string message = GetExceptionMessage(element, "\"{0}\" attribute must not be empty.", name); throw new InvalidContentException(message, identity); } return s; }
static internal TextureContent Import(string filename, ContentImporterContext context) { var identity = new ContentIdentity(filename); TextureContent output = null; using (var reader = new BinaryReader(new FileStream(filename, FileMode.Open, FileAccess.Read))) { // Read signature ("DDS ") var valid = reader.ReadByte() == 0x44; valid = valid && reader.ReadByte() == 0x44; valid = valid && reader.ReadByte() == 0x53; valid = valid && reader.ReadByte() == 0x20; if (!valid) throw new ContentLoadException("Invalid file signature"); var header = new DdsHeader(); // Read DDS_HEADER header.dwSize = reader.ReadUInt32(); if (header.dwSize != 124) throw new ContentLoadException("Invalid DDS_HEADER dwSize value"); header.dwFlags = (Ddsd)reader.ReadUInt32(); header.dwHeight = reader.ReadUInt32(); header.dwWidth = reader.ReadUInt32(); header.dwPitchOrLinearSize = reader.ReadUInt32(); header.dwDepth = reader.ReadUInt32(); header.dwMipMapCount = reader.ReadUInt32(); // The next 11 DWORDs are reserved and unused for (int i = 0; i < 11; ++i) reader.ReadUInt32(); // Read DDS_PIXELFORMAT header.ddspf.dwSize = reader.ReadUInt32(); if (header.ddspf.dwSize != 32) throw new ContentLoadException("Invalid DDS_PIXELFORMAT dwSize value"); header.ddspf.dwFlags = (Ddpf)reader.ReadUInt32(); header.ddspf.dwFourCC = (FourCC)reader.ReadUInt32(); header.ddspf.dwRgbBitCount = reader.ReadUInt32(); header.ddspf.dwRBitMask = reader.ReadUInt32(); header.ddspf.dwGBitMask = reader.ReadUInt32(); header.ddspf.dwBBitMask = reader.ReadUInt32(); header.ddspf.dwABitMask = reader.ReadUInt32(); // Continue reading DDS_HEADER header.dwCaps = (DdsCaps)reader.ReadUInt32(); header.dwCaps2 = (DdsCaps2)reader.ReadUInt32(); // dwCaps3 unused reader.ReadUInt32(); // dwCaps4 unused reader.ReadUInt32(); // dwReserved2 unused reader.ReadUInt32(); // Check for the existence of the DDS_HEADER_DXT10 struct next if (header.ddspf.dwFlags == Ddpf.FourCC && header.ddspf.dwFourCC == FourCC.Dx10) { throw new ContentLoadException("Unsupported DDS_HEADER_DXT10 struct found"); } int faceCount = 1; int mipMapCount = (int)(header.dwCaps.HasFlag(DdsCaps.MipMap) ? header.dwMipMapCount : 1); if (header.dwCaps.HasFlag(DdsCaps.Complex)) { if (header.dwCaps2.HasFlag(DdsCaps2.Cubemap)) { if (!header.dwCaps2.HasFlag(DdsCaps2.CubemapAllFaces)) throw new ContentLoadException("Incomplete cubemap in DDS file"); faceCount = 6; output = new TextureCubeContent() { Identity = identity }; } else { output = new Texture2DContent() { Identity = identity }; } } else { output = new Texture2DContent() { Identity = identity }; } bool rbSwap; var format = GetSurfaceFormat(ref header.ddspf, out rbSwap); for (int f = 0; f < faceCount; ++f) { var w = (int)header.dwWidth; var h = (int)header.dwHeight; var mipMaps = new MipmapChain(); for (int m = 0; m < mipMapCount; ++m) { var content = CreateBitmapContent(format, w, h); var byteCount = GetBitmapSize(format, w, h); var bytes = reader.ReadBytes(byteCount); content.SetPixelData(bytes); mipMaps.Add(content); w = MathHelper.Max(1, w / 2); h = MathHelper.Max(1, h / 2); } output.Faces[f] = mipMaps; } } return output; }