前言
撰寫本內容僅僅是為了完善當前的教程體系,入門級別的內容其實基本上都是千篇一律,僅有一些必要細節上的擴充,要入門HLSL,只是掌握入門語法,即便把HLSL的全部語法也吃透了也并不代表你就能著色器代碼了,還需要結合到渲染管線中,隨著教程的不斷深入來不斷學習需要用到的新的語法,然后嘗試修改著色器,再根據實際需求自己撰寫著色器來實作特定的效果,
注意:在翻閱HLSL檔案的時候,要避開Effects11相關的內容,因為當前教程與Effects11是不兼容的,
DirectX11 With Windows SDK完整目錄
Github專案原始碼
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,
資料型別
標量
常用標量型別如下:
| 型別 | 描述 |
|---|---|
| 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)
矩陣(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[0][1]和M[1][0]
無論是向量還是矩陣,乘法運算子都是用于對每個分量進行相乘,例如:
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中基本上都能用,也包括位運算,這里只列出運算子的一些特例情況,
- 模運算子%不僅可以用于整數,還能用于浮點數,而且,要進行模運算就必須保證取模運算子左右運算元都具有相同的符號(要么都為正數,要么都為負數),
- 基于運算子的向量間的運算都是以分量為展開的,
例如:
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的提升, - 對于二元運算來說,如果運算子左右的運算元型別不同,那么低精度變數的型別將被隱式提升為高精度變數的型別,這點和C/C++是類似的,
控制流
條件陳述句
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),關于紋理緩沖區,我們不在這討論,
常量緩沖區(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個常量緩沖暫存器的其中一個位置,這些暫存器的名字為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完整目錄
Github專案原始碼
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/5302.html
標籤:其他
