深入Managed DirectX9(十九)

第十一章 可编程渲染管道以及高级着色语言入门

  至今为止,我们都在使用固定功能的管道(fixed-function pipeline)进行渲染。回到那段古老的日子(DirectX 8.0之前),这是唯一渲染物体的方法。固定功能的渲染管道本质上就是一系列用来控制如何渲染特定类型数据的规则以及行为。虽然在某些方面这已经够用了,但却限制了许多开发人员使用高级功能的能力。举个例子,使用固定功能的管道,灯光是根据每个顶点而不是每个象素来计算的。由于渲染管道是“固定”的,因此根本没欧办法来改变许多渲染选项,刚才说的灯光就是一个例子。

  DirectX 8.0带来了可编程的管道。随着这个版本带来的革命性功能,开发人员几乎可以控制管道的所有方面。他们可以使用称为顶点着色器(Vertex Shaders)的功能控制顶点的处理过程,使用像素着色器(Pixel Shader)控制像素的处理。这些着色程序相当强大,但使用起来却不是太方便,因为这些程序都是使用汇编语言来写的。

  在DirectX 9中发布了高级着色语言(High Level Shader Language, HLSL)。HLSL是一种和C很类似的语言,可以编译为着色器代码,却相当的利于开发人员阅读、维护以及编写。在带来了方便的同时,保留了可编程管道的强大力量。这一章,我们将讨论HLSL的基本内容,包括:

*使用可编程管道
*顶点变换
*使用像素着色器


使用可编程管道渲染三角形

  说了这么多,到底什么是“固定功能的管道”呢?固定功能的管道控制了一切:如何渲染顶点,如何对他们进行变换,如何照亮物体,几乎包含了所有方面。当你设置device的顶点格式时,实际上是在告诉device根据所给的格式,使用特定方法来渲染这些顶点。

  这样设计,最大的缺点就在于对于图形卡所支持的每一种功能,都必须设计和实现一个固定功能的API与之相对应。但是,由于如今显卡的发展速度之快(甚至超过了CPU的发展速度),原来的API可能很快就过时了。就算设计了数量庞大并且难懂的API来完成这些任务,还有一个潜在的问题:开发者不知道使用这些API时,到底发生了什么。对程序员来说,获得完全的控制才是他们想要的 。

  本书的第一个例子讨论了如何显示一个旋转的三角形。现在来看看使用可编程管道如何来完成同样的任务。创建一个新工程,做好各种所需的准备,添加如下变量:

private VertexBuffer vb = null;
private Effect effect = null;
private VertexDeclaration decl = null;
private Matrix worldMatrix;
private Matrix viewMatrix;
private Matrix projMatrix;
private float angle = 0.0f;

  自然,使用顶点缓冲来储存顶点数据。由于不使用device进行变换,所以需要为可编程管道单独保存这些变换矩阵。第二、三个变量则是新内容。Effect对象就是用来处理HLSL的主要对象。Vertex Declaration类则与固定功能管道中的VertexFormat枚举类似。他会告诉Direct3D运行时应该从顶点缓冲中读取的数据大小和类型。

  由于这个技术会使用一些相对比较新的图形卡功能,完全有可能你的显卡不支持它。如果这样,那么你将不得不使用DirectX SDK中的参考设备(reference device)。参考设备将以纯软件的方式来实现所有API,可能会相当慢。这一次,初始化的代码将复杂一点

