DirectX11--HLSL语法入门

前言

编写本内容仅仅是为了完善当前的教程体系,入门级别的内容其实基本上都是千篇一律,仅有一些必要细节上的扩充。要入门HLSL,只是掌握入门语法,即便把HLSL的全部语法也吃透了也并不代表你就能着色器代码了,还需要结合到渲染管线中,随着教程的不断深入来不断学习需要用到的新的语法,然后尝试修改着色器,再根据实际需求自己编写着色器来实现特定的效果。

注意:在翻阅HLSL文档的时候,要避开Effects11相关的内容。因为当前教程与Effects11是不兼容的。

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

数据类型

标量

常用标量类型如下:

类型描述
bool32位整数值用于存放逻辑值true和false
int32位有符号整数
uint32位无符号整数
half16位浮点数(仅提供用于向后兼容)
float32位浮点数
double64位浮点数

注意:一些平台可能不支持int, halfdouble,如果出现这些情况将会使用float来模拟

此外,浮点数还有规格化的形式:

  1. snorm float是IEEE 32位有符号且规格化的浮点数,表示范围为-1到1
  2. unorm float是IEEE 32位无符号且规格化的浮点数,表示范围为0到1

向量

向量类型可以支持2到4个同类元素

一种表示方式是使用类似模板的形式来描述

vector<float, 4> vec1;  // 向量vec1包含4个float元素
vector<int, 2> vec2;    // 向量vec2包含2个int元素

另一种方式则是直接在基本类型后面加上数字

float4 vec1;    // 向量vec1包含4个float元素
int3 vec2;      // 向量vec2包含3个int元素

当然,只使用vector本身则表示为一种包含4个float元素的类型

vector vec1;	// 向量vec1包含4个float元素

向量类型有如下初始化方式:

float2 vec0 = {0.0f, 1.0f};
float3 vec1 = float3(0.0f, 0.1f, 0.2f);
float4 vec2 = float4(vec1, 1.0f);

向量的第1到第4个元素既可以用x, y, z, w来表示,也可以用r, g, b, a来表示。除此之外,还可以用索引的方式来访问。下面展示了向量的取值和访问方式:

float4 vec0 = {1.0f, 2.0f, 3.0f, 0.0f};
float f0 = vec0.x;  // 1.0f
float f1 = vec0.g;  // 2.0f
float f2 = vec0[2]; // 3.0f
vec0.a = 4.0f;		// 4.0f

我们还可以使用swizzles的方式来进行赋值,可以一次性提供多个分量进行赋值操作,这些分量的名称可以重复出现:

float4 vec0 = {1.0f, 2.0f, 3.0f, 4.0f}; 
float3 vec1 = vec0.xyz;     // (1.0f, 2.0f, 3.0f)
float2 vec2 = vec0.rg;      // (1.0f, 2.0f)
float4 vec3 = vec0.zzxy;    // (3.0f, 3.0f, 1.0f, 2.0f)
vec3.wxyz = vec3;           // (3.0f, 1.0f, 2.0f, 3.0f)
vec3.yw = vec1.yy;           // (3.0f, 2.0f, 2.0f, 2.0f)

矩阵(matrix)

矩阵有如下类型(以float为例):

float1x1 float1x2 float1x3 float1x4
float2x1 float2x2 float2x3 float2x4
float3x1 float3x2 float3x3 float3x4
float4x1 float4x2 float4x3 float4x4

此外,我们也可以使用类似模板的形式来描述:

matrix<float, 2, 2> mat1;	// float2x2

而单独的matrix类型的变量实际上可以看做是一个包含了4个vector向量的类型,即包含16个float类型的变量。matrix本身也可以写成float4x4

matrix mat1;	// float4x4

矩阵的初始化方式如下:

float2x2 mat1 = {
	1.0f, 2.0f,	// 第一行
	3.0f, 4.0f  // 第二行
};
float3x3 TBN = float3x3(T, B, N); // T, B, N都是float3

矩阵的取值方式如下:

matrix M;
// ...

