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,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索