** 第十一章 ** ** ** ** 可编程渲染管道以及高级着色语言入门 **
**
翻译:clayman
[email protected]
仅供个人学习之用,勿用于任何商业用途,转载请注明作者^_^
至今为止,我们都在使用固定功能的管道( fixed-function pipeline )进行渲染。回到那段古老的日子( DirectX 8.0 之前),这是唯一渲染物体的方法。固定功能的渲染管道本质上就是一系列用来控制如何渲染特定类型数据的规则以及行为。虽然在某些方面这已经够用了,但却限制了许多开发人员使用高级功能的能力。举个例子,使用固定功能的管道,灯光是根据每个顶点而不是每个象素来计算的。由于渲染管道是“固定”的,因此根本没欧办法来改变许多渲染选项,刚才说的灯光就是一个例子。
DirectX 8.0 带来了可编程的管道。随着这个版本带来的革命性功能,开发人员几乎可以控制管道的所有方面。他们可以使用称为顶点着色器( Vertex Shaders )的功能控制顶点的处理过程,使用像素着色器( Pixel Shader )控制像素的处理。这些着色程序相当强大,但使用起来却不是太方便,因为这些程序都是使用汇编语言来写的。
在 DirectX 9 中发布了高级着色语言( High Level Shader Language, HLSL )。 HLSL 是一种和 C 很类似的语言,可以编译为着色器代码,却相当的利于开发人员阅读、维护以及编写。在带来了方便的同时,保留了可编程管道的强大力量。这一章,我们将讨论 HLSL 的基本内容,包括:
n 使用可编程管道
n 顶点变换
n 使用像素着色器
** 使用可编程管道渲染三角形 ** **
**
说了这么多,到底什么是“固定功能的管道”呢?固定功能的管道控制了一切:如何渲染顶点,如何对他们进行变换,如何照亮物体,几乎包含了所有方面。当你设置 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 就可以了。