How to: Write a Custom Importer and Processor
To add support for a new art asset file format, you may need to write a custom processor, serializer, and reader as well as a custom importer.
XNA Game Studio Express already provides standard Content Pipeline importers and processors to support common art-asset file formats, as described in Standard Importers and Processors, and third parties also provide custom importers and processors to support additional formats. 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 Express, but processing individual pixel shaders rather than complete effects. This a good, simple example to illustrate the steps you'd take to write an importer and processor, along with the serializer and reader needed to save and load the results. The sections below describe each of the steps in turn.
- Creating a New Project
- Writing a Simple Importer
- Writing a Processor to Compile the Shader
- Writing a Serializer for the Compiled Shader
- Writing a Reader for the Pixel Shaders
- Configuring and Building
- Using the Output of the New Processor in Your Game
Creating a New Project
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 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 Express, load the solution in which you are developing your game (for this example, let's assume it's called
GameUsingCustomPixelShaders
). - From Solution Explorer, right-click the Solution node, then click Add, and then click New Project.
- From the Add New Project dialog box, select the Windows Game Library template, assign a name to the new project at the bottom of the dialog box (name this project
PixelShaderProcessorLib
), and click Ok. - Although the new library project already contains a reference to the XNA Framework run-time assemblies, it also needs to reference the design-time Content Pipeline assembly. To add the new reference:
- Right-click the References node of the new processor project and click Add Reference.
- From the .NET tab of this dialog box, click Microsoft.Xna.Framework.Content.Pipeline, and then click OK.
Once you have added the Content Pipeline reference, the new project is ready for your custom importer and processor.
Writing a Simple Importer
To begin with, create a class to hold the input data you're importing, which in this case takes the form of a string of HLSL source code.
Add a new C# class named PixelShaderSourceCode
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 PixelShaderSourceCode
{
public PixelShaderSourceCode( 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 PixelShaderSourceCode
class.
Add a new C# class to the called PixelShaderImporter
to the project, and once again add the using
statements you will need at the top of the file:
using Microsoft.Xna.Framework.Content.Pipeline;
using System.IO;
Now define the class as follows:
[ContentImporter( ".psh", DefaultProcessor = "PixelShaderProcessor" )]
class PixelShaderImporter : ContentImporter<PixelShaderSourceCode>
{
public override PixelShaderSourceCode Import( string filename, ContentImporterContext context )
{
string sourceCode = File.ReadAllText( filename );
return new PixelShaderSourceCode( sourceCode );
}
}
The ContentImporter attribute applied to the PixelShaderImporter
class provides some context for the user interface of XNA Game Studio Express. Since this importer supports files with a .psh extension, XNA Game Studio Express will automatically select the PixelShaderImporter
importer when a .psh file is added to the project. In addition, the DefaultProcessor argument specifies the correct processor XNA Game Studio Express selects when a .psh file is added.
Writing a Processor to Compile the Shader
After the new importer has read in 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 CompiledPixelShader
to the project, and define the new class as follows:
class CompiledPixelShader
{
public CompiledPixelShader( byte[] compiledShader )
{
this.compiledShader = compiledShader;
}
private byte[] compiledShader;
public byte[] CompiledShader { get { return (byte[])compiledShader.Clone(); } }
}
Now you're ready to write the processor class itself, which converts a PixelShaderSourceCode
object into a CompiledPixelShader
object. Add a C# class called PixelShaderProcessor
to the project, and add the using
statements you will need at the top of the file.
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Graphics;
Now define the class as follows:
[ContentProcessor]
class PixelShaderProcessor : ContentProcessor<PixelShaderSourceCode, CompiledPixelShader>
{
public override CompiledPixelShader Process( PixelShaderSourceCode 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 CompiledPixelShader( shader.GetShaderCode() );
}
}
The Framework.Graphics.ShaderCompiler class performs the actual compilation to code that will run on the platform your game targets, which is specified by the context.TargetPlatform argument. If an error occurs during compilation, PixelShaderProcessor
throws an InvalidContentException, which appears in the Error List pane of XNA Game Studio Express.
Writing a Serializer for the Compiled Shader
The final design-time class to implement is a serializer that saves the compiled pixel shader produced by your processor as a binary .xnb file.
Add a C# class called PixelShaderWriter
to the project, and add the using
statements you will need at the top of the file:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
using Microsoft.Xna.Framework.Graphics;
Deriving from ContentTypeWriter and overriding the Write method, define the new class as follows:
[ContentTypeWriter]
class PixelShaderWriter : ContentTypeWriter<CompiledPixelShader>
{
protected override void Write( ContentWriter output, CompiledPixelShader 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 "GameUsingPixelShaders.PixelShaderReader, MyGameAssembly, Version=1.0.0.0, Culture=neutral";
}
}
The GetRuntimeType method identifies the type of object your game should load from the .xnb file that your serializer writes. In this instance, the .xnb file contains the binary array from your custom CompiledPixelShader
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 PixelShaderProcessorLib
library is complete.
Writing a Reader for the Pixel Shaders
Now move from the PixelShaderProcessorLib
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 serializer specified above as its reader.
In your game project, add a C# class called PixelShaderReader
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 PixelShaderReader : ContentTypeReader<PixelShader>
{
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 );
}
}
You're ready to configure and build the importer and processor that you have created.
Configuring and Building
Setting project dependencies helps ensure that build steps happen in the right order. In this case, you want your PixelShaderProcessorLib
library to be built before your game is, because it will actually be used in the game build. Here's how to establish that dependency:
- Right-click on your game project and click Project Dependencies.
- In the Project Dependencies dialog, click the check box next to
PixelShaderProcessorLib
in the Depends on: pane. - Click OK.
Now build your solution so as to create the library assembly to install in XNA Game Studio Express.
Before you build, set PixelShaderProcessorLib
to Release configuration:PixelShaderProcessorLib
- On the Build menu, choose Configuration Manager....
- In the Configuration column of the Configuration Manager dialog, set the value for
PixelShaderProcessorLib
to Release. At the same time, make sure that its target platform is x86. - Click Close.
Now build your solution. Once you have built successfully, you can install your new importer and processor in your project build as follows:
- Double-click the Properties node of your game project.
- In the Properties window, click the Content Pipeline tab.
- Click the Add button and browse for the
PixelShaderProcessorLib.dll
file that you just built. It should be located in thePixelShaderProcessorLib\bin\x86\Release
folder under your main project folder. - Save and close the Properties window.
At this point, you're 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
TestShader.psh
. - Right-click on your game project in Solution Explorer, select Add, then Existing Item..., and then select
TestShader.psh
. - Once the file is added, right-click on it in Solution Explorer and select Properties.
- You should now see entries in its Properties sheet assigning PixelShaderImporter as its Content Importer and PixelShaderProcessor as its Content Processor.
Next time you build your game, TestShader.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" );
See Also
Overview of the Content Pipeline
Content Pipeline Architecture
Extending an XNA Framework Standard Processor