public bool InitializeGraphics()
{
    PresentParameters presentParams = new PresentParameters();
    presentParams.Windowed = true;
    presentParams.SwapEffect = SwapEffect.Discard;
    presentParams.AutoDepthStencilFormat = DepthFormat.D16;
    bool canDoShaders = true;
    Caps hardware = Manager.GetDeviceCaps(0,DeviceType.Hardware);
    if(hardware.VertexShaderVersion >= new Version(1,1))
    {
        CreateFlags flags = CreateFlags.SoftwareVertexProcessing;
        if(hardware.DeviceCaps.SupportsHardwareTransformAndLight)
            flags = CreateFlags.HardwareVertexProcessing;
        if(hardware.DeviceCaps.SupportsPureDevice)
            flags |= CreateFlags.PureDevice;
        device = new Device(0,DeviceType.Hardware,this,flags,presentParams);
    }
    else
    {
        canDoShaders = false;
        device = new Device(0,DeviceType.Reference,this,CreateFlags.SoftwareVertexProcessing,presentParams);
    }
    vb = new VertexBuffer(typeof(CustomVertex.PositionOnly),3,device,Usage.Dynamic | Usage.WriteOnly,CustomVertex.PositionOnly.Format,Pool.Managed);
    projMatrix = Matrix.PerspectiveFovLH((float)Math.PI/4, this.Width/this.Height,1.0f,100.0f);
    viewMatrix = Matrix.LookAtLH(new Vector3(0,0,5.0f),new Vector3(),new Vector3(0,1,0));
    VertexElement[] elements = new VertexElement[]
    {
        new VertexElement(0,0,DeclarationType.Float3,DeclarationMethod.Default,DeclarationUsage.Position,0), VertexElement.VertexDeclarationEnd
    };
    decl = new VertexDeclaration(device,elements);
    return canDoShaders;
}

  这段代码假设使用默认的适配器进行渲染。为了简单,这里实际山还省略了许多因该做的枚举。创建设备之前,应该先检查所创建设备的能力,因此,先获得Caps结构。这个程序我们只使用可编程的管道进行渲染,因此,必须确保显卡至少支持第一代的顶点着色器。随着新版本的API发布,顶点和相色着色器的版本也是不断更新的。比如,DirectX 9就允许使用顶点和像素着色器的3.0版本。第一代的着色器自然是1.0版,不过在DirectX 9中已经使用1.1来代替这个版本,所以我们在这里检测是否支持它。

  假设你的显卡支持这些功能,那么就能创建一个“最理想”的设备了。默认使用software vertex processing,但是如果可以使用hardware vertex processing以及pure device,那么就使用这些功能。如果显卡不支持着色器,那么就使用参考设备。

接下来创建顶点缓冲。对于渲染一个三角形来说,只需要3个带有位置信息的顶点就可以了。

private void OnVertexBufferCreate(object sender, EventArgs e)
{
    VertexBuffer buffer = (VertexBuffer)sender;
    CustomVertex.PositionOnly[] verts = new CustomVertex.PositionOnly[3];
    verts[0].Position = new Vector3(0.0f,1.0f,1.0f);
    verts[1].Position = new Vector3(-1.0f,-1.0f,1.0f);
    verts[2].Position = new Vector3(1.0f,-1.0f,1.0f);
    buffer.SetData(verts,0,LockFlags.None);
}

  创建了顶点缓冲之后,保存即将用到的观察和投影矩阵。用和以前一样的方法创建这些矩阵。最后,来到顶点声明的部分。顶点申明告诉DirectX关于这些即将传入到编程管道中的顶点的所有所需信息。在创建vertex declaration对象时,把所用的device和一个vertex element数组作为参数,vertex element数组中的每一个成员都描述了顶点数据中的一个元素(component)。来看看 vertex elment的构造函数:

public VertexElement(short stream, short offset, DeclarationType declType, DeclarationMethod declMethod, DeclarationUsage declUsage, byte usageIndex);

  其中,第一个参数是流所使用的顶点数据。当对device调用SetStreamSource方法时,就把顶点缓冲中的数据分配为一个流,作为第一个参数。至今为止,我们都把所有数据保存在一个顶点缓冲中,并转换为一个流来使用,但是,完全有可能把来自多个顶点缓冲中的数据分配为多个流来渲染一个对象。由于在0号流中只有一个顶点缓冲,所以直接把这个参数设置为0。

  第二个参数是缓冲中数据开始的偏移位置。这里,顶点缓冲中只有一种类型的数据,自然值就为0。但是,如果包含了多种元素(component),则需要相应的进行偏移。举个例子,第一种元素是位置信息(三个float值),第二种元素是法线(同样也是三个float值),那么第一个元素的偏移值为0(因为它是第一个元素),同时,法线元素的偏移值就为12(3个float占用12字节)。

  第三个参数用来通知Direct3D所要使用的数据类型。由于只需要位置信息,所以可以使用Float3(我们将在后面讨论这个类型)。

  第四个参数描述了这个声明所使用的方法。在大多数情况下(除非你使用高要求的图元),使用默认值就可以了。

  第五个参数描述了每个元素的usage,比如位置、法线、颜色等等。使用三个浮点数来描述位置。最后一个参数是用来控制usage数据的,允许你指定多种usage类型。大多数情况下使用0就可以了。

  需要特别注意的是,vertex element数组必须使用VertexElement.VertexDeclarationEnd作为他的最后一个元素。好了,对于最简单的情况来说,我们使用编号为0的流,使用3个腹点值来表示位置。在创建了vertex elemen数组之后,就可以创建vertex declaration对象了。最后,返回一个布尔值,true表示可以硬件支持使用着色器,false则使用参考设备。

