问题

你想使用一个自定义内容处理器改变模型的effect而不是实时处理。通过这种方式,模型可以正确地加载到XNA项目中,而无需在实时保存原始effect中的所有纹理和其他信息。

注意:如果你想学习如何扩展默认模型处理器,最好先看下一个教程。

解决方案

扩展默认模型处理器并重写ConvertMaterial方法,这个方法被模型中的每个MaterialContent调用,把这个方法重写将所有的MaterialContent信息传递到自定义材质处理器。

在这个自定义材质处理器中,你将创建一个空的材质对象,然后将自定义的effect链接到这个对象。你也可以将原始MaterialContent的纹理复制到这个新对象,这个就可以设置effect的所有HLSL变量了。

工作原理

因为这个教程需要扩展默认内容处理器,所以请确保已经读过了教程3-9中内容管道的知识。首先建立一个新的内容管道项目,具体解释可见教程3-9中的“扩展一个已存在的内容处理器”一节。

本教程中,你将扩展默认模型处理器,如图4-16所示。当默认模型导入器从磁盘读取一个模型并创建一个NodeContent对象时,它保存了MaterialContent对象中的effect。你的模型处理器只改变这些MaterialContent对象并存储在NodeContent中,NodeContent对象的其他部分保持不变。

image

图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,因为它也在列表中)。

image

图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); 
} 

image