return output; }
下面就针对上述代码讲解一下HLSL着色器程序的编写:
1.3.1全局变量
代码中声明了两个全局变量:
matrix WVPMatrix; vector color;
变量WVPMatrix是一个矩阵类型,它包含了世界、观察、投影的合矩阵,用于对顶点进行坐标变换; 变量color是一个向量类型,它用于设定顶点颜色; 代码中并没有对全局变量进行初始化,这是因为我们对全局变量的初始化过程将在应用程序中进行,全局变量在应用程序中赋值而在着色器程序中使用,这是应用程序和着色器通信的关键所在。具体赋值过程将在后续部分讲述。
1.3.2输入输出
? 输入输出结构
程序中定义了两个输入输出结构VS_INPUT和VS_OUTPUT
struct VS_INPUT { vector position : POSITION; };
struct VS_OUTPUT { vector position : POSITION; vector color : COLOR; };
自定义的结构可以采用任意名称,结构不过是一种组织数据的方式,并不是强制的,你也可以不使用,而将本程序的输入改为:
vector position : POSITION; ? 标志符
用于输入输出的变量采用用一种特殊的声明方式:
Type VariableName : Semantic
这个特殊的冒号语法表示一个语义,冒号后面的标志符用来指定变量的用途,如
vector position : POSITION;
其中,POSITION标志符表明该变量表示顶点位置,另外还有诸如COLOR、NORMAL等很多表示其他意义的标志符。 本节所说的输入输出其实是指着色器代码和编译器、GPU之间的通信,和应用程序是无关的,所以这些变量不需要在应用程序中进行赋值,标志符告诉编译器各个输入输出变量的用途(顶点位置、法线、颜色等),这是着色器代码和编译器、GPU之间通信的关键。
1.3.3入口函数
程序中还定义了一个函数SetColor:
OUTPUT SetColor(INPUT input) { VS_OUTPUT output = (VS_OUTPUT)0; output.position = mul(input.position, WVPMatrix); output.color = color; return output; }
1. 该函数以input和output类型作为输入输出;
2. 使全局变量WVPMatrix和input.position相乘,以完成顶点的世界、观察、投影变换,并把结果赋值到
output.position; output.position = mul(input.position, WVPMatrix);
3. 将全局变量color的值赋给output.color;
output.color = color;
4. 在同一个着色器代码文件中,可以有多个用户自定义函数,因此在应用程序中需要指定一个入口函数,相当于
windows程序的WinMain函数,本程序只包含SetColor一个函数而且它将被做为入口函数使用。
1.3.4总结
至此,一个HLSL着色器编写完毕,渲染过程中,当一个顶点被送到着色器时:
1. 全局变量WVPMatrix、color将在应用程序中被赋值;
2. 入口函数SetColor被调用编译器根据标志符将顶点信息填充到VS_INPUT中的各个字段;
3. SetColor函数中,首先定义一个VS_OUTPUT信息,之后根据WVPMatrix和color变量完成顶点的坐标
变换和颜色设定操作,最后函数返回VS_OUTPUT结构;
4. 编译器将会再次根据标志符把返回的VS_OUTPUT结构中的各字段映射为顶点相应的信息。 5. 顶点被送往下一个流程接受进一步处理。
上述过程中,全局变量在应用程序中赋值而在着色器程序中使用,这是应用程序和着色器通信的关键所在;标志符告诉编译器各个输入输出变量的用途(顶点位置、法线、颜色等),这是着色器代码和编译器、GPU之间通信的关键。个人认为这是着色器中最为精义的地方:)
1.4怎么用HLSL着色器
应用程序中对HLSL着色器的使用分为以下步骤: 1. 加载(称为编译更为妥当)着色器代码; 2. 创建(顶点/像素)着色器;
3. 对着色器中的变量进行赋值,完成应用程序和着色器之间的通信。 4. 把着色器设定到渲染管道中;
本例使用的着色器是一个顶点着色器,因此我们将通过顶点着色器的使用来讲解着色器的使用过程,像素着色器的使用过程与此大同小异,二者之间仅有些微差别。
1.4.1声明全局变量
IDirect3DVertexShader9* BasicShader = 0; //顶点着色器指针
ID3DXConstantTable* BasicConstTable = 0; //常量表指针
D3DXHANDLE WVPMatrixHandle = 0; D3DXHANDLE ColorHandle = 0;
ID3DXMesh* Teapot = 0; //指向程序中D3D茶壶模型的指针
1.4.2编译着色器
通过D3DXCompileShaderFromFile函数从应用程序外部的文本文件BasicHLSL.txt中编译一个着色器:
//编译后的着色器代码将被放在一个buffer中,可以通过ID3DXBuffer接口对其进行访问,之后的着色器将从这里创建
ID3DXBuffer* shaderBuffer = 0; //用于接受错误信息
ID3DXBuffer* errorBuffer = 0; //编译着色器代码
D3DXCompileShaderFromFile(\着色器代码文件名
0, 0,
\入口函数名称 \顶点着色器版本号
D3DXSHADER_DEBUG,// Debug模式编译 &shaderBuffer, //指向编译后的着色器代码的指针 &errorBuffer,
&BasicConstTable); //常量表指针
1.4.3创建着色器
应用程序通过CreateVertexShader创建一个顶点着色器,注意使用了上一步得到的shaderBuffer:
g_pd3dDevice->CreateVertexShader((DWORD*)shaderBuffer->GetBufferPointer(), &BasicShader); 1.4.3对着色器中的变量进行赋值
1.3.4节说到着色器的全局变量在应用程序中赋值而在着色器程序中使用,这是应用程序和着色器通信的关键所在,这里就具体说明赋值过程。
着色器中的全局变量在编译后都被放在一个叫常量表的结构中,我们可以使用ID3DXConstantTable接口对其进行访问,参照1.4.1中编译着色器函数D3DXCompileShaderFromFile的最后一个参数,该参数即返回了指向常量表的指针。
对一个着色器中变量进行赋值的步骤如下:
1. 通过变量名称得到指向着色器变量的句柄;
还记得在BasicHLSL.x着色器文件中我们声明的两个全局变量吗:
matrix WVPMatrix; vector color;
我们在应用程序中相应的声明两个句柄:
D3DXHANDLE WVPMatrixHandle = 0; D3DXHANDLE ColorHandle = 0; 然后通过变量名得到分别得到对应的两个句柄:
WVPMatrixHandle = BasicConstTable->GetConstantByName(0, \ ColorHandle = BasicConstTable->GetConstantByName(0, \2. 通过句柄对着色器变量进行赋值;
我们可以先设置各变量为默认值:
BasicConstTable->SetDefaults(g_pd3dDevice);
之后,可以使用ID3DXConstantTable::SetXXX函数对各个变量进行赋值:
HRESULT SetXXX(
LPDIRECT3DDEVICE9 pDevice, D3DXHANDLE hConstant, XXX value );
其中XXX代表变量类型,例如Matrix类型的变量就要使用SetMatrix函数赋值,而Vector类型的则要使用SetVector来赋值。
1.4.4把着色器设定到渲染管道中
这里我们使用SetVertexShader方法把顶点着色器设定到渲染管道中:
g_pd3dDevice->SetVertexShader(BasicShader);
1.4.5整个渲染过程如下
在渲染过程中,我们设定顶点的变换坐标和颜色值,渲染代码如下:
g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(153,153,153), 1.0f, 0 ); //开始渲染
g_pd3dDevice->BeginScene();
//得到世界矩阵、观察矩阵和投影矩阵
D3DXMATRIX matWorld, matView, matProj;
g_pd3dDevice->GetTransform(D3DTS_WORLD, &matWorld); g_pd3dDevice->GetTransform(D3DTS_VIEW, &matView);
g_pd3dDevice->GetTransform(D3DTS_PROJECTION, &matProj); D3DXMATRIX matWVP = matWorld * matView * matProj;
//通过句柄对着色器中的WVPMatrix变量进行赋值
BasicConstTable->SetMatrix(g_pd3dDevice, WVPMatrixHandle, &matWVP);
D3DXVECTOR4 color(1.0f, 1.0f, 0.0f, 1.0f);
//通过句柄对着色器中的color变量进行赋值,这里我们赋值为黄色
BasicConstTable->SetVector(g_pd3dDevice, ColorHandle, &color);
//把顶点着色器设定到渲染管道中
g_pd3dDevice->SetVertexShader(BasicShader);
//绘制模型子集
Teapot->DrawSubset(0);
//渲染完毕
g_pd3dDevice->EndScene();
g_pd3dDevice->Present(NULL, NULL, NULL, NULL);
编译运行程序,运行效果如图1.2所示,这里我们将顶点颜色设置为黄色,如果读者在渲染过程中不断变换对着色器变量color的赋值,你将会得到一个色彩不断变幻的D3D茶壶。
D3DXVECTOR4 color(1.0f, 1.0f, 0.0f, 1.0f); //读者可以尝试改变颜色值
BasicConstTable->SetVector(g_pd3dDevice, ColorHandle, &color);
图1.2 着色器效果
第二章 HLSL着色语言
这一章我们将学习HLSL的大部分基本内容,包括丰富的变量类型,如何定义变量,和如何编写shader代码。你可能还
想知道如何在shader中使用函数。虽然它们是使用HLSL编写shader的必须元素,但我还是决定把所有关于函数的内容作为单独一章来讲解。在第三章中,我们将详细讲解如何定义函数,以及HLSL为开发者所编写的内置函数。
好了,闲谈到此为止,让我们直接进入本章主题,讨论在任何语言中都是最重要的元素,数据类型。
在继续学习之前,需要进行一点点说明,读者在前一章学习HLSL语法时可能注意到了诸如techniques,渲染状态,pas
s之类的概念。虽然它们也是HLSL语言的一部分,但事实上它们只在effect文件中使用,而effect 文件则是HLSL的一个超集。
数据类型
数据类型是任何一门编程语言的核心。没有它们你将无法定义变量或者在函数之间传递数据。如果连呈现数据的方法都没有,又怎么能储存数据呢?和其他高级语言一样,HLSL有一系列内置定义类型,可以把它们分为以下几类:
? 标量类型 ? 矢量类型 ? 矩阵类型 ? 对象类型
除了上面的内置类型外,HLSL同样允许自定义的复合类型,比如数组和结构。在介绍HLSL特定数据类型前,先来仔细看看大部分3D硬件上呈现数据的方式。当处理数据时,基本的选项是浮点值,根据硬件构架的不同,这些值可能为16或32位。记住,虽然可以让硬件同一时间只处理一个值,但这并不是最高效的方法。相反,如果硬件每次处理一个包含四个分量值的单元(或矢量),那么则可以同时对四个分量进行处理。当然,这就需要考虑硬件如何来保存非矢量的值。我稍后会讲解这一点。
除了浮点值以外,较新的硬件也内置支持整型和布尔值,但它们主要用于分支,循环,和条件测试语句。接下来的几页中,我将浏览每一种数据类型,并解释如何来使用它们。
标量类型
标量类型是由HLSL标准定义的,他们是最基本的类型。所有复杂类型,比如矢量,矩阵,和结构都由标量组成。表2-1列出了所有可用的标量类型以及它们所表示的值。
表 2 -1 HLSL标量类型 标量类型 值
bool int half float double
true或false 32位有符号的整型 16位浮点数 32位浮点数 64位浮点数
需要注意并不是所有shader目标都天生支持整型,half和double值。如果要将shader编译为不支持指定类型数据的目标,那么将模拟处理float值的方式来处理它们。与使用原生(native)类型相比,结果可能不正确。在确定目标平台支持某个特定类型之前,为了保证一致性和可移植性,最好坚持使用标准浮点数。
注意
我们在上一章已经简要讨论过profile,它用来告诉HLSL编译器为哪种构架的shader产生代码。通常Profile的值直接对应于可用的顶点和像素着色器版本,但对于指令集没有改变的情况可能会有例外。