DirectX11--HLSL语法入门

前言

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

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

DirectX11 With Windows SDK完整目录github

Github项目源码数组

欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。函数

数据类型

标量

经常使用标量类型以下:oop

类型 描述
bool 32位整数值用于存放逻辑值true和false
int 32位有符号整数
uint 32位无符号整数
half 16位浮点数(仅提供用于向后兼容)
float 32位浮点数
double 64位浮点数

注意:一些平台可能不支持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;    // (4.0f, 4.0f, 1.0f, 2.0f)
vec3.wxyz = vec3;           // (2.0f, 4.0f, 4.0f, 1.0f)
vec3.yw = ve1.zz;           // (2.0f, 3.0f, 4.0f, 3.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开始)
float4 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份量是否按位全为1 1.1
any 测试x份量是否按位存在1 1.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份量的值小于0 1.1
cos 求x份量的余弦值 1.1
cosh 求x份量的双曲余弦值 1.1
countbits 计算输入整数的位1个数(对每一个份量) 5.0
cross 计算两个3D向量的叉乘 1.1
ddx 估算屏幕空间中的偏导数\(\partial \mathbf{p} / \partial x\)。这使咱们能够肯定在屏幕空间的x轴方向上,相邻像素间某属性值\(\mathbf{p}\)的变化量 2.1
ddy 估算屏幕空间中的偏导数\(\partial \mathbf{p} / \partial y\)。这使咱们能够肯定在屏幕空间的y轴方向上,相邻像素间某属性值\(\mathbf{p}\)的变化量 2.1
degrees 将x份量从弧度转换为角度制 1.1
determinant 返回方阵的行列式 1.1
distance 返回两个点的距离值 1.1
dot 返回两个向量的点乘 1.1
dst 计算距离向量 5.0
exp 计算e^x 1.1
exp2 计算2^x 1.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 + bvalue 1.1
max 返回x份量和y份量的最大值 1.1
min 返回x份量和y份量的最小值 1.1
modf 将值x分开成整数部分和小数部分 1.1
mul 矩阵乘法运算 1
normalize 计算规格化的向量 1.1
pow 返回x^y 1.1
radians 将x份量从角度值转换成弧度值 1
rcp 对每一个份量求倒数 5
reflect 返回反射向量 1
refract 返回折射向量 1.1
reversebits 对每一个份量进行位的倒置 5
round x份量进行四舍五入 1.1
rsqrt 返回1/sqrt(x) 1.1
saturate 对x份量限制在[0,1]范围 1
sign 计算符号函数的值,x大于0为1,x小于0为-1,x等于0则为0 1.1
sin 计算x的正弦 1.1
sincos 返回x的正弦和余弦 1.1
sinh 返回x的双曲正弦 1.1
smoothstep 给定范围[min, max],映射到值[0, 1]。小于min的值取0,大于max的值取1 1.1
step 返回(x >= a) ? 1 : 0 1.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,以及有什么问题也能够在这里汇报。

相关文章
相关标签/搜索