How To: Write a Custom Importer and Processor
The Complete Sample
The code in this tutorial illustrates the technique described in the text. A complete code sample for this tutorial is available for you to download, including full source code and any additional supporting files required by the sample.
Overview
XNA Game Studio already provides standard Content Pipeline importers and processors to support common art-asset file formats, as described in Standard Importers and Processors. Third parties also provide custom importers and processors to support additional formats. Currently, the XNA Game Studio Content Document Object Model (DOM) provides support for meshes, materials, textures, sprite-fonts, and animations. Outside of these, a custom importer can return a ContentItem with custom information in its opaque data, or a custom type you have developed.
The following diagram lists the complete Content DOM.
However, if you want to support a new type in the Content Pipeline, writing your own importer and processor can be fairly straightforward.
Suppose, for example, that you want to compile HLSL source files into pixel shaders, somewhat like the EffectImporter and EffectProcessor classes built into XNA Game Studio, but processing individual pixel shaders rather than complete effects. This a good, simple example to illustrate the steps you would take to write an importer and processor, and also the writer and reader needed to save and load the results. The sections below describe each of the steps in turn.
- Creating a Content Pipeline Extension Library
- Implementing a Simple Importer
- Implementing a Processor to Compile the Shader
- Implementing a Writer for the Compiled Shader
- Implementing a Reader for the Pixel Shaders
- Using the Output of the New Processor in Your Game
Creating a Content Pipeline Extension Library
The first step in writing an importer and processor is to create a new project for them. You need to do this because your importer and processor are used by the Content Pipeline when your game is being built, but are not part of the game itself. As a result, you need to provide them as a separate library assembly that the Content Pipeline can link to when it needs to build the new file format you are supporting.
- In XNA Game Studio, load a game solution you are developing (the sample uses "CPExtPixelShader").
- From Solution Explorer, right-click the Solution node, then click Add, and then click New Project.
- From the Add New Project dialog box, under the Visual C# node, from the Project types: pane, select the XNA Game Studio 2.0 node.
- Select the Content Pipeline Extension Library (2.0) template, assign a name to the new project at the bottom of the dialog box (name this project
PSProcessorLib), and click Ok. - From Solution Explorer, right-click the ContentProcessor1.cs item and select Delete.
-
From Solution Explorer, right-click the Content node of the CPExtPixelShader project, and then click Add Reference.
- From the Projects tab, select your content extension project and click OK.
The remaining steps create a reference to the PSProcessorLib content extension project.
The new project is now ready for your custom importer and processor implementation.
Implementing a Simple Importer
First, create a class to hold the input data you are importing, which in this case takes the form of a string of HLSL source code.
Add a new C# class named PSSourceCode to the processor project. The first thing to do in the file containing your new class definition is add the following using statement at the beginning of the file:
using Microsoft.Xna.Framework.Content.Pipeline;
Now define the class as follows:
class PSSourceCode { public PSSourceCode(string sourceCode) { this.sourceCode = sourceCode; } private string sourceCode; public string SourceCode { get { return sourceCode; } } }
The next step is to write an importer class to import the HLSL source code. This class must be derived from ContentImporter and implements the Import method. All it does is read a text file containing HLSL source code into your PSSourceCode class.
Using the New Item dialog box, add a new Content Importer item (called PSImporter) to the processor project.
Now define the class as follows:
[ContentImporter(".psh", DefaultProcessor = "PSProcessor", DisplayName = "Pixel Shader Importer")] class PSImporter : ContentImporter<PSSourceCode> { public override PSSourceCode Import(string filename, ContentImporterContext context) { string sourceCode = System.IO.File.ReadAllText(filename); return new PSSourceCode(sourceCode); } }
The ContentImporter attribute applied to the PSImporter class provides some context for the user interface of XNA Game Studio. Since this importer supports files with a .psh extension, XNA Game Studio will automatically select the PSImporter importer when a .psh file is added to the project. In addition, the DefaultProcessor argument specifies the correct processor XNA Game Studio selects when a .psh file is added.
Note |
|---|
To specify multiple file types, separate the file extensions, listed in the ContentImporterAttribute with a comma. For example, [ContentImporter (".bmp",".dds",".tga")]
declares an importer that accepts .bmp, .dds, and .tga file types. Normally, an importer that accepts multiple file formats is specialized to generate one particular kind of output type, such as textures. However, aside from difficulties of maintenance, there is nothing to prevent a single importer from being written to handle many different content types. |
When the game is built, the ContentImporter.Import function is called once for each XNA content item in the current project.
When invoked against an input file in the appropriate format, a custom importer is expected to parse the file and produce as output one or more content objects of appropriate types. Since an importer's output is passed directly to a Content Pipeline processor, each type that an importer generates must have at least one processor available that can accept it as input.
Tip |
|---|
An importer that generates DOM objects may also automatically generate an intermediate XML cache file that serializes these objects, provided that the importer has been implemented with the CacheImportedData attribute flag set to true (this flag is false by default). To set the attribute flag to true, begin the implementation of your Importer class like this:
[ContentImporter( ".MyExt", CacheImportedData = true )]
class PSImporter : ContentImporter<PSSourceCode>
{
...
}
|
Implementing a Processor to Compile the Shader
After the new importer has read in the pixel shader source code from a text file, your content processor takes over and compiles the shader into binary form. To write the processor, again start by creating a class to store the compiled output, which in this case takes the form of an array of bytes. Add a C# class called "CompiledPS" to the processor project, and define the new class as follows:
class CompiledPS { public CompiledPS(byte[] compiledShader) { this.compiledShader = compiledShader; } private byte[] compiledShader; public byte[] CompiledShader { get { return (byte[])compiledShader.Clone(); } } }
Now you are ready to write the processor class, which converts a PSSourceCode object into a CompiledPS object. Using the New Item dialog box, add a new Content Processor item (called PSProcessor) to the processor project.
Now define the class as follows:
[ContentProcessor(DisplayName = "Pixel Shader Processor")] class PSProcessor : ContentProcessor<PSSourceCode, CompiledPS> { public override CompiledPS Process(PSSourceCode input, ContentProcessorContext context) { CompiledShader shader = ShaderCompiler.CompileFromSource(input.SourceCode, null, null, CompilerOptions.None, "main", ShaderProfile.PS_2_0, context.TargetPlatform); if (!shader.Success) { throw new InvalidContentException(shader.ErrorsAndWarnings); } return new CompiledPS(shader.GetShaderCode()); } }
The Framework.Graphics.ShaderCompiler class performs the actual compilation to code that will run on the game machine your game targets, which is specified by the context.TargetPlatform argument. If an error occurs during compilation, PSProcessor throws an InvalidContentException, which appears in the Error List window of XNA Game Studio.
Implementing a Writer for the Compiled Shader
The final design-time class to implement is a writer that saves the compiled pixel shader produced by your processor as a binary .xnb file.
Using the New Item dialog box, add a new Content Type Writer item (called PSWriter) to the processor project.
Define the new class as follows:
[ContentTypeWriter] class PSWriter : ContentTypeWriter<CompiledPS> { protected override void Write(ContentWriter output, CompiledPS value) { output.Write(value.CompiledShader.Length); output.Write(value.CompiledShader); } public override string GetRuntimeType(TargetPlatform targetPlatform) { return typeof(PixelShader).AssemblyQualifiedName; } public override string GetRuntimeReader(TargetPlatform targetPlatform) { return "CPExtPixelShader.PSReader, CPExtPixelShader, Version=1.0.0.0, Culture=neutral"; } }
The GetRuntimeType method identifies the type of object your game should load from the .xnb file written by the writer object. In this instance, the .xnb file contains the binary array from your custom CompiledPS type, and this method identifies how that array will be mapped to a standard Framework.Graphics.PixelShader object type at load time.
The GetRuntimeReader method specifies what reader should be invoked to load the .xnb file in your game. It returns the namespace and name of the reader class, followed by the name of the assembly in which that class is physically located. In your code, change the assembly name to match the actual name of your game and its assembly, since that is where you will be loading the shaders.
At this point, the code for your PSProcessorLib library is complete.
Implementing a Reader for the Pixel Shaders
Now move from the PSProcessorLib library project back to your game project and write the class that your game uses to load the .xnb files that your processor creates. This is the class that your writer specified above as its reader.
In your game project, add a C# class called PSReader to your game project, and add the using statements you will need at the top of the file:
using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics;
Deriving from the ContentTypeReader generic class for the PixelShader type, override the Read method, and define your class as follows:
class PSReader : ContentTypeReader<PixelShader> { /// <summary> /// Loads an imported shader. /// </summary> protected override PixelShader Read(ContentReader input, PixelShader existingInstance) { int codeSize = input.ReadInt32(); byte[] shaderCode = input.ReadBytes(codeSize); IGraphicsDeviceService graphicsDeviceService = (IGraphicsDeviceService)input.ContentManager.ServiceProvider.GetService( typeof(IGraphicsDeviceService)); return new PixelShader(graphicsDeviceService.GraphicsDevice, shaderCode); } }
At this point, build the processor project. Once it has completed, you are ready to use the new importer and processor to build pixel shaders into your game.
Using the Output of the New Processor in Your Game
Try adding a test HLSL source file with a .psh extension to your game project and see how it works:
- Copy some simple HLSL source file that you know to be free of bugs into a folder in your game project and rename it to "Ripple.psh."
- Right-click on your game project in Solution Explorer, select Add, then Existing Item, and then select Ripple.psh.
- Once the file is added, right-click on it in Solution Explorer and select Properties.
- You should now see entries in its Properties dialog box assigning PSImporter as its Content Importer and PSProcessor as its Content Processor.
Next time you build your game, Ripple.psh will be built into TestShader.xnb in a form appropriate for your target platform.
To use the resulting pixel shader in your game, load it using ContentManager.Load as follows:
PixelShader shader = content.Load<PixelShader>( "TestShader" );
Tips for Developing Custom Importers
The following discusses some helpful information when developing content pipeline extensions.
Tips for Importing Basic Graphics Objects
These rules are helpful when importing basic graphics objects.
- Make your coordinate system right-handed. This means that, to the observer, the positive x-axis points to the right, the positive y-axis points up, and the positive z-axis points toward you (out from the screen).
- Create triangles that have a clockwise winding order. The default culling mode removes triangles that have a counterclockwise winding order. Call SwapWindingOrder to change the winding order of a triangle.
- Set the scale for graphical objects to 1 unit = 1 meter. Call TransformScene to change the scale of an object.
Take Advantage of Content Pipeline Mesh Classes
There are several properties and classes that are particularly useful when using NodeContent objects to represent a 3D scene or mesh.
- The NodeContent.Children property represents hierarchical information.
- The NodeContent.Transform property contains the local transform of the 3d object.
- The Pipeline.Graphics.MeshContent class (a subclass of Pipeline.Graphics.NodeContent) is used to represent meshes.
The Content Pipeline provides two classes that make it easier to create and work with Pipeline.Graphics.MeshContent objects.
- The Pipeline.Graphics.MeshBuilder class creates new Pipeline.Graphics.MeshContent objects when necessary.
- The Pipeline.Graphics.MeshHelper class implements useful operations on existing Pipeline.Graphics.MeshContent objects.
Debugging Custom Importers and Processors
Similar to projects that create a DLL, content pipeline extension projects cannot be directly run or debugged. However, after completing a few simple steps, you can debug any custom importers and processors used by your game. The following procedure details these steps.
Note |
|---|
| The Start External program: control (located on the Debug page of a project's property pages) is not available in the Microsoft Visual C# 2005 Express Edition development environment. |
To Debug a Custom Importer or Processor
- Load an existing XNA Game Studio 2.0 content pipeline extension project (later referred to as ProjCP) containing the custom importers and/or processors to be debugged.
- Create a separate test game project (later referred to as "ProjG").
- In the References node of ProjG's nested content project, add a project-to-project reference to ProjCP.
- Add one or two appropriate items of test content to ProjG. Ensure they are set to use the importer or processor (in ProjCP) you wish to debug.
- Open the property pages for ProjCP.
- Click the Debug tab, and select Start external program:.
-
Enter the path to the local version of MSBuild.exe. For example, C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\msbuild.exe.
-
For the Command line arguments control, enter the path to ProjG's nested content project. If this path contains spaces, the entire path should be quoted.
- Set any required breakpoints in the importer or processor code in ProjCP.
- Build and debug ProjCP.
Debugging ProjCP causes MSBuild to compile your test content while running under the debugger, which in turn enables you to hit your breakpoints in ProjCP and step through your code.
Note