float f0 = M._m00;      // 第一行第一列元素(索引从0开始)
float f1 = M._12;       // 第一行第二列元素(索引从1开始)
float f2 = M[0][1];     // 第一行第二列元素(索引从0开始)
float2 f3 = M._11_12;   // Swizzles

矩阵的赋值方式如下:

matrix M;
vector v = {1.0f, 2.0f, 3.0f, 4.0f};
// ...

M[0] = v;               // 矩阵的第一行被赋值为向量v
M._m11 = v[0];          // 等价于M[1][1] = v[0];和M._22 = v[0];
M._12_21 = M._21_12;    // 交换M[2][3]和M[3][2]

无论是向量还是矩阵,乘法运算符都是用于对每个分量进行相乘,例如:

float4 vec0 = 2.0f * float4(1.0f, 2.0f, 3.0f, 4.0f);    //(2.0f, 4.0f, 6.0f, 8.0f)
float4 vec1 = vec0 * float4(1.0f, 0.2f, 0.1f, 0.0f);    //(2.0f, 0.8f, 0.6f, 0.0f)

若要进行向量与矩阵的乘法,则需要使用mul函数。

在C++代码层中,DirectXMath数学库创建的矩阵都是行矩阵,但当矩阵从C++传递给HLSL时,HLSL默认是列矩阵的,看起来就好像传递的过程中进行了一次转置那样。如果希望不发生转置操作的话,可以添加修饰关键字row_major

row_major matrix M;

数组

和C++一样,我们可以声明数组:

float M[4][4];
int p[4];
float3 v[12];	// 12个3D向量

结构体(struct)

HLSL的结构体和C/C++的十分相似,它可以存放任意数目的标量,向量和矩阵类型,除此之外,它还可以存放数组或者别的结构体类型。结构体的成员访问也和C/C++相似:

struct A
{
    float4 vec;
};

struct B
{
    int scalar;
    float4 vec;
    float4x4 mat;
    float arr[8];
    A a;
};

// ...
B b;
b.vec = float4(1.0f, 2.0f, 3.0f, 4.0f);

变量的修饰符

关键字含义
static该着色器变量将不会暴露给C++应用层,需要在HLSL中自己初始化,否则使用默认初始化
extern与static相反,该着色器变量将会暴露给C++应用层
uniform该着色器变量允许在C++应用层被改变,但在着色器执行的过程中,其值始终保持不变(运行前可变,运行时不变)。着色器程序中的全局变量默认为既uniform又extern
const和C++中的含义相同,它是一个常量,需要被初始化且不可以被修改

类型转换

HLSL有着极其灵活的类型转换机制。HLSL中的类型转换语法和C/C++的相同。下面是一些例子:

float f = 4.0f;
float4x4 m = (float4x4)f;	// 将浮点数f复制到矩阵m的每一个元素当中

float3 n = float3(...);
float3 v = 2.0f * n - 1.0f;	// 这里1.0f将会隐式转换成(1.0f, 1.0f, 1.0f)

float4x4 WInvT = float4x4(...);
float3x3 mat = (float3x3)WInvT;	// 只取4x4矩阵的前3行前3列

typedef关键字

和C++一样,typedef关键字用来声明一个类型的别称:

typedef float3 point;			
typedef const float cfloat;

point p;	// p为float3
cfloat f = 1.0f;	// f为const float

运算符的一些特例

本教程不列出关键字,在学习的时候再逐渐接触需要用到的会好一点。

C/C++中能用的运算符在HLSL中基本上都能用,也包括位运算。这里只列出运算符的一些特例情况。

  1. 模运算符%不仅可以用于整数,还能用于浮点数。而且,要进行模运算就必须保证取模运算符左右操作数都具有相同的符号(要么都为正数,要么都为负数)。
  2. 基于运算符的向量间的运算都是以分量为展开的。

例如:

float3 pos = {1.0f, 2.0f, 3.0f};
float3 p1 = pos * 2.0f;		// (2.0f, 4.0f, 6.0f)
float3 p2 = pos * pos;		// (1.0f, 4.0f, 9.0f)
bool3 b = (p1 == p2);		// (false, true, false)
++pos;					// (2.0f, 3.0f, 4.0f)

