• 在3D世界中创建不同的相机模式——创建一个PostProcessing Framework


    2.12 创建一个Post-Processing Framework

    问题

    你想在最终的图像上添加一个2D post-processing effect,诸如模糊,扭曲,摇晃,变焦,边缘检测等。

    解决方案

    首先将2D或3D场景绘制到屏幕,在Draw过程的最后,在后备缓冲的内容还没有发送到屏幕前,你需要将这个后备缓冲的内容存储在一张2D图像中,解释请见教程3-8。

    然后,将这张2D图像绘制到屏幕上,但通过一个自定义的pixel shader实现,这也是本教程中有趣的部分。在pixel shader中,

    你可以单独处理图像中的每个像素。你可以通过使用一个简单的SpriteBatch实现以上操作(见教程3-1),但是SpriteBatch不支持多个pass的alpha混合(如下一个教程介绍的effect)。要解决这个问题并创建一个支持所有post-processing effect的框架,你需要手动定义覆盖整个屏幕的三角形,在这个三角形上施加最终的图像。通过这种方式,你可以使用任意的pixel shader处理最终图像的像素。

    如果你想组合多个post-processing effect,你可以在每个effect之后将结果图像绘制到一个RenderTarget2D变量中而不是绘制到后备缓冲中。这样最后一个effect的最终结果才会被绘制到后备缓冲中。

    工作原理

    首先需要在程序中添加一些变量,这些变量包括ResolveTexture2D,它用来获取后备缓冲的内容(可见教程3-8),RenderTarget2D,它用来对多个effects进行排列。你还需要一个effect文件保存post-processing technique(s)。

    VertexPositionTexture[] ppVertices; 
    RenderTarget2D targetRenderedTo; 
    ResolveTexture2D resolveTexture; 
    Effect postProcessingEffect; 
    float time = 0; 

    因为你要定义两个三角形覆盖整个屏幕,所以需要定义顶点:

    private void InitPostProcessingVertices()
    {
        ppVertices = new VertexPositionTexture[4]; int i = 0;
        ppVertices[i++] = new VertexPositionTexture(new Vector3(-1, 1, 0f), new Vector2(0, 0));
        ppVertices[i++] = new VertexPositionTexture(new Vector3(1, 1, 0f), new Vector2(1, 0));
        ppVertices[i++] = new VertexPositionTexture(new Vector3(-1, -1, 0f), new Vector2(0, 1)); 
        ppVertices[i++] = new VertexPositionTexture(new Vector3(1, -1, 0f), new Vector2(1, 1));
    }

    这个方法定义了矩形的四个顶点,用来绘制TriangleStrip (可参见教程5-1)形式的两个三角形。请记住屏幕坐标的范围是[-1,1],而纹理坐标的范围是[0,1]。

    你可以看到刚才定义的位置就是屏幕坐标,点(-1,-1)对应窗口的左上角,点(1,1)对应右下角。你也可以指定纹理坐标的左上角(0,0)位于窗口的左上角(-1,-1),纹理坐标的右下角(1,1)位于窗口右下角。如果将这个矩形绘制到这个屏幕中,图像就会覆盖整个窗口。

    注意:因为窗口是2D的,在顶点的位置中可以无需第三个坐标。但是,对屏幕的每个像素,XNA会将距离相机的位置保存到深度缓冲中,这实际上就是第三个坐标。通过将这个距离指定为0,表示将图像绘制到尽可能离相机*的地方(更确切的说,将图像绘制在*裁*面上)。

    别忘了在Initialize方法中调用这个方法:

    InitPostProcessingVertices(); 

    最后三个变量应在LoadContent方法中进行初始化:

    PresentationParameters pp = GraphicsDevice.PresentationParameters;
    targetRenderedTo = new RenderTarget2D(device, pp.BackBufferWidth, pp.BackBufferHeight, 1, device.DisplayMode.Format); 
    resolveTexture = new ResolveTexture2D(device, pp.BackBufferWidth, pp.BackBufferHeight, 1, device.DisplayMode.Format); 
    postProcessingEffect = content.Load<Effect>("content/postprocessing"); 

    可参见教程3-8学习更多有关渲染目标的知识。这种情况中重要的是新的渲染目标和窗口的属性一样,它需要有相同的宽度,高度,颜色格式,这些都可以从图形设备的PresentationParameters结构中获取。通过这种方式,你可以很简单地获取纹理,然后对它进行post-process,将结果发送到屏幕,而无需做任何缩放和颜色映射的操作。因为你使用的是完全尺寸的纹理,无需任何mipmaps (可参见教程3-7的注释)。这意味着你只需一个mipmap level,就是纹理的原始大小。你还要加载包含post-processing technique(s)的effect文件。

    加载了变量后,就可以开始下面的工作了。在与以往一样绘制了场景后,你想调用一个方法可以获取后备缓冲中的内容,然后对它进行处理,将结果发送到后备缓冲。这就是PostProcess方法要进行的操作:

    private void PostProcess()
    {
        device.ResolveBackBuffer(resolveTexture, 0);
        Texture2D textureRenderedTo = resolveTexture;
    }

    第一行代码将后备缓冲中的当前内容转换到一个ResolveTexture2D,即本例中的resolveTexture变量。这个变量包含了要绘制到屏幕中的场景。你将这个变量存储为一个普通的Texture2D,叫做textureRenderedTo。

    然后,使用post-processing effect将这个textureRenderedTo绘制到覆盖整个窗口的矩形中。在这个简单教程中,你将定义一个叫做Invent的effect,它可以将图像中的每个像素的颜色反相。

    postProcessingEffect.CurrentTechnique = postProcessingEffect.Techniques["Invert"]; 
    postProcessingEffect.Begin();
    postProcessingEffect.Parameters["textureToSampleFrom"]. SetValue(textureRenderedTo);
    foreach (EffectPass pass in postProcessingEffect.CurrentTechnique.Passes)
    {
         pass.Begin();
         device.VertexDeclaration = new VertexDeclaration(device, VertexPositionTexture.VertexElements); 
         device.DrawUserPrimitives<VertexPositionTexture> (PrimitiveType.TriangleStrip, ppVertices, 0, 2);
         pass.End();
    }
    postProcessingEffect.End();

    你首先选择了用来将最终图像绘制到屏幕的post-processing technique,开始这个effect。

    然后,将textureRenderedTo传递到显卡上,这样effect可以从中进行采样。最后,对post-processing technique的每个pass,你让显卡绘制覆盖整个屏幕的两个三角形。你需要编写effect的代码让两个三角形显示这个图像,以你选择的方式进行处理。

    注意:当在3D世界中绘制物体时,你总要设置World, View和Projection矩阵。这些矩阵让显卡中的vertex shader将3D坐标映射到屏幕的对应像素上。但在这个例子中,你已经在屏幕空间中定义了两个三角形的位置,所以无需设置这些矩阵,因为现在vertex shader不会改变顶点的位置,只是简单地将它们传递到pixel shader中。

    别忘了在调用Draw方法中调用这个方法:

    PostProcess(); 
    HLSL

    只剩最后一步了:在HLSL中定义post-processing technique。不要担心,因为这里使用的HLSL非常简单。所以,打开一个新文件,命名为postprocessing. fx。

    texture textureToSampleFrom;
    sampler textureSampler = sampler_state
    {
        texture = <textureToSampleFrom>;
        magfilter = POINT; 
        minfilter = POINT;
        mipfilter = POINT; 
    };
    
    struct PPVertexToPixel
    {
        float4 Position : POSITION; 
        float2 TexCoord    : TEXCOORD0; 
    };
    
    struct PPPixelToFrame
    {
        float4 Color    : COLOR0;
    };
    
    PPVertexToPixel PassThroughVertexShader(float4 inPos: POSITION0,float2 inTexCoord: TEXCOORD0)
    {
        PPVertexToPixel Output = (PPVertexToPixel)0;
        Output.Position = inPos;
        Output.TexCoord = inTexCoord;
        return Output;
    }
    
    //    PP Technique: Invert     
    PPPixelToFrame InvertPS(PPVertexToPixel PSIn) : COLOR0 
    {
        PPPixelToFrame Output = (PPPixelToFrame)0;
    
        float4 colorFromTexture = tex2D(textureSampler, PSIn.TexCoord); 
        Output.Color = 1-colorFromTexture;
    
        return Output; 
    }
    
    technique Invert
    {
        pass Pass0
        {
            VertexShader = compile vs_1_1 PassThroughVertexShader(); 
            PixelShader = compile ps_1_1 InvertPS();
        }
    }

    这个代码还可以再短一点,但我想和前面教程中的HLSL代码的结构保持一致。在technique 定义的底部,表示的是technique的名称和使用的vertex shader和pixel shader。在它之上是vertex shader和pixel shader,在代码顶部是可以从XNA程序中设置的变量。对这个简单例子,你只需设置从后备缓冲获取的2D图像。

    然后,在显卡中创建一个纹理采样器,这也是后面的pixel shader从中进行颜色采样的变量。你将这个采样器连接到刚才定义的纹理,然后声明如果代码要求的一个坐标的颜色不是100%对应一个像素时应该进行的操作,这里你指定采样器提取最*像素的颜色。

    注意:纹理坐标是一个float2,X和Y值在0和1之间。因为这些数字是floats,对它们的任何运算几乎都会导致一个四舍五入的误差。这意味着当你使用这样一个坐标从纹理采样时,大多数纹理坐标不会精确地对应纹理上的一个像素,但会非常接*。这就是为什么你需要指定纹理采样器应该怎样做的原因。

    然后,你定义了两个结构:一个保存从vertex shader发送到pixel shader的信息。这个信息只包含屏幕坐标和纹理到哪采样获取像素的颜色。第二个结构保存pixel shader输出。对每个像素,pixel shader只需要计算颜色。

    vertex shader让你处理发送到显卡的每个顶点的数据。3D程序中vertex shader最重要的任务之一就是将3D坐标转换为2D屏幕坐标。在post-processing effects的情况中,vertex shader并不真正有用,因为你已经定义了两个三角形的顶点的屏幕坐标!所以,你只需让vertex shader将输入的位置传递到输出就可以了。

    然后,在pixel shader中才是post-processing effect的处理。对绘制到屏幕的每个像素,调用这个方法,让你可以改变像素的颜色。在pixel shader中,首先创建一个空的叫做Outputde 输出结构, 然后,这个颜色从textureSampler进行采样。如果pixel shader只是简单地输出这个颜色,那么输出的图像与原始图像是一样的,因为窗口每个像素都是从原始图像的原始位置采样它的颜色的。所以你想改变采样的坐标或从原始图像获取的颜色,这会在下一段中进行这个操作。

    colorFromTexture变量包含四个介于0和1之间的值(红,绿,蓝和alpha)。本例中,通过从1减去这些值将它们反相。将这个反相过的颜色保存到Output结构中并返回。

    当运行代码时,场景会被保存到textureRenderedTo纹理中,每个像素的颜色会在绘制到屏幕前被反相。

    多个Post-Processing Effects队列

    再加一些代码让你可以处理多个post-processing effects队列。在Draw方法中,你将创建一个集合包含要施加的post-processing techniques,然后将这个集合传递到PostProcess方法中:

    List<string> ppEffectsList = new List<string>(); 
    ppEffectsList.Add("Invert"); 
    ppEffectsList.Add("Invert"); 
    PostProcess(ppEffectsList); 

    现在你只定义了一个Invert technique,所以这个简单例子中你使用了这个technique 两次。通过反相一个反相过的图像,结果是再次获得了原始图像,这有什么令人激动的?

    你要调整PostProcess方法让它接受effect集合作为参数。如你所见,这个方法的开始部分被扩展为可以处理多个 Post-Processing Effects:

    public void PostProcess(List<string> ppEffectsList)
    {
        for (int currentTechnique = 0; currentTechnique < ppEffectsList.Count; currentTechnique++) 
        {
            device.SetRenderTarget(0, null);
            Texture2D textureRenderedTo;
    
            if (currentTechnique == 0)
            {
                device.ResolveBackBuffer(resolveTexture, 0);
                textureRenderedTo = resolveTexture;
            } 
           else
           {
               textureRenderedTo = targetRenderedTo.GetTexture();
           }
    
           if (currentTechnique == ppEffectsList.Count - 1) 
               device.SetRenderTarget(0, null);
           else
               device.SetRenderTarget(0, targetRenderedTo);
    
            postProcessingEffect.CurrentTechnique= postProcessingEffect.Techniques[ppEffectsList[currentTechnique]]; 
    
            postProcessingEffect.Begin();
            postProcessingEffect.Parameters["textureToSampleFrom"]. SetValue(textureRenderedTo);
    
            foreach (EffectPass pass in postProcessingEffect.CurrentTechnique.Passes) 
            {
                pass.Begin();
                device.VertexDeclaration = new VertexDeclaration(device, VertexPositionTexture.VertexElements); 
                device.DrawUserPrimitives<VertexPositionTexture> (PrimitiveType.TriangleStrip, ppVertices, 0, 2);
                pass.End();
                postProcessingEffect.End();
            }
        }
    }

    这个方法的思路如图2-14所示。对集合中的每个effect,你将渲染目标中的内容保存到一张纹理中,然后使用当前的effect再次将它绘制到渲染目标中。这个规则有两个例外。

    首先,对第一个effect,获取后备缓冲中的内容,而不是RenderTarget的内容。最后,对最后一个effect,将结果绘制到后备缓冲,这样它会被绘制到屏幕。这个过程如图2-14所示。

    2-14

    图2-14 多个post-processing effects队列

    前面的代码显示了工作流程。如果是第一个technique,则将后备缓冲中的内容存储到textureRenderedTo,否则,将渲染目标的内容存储到textureRenderedTo。无论哪种方式, textureRenderTo都会包含最终要绘制的内容。如教程3-8的解释,在调用RenderTarget 的GetTexture前,你必须激活另一个渲染目标,这是由这个方法的第一行代码实现的。

    然后检查当前technique是否是集合中的最后一个,如果是,通过在device. SetRenderTarget方法中传递null(你也可以不使用这行代码,因为在方法顶部已经做了这个操作)将后备缓冲设置为当前渲染目标。否则,将自定义的渲染目标作为当前渲染目标。

    代码的其他部分保持不变。

    作为post-processing technique的第二个简单例子,你可以根据时间改变颜色值。将这个代码添加到. fx文件的顶部:

    float xTime; 

    这个变量可以在XNA程序中设置,在HLSL代码中读取。将这行代码添加到. fx文件的最后:

    //    PP Technique: TimeChange     
    PPPixelToFrame TimeChangePS(PPVertexToPixel PSIn) : COLOR0 
    {
        PPPixelToFrame Output = (PPPixelToFrame)0;
    
        Output.Color = tex2D(textureSampler, PSIn.TexCoord); 
        Output.Color.b *= sin(xTime);
        Output.Color.rg *= cos(xTime);
        Output.Color += 0.2f;
    
        return Output; 
    }
    
    technique TimeChange
    {
        pass Pass0
        {
            VertexShader = compile vs_1_1 PassThroughVertexShader(); 
            PixelShader = compile ps_2_0 TimeChangePS();
        }
    }

    对图像的每个像素,蓝色通道会乘以由xTime变量决定的正弦值,红色和绿色乘以余弦值。记住,正弦和余弦产生一个介于–1和+1之间的波形,颜色通道的负值会被截取到0。

    使用这个technique绘制最终图像:

    List<string> ppEffectsList = new List<string>(); 
    ppEffectsList.Add("Invert"); 
    ppEffectsList.Add("TimeChange"); 
    postProcessingEffect.Parameters["xTime"].SetValue(time); 
    PostProcess(ppEffectsList); 

    注意你将xTime变量设置为time,需要在XNA代码中指定这个time变量:

    float time; 

    在Update方法中更新变量:

    time += gameTime.ElapsedGameTime.Milliseconds / 1000.0f; 

    当运行代码时,你会看到图像的颜色会随时间发生变化。还不是很漂亮,但是你可以只基于它们的原始颜色改变像素的颜色。在下一个教程中,还要考虑像素周围的颜色决定最终颜色。

    代码

    下面的代码定义顶点,这些顶点构成矩形用来显示最终图像:

    private void InitPostProcessingVertices()
    {
        ppVertices = new VertexPositionTexture[4];
        int i = 0;
        ppVertices[i++] = new VertexPositionTexture(new Vector3(-1, 1, 0f), new Vector2(0, 0));
        ppVertices[i++] = new VertexPositionTexture(new Vector3(1, 1, 0f), new Vector2(1, 0));
        ppVertices[i++] = new VertexPositionTexture(new Vector3(-1, -1, 0f), new Vector2(0, 1));
        ppVertices[i++] = new VertexPositionTexture(new Vector3(1, -1, 0f), new Vector2(1, 1));
    }

    在Draw方法中,你想往常一样绘制场景。在绘制之后,定义使用哪个post-processing effects,并将集合传递到PostProcess方法中:

    protected override void Draw(GameTime gameTime)
    {
        device.Clear(ClearOptions.Target|ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0);
    
        //draw model
        Matrix worldMatrix = Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(0, 0, 0); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms);
        foreach (ModelMesh mesh in myModel.Meshes)
        {
            foreach (BasicEffect effect in mesh.Effects)
            {
                effect.EnableDefaultLighting();
                effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; 
                effect.View = fpsCam.ViewMatrix;
                effect.Projection = fpsCam.ProjectionMatrix;
            }
            mesh.Draw();
        }
    
        //draw coordcross
        cCross.Draw(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix);
    
        List<string> ppEffectsList = new List<string>(); 
        ppEffectsList.Add("Invert");
        ppEffectsList.Add("TimeChange"); 
        postProcessingEffect.Parameters["xTime"].SetValue(time); 
        PostProcess(ppEffectsList);
    
        base.Draw(gameTime);
    }

    在Draw方法的最后,调用PostProcess方法,这个方法获取后备缓冲, 使用一个或多个post-processing effects 将图像绘制到屏幕中:

    public void PostProcess(List<string> ppEffectsList)
    {
        for (int currentTechnique = 0; currentTechnique < ppEffectsList.Count; currentTechnique++)
        {
            device.SetRenderTarget(0, null); 
            Texture2D textureRenderedTo;
    
            if (currentTechnique == 0)
            {    
                device.ResolveBackBuffer(resolveTexture, 0);
                textureRenderedTo = resolveTexture;
             }
             else
             {
                 textureRenderedTo = targetRenderedTo.GetTexture();
              }
    
              if (currentTechnique == ppEffectsList.Count - 1) 
                  device.SetRenderTarget(0, null);
              else
                  device.SetRenderTarget(0, targetRenderedTo);
    
              postProcessingEffect.CurrentTechnique = postProcessingEffect.Techniques[ppEffectsList[currentTechnique]]; 
              postProcessingEffect.Begin();
              postProcessingEffect.Parameters["textureToSampleFrom"]. SetValue(textureRenderedTo);
    
              foreach (EffectPass pass in postProcessingEffect.CurrentTechnique.Passes)
              {
    pass.Begin();
    device.VertexDeclaration = new VertexDeclaration(device, VertexPositionTexture.VertexElements);
    device.DrawUserPrimitives<VertexPositionTexture> (PrimitiveType.TriangleStrip, ppVertices, 0, 2);
    pass.End();
    }
    postProcessingEffect.End();
    }
    }

    在HLSL文件中,确保将纹理采样器连接到textureToSampleFrom变量上:

    float xTime;
    
    texture textureToSampleFrom;
    sampler textureSampler = sampler_state
    {
        texture = <textureToSampleFrom>;
        magfilter = POINT; 
        minfilter = POINT; 
        mipfilter = POINT;
    }
    
    struct PPVertexToPixel
    {
        float4 Position : POSITION;
        float2 TexCoord    : TEXCOORD0;
    };
    
    struct PPPixelToFrame
    {
         float4 Color    : COLOR0;
    };
    
    PPVertexToPixel PassThroughVertexShader(float4 inPos: POSITION0, float2 inTexCoord: TEXCOORD0)
    {
         PPVertexToPixel Output = (PPVertexToPixel)0;
        Output.Position = inPos;
        Output.TexCoord = inTexCoord;
        return Output;
    }
    
    //    PP Technique: Invert     
    PPPixelToFrame InvertPS(PPVertexToPixel PSIn) : COLOR0 
    {
        PPPixelToFrame Output = (PPPixelToFrame)0;
    
        float4 colorFromTexture = tex2D(textureSampler, PSIn.TexCoord); 
        Output.Color = 1-colorFromTexture;
    
        return Output; 
    }
    
    technique Invert
    {
        pass Pass0
        {
            VertexShader = compile vs_1_1 PassThroughVertexShader(); 
            PixelShader = compile ps_1_1 InvertPS();
        } 
    }
    
    //    PP Technique: TimeChange
    PPPixelToFrame TimeChangePS(PPVertexToPixel PSIn) : COLOR0 
    {
        PPPixelToFrame Output = (PPPixelToFrame)0;
    
        Output.Color = tex2D(textureSampler, PSIn.TexCoord); 
        Output.Color.b *= sin(xTime);
        Output.Color.rg *= cos(xTime);
        Output.Color += 0.2f;
    
        return Output;
    }
    
    technique TimeChange
    {
        pass Pass0
        {
            VertexShader = compile vs_1_1 PassThroughVertexShader(); 
            PixelShader = compile ps_2_0 TimeChangePS();
        }
    }

    00

  • 相关阅读:
    Linux共享irq的实现描述
    GStreamer使用playbin,如何给动态生成的source组件设置属性?
    如何从H.263 raw data中取出视频的宽高以及Framerate
    GStreamer pipeline的basetime是如何计算出来的?
    GstPad setcaps,getcaps,set_setcaps_function...caps协商解说
    使用aplay播放一个PCM?PCM和WAV文件的区别?
    GStreamer中对RTP包seqnum是否wraparound的判断只用一句代码
    GStreamer如何让videosink在指定的窗口进行图像绘制?
    GStreamer如何获取播放的duration和当前的播放position?
    函数声明与函数表达式
  • 原文地址:https://www.cnblogs.com/AlexCheng/p/2120165.html
Copyright © 2020-2023  润新知