在处理完初始化方法之后, 你需要处理它的返回值了,更新main方法:

static void Main()
{
    using(Form1 frm = new Form1())
    {
        frm.Show();
        if(!frm.InitializeGraphics())
        {
            MessageBox.Show("your card does not support shaders, This application will run in ref mode instead");
        }
        Application.Run(new Form1());
    }
}

还有什么没完成呢?只剩下渲染场景了。这里和贯穿本书的所有渲染方法一样,重载OnPaint方法:

protected override void OnPaint(PaintEventArgs e)
{
    device.Clear(ClearFlags.Target | ClearFlags.ZBuffer,Color.CornflowerBlue,1.0f,0);
    UpdateWorld();
    device.BeginScene();
    device.SetStreamSource(0,vb,0);
    device.VertexDeclaration = decl;
    device.DrawPrimitives(PrimitiveType.TriangleList,0,1);
    device.EndScene();
    device.Present();
    this.Invalidate();
}

Clearn了设备之后,调用UpdateWorld方法,这个方法只是简单的增加旋转角度以及修改世界矩阵而已:

private void UpdateWorld()
{
    worldMatrix = Matrix.RotationAxis(new Vector3(angle/((float)Math.PI * 2.0f),angle/((float)Math.PI*4.0f),angle/((float)Math.PI*6.0f)),angle/(float)Math.PI);
    angle += 0.1f;
}

  好了,除了用vertex devlaration属性代替vertex format属性以外,以上大部分的代码都是很熟悉的,现在运行程序看看,除了一个蓝色的屏幕外,什么也没有。为什么?Direct3D运行时并不知道你想做什么。你需要为编程管道编写一个“程序”。

给工程添加一个名为“simple.fx”的空白文件。你将使用这个文件来保存HLSL程序。 让我们来添加HLSL代码吧:

struct VS_OUTPUT
{
    float4 pos : POSITION;
    float4 diff : COLOR0;
};
float4x4 WorldViewProj : WORLDVIEWPROJECTION;
float Time = 1.0f;
VS_OUTPUT Transform(
float4 Pos : POSITION)
{
    VS_OUTPUT Out = (VS_OUTPUT)0;
    Out.pos = mul(Pos, WorldViewProj);
    Out.diff.r = 1 - Time;
    Out.diff.b = Time * WorldViewProj[2].yz;
    Out.diff.ga = Time * WorldViewProj[0].xy;
    return Out;
}

  你看,HLSL实际上和C很类似。先来看看这段代码干了些什么吧。首先声明了顶点程序输出数据的结构。这里变量的声明有一点点特别。每个变量的后面都添加了一个语义标识符。语义表示了如何把这些变量与图形流水线相连接。

  问什么顶点缓冲只包含了位置信息,但是输出结构里却要同时包含位置和颜色信息呢?只有输出结构包含了位置和颜色数据(以及相应的语义),Direct3D才知道使用顶点程序返回的值来渲染,而不是使用顶点缓冲中的值。

  接下来,有两个“全局”变量;综合了世界,观察,投影变换的矩阵,以及一个用来改变颜色的时间变量。程序运行时,每一帧都必须更新这几个变量。

  最后,就是实际的顶点程序方法了。它把我们刚才所创建的结构作为返回值,接受一个顶点位置作为参数。这个方法将被所有顶点调用一次。需要注意的是输入值同样包含了语义,以便让Direct3D知道在处理什么类型的数据。

  这个方法之内的代码实际上是很简单的。你声明了作为返回值的变量——Out。把顶点和变换矩阵相乘,完成坐标变换。你使用了内置函数mul,因为矩阵的类型是float4*4,而位置是float4。使用标准的相乘符号会导致类型不匹配。

  之后,使用了一个公式对颜色的每一个因素作恒定的变换。注意看我们是如何设置颜色的每个元素的,首先是红色,接下来是蓝色,最后是绿色和alpha。位置和颜色都设置好之后,就可以填充返回的结构了。
