Export (0) Print
Expand All

How To: Write a Custom Importer and Processor

Describes how a content importer adds support for a new art asset file format.

You will need to write a new content importer to add support. Also, you may need to write a custom processor, writer, and reader for the new art asset type after it has been imported.

The Complete Sample

The code in this topic shows you the technique. You can download a complete code sample for this topic, 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.

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.

For example, suppose you want to compile HLSL source files into pixel shaders. You want the result to be somewhat like the EffectImporter and EffectProcessor classes built into XNA Game Studio, but you want to process individual pixel shaders rather than complete effects. This topic provides a simple example to show you the steps you take to write an importer and processor, and also the writer and reader you need to save and load the results. The sections below describe each of the steps.

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. They 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.

To create a content pipeline extension library

  1. In XNA Game Studio, load a game solution you are developing (the sample uses "CPExtPixelShader").

  2. From Solution Explorer, right-click the Solution node, click Add, and then click New Project.

  3. From the Add New Project dialog box, under the Visual C# node, from the Project types: pane, select the XNA Game Studio 3.0 node.

  4. Select the Content Pipeline Extension Library (3.0) template, assign a name to the new project at the bottom of the dialog box (name this project PSProcessorLib), and click OK.

  5. From Solution Explorer, right-click the ContentProcessor1.cs item, and click Delete.

    The remaining steps create a reference to the PSProcessorLib content extension project.

  6. From Solution Explorer, right-click the Content node of the CPExtPixelShader project, and then click Add Reference.

  7. From the Projects tab, select your content extension project, and click OK.

The new project is now ready for your custom importer and processor implementation.

Implementing a Simple Importer

Follow these steps to add a content importer to your processor.

To implement a simple importer

  1. Create a class to hold the input data you are importing.

    In this case, it takes the form of a string of HLSL source code.

  2. 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;
    
  3. Now define the class as follows:

    class PSSourceCode
    {
        public PSSourceCode(string sourceCode)
        {
            this.sourceCode = sourceCode;
        }
    
        private string sourceCode;
        public string SourceCode { get { return sourceCode; } }
    }
    
  4. Write an importer class to import the HLSL source code.

    This class must be derived from ContentImporter and implement the Import method. All it does is read a text file containing HLSL source code into your PSSourceCode class.

  5. Using the New Item dialog box, add a new Content Importer item (called PSImporter) to the processor project.

  6. 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 automatically selects the PSImporter importer when a .psh file is added to the project. In addition, the DefaultProcessor argument specifies which processor XNA Game Studio selects when a .psh file is added.

Bb447754.note(en-US,XNAGameStudio.30).gifNote
To specify multiple file types, separate with a comma the file extensions listed in the ContentImporterAttribute. 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.

Bb447754.note(en-US,XNAGameStudio.30).gifTip
An importer that generates DOM objects may also automatically generate an intermediate XML cache file that serializes these objects. For this to happen, the importer must be 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

  1. Create a class to store the compiled output, which in this case takes the form of an array of bytes.

  2. 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.

  3. Using the New Item dialog box, add a new Content Processor item (called PSProcessor) to the processor project.

  4. 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 compiles the shader to binary code that runs on the platform targeted by your game. The context.TargetPlatform argument targets the platform. If an error occurs during compilation, PSProcessor throws an InvalidContentException. The error 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.

To implement the writer for the compiled shader

  1. Using the New Item dialog box, add a new Content Type Writer item (called PSWriter) to the processor project.

  2. 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.

  3. 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 previously as its reader.

To implement a reader for the pixel shaders

  1. In your game project, add a C# class called PSReader to your game project.

  2. Add the using statements you will need at the top of the file:

    using Microsoft.Xna.Framework.Content;
    using Microsoft.Xna.Framework.Graphics;
    
  3. 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);
        }
    }
    
  4. 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.

To test your output

  1. Copy into a folder in your game project a simple HLSL source file that you know is free of bugs, and rename the file "Ripple.psh."

  2. Right-click on your game project in Solution Explorer, click Add, click Existing Item, and then click Ripple.psh.

  3. Once you add the file, right-click it in Solution Explorer, and click 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.

  4. 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 information should help you when you develop content pipeline extensions.

Importing Basic Graphics Objects

The following information should help you import basic graphics objects.

  • Make your coordinate system right-handed.

    From the standpoint of 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.

Taking 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 content pipeline provides two classes that make it easier to create and work with Pipeline.Graphics.MeshContent objects.

Debugging Custom Importers and Processors

In a way that is 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.

Bb447754.note(en-US,XNAGameStudio.30).gifNote
The Start External program: control (located on the Debug page of a project's property pages) is not available in the Microsoft Visual C# Express Edition development environment.

To Debug a Custom Importer or Processor

  1. Load an existing XNA Game Studio 3.0 content pipeline extension project (later referred to as ProjCP) containing the custom importers and/or processors to be debugged.

  2. Create a separate test game project (later referred to as "ProjG").

  3. In the References node of ProjG's nested content project, add a project-to-project reference to ProjCP.

  4. Add one or two appropriate items of test content to ProjG, and ensure they are set to use the importer or processor (in ProjCP) you wish to debug.

  5. Open the property pages for ProjCP.

  6. Click the Debug tab, and select Start external program:.

  7. Enter the path to the local version of MSBuild.exe.

    For example, C:\WINDOWS\Microsoft.NET\Framework\v3.5\msbuild.exe.

  8. For the Command line arguments control, enter the path to ProjG's nested content project.

    If this path contains spaces, quote the entire path.

  9. Set any required breakpoints in the importer or processor code in ProjCP.

  10. Build and debug ProjCP.

Debugging ProjCP causes MSBuild to compile your test content while running under the debugger. This enables you to hit your breakpoints in ProjCP and step through your code.

Community Additions

ADD
Show:
© 2015 Microsoft