问题
你想使用一个自定义内容处理器改变模型的effect而不是实时处理。通过这种方式,模型可以正确地加载到XNA项目中,而无需在实时保存原始effect中的所有纹理和其他信息。
注意:如果你想学习如何扩展默认模型处理器,最好先看下一个教程。
解决方案
扩展默认模型处理器并重写ConvertMaterial方法,这个方法被模型中的每个MaterialContent调用,把这个方法重写将所有的MaterialContent信息传递到自定义材质处理器。
在这个自定义材质处理器中,你将创建一个空的材质对象,然后将自定义的effect链接到这个对象。你也可以将原始MaterialContent的纹理复制到这个新对象,这个就可以设置effect的所有HLSL变量了。
工作原理
因为这个教程需要扩展默认内容处理器,所以请确保已经读过了教程3-9中内容管道的知识。首先建立一个新的内容管道项目,具体解释可见教程3-9中的“扩展一个已存在的内容处理器”一节。
本教程中,你将扩展默认模型处理器,如图4-16所示。当默认模型导入器从磁盘读取一个模型并创建一个NodeContent对象时,它保存了MaterialContent对象中的effect。你的模型处理器只改变这些MaterialContent对象并存储在NodeContent中,NodeContent对象的其他部分保持不变。
图4-16 内容管道中的自定义模型处理器
注意:如果你比较一下图4-15和图3-6,你会发现你也是扩展了处理器。但是,因为这个处理器处理的是模型而不是纹理,所以它的输入和输出是不同的。
通过扩展Process方法,你可以在模型对象加载到XNA项目之前完全控制它的内容。在本教程中,因为effect是存储在材质信息中的,所以你想改变处理模型中材质的方式。在自定义模型处理器类中,你无需重写Process方法,但要重写ConvertMaterial方法,每次当模型加载材质时默认处理器都会调用这个方法。
所以用以下代码替换Process方法:
[ContentProcessor] public class ModelCustomEffectProcessor : ModelProcessor { protected override MaterialContent ConvertMaterial(MaterialContent material, ContentProcessorContext context) { return context.Convert<MaterialContent, MaterialContent>(material, "MaterialCustomEffectProcessor"); } }
以上代码创建一个ModelCustomEffectProcessor类。因为你只重写了ConvertMaterial方法,所以这个处理器会使用与默认模型处理器同样的方式处理几何数据。在ConvertMaterial 方法中,你指定材质需要被MaterialCustomEffectProcessor类处理。
在ModelCustomEffectProcessor类之后,添加这个MaterialCustomEffectProcessor类:
[ContentProcessor] public class MaterialCustomEffectProcessor : MaterialProcessor { public override MaterialContent Process(MaterialContent input, ContentProcessorContext context) { return base.Process(input, context); } }
从参数可以看出这个处理器处理的是材质。通过从材质处理器继承,你以一个MaterialContent作为输入,然后进行处理,返回更新过的MaterialContent对象。这次,因为你想改变处理MaterialContent对象的方式,所以需要重写MaterialCustomEffectProcessor的Process方法。
注意:在ModelCustomEffectProcessor类中,你重写了NodeObject 中的所有MaterialContent对象的处理方式。与之类似,因为纹理和effect也是存储在MaterialContent 对象中的,你也可以通过重写MaterialCustomEffectProcessor 类中的BuildEffect或BuildTexture方法改变EffectContent和TextureContent对象的处理方式。
现在Process方法只是调用了它的基类MaterialProcessor。当然,你需要改变这个方法让你可以改变MaterialContent的处理过程。
现在,将Process方法的内容改为以下代码:
EffectMaterialContent myMaterial = new EffectMaterialContent(); string map = Path.GetDirectoryName(input.Identity.SourceFilename); string effectFile = Path.Combine(map, "colorchannels.fx"); myMaterial.Effect = new ExternalReference<EffectContent>(effectFile);
上述代码会创建一个空的EffectMaterialContent对象并存储一个指向自定义effect文件的链接。首先找到模型存储的文件夹并附加上effect文件的名称,当编译器运行到这行代码时,会将这个effect添加到素材列表中,这样可以自动加载默认EffectProcessor使用的effect,但你也确保effect文件存储在与模型文件相同的目录中。
大多数模型还包含纹理,你需要将原始MaterialContent对象转换成新的。本例中使用的effect从一个叫做xTexture的纹理中采样颜色,让你可以单独设置红绿篮颜色通道的强度。大多数模型每个effect只有一张纹理,但有些有多张纹理(例如,一个凹凸映射,可见教程5-15)。因为没有和原始effect的纹理序号联系,所以这个代码只能获取xTexture 变量的最后一个纹理,但代码也展示了如何变量多个纹理的方法。
你可以访问原始effect的纹理集合并将这些纹理变量绑定到自定义effect的HLSL纹理变量上。
if (input.Textures != null) foreach (string key in input.Textures.Keys) myMaterial.Textures.Add("xTexture", input.Textures[key]);
如果你想在加载模型时调整某些HLSL参数,下面是方法:
myMaterial.OpaqueData.Add("xRedIntensity", 1.1f); myMaterial.OpaqueData.Add("xGreenIntensity", 0.8f); myMaterial.OpaqueData.Add("xBlueIntensity", 0.8f);
注意:在HLSL effect文件中,每个像素的红、绿、篮颜色通道需要乘以这个变量。上面的代码让红色比其他颜色强度更大,模拟出太阳照射的效果。这在简单的post-processing effect也是很有用的!
最后需要返回新建的MaterialContent。你可以直接将它返回,但最好将这个对象传递到默认材质处理器,这样做可以保证错误(例如你没有指定需要的域)会被修正。你可以通过调用base. Process类或使用下列代码做到这点,两者效果一样。
return context.Convert<MaterialContent, MaterialContent>(myMaterial, "MaterialProcessor");
这行代码搜索叫做MaterialProcessor的处理器并将一个MaterialContent对象作为输入参数并生成另一个MaterialContent对象,这个新创建的Material对象被返回并存储在模型中。
注意:比较一下这行代码和ConvertMaterial方法中的最后一行代码(译者注:即return context.Convert<MaterialContent, MaterialContent>(material, "MaterialCustomEffectProcessor"))。
使用自定义Effect 将模型导入到XNA项目中,然后选择新的ModelCustomEffectProcessor,如图4-17所示 (不要选择MaterialCustomEffectProcessor,因为它也在列表中)。
图4-17 选择自定义模型处理器
在LoadContent method方法中加载这个模型:
myModel = content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count];
现在你的模型已经包含了指定的effect!当绘制模型时,你可以使用清晰得多的代码设置参数:
myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (Effect effect in mesh.Effects) { effect.Parameters["xWorld"]. SetValue(modelTransforms[mesh.ParentBone.Index]); effect.Parameters["xView"].SetValue(fpsCam.GetViewMatrix()); effect.Parameters["xProjection"].SetValue(fpsCam.GetProjectionMatrix()); effect.Parameters["xRedIntensity"].SetValue(1.2f); } mesh.Draw(); }
这个方法比教程4-7更加清晰。
代码
首先你要让MaterialCustomEffectProcessor处理模型中的每个MaterialContent对象,当然,你还要定义这个自定义材质处理器。
namespace ModelCustomEffectPipeline { [ContentProcessor] public class ModelCustomEffectProcessor : ModelProcessor { protected override MaterialContent ConvertMaterial(MaterialContent material, ContentProcessorContext context) { return context.Convert<MaterialContent, MaterialContent>(material, "MaterialCustomEffectProcessor"); } } [ContentProcessor] public class MaterialCustomEffectProcessor : MaterialProcessor { public override MaterialContent Process(MaterialContent input, ContentProcessorContext context) { EffectMaterialContent myMaterial = new EffectMaterialContent(); string map = Path.GetDirectoryName(input.Identity.SourceFilename); string effectFile = Path.Combine(map, "colorchannels.fx"); myMaterial.Effect = new ExternalReference<EffectContent>(effectFile); if (input.Textures != null) foreach (string key in input.Textures.Keys) myMaterial.Textures.Add("xTexture", input.Textures[key]); myMaterial.OpaqueData.Add("xRedIntensity", 1.1f); myMaterial.OpaqueData.Add("xGreenIntensity", 0.8f); myMaterial.OpaqueData.Add("xBlueIntensity", 0.8f); return context.Convert<MaterialContent, MaterialContent>(myMaterial,"MaterialProcessor"); } } }
接下来,将模型导入到XNA项目中,选择自定义模型处理器处理这个模型并加载:
protected override void LoadContent() { device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); cCross = new CoordCross(device); myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; }
当执行这一步时,模型会加载自定义effect,让你可以在绘制前设置参数:
protected override void Draw(GameTime gameTime) { device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0); cCross.Draw(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix); //draw model Matrix worldMatrix = Matrix.CreateScale(0.01f, 0.01f, 0.01f); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (Effect effect in mesh.Effects) { effect.Parameters["xWorld"]. SetValue(modelTransforms[mesh.ParentBone.Index] * worldMatrix); effect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); effect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); effect.Parameters["xRedIntensity"].SetValue(1.2f); } mesh.Draw(); } base.Draw(gameTime); }