(此处省略介绍HLSL语法的若干内容,建议大家去看《Cg教程——可编程实时图像权威指南》)

Rendering Shader Programs with Techniques

  现在完成了顶点程序的编写,如何使用它呢?顶点程序中并没有一个“入口点”,只有一个方法而言。如何调用这个方法呢?很简单,你只需要使用一个称为“technique”的东西就可以了。一个technique由一个或多个pass组成。每一个pass都能设置device state同时设置顶点和像素着色器来使用你的HLSL代码。来看看可用于这个程序的technique,在simple.fx文件中添加代码:

technique TransformDiffuse
{
    pass P0
    {
        CullMode = None;
        VertexShader = compile vs_1_1 Transform();
        PixelShader = NULL;
    }
}

  这里,你声明了一个称为TransformDiffuse的technique。这个名字仅仅是做修饰而以,没有什么特别的用处。在这个technique里,声明了一个pass。裁剪模式被设置为null,可以同时看到三角形的正面和背面。还记得以前写过的程序吗,这个任务是通过编写C#来完成的。使用pass来保存device state是一个不错的地方,所以在这里设置裁剪模式是很合适的。

  接下来,你想对即将进行渲染的顶端使用这个顶点程序,因此,使用vs_1_1标志来编译它。这个标志符应该根据你显卡的能力来进行选择。由于以及知道了拟定设备至少支持vertex shader1.1,所以使用这个标志。如果使用vertex shader2.0,那么标志就设置为vs_2_0。而相应的pixel shader标志就设置为ps_2_0。

  由于没有像素程序,因此把PixelShader成员设置为null就可以了。Pass到这里就完成了。接下来需要更新C#代码了。

  首先,我们将使用Effect对象,应为他是处理HLSL的主要对象。在InitializeGraphics方法中添加如下代码(在创建device之后):

effect = Effect.FromFile(device,@"..\..\simple.fx",null,ShaderFlags.None,null);
effect.Technique = "TransformDiffuse";

  我们通过一个文件来创建effect对象。同时设置了technique成员,因为整个程序都使用同一个technique。Effect对象会完成所有对HLSL的处理工作。另外还有两个需要每帧都更新的变量。在UpdateWorld方法的最后添加如下代码:

Matrix worldViewProj = worldMatrix * viewMatrix * projMatrix;
effect.Setvalue("Time", (float)Math.Sin(angle / 5.0f));
effect.Setvalue("WorldViewProj", worldViewProj);

这里,储存并合并了矩阵。同时更新HLSL中的变量。最后要做的只剩下更新渲染对象的代码了,在DrawPrimitives方法中添加代码:

int numPasses = effect.Begin(0);
for (int i = 0; i < numPasses; i++)
{
    effect.Pass(i);
    device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);
}
effect.End();

(注意,新版的DirectX已经改为了
int numPasses = effect.Begin(0);
for (int i = 0; i < numPasses; i++)
{
    effect.BeginPass(i);
    device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);
    effect.EndPass();
}
effect.End();


  由于已经为effect设置了technique,所以当渲染的时候,需要调用Begin方法。这个方法的参数允许你选择是否保存一个特定的状态,对于这个例子来说,不是太重要。这个方法同时还返回了当前technique中pass的数量。就算只有一个pass在technique里,最好还是使用一个循环来保证渲染了所有希望的technique。最后在渲染之前,还必须调用effect对象的Pass方法。这个方法唯一的参数就是你所使用的pass索引。这个方法让设备为渲染指定的pass做好准备,更新设备状态,同时设置顶点和像素着色器方法。最后嗲用DrawPrimitive方法。渲染完成之后,还必须调用effect对象的End方法。好了,现在运行程序看看吧。

~~~~~~~~~~~~~~~~未完待续~~~~~~~~~~~~~~~~~~~

  下载代码

转载于:https://www.cnblogs.com/yurow/articles/951626.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值