我正在使用XNA 4.0内容管道构建一些自定义内容类型。
我的类TerrainModelSetContent
和TerrainModelSet
分别有一个自定义的ContentTypeWriter
和ContentTypeReader
,它们分别是构建时类和运行时类。
地形模型集当然包括/是用于一个级别的地形部分的模型集合,因此具有一个或多个按顺序序列化为单个的模型xnb内容文件。
不管怎样,我能找到的几乎所有XNA文档或教程(来自微软或其他公司)都清楚地表明,XNA已经配备了现成的模型编写器和阅读器。
所以我的问题是,为什么它在写作和阅读之间没有保留任何实际的模型数据?它只保留了像BoundingSpheres
和PrimitiveCounts
这样琐碎的额外东西——顶点或索引缓冲区中没有任何实际的几何体,它必须保留这些几何体才能使用。
在构建时,TerrainModelSet
被序列化为xnb文件通过此类的Write(...)
函数:
[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.
}
当然,TerrainModelSet
在运行时在相应类的Read(...)
方法中被反序列化:
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]));
}
}
}
在Write调用之前,几何体就在那里了——在适当的缓冲区中,它既漂亮又整洁。我已与调试器进行了核对。但是,在Read调用之后,m的网格中的所有缓冲区都为null。它们甚至不是空的——只是空的。这是怎么回事?有人能启发我吗?
好吧,我终于找到了答案,它说明了XNA Game Studio的丑陋之处。
首先,我从文档中了解到,对于任何给定的类型T
,只能有一个ContentTypeWriter<T>
或ContentTypeReader<T>
。据推测,为T
创建第二个写入程序或读取器将导致抛出InvalidOperationException
,因为管道无法决定使用哪个。这是有道理的。
这也意味着我们可以通过尝试写一个Writer或Reader来检查它是否已经存在。我照做了。我添加了继承自以下的类:
ContentTypeWriter<Model>
ContentTypeReader<Model>
ContentTypeWriter<ModelMesh>
ContentTypeReader<ModelMesh>
所有操作都运行无错误,表明内置的类型写入器和读卡器不可用于output.WriteObject<T>(value)
上的用户调用。如果我们想在我们自己的自定义/扩展内容管道中序列化这些类,我们必须自己重新编写这些作者/读者,并重新发明轮子。非常愚蠢,我知道。
无论如何,这给我们带来了另一个问题。当我咬紧牙关开始实现Write和Read函数时,我发现所有内置的XNA图形类——Model
、ModelMesh
、ModelMeshPart
、Bone
等,以及它们的构建时Content*
类——都是只读的,sealed
带有internal
构造函数,这使得这些类的任何自定义用户实例化和随后的反序列化都是不可能的。
该框架的设计初衷是迫使用户完全重写自己的图形组件框架。为什么有人会写一个工具,然后制作它,让工具的用户自己重新编写工具,即使现有的工具不需要修改就可以满足他们的需求?愚蠢的只是愚蠢。
好吧,够了。我正准备回答。我知道有问题的作家和读者存在于某个地方。毕竟,如果您使用默认的XImporter
和ModelProcessor
将模型资产添加到项目中,而不进行任何管道自定义(例如,我们让XNA单独序列化和反序列化我们的内容,而不使用WriteObject<Model>(model)
在单个文件中内联序列化多个项),它将起作用。显然,这些编写器/读取器是内部的,不用于自定义操作。
我想说这是有道理的,这样用户就可以在不产生冲突的情况下编写自己的类,除非如前所述,这无论如何都是不可能的。
所以答案是:我认为太愚蠢而不可能是真的,是真的。开箱即用的图形内容管道中唯一具有实际用途的部分是XImporter
。如果您希望以任何方式自定义其功能,则用户必须完全重写其余部分。
- 您必须编写自己的
Model
类,即使它只是内建类的副本 - 您必须编写自己的
ModelMesh
类 - 您必须编写自己的
ModelMeshPart
类 - 您必须编写自己的
ModelContent
类 - 您必须编写自己的
ModelMeshContent
类 - 您必须编写自己的
ModelMeshPartContent
类 - 您必须为每个类型编写自己的自定义类型编写器和读取器
- 您还必须为您正在使用的任何可以实例化的内置类编写自定义编写器和读取器,例如:
Texture2DContent
和Texture2D
VertexBufferContent
和VertexBuffer
IndexCollection
和IndexBuffer
EffectContent
和Effect
/BasicEffect
- 等等
- 您必须编写自己的自定义
ModelProcessor
,才能将XImporter
的NodeContent
输出转换为自己的ModelContent
及其子内容类 - 我忘了什么吗?哦,是的。您必须编写自己的
Draw(...)
函数才能使用这些类。XNA模型的内置Draw(GameTime)
对您来说显然毫无用处
截至本文撰写之时,我刚刚完成了这些任务,但在测试这种方法是否有效之前,还有一段路要走。(需要制作一些内容来测试,因为编写这个新系统使我的许多旧内容无法使用)。当(如果)我让它工作时,我会编辑这个答案和结果。
如果有人想让我在他们准备好后把最后的工作课作为教程发布在某个地方,请评论。我非常乐意。
p。S.——在我尝试为其编写自定义编写器/读取器之前,该框架之所以能够编写/读取模型,是因为该框架在第一次调用WriteObject<T>(tObj)
或ReadObject<T>()
时使用反射生成未知类型的编写器/阅读器。然而,这只处理属性,并丢失通过方法调用(如VertexBuffer.GetData(...)
)获得的数据。此外,VertexBuffer
和IndexBuffer
没有默认构造函数,这使得基于反射的反序列化程序无法知道如何生成它们。这就是为什么我的VertexBuffers
和IndexBuffers
都为空的原因(我很确定这就是为什么)。