3.9 扩展图像内容处理器
问题
你想扩展默认的图像内容导入器控制像素,或者你想学习内容管道(content pipeline)。
解决方案
因为XNA已经提供了一个内容导入器将一个图像文件作为源并最终将它创建为一个Texture2D对象,你要做的只是扩展这个内容导入器。本教程中,你可以调用PixelBitmapContent辅助类的ReplaceColor方法,它是由内容管道框架提供的。
注意:如果你只对alpha颜色感兴趣,图像内容处理器会自动将品红色的像素变为透明。本教程主要是介绍如何扩展内容导入器。
工作原理
本教程主要介绍内容管道和如何扩展已经存在的内容处理器,所以,对内容管道的工作流程有所认识是很重要的,如图3-5所示。
内容(素材)管道(Content Pipeline)
在可以使用一个图像文件前,你需要首先加载它,加载过程包括读取字节,选择有用的数据,如有必要还要解压缩数据。
其他素材如3D模型也是如此,模型的数据从一个文件加载后,经过大量的数据处理,基于这些数据创建一个模型对象。
整个处理过程由磁盘上的一个文件开始,最终生成XNA可用的由内容管道管理的对象。事实上,每种素材都有各自的内容管道,如图3-5所示,一个完整的内容管道是由导入器 (importer),处理器(processor),串行化器(serializer)和反串行化器(deserializer)组成的。
图3-5 内容管道工作流
在编译时(按F5可以进行编译),源文件会从磁盘读取,它的内容会被处理,最终结果会被串行化至一个. xnb二进制文件。你可以在. exe 文件所在目录的Content子目录下找到这些.xnb文件。当游戏运行时,这个. xnb二进制文件会被反串行化,这样所有有用的信息变得可用而无需进一步的处理。这种方法最明显的优点是所有处理过程只需进行一次(在编译时),而不是在游戏运行时每一次都要进行处理。第二个优点是. xnb文件是平台无关的,你可以同时用在PC和Xbox 360平台上。
让我们分析一下编译过程,因为这个过程在每次编译项目时都会执行,此过程分成三个子过程:
- 导入器:读取源文件并提取有用的数据,这个数据是存储在一个指定的标准格式中的。例如对一个模型,标准格式是NodeContent对象,对一个图像,标准格式是TextureContent 对象。这种标准格式叫做DOM对象,你可以在表3-1中看到一个默认DOM对象的表格。
- 处理器:处理包含在DOM对象中的数据并生成在游戏中可用的对象。例如对模型来说,处理器可以添加法线数据,计算切线,设置effect等。
- 串行化器或TypeWriter:定义了如何从处理器的输出生成.xnb二进制文件。
这种方法的一个额外优点是当你想在编译过程改变点什么,你可能只需改变其中一个子过程。例如,你想创建一个XNA内置不支持的格式的模型,你只需写一个新的导入器,这个导入器读取文件并创建一个NodeContent对象,你可以把其他工作留给默认的内容管道组件,因为NodeContent 的默认处理器会从那儿获取你的对象。
在运行时,只有一个小过程需要被执行:
- 反串行化器或TypeReader:定义了如何从存储在.xnb文件的二进制数据流中构建游戏对象。因为这里不需要处理计算,相对于编译过程来说这个过程几乎不花费时间。
XNA自带了很多默认内容导入器和处理器。组合TextureImporter和TextureProcessor,你可以导入几乎任何图像格式。组合Ximporter或FbxImporter和ModelImporter,你可以导入. x 或. Fbx模型。
注意:分离导入器和处理器被证明很有用。Ximporter和FbxImporter都能从磁盘导入数据并将它们格式化为一个简单的NodeContent对象,这两种情况中都传递到ModelProcessor,由ModelProcessor进行繁重的工作。
表3-1 XNA Framework中的默认内容导入器和处理器
内容导入器 | 输入(文件) | 输出(DOM对象) |
TextureImporter | bmp, .dds, .dib, .hdr, .jpg, .pfm, .png,. ppm, . tga | TextureContent |
XImporter | .x | NodeContent |
FbxImporter | . fbx | NodeContent |
EffectImporter | . fx | EffectContent |
FontDescriptionImporter | .spritefont | FontDescription |
XmlImporter | .xml | User-defined |
XACT Project | .xap | Audio project |
内容处理器 | 输入(DOM对象) | 输出(Game OM) |
TextureProcessor | TextureContent | TextureContent |
ModelProcessor | NodeContent | ModelContent |
EffectProcessor | EffectContent | CompiledEffect |
PassThroughProcessor | Any | Any |
FontDescriptionProcessor | FontDescription | SpriteFontContent |
FontTextureProcessor | TextureContent | SpriteFontContent |
FontTextureProcessor | Texture2DContent | SpriteFontContent |
XNA内容管道带有在编译时将这些对象写入二进制文件的默认串行化器和运行时从一个二进制文件重建对象的反串行化器。化器。
使用默认内容管道框架的组件
写入/扩展一个内容处理器的关键是尽可能地复用已经存在于内容管道中的组件。
本教程中,你将扩展默认TextureProcessor使你可以在图像数据加载到XNA项目之前对它做出改变。在编译时,想从文件读取图像,将内容存储在一个2D数组中形成图像,你还想改变一些像素并将结果放在一个.xnb文件中。
当运行程序时,.xnb文件会从文件读取,并包含了你施加的改变。
首先,从磁盘读取文件并将它们转换为一个2D的颜色数组(无论图像文件的格式是什么),你应该使用默认的导入器。
然后,你需要扩展TextureProcessor。因为本教程关心的是如何设置一个自定义的内容管道,所以你只需将所有的黑色变成白色。更高级的应用请见下一个教程。
你要确保处理器的输出是一个TextureContent对象,这样你可以使用默认的串行化器将它保存为. xnb文件并可以使用默认反串行化器加载这个. Xnb文件。
图 3-6显示了整个过程。找到你将扩展的处理器,和从XNA中借用的默认组件。
图3-6 你将重写的内容管道处理器的位置
本书中,每次当你处理内容管道时我都会显示一张类似于图3-6的图片,这样你可以清楚地知道你将自己处理哪一部分。扩展一个已存在的内容处理器要扩展一个已存在的内容处理器,需要先进行几个步骤。虽然这些步骤对于一个有经验的.NET 程序员来说是很简单的,我还是将它们罗列出来,以后的教程中你可以参考这些步骤,本章接下来的部分会解释这些步骤。
- 在解决方案中添加一个新的内容管道项目。
- 在新项目中,添加对Microsoft.XNA. Framework.Content. Pipeline的引用。
- 添加管道命名空间。
- 指定要扩展的部分(即你要复写的方法)。
- 编译新的内容管道项目。
- 在主项目中添加新建的这个项目的引用。
- 对素材选择新建的处理器。
- 设置项目依赖项。
- 初始化所有东西后,在第4步中创建的方法中编写代码。
在解决方案中添加一个新的内容管道项目
要扩展或创建一个新的内容导入器/处理器,你需要在解决方案中添加一个新项目。右击你的解决方案选择Add→New Project,如图3-7所示。在弹出的对话框中,选择Content Pipeline Extension Library并起一个合适的名称,如图3-8所示。
图3-7 在解决方案中添加一个新项目
图3-8 创建一个content pipeline extension library
项目中有一个新文件,包含了你定义的命名空间,还包括一个默认的Processor类。新项目已经添加到了解决方案中,如图3-9所示。
图3-9 添加到解决方案中的Content pipeline项目
添加对Microsoft.XNA.Framework.Content.Pipeline的引用
在新项目中,确保对Microsoft. XNA. Framework. Content. Pipeline (version 2.0.0.0)的引用。打开Project菜单选择Add Reference。从列表中添加正确的引用,如图3-10所示。
图3-10 选择XNA pipeline引用
使用代码块添加Pipeline的命名空间
你还要使编译器链接到新的命名空间,添加下列代码块:
using System.IO; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Content.Pipeline; using Microsoft.Xna.Framework.Content.Pipeline.Graphics; using Microsoft.Xna.Framework.Content.Pipeline.Processors; using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
指定要扩展的部分
文件已经包含了一个ContentProcessor的模板,用以下代码替换这些代码,指定新处理器的输入和输出。所有这些自定义处理器使用它的基类(默认纹理处理器)处理输入,所以这个新ContentProcessor工作方式与默认纹理处理器是一样的:
namespace MyImagePipeline { [ContentProcessor(DisplayName = "ExtendedExample")] public class ExtentedTextureProcessor : TextureProcessor { public override TextureContent Process(TextureContent input, ContentProcessorContext context) { return base.Process(input, context); } } }
本教程中我们将扩展默认TextureProcessor,所以你需要从TextureProcessor类继承。由于这个原因,你的Process方法接受一个TextureContent对象并将一个TextureContent对象作为输出。
等一会儿你将编写一个真正的Process方法,但现在这个方法只从默认TextureProcessor类继承。
注意:ExtendedExample是你的新自定义处理器的名称。如果你省略了DisplayName标签,你的处理器会使用类的名称,本例中是ExtendedTextureProcessor。
注意:请确保将这个DisplayName放在[ContentProcessor]中。如果不是,XNA不会把这个类当成内容处理器。结果是当浏览可用内容处理器列表时你的处理器不会显示。
编译新的内容管道项目
在下一步你要将这个新项目的引用添加到主项目中,所以你需要首先编译这个项目。按F6可以进行编译,而按F5会编译并运行整个解决方案。
注意:这个编译过程要花几秒钟的时间;你可以在窗口的左下角看到这个处理过程的信息。
将新建的引用添加到主程序中
编译了自定义处理器后,你还需要将它应用到主程序中。在解决方案中找到Content,右击References并选择Add Reference,如图3-11所示。
图3-11 添加到自定义内容管道项目的引用
在弹出的对话框中,找到Projects选项卡,并从列表中选择你的内容管道项目,如图3-12所示。
图3-12 选择内容管道项目
注意:如果内容管道项目不在列表中,你肯定遗漏了前面的编译步骤。
选择新建的处理器处理一个图像文件
当你将一个图像文件导入到项目中时,你可以选择新建的内容处理器处理这个图像文件,如图3-13所示。
图3-13 选择自定义的内容处理器处理图像
现在当你编译项目时,你的自定义处理器就可以用来处理图像文件了!
设置项目依赖项
你对自定义内容处理器进行的每次改变都需手动重新编译项目。要解决这个问题,你可以让主项目依赖于内容管道项目,这样当你每次重新编译主项目时,内容管道项目会首先编译(如果在上次编译后你进行了某些改变)。你可以通过右击主项目选择Project Dependencies设置依赖项。在弹出的对话框中,你需要选择主程序依赖于第二个项目,如图3-14所示。这样,当你按F5编译主程序时,内容管道项目的. dll文件会首先被编译。
图3-14 选择项目依赖项
扩展默认纹理处理器
现在所有东西都进行了初始化,你可以编写自定义代码了。
你已经创建了一个ExtendedTextureProcessor类,这个类从默认的TextureProcessor类继承,你也声明了将复写Process方法。所有内容处理器这个方法都叫做Process并接受一个DOM 对象(在TextureProcessor的情况中是TextureContent对象,如表3-1所示) 和一个ContentProcessorContext对象作为参数。这个context对象用来创建多个生成(nested builds)。例如,当导入一个Model时,这个对象会包含所有纹理文件的名称,这些名称也需要和模型一起被加载,在教程4-12可学到更多这方面的知识。
如表格3-1所示,在TextureProcessor情况中,你需要返回一个TextureContent对象。这种情况中纹理作为处理器的输入和输出,现在的处理器只是将输入传递到基类中:
[ContentProcessor(DisplayName = "ExtendedExample")] public class ExtentedTextureProcessor : TextureProcessor { public override TextureContent Process(TextureContent input, ContentProcessorContext context) { return base.Process(input, context); } }
这一步使用TextureProcessor 类的Process方法处理输入(因为你从默认的TextureProcessor 类继承而来)并返回结果图像,所以这也做好了扩展这个处理过程的准备。直到现在,你只是简单地将输入直接传递到输出,所以你的处理器会获得和默认TextureProcessor同样的结果。
注意:这种情况下的TextureProcessor是特殊的,因为输入和输出对象的类型是一样的。在更复杂的情况下,你想让base. Process方法可以将输入对象转换为一个输出模型。例如,如果你扩展了模型处理器,你会首先让base. Process方法将输入的NodeContent对象转换为一个ModelContent对象,这包含大量的工作。一旦你有了默认的ModelContent对象,你就可以方便地扩展/改变它们。可见教程4-12学习一个扩展模型处理器的例子。
在本教程中,你不想让输入立即传递到输出,你想改变一些颜色值。使用标准DOM对象的一个好处是你可以使用已经定义好的默认内容管道。例如,TextureContent类有一个有用的ConvertBitmapType方法可以改变纹理内容的格式,下面的代码改变了texContent 对象内容的颜色:
TextureContent texContent = base.Process(input, context); texContent.ConvertBitmapType(typeof(PixelBitmapContent<Color>));
因为TextureContent类是抽象类,如果是一个2D图像,texContent对象会实例化为一个TextureContent2D对象(也可以是一个TextureContent3D或TextureContentCube对象)。这意味着一张图像可以有多个face和多个mipmap (可见教程3-7了解mipmap的知识)。下面的代码选择第一个face和第一个mipmap,一个简单的2D纹理只有一个face一个mipmap level。
PixelBitmapContent<Color> image = (PixelBitmapContent<Color>)input.Faces[0][0];
PixelBitmapContent类有一个有用的ReplaceColor方法,这个方法可以将图像中指定颜色替换成另一个颜色:
Color colorToReplace = Color.Black; image.ReplaceColor(Color.Black, Color.White);
这就是你的自定义处理器所做的工作,最后返回TextureContent对象:
return texContent;
在编译过程中,这个对象会被传递到串行化器,串行化器将这个对象保存为二进制文件。在运行过程中这个二进制文件有反串行化器加载并创建一个Texture2D对象。现在确保在XNA项目中导入一个图像,选择自定义处理器处理这张图像,并在LoadContent方法中加载这个Texture2D对象:
myTexture = Content.Load<Texture2D>("image");
多个Face/Mipmap
本例中你从TextureContent处理器继承,这个处理器会生成mipmap (可以代替SpriteTextureProcessor)。关于mipmap的更多信息可见教程3-7。如果导入的图像是一张立方图像,那么这个纹理有六个面。要使教程完整,还需要遍历face和mipmap的代码:
for (int face = 0; face < texContent.Faces.Count; face++) { MipmapChain mipChain = texContent.Faces[face]; for (int mipLevel = 0; mipLevel < mipChain.Count; mipLevel++) { PixelBitmapContent<Color> image = (PixelBitmapContent)input.Faces[face][mipLevel]; image.ReplaceColor(Color.Black, Color.White); } }
Faces属性的第一个索引是图像的face。标准2D图像只有一个face,而立方纹理有六个 face。第二个索引是mipmap level,不使用mipmapp的图像只有一个level。这个代码对纹理的每个face的每个mipmap level将你选择的颜色(本例中是黑色)变成白色(译者注:实际代码中是将小女孩蓝色的头发替换成黄色)。
代码
下面是扩展默认内容处理器的所有代码,记住要使代码正常工作,你需要在新项目中添加对Microsoft.XNA. Framework. Content. Pipeline的引用。
using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Content.Pipeline; using Microsoft.Xna.Framework.Content.Pipeline.Graphics; using Microsoft.Xna.Framework.Content.Pipeline.Processors; using System.IO; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; namespace MyImagePipeline { [ContentProcessor(DisplayName = "ExtendedExample")] public class ExtentedTextureProcessor : TextureProcessor { public override TextureContent Process(TextureContent input, ContentProcessorContext context) { TextureContent texContent = base.Process(input, context); texContent.ConvertBitmapType(typeof(PixelBitmapContent<Color>)); for (int face = 0; face < texContent.Faces.Count; face++) { MipmapChain mipChain = texContent.Faces[face]; for (int mipLevel = 0; mipLevel < mipChain.Count; mipLevel++) { PixelBitmapContent<Color> image = (PixelBitmapContent<Color>) input.Faces[face][mipLevel]; image.ReplaceColor(Color.Black, Color.White); } } return texContent; } } }
发布时间:2009/6/18 下午12:10:07 阅读次数:6056