因此,如果乘法运算符的两边都是矩阵,则表示为矩阵的分量乘法,而不是矩阵乘法。

最后是二元运算中变量类型的提升规则:

  1. 对于二元运算来说,如果运算符左右操作数的维度不同,那么维度较小的变量类型将会被隐式提升为维度较大的变量类型。但是这种提升仅限于标量到向量的提升,即x会变为(x, x, x)。但是不支持像float2float3的提升。
  2. 对于二元运算来说,如果运算符左右的操作数类型不同,那么低精度变量的类型将被隐式提升为高精度变量的类型,这点和C/C++是类似的。

控制流

条件语句

HLSL也支持if, else, continue, break, switch关键字,此外discard关键字用于像素着色阶段抛弃该像素。

条件的判断使用一个布尔值进行,通常由各种逻辑运算符或者比较运算符操作得到。注意向量之间的比较或者逻辑操作是得到一个存有布尔值的向量,不能够直接用于条件判断,也不能用于switch语句。

判断与动态分支

基于值的条件分支只有在程序执行的时候被编译好的着色器汇编成两种方式:判断(predication)动态分支(dynamic branching)

如果使用的是判断的形式,编译器会提前计算两个不同分支下表达式的值。然后使用比较指令来基于比较结果来"选择"正确的值。

而动态分支使用的是跳转指令来避免一些非必要的计算和内存访问。

着色器程序在同时执行的时候应当选择相同的分支,以防止硬件在分支的两边执行。通常情况下,硬件会同时将一系列连续的顶点数据传入到顶点着色器并行计算,或者是一系列连续的像素单元传入到像素着色器同时运算等。

动态分支会由于执行分支指令所带来的开销而导致一定的性能损失,因此要权衡动态分支的开销和可以跳过的指令数目。

通常情况下编译器会自行选择使用判断还是动态分支,但我们可以通过重写某些属性来修改编译器的行为。我们可以在条件语句前可以选择添加下面两个属性之一:

属性描述
[branch]根据条件值的结果,只计算其中一边的内容,会产生跳转指令。默认不加属性的条件语句为branch型。
[flatten]两边的分支内容都会计算,然后根据条件值选择其中一边。可以避免跳转指令的产生。

用法如下:

[flatten]
if (x)
{
    x = sqrt(x);
}

循环语句

HLSL也支持for, whiledo while循环。和条件语句一样,它可能也会在基于运行时的条件值判断而产生动态分支,从而影响程序性能。如果循环次数较小,我们可以使用属性[unroll]来展开循环,代价是产生更多的汇编指令。用法如下:

times = 4;
sum = times;
[unroll]
while (times--)
{
    sum += times;
}

若没有添加属性,默认使用的则为[loop]

函数

函数的语法也和C/C++的十分类似,但它具有以下属性:

  1. 参数只能按值传递
  2. 不支持递归
  3. 只有内联函数(避免产生调用的跳转来减小开销)

此外,HLSL函数的形参可以指定输入/输出类别:

输入输出类别描述
in仅读入。实参的值将会复制到形参上。若未指定则默认为in
out仅输出。对形参修改的最终结果将会复制到实参上
inout即in和out的组合

例如:

bool foo(in bool b,			// 输入的bool类型参数
	out int r1,				// 输出的int类型参数
	inout float r2)			// 具备输入/输出的float类型参数
{
    if (b)
    {
        f1 = 5;
    }
    else
    {
        r1 = 1;
    }
    
    // 注意r1不能出现在等式的右边
    
    // r2既可以被读入,也可以写出结果到外面的实参上
    r2 = r2 * r2 * r2;
    
    return true;
}

内置函数

HLSL提供了一些内置全局函数,它通常直接映射到指定的着色器汇编指令集。这里只列出一些比较常用的函数:

函数名描述最小支持着色器模型
abs每个分量求绝对值1.1
acos求x分量的反余弦值1.1
all测试x分量是否按位全为11.1
any测试x分量是否按位存在11.1
asdouble将值按位重新解释成double类型5.0
asfloat将值按位重新解释成float类型4.0
asin求x分量的反正弦值1.1
asint将值按位重新解释成int类型4.0
asuint将值按位重新解释成uint类型4.0
atan求x分量的反正切值值1.1
atan2求(x,y)分量的反正切值1.1
ceil求不小于x分量的最小整数1.1
clamp将x分量的值限定在[min, max]1.1
clip丢弃当前像素,如果x分量的值小于01.1
cos求x分量的余弦值1.1
cosh求x分量的双曲余弦值1.1
countbits计算输入整数的位1个数(对每个分量)5.0
cross计算两个3D向量的叉乘1.1
ddx估算屏幕空间中的偏导数 ∂ p / ∂ x \partial \mathbf{p} / \partial x p/x。这使我们可以确定在屏幕空间的x轴方向上,相邻像素间某属性值 p \mathbf{p} p的变化量2.1
ddy估算屏幕空间中的偏导数 ∂ p / ∂ y \partial \mathbf{p} / \partial y p/y。这使我们可以确定在屏幕空间的y轴方向上,相邻像素间某属性值 p \mathbf{p} p的变化量2.1
degrees将x分量从弧度转换为角度制1.1
determinant返回方阵的行列式1.1
distance返回两个点的距离值1.1
dot返回两个向量的点乘1.1
dst计算距离向量5.0
exp计算e^x1.1
exp2计算2^x1.1
floor求不大于x分量的最大整数1.1
fmod求x/y的余数1.1
frac返回x分量的小数部分1.1
isfinite返回x分量是否为有限的布尔值1.1
isinf返回x分量是否为无穷大的布尔值1.1
isnan返回x分量是否为nan的布尔值1.1
length计算向量的长度1.1
lerp求x + s(y - x)1.1
lit返回一个光照系数向量(环境光亮度, 漫反射光亮度, 镜面光亮度, 1.0f)1.1
log返回以e为底,x分量的对数1.1
log10返回以10为底,x分量的对数1.1
log2返回以2为底,x分量的自然对数1.1
mad返回mvalue * avalue + bvalue1.1
max返回x分量和y分量的最大值1.1
min返回x分量和y分量的最小值1.1
modf将值x分开成整数部分和小数部分1.1
mul矩阵乘法运算1
normalize计算规格化的向量1.1
pow返回x^y1.1
radians将x分量从角度值转换成弧度值1
rcp对每个分量求倒数5
reflect返回反射向量1
refract返回折射向量1.1
reversebits对每个分量进行位的倒置5
roundx分量进行四舍五入1.1
rsqrt返回1/sqrt(x)1.1
saturate对x分量限制在[0,1]范围1
sign计算符号函数的值,x大于0为1,x小于0为-1,x等于0则为01.1
sin计算x的正弦1.1
sincos返回x的正弦和余弦1.1
sinh返回x的双曲正弦1.1
smoothstep给定范围[min, max],映射到值[0, 1]。小于min的值取0,大于max的值取11.1
step返回(x >= a) ? 1 : 01.1
tan返回x的正切值1.1
tanh返回x的双曲正切值1.1
transpose返回矩阵m的转置1
trunc去掉x的小数部分并返回1

语义

语义通常是附加在着色器输入/输出参数上的字符串。它在着色器程序的用途如下:

  1. 用于描述传递给着色器程序的变量参数的含义
  2. 允许着色器程序接受由渲染管线生成的特殊系统值
  3. 允许着色器程序传递由渲染管线解释的特殊系统值

顶点着色器语义

输入描述类型
BINORMAL[n]副法线(副切线)向量float4
BLENDINDICES[n]混合索引uint
BLENDWEIGHT[n]混合权重float
COLOR[n]漫反射/镜面反射颜色float4
NORMAL[n]法向量float4
POSITION[n]物体坐标系下的顶点坐标float4
POSITIONT变换后的顶点坐标float4
PSIZE[n]点的大小float
TANGENT[n]切线向量float4
TEXCOORD[n]纹理坐标float4
Output仅描述输出Type
FOG顶点雾float

