编写本内容仅仅是为了完善当前的教程体系,入门级别的内容其实基本上都是千篇一概,仅有一些必要细节上的扩充。要入门HLSL,只是掌握入门语法,即使把HLSL的所有语法也吃透了也并不表明你就能着色器代码了,还须要结合到渲染管线中,随着教程的不断深刻来不断学习须要用到的新的语法,而后尝试修改着色器,再根据实际需求本身编写着色器来实现特定的效果。html
注意:在翻阅HLSL文档的时候,要避开Effects11相关的内容。由于当前教程与Effects11是不兼容的。git
DirectX11 With Windows SDK完整目录github
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。函数
经常使用标量类型以下:oop
类型 | 描述 |
---|---|
bool | 32位整数值用于存放逻辑值true和false |
int | 32位有符号整数 |
uint | 32位无符号整数 |
half | 16位浮点数(仅提供用于向后兼容) |
float | 32位浮点数 |
double | 64位浮点数 |
注意:一些平台可能不支持
int
,half
和double
,若是出现这些状况将会使用float
来模拟布局
此外,浮点数还有规格化的形式:性能
snorm float
是IEEE 32位有符号且规格化的浮点数,表示范围为-1到1unorm 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)
矩阵有以下类型(以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向量
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列
和C++同样,typedef关键字用来声明一个类型的别称:
typedef float3 point; typedef const float cfloat; point p; // p为float3 cfloat f = 1.0f; // f为const float
本教程不列出关键字,在学习的时候再逐渐接触须要用到的会好一点。
C/C++中能用的运算符在HLSL中基本上都能用,也包括位运算。这里只列出运算符的一些特例状况。
例如:
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)
所以,若是乘法运算符的两边都是矩阵,则表示为矩阵的份量乘法,而不是矩阵乘法。
最后是二元运算中变量类型的提高规则:
x
会变为(x, x, x)
。可是不支持像float2
到float3
的提高。HLSL也支持if
, else
, continue
, break
, switch
关键字,此外discard
关键字用于像素着色阶段抛弃该像素。
条件的判断使用一个布尔值进行,一般由各类逻辑运算符或者比较运算符操做获得。注意向量之间的比较或者逻辑操做是获得一个存有布尔值的向量,不可以直接用于条件判断,也不能用于switch
语句。
基于值的条件分支只有在程序执行的时候被编译好的着色器汇编成两种方式:判断(predication)和动态分支(dynamic branching)。
若是使用的是判断的形式,编译器会提早计算两个不一样分支下表达式的值。而后使用比较指令来基于比较结果来"选择"正确的值。
而动态分支使用的是跳转指令来避免一些非必要的计算和内存访问。
着色器程序在同时执行的时候应当选择相同的分支,以防止硬件在分支的两边执行。一般状况下,硬件会同时将一系列连续的顶点数据传入到顶点着色器并行计算,或者是一系列连续的像素单元传入到像素着色器同时运算等。
动态分支会因为执行分支指令所带来的开销而致使必定的性能损失,所以要权衡动态分支的开销和能够跳过的指令数目。
一般状况下编译器会自行选择使用判断仍是动态分支,但咱们能够经过重写某些属性来修改编译器的行为。咱们能够在条件语句前能够选择添加下面两个属性之一:
属性 | 描述 |
---|---|
[branch] | 根据条件值的结果,只计算其中一边的内容,会产生跳转指令。默认不加属性的条件语句为branch型。 |
[flatten] | 两边的分支内容都会计算,而后根据条件值选择其中一边。能够避免跳转指令的产生。 |
用法以下:
[flatten] if (x) { x = sqrt(x); }
HLSL也支持for
, while
和do while
循环。和条件语句同样,它可能也会在基于运行时的条件值判断而产生动态分支,从而影响程序性能。若是循环次数较小,咱们可使用属性[unroll]
来展开循环,代价是产生更多的汇编指令。用法以下:
times = 4; sum = times; [unroll] while (times--) { sum += times; }
若没有添加属性,默认使用的则为[loop]
。
函数的语法也和C/C++的十分相似,但它具备如下属性:
此外,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 |
语义一般是附加在着色器输入/输出参数上的字符串。它在着色器程序的用途以下:
输入 | 描述 | 类型 |
---|---|---|
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)。关于纹理缓冲区,咱们不在这讨论。
常量缓冲区容许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个常量缓冲寄存器的其中一个位置。这些寄存器的名字为b0
到b14
:
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完整目录
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。