2
votes

I'm building some custom content types with the XNA 4.0 content pipeline.

I have a custom ContentTypeWriter and ContentTypeReader for my classes TerrainModelSetContent and TerrainModelSet respectively, which are each the build time and run time classes.

A terrain model set of course includes/is a collection of models used for the parts of a level's terrain, and as such have one or more models serialized sequentially into a single .xnb content file.

Anyway, pretty much any XNA documentation or tutorials out there that I can find (from Microsoft or otherwise) make it pretty clear that XNA already comes with an out-of-the-box Writer and Reader for Models.

So my question is, why is it not preserving any of the actual model data between writing and reading? It only preserves the frivolous extra things like the BoundingSpheres and PrimitiveCounts--none of the actual geometry in the Vertex or Index buffers that it would have to preserve in order to be even remotely useful.

At build time, the TerrainModelSet gets serialized into a .xnb file via the Write(...) function of this class:

[ContentTypeWriter]
public class TerrainModelSetWriter : ContentTypeWriter<TerrainModelSetContent>
{
    protected override void Write(ContentWriter output, TerrainModelSetContent value) {
        //Write starting TerrainModelSet data...
        output.Write(value.GraphicsMeshes.Count); //value.GraphicsMeshes is a dictionary of string-keys and ModelContent-values.
        foreach (KeyValuePair<string, ModelContent> item in value.GraphicsMeshes) {
            output.Write(item.Key);
            //At this point, all geometry data is present in the Vertex and
            //Index buffers of the Model item.Value's ModelMesh's
            //ModelMeshParts, nice and neat how we would expect it. I made
            //sure if this with the debugger.
            output.WriteObject<ModelContent>(item.Value);
        }
    }
    //GetRuntimeReader(...) and GetRuntimeType(...) functions are overridden here as well.
}

And the TerrainModelSet is of course deserialized at run-time in the Read(...) method of this corresponding class:

public class TerrainModelSetReader : ContentTypeReader<TerrainModelSet>
{
    protected override TerrainModelSet Read(ContentReader input, TerrainModelSet existingInstance) {
        if (existingInstance == null)
            existingInstance = new TerrainModelSet();
        //Read starting TerrainModelSet data...
        int numItems = input.ReadInt32();
        for (int i = 0; i < numItems; i++) {
            string itemName = input.ReadString();
            Model m = input.ReadObject<Model>();
            //Here, we use the debugger again to check the state of m, and
            //find that the XNA Framework Content Pipeline has UTTERLY
            //FAILED to preserve ANY of the geomentry data.
            //All Vertex and Index buffers in any ModelMeshParts of any
            //ModelMeshes of m are null. Not even empty, just null. WTF?
            existingInstance.GraphicsMeshes.Add(itemName, m.Meshes[0]);
            existingInstance.CollisionMeshes.Add(itemName, CollisionMesh.FromModelMesh(m.Meshes[0]));
        }
    }
}

The geometry is all there before the Write call--nice and neat in its appropriate buffers. I've checked with the debugger. However, after the Read call, all the buffers in the meshes of m are null. They aren't even empty--just null. What is going on here? Can anyone enlighten me?

1

1 Answers

0
votes

Well, I've finally discovered my answer, and it says something ugly about XNA Game Studio.

To start with, I've learned from the documentation that there can be only one ContentTypeWriter<T> or ContentTypeReader<T> for any given type T. Supposedly, creating a second writer or reader for T will result in an InvalidOperationException being thrown as the pipeline can't decide which to use. This makes sense.

It also means that we can check if a Writer or Reader already exists by attempting to write one. So I did. I added classes that inherit from the following:

  • ContentTypeWriter<Model>
  • ContentTypeReader<Model>
  • ContentTypeWriter<ModelMesh>
  • ContentTypeReader<ModelMesh>

Everything ran without errors, indicating that the built in Type Writers and Readers are not available to user calls on output.WriteObject<T>(value). If we want to serialise these classes in our own custom/extended content pipeline, we must write those writers/readers all over again ourselves and re-invent the wheel. Very stupid, I know.

Anyhow, this brings us to another problem. As I grit my teeth and start implementing the Write and Read functions, I find that all the built in XNA graphics classes--Model, ModelMesh, ModelMeshPart, Bone, etc., and their build-time Content* classes--are read only and sealed with internal contructors, making any custom user instantiation and subsequent deserialization of these classes impossible.

The framework was PURPOSEFULLY designed to force users to completely re-write their own framework of graphics components. Why on EARTH would someone write a tool, and then make it so that users of the tool are supposed to re-write the tool all over again themselves, even if the existing tool suits their needs without modification? Stupid. Just Stupid.

OK, enough ranting. I'm leading up to my answer. I know the Writers and Readers in question exist--somewhere. After all, if you add model assets to the project using the default XImporter and ModelProcessor without doing ANY pipeline customization, (e.g. we let XNA serialize and deserialize our content all on its own without using WriteObject<Model>(model) to serialize multiple item together inline in a single file) it will work. Apparently though, these writers/readers are internal, and not used for custom operations.

I WOULD say that makes sense, so that the user can write their own classes without creating a conflict, except that, as stated before, that has been made impossible anyway.

So the answer is this: What I thought was too stupid to possibly be true, is true. The ONLY part of the out-of-the-box graphics content pipeline that is of any practical use whatsoever is the XImporter. All the rest must be completely re-written by the user if you wish to customize its functionality in any way.

  • You must write your own Model class, even if it is nothing but a duplicate of the built in one.
  • You must write your own ModelMesh class.
  • You must write your own ModelMeshPart class.
  • You must write your own ModelContent class.
  • You must write your own ModelMeshContent class.
  • You must write your own ModelMeshPartContent class.
  • You must write your own custom Type Writers and Readers for each.
  • You must ALSO write custom Writers and Readers for any built-in classes you are using that you CAN instantiate, such as:
    • Texture2DContent and Texture2D
    • VertexBufferContent and VertexBuffer
    • IndexCollection and IndexBuffer
    • EffectContent and Effect/BasicEffect
    • etc...
  • You must write your own custom ModelProcessor to convert the XImporter's NodeContent output into your own ModelContent and its sub-content classes.
  • Did I forget anything? Oh yes. You must write you own Draw(...), functions to make use of these classes. The built in Draw(GameTime) for the XNA Model is obviously useless to you.

As of the time of this post, I have just about completed these tasks, but still have a ways to go before I can test if this approach even works. (Need to make some content to test for one, since writing this new system has made a lot of my old content un-usable). I will edit this answer with the results when (if) I get it working.

If anyone would like me to post the final working classes somewhere as a tutorial when they are ready, please comment. I would be more than happy to.

P.S. -- The reason the framework was able to write/read models at all before I tried to write custom writers/readers for them, was because the framework uses reflection to generate a writer/reader for unknown types the first time WriteObject<T>(tObj) or ReadObject<T>() is called. This however, only handles properties, and loses data obtained by method calls such as VertexBuffer.GetData(...). Furthermore, VertexBuffer and IndexBuffer don't have default constructors, making it impossible for the reflection-based deserializer to know how to make them. This is why my VertexBuffers and IndexBuffers were all winding up null (I'm pretty sure that's why anyway).