Custom Model Class Sample
This sample shows how to go beyond the limits of the Model class that comes built into the XNA Framework, loading geometry data into a custom class that can be extended more easily to cope with specialized requirements.Sample Overview
The Model class that comes built into the XNA Framework provides a convenient way to load and display graphics, but it is not especially flexible. Model can be extended in a limited manner by attaching custom data to the Tag property (in fact, many other samples such as the Skinned Model sample or Picking with Triangle-Accuracy do just that), but beyond a certain point, trying to cajole Model into handling scenarios it was never really designed for can become more trouble than it is worth.
Fortunately, the layered design of the Content Pipeline makes it easy to replace Model with a custom (and, hence, more easily extensible) alternative. There is no need to alter the importer behavior, so our custom model replacement can still import data from standard file formats such as .X or .FBX. This sample implements a CustomModel class, along with a CustomModelProcessor that extracts the necessary geometry data from the NodeContent format that was output by the importer.
Sample Controls
This sample uses the following keyboard and gamepad controls.
| Action | Keyboard control | Gamepad control |
|---|---|---|
| Exit the sample | ESC or ALT+F4 | BACK |
How the Sample Works
Creating a Custom Processor
The sample contains two projects, one containing the game code, and one containing the custom Content Pipeline processor. The game project has versions for both Windows and Xbox 360, but the Content Pipeline processor project isfor Windows only. The Content Processor always runs on a Windows computer as part of the build process, even when the output content is going to be used on Xbox 360. For more information about creating your own custom processor project, see the topic "How to: Write a Custom Importer and Processor" in the XNA Game Studio documentation.
If you want to reuse the custom processor from this sample, but would like to move the CustomModel class into a different main game assembly, you must update the CustomModelContent class to correctly reference this new assembly, by changing its ContentSerializerRuntimeType attribute.
Building the Custom Model Data
When you build an asset that has been set to use the CustomModelProcessor, data flows through the Content Pipeline in this order:
- First, an importer is used to read model data from a file format such as .X or .FBX. This functionality comes built into the framework, so we do not need to do anything special to enable it.
- The importer creates a tree of NodeContent objects. This lets you access the geometry data via a standardized managed object model, regardless of which file format it was originally imported from.
-
The tree of NodeContent data is passed into our CustomModelProcessor.Process method. This recurses over the input data, performing various actions:
- It uses MeshHelper.TransformScene to bake any local transform data into the vertex positions. In the interest of simplicity, this sample does not bother supporting local transforms in the run-time CustomModel class, so we want to get rid of any such data in the processor.
- It examines each NodeContent to see which ones are MeshContent instances.
- It calls MeshHelper.OptimizeForCache, which reorders the vertex and index data into a format that will be more efficient to render.
- It loops over all the GeometryContent data inside each MeshContent.
- It calls VertexContent.CreateVertexBuffer to flatten vertex data from the flexible multiple channel input format into a simple byte array ready to be fed to the GPU.
- It chains to the MaterialProcessor class that comes built into the framework, using this to convert whatever material is attached to the geometry. This processor will automatically go off and build any effects or textures that are referenced by the material. When you load the resulting material at run time, you will get back an Effect instance that has the appropriate textures already loaded into it and ready to go.
- Finally, it stores the result of all this work as part of the output CustomModelContent.
- CustomModelProcessor.Process returns an instance of our new type, CustomModelContent. This class is similar in shape to the run-time CustomModel, but stores the data as simple managed objects rather than GPU data types. This avoids having to instantiate any actual GPU objects during the Content Pipeline build process, which is essential when building graphics for Xbox 360. The build always runs on Windows, and it would be problematic if we tried to instantiate Xbox types on the Windows GPU during this process!
- Our CustomModelContent object is passed to the Content Pipeline XNB serializer, which saves the data in a binary XNB file. This serialization process is controlled by the ContentSerializerRuntimeType and ContentSerializer attributes that are attached to the CustomModelContent class. The SharedResource flag, for example, tells the serializer that the MaterialContent property should be serialized in a special way.
- At run time, the sample game calls ContentManager.Load<CustomModel> . This deserializes data from the XNB file into our CustomModel class. For this to work, it is important that the design-time CustomModelContent class and the run-time CustomModel class have the same data fields in the same order, and the same ContentSerializer control attributes.
- CustomModel contains a vertex buffer and index buffer storing the geometry data, as well as an effect containing texture and material settings. It provides a simple Draw method to display the model.
A subtle point to notice is that although the design time CustomModelContent class looks similar to the run-time CustomModel, not all fields are of the same type! At design time, data is stored in managed types provided by the Content Pipeline, while the run-time CustomModel uses actual GPU types from the Microsoft.Xna.Framework.Graphics namespace. The Content Pipeline knows about the relationship between the design-time types and run-time types, so it can automatically translate one to the other. At design time, we can use types such as VertexBufferContent, but when we read this data back in at run time, it will be loaded into the corresponding VertexBuffer class. The following table lists some of the graphics types that are translated from design-time versions to run-time versions in this way.
| Design-time type | Run-time type |
|---|---|
| VertexBufferContent | VertexBuffer |
| IndexCollection | IndexBuffer |
| TextureContent | Texture |
| EffectMaterialContent | Effect |
| BasicMaterialContent | BasicEffect |
| CompiledEffect | Effect |
Extending the Sample
This sample discards any nested transforms from the input scene by calling the MeshHelper.TransformScene method at the top of CustomModelProcessor.ProcessNode. You might prefer to retain this data, saving out the local transform matrices for use at run time.
The CustomModelProcessor.ProcessMaterial method just chains to the built-in MaterialProcessor class. You might prefer to customize this to build your effects in a more specialized way, or perhaps to use some different material format altogether. Materials do not have to be effects at all if you want to use some other system!
You could add extra data to the custom model—for example, animation or collision information.
There is a great deal of potential for a smart processor to optimize the geometry data. You could merge the vertex and index data for each ModelPart into larger vertex and index buffers by using different regions of those single buffers for each part. You could detect when more than one ModelPart shares the same vertex declaration, and combine any duplicate declarations. You could even detect if more than one ModelPart uses the same material and vertex declaration, merging the ModelPart data entirely! You also could optimize the size of the vertex data by removing unused channels, or by compressing channels to use more compact data formats.