n是一个可选的整数,从0开始。比如POSITION0, TEXCOORD1等等。

像素着色器语义

输入描述类型
COLOR[n]漫反射/镜面反射颜色float4
TEXCOORD[n]纹理坐标float4
Output仅描述输出Type
DEPTH[n]深度值float

系统值语义

所有的系统值都包含前缀SV_。这些系统值将用于某些着色器的特定用途(并未全部列出)

系统值描述类型
SV_Depth深度缓冲区数据,可以被任何着色器写入/读取float
SV_InstanceID每个实例都会在运行期间自动生成一个ID。在任何着色器阶段都能读取uint
SV_IsFrontFace指定该三角形是否为正面。可以被几何着色器写入,以及可以被像素着色器读取bool
SV_Position若被声明用于输入到着色器,它描述的是像素位置,在所有着色器中都可用,可能会有0.5的偏移值float4
SV_PrimitiveID每个原始拓扑都会在运行期间自动生成一个ID。可用在几何/像素着色器中写入,也可以在像素/几何/外壳/域着色器中读取uint
SV_StencilRef代表当前像素着色器的模板引用值。只可以被像素着色器写入uint
SV_VertexID每个实例都会在运行期间自动生成一个ID。仅允许作为顶点着色器的输入uint

通用着色器的核心

所有的着色器阶段使用通用着色器核心来实现相同的基础功能。此外,顶点着色阶段、几何着色阶段和像素着色阶段则提供了独特的功能,例如几何着色阶段可以生成新的图元或删减图元,像素着色阶段可以决定当前像素是否被抛弃等。下图展示了数据是怎么流向一个着色阶段,以及通用着色器核心与着色器内存资源之间的关系:

Input Data:顶点着色器从输入装配阶段获取数据;几何着色器则从上一个着色阶段的输出获取等等。通过给形参引入可以使用的系统值可以提供额外的输入

Output Data:着色器生成输出的结果然后传递给管线的下一个阶段。有些输出会被通用着色器核心解释成特定用途(如顶点位置、渲染目标对应位置的值),另外一些输出则由应用程序来解释。

Shader Code:着色器代码可以从内存读取,然后用于执行代码中所期望的内容。

Samplers:采样器决定了如何对纹理进行采样和滤波。

Textures:纹理可以使用采样器进行采样,也可以基于索引的方式按像素读取。

Buffers:缓冲区可以使用读取相关的内置函数,在内存中按元素直接读取。

Constant Buffers:常量缓冲区对常量值的读取有所优化。他们被设计用于CPU对这些数据的频繁更新,因此他们有额外的大小、布局和访问限制。

着色器常量

着色器常量存在内存中的一个或多个缓冲区资源当中。他们可以被组织成两种类型的缓冲区:常量缓冲区(cbuffers)和纹理缓冲区(tbuffers)。关于纹理缓冲区,我们不在这讨论。

常量缓冲区(Constant Buffer)

常量缓冲区允许C++端将数据传递给HLSL中使用,在HLSL端,这些传递过来的数据不可更改,因而是常量。常量缓冲区对这种使用方式有所优化,表现为低延迟的访问和允许来自CPU的频繁更新,因此他们有额外的大小、布局和访问限制。

声明方式如下:

cbuffer VSConstants
{
    float4x4 g_WorldViewProj;
    fioat3 g_Color;
    uint g_EnableFog;
    float2 g_ViewportXY;
    float2 g_ViewportWH;
}

由于我们写的是原生HLSL,当我们在HLSL中声明常量缓冲区时,还需要在HLSL的声明中使用关键字register手动指定对应的寄存器索引,然后编译器会为对应的着色器阶段自动将其映射到15个常量缓冲寄存器的其中一个位置。这些寄存器的名字为b0b14

cbuffer VSConstants : register(b0)
{
    float4x4 g_WorldViewProj;
    fioat3 g_Color;
    uint g_EnableFog;
    float2 g_ViewportXY;
    float2 g_ViewportWH;
}

