6.3 HLSL语言基础
6.3.1基本类型
“高级着色器语言”具有几种在所有着色器配置文件中通用的基本体类型。与C++中的基元类型一样,HLSL基元由各种整数和浮点数字类型组成。
类型 | 描述 |
---|---|
bool | 32-bit整数,用于表示逻辑上的true或false |
int | 32-bit整数 |
uint | 32-bit无符号整数 |
half | 16-bit float |
float | 32- float |
double | 64-float |
- bool: True or false value. Note that the HLSL provides the true and false keywords like in C++.
- int: 32-bit signed integer.
- half: 16-bit-floating point number.
- float: 32-bit-floating point number.
- double: 64-bit-floating point number
与在C或C++中一样,这些类型可用于声明标量值和数组值。标量变量支持一组标准的数学运算符,包括加法、减法、求反、乘法、除法和模数。标量还支持一组标准的逻辑运算符和比较运算符,使用与C/C++相同的语法。数组变量必须具有在编译时确定的固定静态声明大小,因为HLSL不支持动态内存分配。
6.2.1 vecotrs
HLSL还支持声明向量和矩阵变量。矢量类型支持1-4个分量,每个分量使用基元类型指定的存储格式。
矢量变量的声明语法如清单6.2所示
vector<float, 4> floatVector; // 4-component vector with float components
vector<int, 2> intVector; // 2-component vector with int components
为了方便起见,HLSL为基元类型和组件的所有组合预定义了简写类型定义。清单6.3演示了使用简写表示法的矢量变量声明
float4 floatVector; // 4-component vector with float components
int2 intVector; // 2-component vector with int components
- float2: 2D vector, where the components are of type float.
- float3: 3D vector, where the components are of type float.
- float4: 4D vector, where the components are of type float
float3 v = {1.0f, 2.0f, 3.0f}; float2 w = float2(x, y); float4 u = float4(w, 3.0f, 4.0f); // u = (w.x, w.y, 3.0f, 4.0f)
当使用数组初始值设定项语法声明时,可以初始化向量类型,其中值的数量与组件的数量匹配。向量类型还支持构造函数语法,该语法允许传递标量值甚至其他向量。此外,可以使用单个标量值初始化矢量,在这种情况下,标量值将复制到所有组件。
清单6.4演示了这些初始化模式。
float2 vec0 = { 0.0f, 1.0f };
float3 vec1 = float3( 0.0f, 0.1f, 0.2f );
float4 vec2 = float4( vec1, 1.0f );
float4 vec3 = 1.0f;
可以使用类似于C++中访问结构的符号来访问向量的各个分量。成员x、y、z和w分别对应于向量的第一、第二、第三和第四分量。可替换地,构件r、g、b和a可以以相同的方式使用。矢量组件还支持使用数组索引语法进行访问,这对于循环组件非常有用。
清单6.5演示了这些访问器的用法。
float4 floatVector = 1.0f;
float firstComponent = floatVector.x;
float secondComponent = floatVector.g;
float thirdComponent = floatVector[2];
vec.x = vec.r = 1.0f;
vec.y = vec.g = 2.0f;
vec.z = vec.b = 3.0f;
vec.w = vec.a = 4.0f;
除了支持单个组件访问外,vectors还支持通过使用swizzle同时访问多个组件。swizzle是一种以任意顺序返回向量的一个或多个分量的操作。swizzle还可以多次复制一个分量,以创建一个新的向量值,该值的分量数大于源向量中的分量数。虽然向量中组件的顺序没有限制,但使用的符号必须一致。换句话说,xyzw表示法不能与rgba表示法在一个swizzle中混合。
清单6.6包含以各种方式使用swizzle的示例代码。
float4 vec0 = float4(0.0f, 1.0f, 2.0f, 3 . 0 f ) ;
float3 vec1 = vec0.xyz;
float2 vec2 = vec1.rg;
float3 vec3 = vec0.zxy;
float4 vec4 = vec2.xyxy;
float4 vec5;
vec5.zyxw = vecS.xyzw;
float4 u = {1.0f, 2.0f, 3.0f, 4.0f};
float4 v = {0.0f, 0.0f, 5.0f, 6.0f};
v = u.wyyx; // v = {4.0f, 2.0f, 2.0f, 1.0f}
float4 u = {1.0f, 2.0f, 3.0f, 4.0f};
float4 v = {0.0f, 0.0f, 5.0f, 6.0f};
v = u.wzyx; // v = {4.0f, 3.0f, 2.0f, 1.0f}
float4 u = {1.0f, 2.0f, 3.0f, 4.0f};
float4 v = {0.0f, 0.0f, 5.0f, 6.0f};
v.xy = u; // v = {1.0f, 2.0f, 5.0f, 6.0f}
如果将矢量值分配给分量较少的矢量变量,则在执行分配时会隐式截断该值。程序员应该始终小心,不要无意中导致这样的截断,因为值会被丢弃。为了使截断显式,可以使用C++风格的强制转换或swizzle。为了方便起见,当发生隐式截断时,HLSL编译器将发出警告。
矢量支持标量变量支持的同一组数学、逻辑和比较运算符。当这些运算符用于矢量类型时,将按每个分量执行运算,并生成分量数等于操作数的矢量结果值。
6.3.3矩阵Matrices
矩阵变量的声明和使用方式与向量变量类似。矩阵声明除了指定行数和列数之外,还指定基元类型。行和列的数量分别列为4行和4列,最多可包含16个单独的分量。分量访问可以使用二维数组语法来完成,其中第一个索引指定行,第二个索引指定列。此外,当只指定单个数组索引时,它会将矩阵的相应行作为向量类型返回。矩阵还支持自己的成员访问语法,这与用于向量的格式不同。
这些格式如清单6.7所示
float4x4 worldMatrix = float4x4( float4( 1.0f, 0.0f, 0.0f, 0.0f ),
float4( 0.0f, 1.0f, 0.0f, 0.0f ),
float4( 0.0f, 0.0f, 1.0f, 0.0f ),
float4( 0.0f, 0.0f, 0.0f, 1.0f ) );
float matVal0 = worldMatrix._m00; // Value from first row, first column
float matVall = worldMatrix._12; // Value from first row, second column
float matVal2 = worldMatrix[0][1]; // Value from first row, second column
float2 matVal3 = worldMatrix._11_12; // Swizzles
与向量一样,用于矩阵类型的运算符是按每个分量执行的。因此,乘法运算符不应用于执行矩阵乘法和变换。相反,为矩阵/矩阵和矩阵/向量乘法提供了叉乘函数。有关更多详细信息,请参阅本章中的“内部函数”部分或SDK文档。
以1为基础索引
M._11 = M._12 = M._13 = M._14 = 0.0f;
M._21 = M._22 = M._23 = M._24 = 0.0f;
M._31 = M._32 = M._33 = M._34 = 0.0f;
M._41 = M._42 = M._43 = M._44 = 0.0f;
以0为基础索引
M._m00 = M._m01 = M._m02 = M._m03 = 0.0f;
M._m10 = M._m11 = M._m12 = M._m13 = 0.0f;
M._m20 = M._m21 = M._m22 = M._m23 = 0.0f;
M._m30 = M._m31 = M._m32 = M._m33 = 0.0f;
通常,着色器程序以行主格式初始化矩阵,并相应地处理所有向量/矩阵变换。然而,在许多情况下,编译器会优化汇编,使矩阵包含列主数据,因为这种格式允许使用四点积在汇编中获得更高效的表达式。此外,编译器默认将常量缓冲区中声明的所有矩阵视为包含列主数据,即使使用mul内在函数执行行主转换也是如此。因此,矩阵可能需要在被设置到常量缓冲区之前由主机应用程序进行转置,或者矩阵必须在着色器程序中进行转置。可以通过将D3D10_SHADER_PACK_MATRIX_ROW_MAJOR传递给着色器编译函数或通过使用ROW_MAJOR修饰符声明矩阵来更改此行为。
6.3.4结构体
HLSL允许自定义结构声明,其规则与C++非常相似。结构可以包含任意数量的标量、向量或矩阵基元类型的成员。它们也可以包含数组类型或其他自定义结构类型的成员。
清单6.8演示了一个简单的结构声明。
truct SurfaceInfo
{
float3 pos;
float3 normal;
float4 diffuse;
float4 spec;
};
SurfaceInfo v;
litColor += v.diffuse;
dot(lightVec, v.normal);
float specPower = max(v.spec.a, 1.0f);
变量修饰前缀
- static;本质上与extern相反;这意味着着色器变量将不会暴露给C++应用程序,只有着色器使用。
static float3 v = {1.0f, 2.0f, 3.0f};
uniform:这意味着变量不会随每个顶点/像素而变化——它对所有顶点/像素都是恒定的,直到我们在C++应用程序级别更改它。统一变量是从着色器程序外部初始化的(例如,通过C++应用程序)。
extern:这意味着C++应用程序可以看到该变量(即,C++应用程序代码可以在着色器文件外部访问该变量)。默认情况下,着色器程序中的全局变量是uniform和extern。
- const:HLSL中的const关键字与C++中的含义相同。也就是说,如果一个变量的前缀是const关键字,那么该变量是常量,不能更改。
const float pi = 3.14f;
6.3.5函数
HLSL允许以类似于C/C++的方式声明自由函数。函数可以具有任何返回类型(包括void),并且可以接受任意数量的参数。
由于HLSL不支持引用或指针类型,因此它有自己的语法来为参数提供输入/输出语义。该语法包括四个参数修饰符,如表6.2所示
参数修饰符 | 描述 |
---|---|
in | 参数是函数的输入。参数值的任何更改是临时的,不会反映在调用代码中。类似于C++中的pass-by-value。这是默认的修改器。 |
out | 参数是函数的输出。参数的值必须为由函数设置,并且此更改反映在调用代码中。 |
inout | 参数既是函数的输入,也是函数的输出。函数可以访问调用代码设置的参数值,函数中发生的更改反映在调用代码中。类似于C中的pass-by-reference++ |
uniform | 参数在执行期间是常量。对于不是入口点的函数,此修饰符等效于in。对于入口点函数,参数被声明为SParam默认常量缓冲区的一部分。 |
使用函数时,重要的是要记住着色器程序不使用C++中的传统堆栈。因此,不可能递归地调用函数。相反,必须使用动态循环结构来实现递归算法.
bool foo(in const bool b, // input bool
out int r1, // output int
inout float r2) // input/output float
{
if(b) // test input value
{
r1 = 5; // output a value through r1
}
else
{
r1 = 1; // output a value through r1
}
// since r2 is inout we can use it as an input
// value and also output a value through it
r2 = r2 * r2 * r2;
return true;
}
6.3.6接口
HLSL中的接口类似于C++中的抽象虚拟类,主要用于启用动态着色器链接。接口只能包含成员函数,不能包含成员变量。清单6.9包含一个简单接口类型的声明。
interface Mylnterface
{
float3 HelloWorld ( ) ;
float2 Method2( float2 param );
} ;
接口中声明的方法总是被认为是纯虚拟函数,因此不需要虚拟关键字。有关在HLSL中使用接口的更多信息,请参阅“动态着色器链接”部分.
6.3.7类
HLSL类和C++中的类一样,可以包含成员变量和成员函数。它们也可以从单个基类继承,也可以从多个接口继承。但是,如果一个类从接口继承,它必须完全实现该接口中声明的所有方法。清单6.10包含一个实现接口的简单类声明
interface Mylnterface
{
float3 Methodl( );
float2 Method2( float2 param );
} ;
class MyClass : Mylnterface
{
float3 Member1;
float3 Method1( )
{
return Member1;
}
};
float2 MyClass: :Method2( float2 param )
{
return Memberl.xy + param.xy;
}
虽然类的实例可以在普通着色器程序中声明并调用方法,但它们主要用于启用动态着色器链接。有关在HLSL中使用类的更多信息,请参见“动态着色器链接”部分.
6.3.8条件Conditionals
通过if和case语句支持条件代码执行,它们的工作方式与C/C++中的相同。所有if语句对单个布尔值进行操作,该布尔值可以通过使用逻辑运算符和比较运算符来创建。需要注意的是,矢量运算的布尔结果不能直接使用,因为这样的运算产生的是矢量结果,而不是单个布尔值。通过扩展,同样的原理也适用于switch语句。
应该注意的是,基于仅在程序执行期间已知的值的条件分支可以由编译的着色器程序集以两种方式之一表示:预测或动态分支。当使用预测时,编译器将发出代码来计算分支两侧的表达式。然后使用比较指令根据比较结果“选择”正确的值。另一方面,动态分支会发出分支指令,这些指令实际上控制着色器程序中的执行流。因此,它可以用于潜在地跳过不需要的计算和内存访问。是否跳过分支中的指令取决于分支的一致性。换句话说,着色器程序的同时执行必须全部选择相同的分支,以防止硬件执行分支的两侧。
通常,硬件将同时执行顶点着色器中的多个连续顶点、几何体着色器中的连续基元以及像素着色器中的像素的连续网格。对于计算着色器,线程被显式映射到线程组,要求每个组内具有一致性。此外,由于执行实际分支指令所产生的开销,动态分支可能会带来恒定的性能损失。
基本纹理采样方法也不能在像素着色器的动态分支中使用,因为偏导数可能不可用,因为四边形中的像素可能具有不同的分支。因此,在考虑分支的一致性的同时,将动态分支的额外开销与潜在跳过的指令数量进行权衡是很重要的。
默认情况下,编译器将使用启发式方法自动在预测和动态分支之间进行选择。但是,也可以覆盖编译器的决策属性。有关更多信息,请参阅本章中的“属性”部分
6.3.9循环
HLSL支持for、while和do-white循环结构。与条件语句一样,它们的语法和用法与等价的C/C++语句相同。它们也类似于条件语句,因为当基于运行时值进行循环时,它们可能会生成动态流控制指令,因此它们在一致性方面具有相同的性能特征。
6.3.10语义Semantics
在HLSL中,语义是附加到变量的字符串元数据。在着色器程序中使用时,它们有三个主要用途:
1.指定用于在着色器阶段之间传递值的变量的绑定
2.允许着色器程序接受管道生成的特殊系统值
3.允许着色器程序通过管道解释的特殊系统值
cbuffer PSConstants
{
float4x4 WVPMatrix;
}
void VSMain( in float4 VPos : POSITION // Binds variable to Input Assembler
in uint VID : SV_VertexID // Binds variable to asystem generated value
out float4 OPos : SV_Position // Tells the pipeline to
// interpret the value as the
// output vertex position
)
{
OutPos = mul( VPos, WVPMatrix );
}
在上述示例中,将POSITION语义应用于输入允许着色器指定它需要的顶点中的哪个元素。因此,输入装配程序将使用输入布局来确定读取适当元素所需的顶点内的适当字节偏移。以同样的方式,可以使用语义来标识从一个着色器阶段传递到另一着色器阶段的值。这允许运行时确保正确的输出值与给定的输入参数相匹配。
用SV前缀表示的系统值语义用于接受来自运行时的值,或将值传递给运行时。在示例着色器中:
SV_VertexID指定应将参数设置为指示顶点缓冲区内顶点索引的值。
SV_Position语义表示输出的值是应用于光栅化的最终变换顶点位置。
6.3.11属性
属性是可以插入HLSL代码中的特殊标记,用于修改编译器发出的程序集代码,或控制管道执行着色器的方式。有些纯粹是可选的,而另一些则是某些着色器类型所必需的。在所有情况下,它们都是通过在它们所影响的函数、分支、循环或语句之前立即声明来使用的。表6.3列出了所有属性,并作了简短说明。
匹配的对齐。一些编译器还支持用于指定打包对齐方式的杂注。HLSL常量缓冲区的打包也可以通过packoffset关键字手动指定。清单6.13演示了使用packoffset声明一个具有四字节对齐的紧密压缩常量缓冲区。
cbuffer VSConstants
{
float4x4 WorldMatrix : packoffset(c0);
float4x4 ViewProjMatrix : packoffset(c4);
fioat3 Color : packoffset(c8);
uint EnableFog : packoffset(c8.w);
float2 ViewportXY : packoffset(c9);
float2 ViewportWH : packoffset(c9.z);
}
当在HLSL中声明常量缓冲区时,编译器会自动将其映射到管道相应阶段的15个常量缓冲寄存器之一。这些寄存器被命名为cb0到cb14,并且当将常量缓冲区绑定到着色器阶段时,寄存器的索引直接对应于传递给SSetConstantBuffers的槽。可以使用反射API查询寄存器索引中的着色器程序,以便主机应用程序知道要指定哪个插槽。或者,可以使用register关键字在HLSL声明中手动指定寄存器索引。清单6.14演示了指定常量缓冲区寄存器索引这个关键字的用法。
cbuffer VSConstants : register(cb0)
{
float4x4 WorldMatrix : packoffset(c0);
float4x4 ViewProjMatrix : packoffset(c4);
float3 Color : packoffset(c8);
uint EnableFog : packoffset(c8.w);
float2 ViewportXY : packoffset(c9);
float2 ViewportWH : packoffset(c9.z);
}