在C++端是通过ID3D11DeviceContext::*SSetConstantBuffers指定特定的槽(slot)来给某一着色器阶段对应的寄存器索引提供常量缓冲区的数据。

如果是存在多个不同的着色器阶段使用同一个常量缓冲区,那就需要分别给这两个着色器阶段设置好相同的数据。

综合前面几节内容,下面演示了顶点着色器和常量缓冲区的用法:

cbuffer ConstantBuffer : register(b0)
{
    float4x4 g_WorldViewProj;
}


void VS_Main(
    in float4 inPos : POSITION,         // 绑定变量到输入装配器
    in uint VID : SV_VertexID,          // 绑定变量到系统生成值
    out float4 outPos : SV_Position)    // 告诉管线将该值解释为输出的顶点位置
{
    outPos = mul(inPos, g_WorldViewProj);
}


上面的代码也可以写成:

cbuffer ConstantBuffer : register(b0)
{
    float4x4 g_WorldViewProj;
}

struct VertexIn
{
	float4 inPos : POSITION;	// 源自输入装配器
	uint VID : SV_VertexID;		// 源自系统生成值
};

float4 VS_Main(VertexIn vIn) : SV_Position
{
    return mul(vIn.inPos, g_WorldViewProj);
}

有关常量缓冲区的打包规则,建议在阅读到时索引缓冲区、常量缓冲区一章时,再来参考杂项篇的HLSL常量缓冲区的打包规则。

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

  • 10
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 《DirectX 3D HLSL 高级实例精讲》是一本关于图形编程技术的书籍,主要讲解了DirectX 3D的高级编程以及HLSL着色语言的使用。 这本书首先介绍了DirectX 3D的基本概念和原理,包括渲染管线、顶点和像素着色器以及纹理等。然后详细介绍了HLSL着色语言,包括语法、变量类型、常量缓冲区和输入输出布局等。通过深入理解HLSL,读者可以编写高效的着色器代码,实现更加逼真和优化的图形效果。 除了基础知识,该书还提供了一些高级实例,涵盖了可见性及遮挡剔除、阴影技术、光照模型、反射折射效果、法线贴图、粒子系统和后期处理等。每一个实例都详细介绍了实现思路和具体的代码实现,读者可以通过跟随实例学习如何使用DirectX 3D和HLSL来实现各种复杂的图形效果。 这本书适合有一定图形编程基础的读者,特别是对DirectX 3D和HLSL感兴趣的开发者。通过学习本书,读者可以深入了解DirectX 3D和HLSL的原理和应用,掌握高级图形编程技术,提升自己在游戏开发、虚拟现实、计算机图形学等领域的能力。 总之,《DirectX 3D HLSL 高级实例精讲》是一本介绍DirectX 3D和HLSL高级编程技术的实用指南,适合有一定图形编程基础的读者学习和参考。它详细讲解了基础原理和实例实现,读者可以通过学习本书提升自己在图形编程领域的能力。 ### 回答2: "DirectX 3D HLSL高级实例精讲" 是一本介绍DirectX 3D和HLSL编程的高级实例教材。该PDF文件详细讲解了如何使用DirectX 3D和HLSL编写高效的图形渲染程序。 该教材涵盖了多个实例,从基本的渲染技术到更高级的图形效果。它从介绍DirectX 3D和HLSL的基础知识开始,然后深入探讨了渲染管线的不同阶段以及如何利用HLSL语言编写高级着色器来实现各种图形效果。 该PDF还提供了大量的代码实例和详细的解释,帮助读者理解和实践所学内容。它包括了如何创建3D对象、设置材质和光照效果、实现纹理映射、使用几何着色器和像素着色器等内容。同时,它还介绍了一些高级技术,如阴影算法、法线贴图、环境光遮蔽等,使读者能够进一步提升图形渲染的质量和效果。 通过学习这本教材,读者可以深入了解DirectX 3D和HLSL的基本原理和技术,掌握如何使用它们来实现各种复杂的图形效果。无论是想要进一步学习图形渲染相关知识的专业人士,还是对图形编程感兴趣的爱好者,该教材都是一本值得阅读的参考书。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值