D3D9 Shader例項教程

燕良發表於2016-09-07

啥是Shader?

Shader是一段執行在GPU上的小程式,是執行在GPU上的Pipeline上的特定的可程式設計單元的小程式。

從D3D9 API層面學習Shader程式設計

隨著Unity3D的流行,很多同學會把Unity ShaderLab和Shader這兩個概念搞混。 ShaderLab是Unity引擎中的Shader系統的名稱,它完成的是組織Shader、渲染狀態、渲染Pass,以及引數繫結等功能,有點像D3D SDK提供的D3D Effect。
如果想系統的學習Shader,就需要從圖形API的Pipeline開始。當然,現在理想的是去學習D3D 12,Valukan,Metal這樣的新一代API,至少也是D3D 11這樣的成熟API,或者Open GL ES,如果你專供移動端圖形的話。
下面這套程式碼,是我在2008年的時候,基於D3D9 API寫的一組教程程式碼,因為最近有同學問起,所以找出來重新發布。

程式碼已經上傳到GitHub:https://github.com/neil3d/myshaders

D3D9 Shader教程文件

實現基本光照

基本光照

程式的基本思路

class SimpleD3DApp處理了win32視窗建立和D3D初始化的工作,然後響應選單,生成相應的ShaderSimpler派生類例項。 一個個ShaderSimpler的派生類為一個shader試驗小程式。

Effect的基本處理步驟

  1. 編輯一個純文字檔案,推薦字尾為fx. shader\SimpleDraw.fx是一個使用vertex shader和pixel shader的最簡單的Effect檔案:
  2. 使用D3DXCreateEffectFromFile函式載入生成一個ID3DXEffect物件;另外也可以使用fxc命令列工具把上述文字檔案編譯成二進位制檔案。
  3. 在渲染前呼叫ID3DXEffect::SetMatrix()設定effect中的引數;
  4. 使用ID3DXEffect::Begin(),BeginPass(),EndPass(),End()來控制渲染流程。

實現一個簡單omni燈光的計算

  1. 對於每個頂點進行燈光計算;
  2. 光照公式使用”距離衰減*(N.L)”;

參考

Phong shading

Phong
這個小程式通過把頂點法線逐象素插值,再計算每個象素的diffuse,specular光照結果來簡單的實現phong模型。因為specular是view depended,所以Effect中增加了eyePos引數。

  • 通過觀察box的渲染可以看出與basic lighting的明顯不同。
  • 這段小程式沒有考慮效能的問題,為了提升速度,可以ps中的L,V,R放到vs中去計算。
  • 鏡頭操作:WS前後,AD左右,ZX上下移動,滑鼠拖動為轉動。

卡通渲染

Cel Shading
卡通渲染主要包括兩個效果:
1. 把普通光照的平滑亮度變化變成幾個強度級別。這是通過計算出光照強調,然後查詢一個貼圖來實現的,此貼圖儲存0~1的4個固定段。
2. 描出輪廓線。法線與視線垂直的點為輪廓,所以使用兩者的點積結果來查詢一個貼圖,然後乘以光照顏色來實現描邊,此貼圖0附近為全黑,其餘為全擺。

參考:

環境對映

Cube Map
Cube Map環境對映特別適合與曲面,實現也很簡單--構造一個視點到頂點的向量,然後計算出相對與normal的反射向量,用此向量索引cube map貼圖即可。

順便比較了一下shpere環境對映的效果。Sphere環境對映的實現也很簡單,把頂點法向量變換到view space,然後使用貼圖座標(n.x/2+0.5,n.y/2+0.5)來索引sphere map貼圖即可。

參考

  • DX9SDK simple, HLSL workshop, Goal 3

Bump Mapping(Normal mapping)

Normal Map

從貼圖生成normal map

normal map可以從一個顏色貼圖對應的高度圖生成。為了計算i,j點的normal,首先可以根據高度差計算出s,t方向的切向量:

 S(i,j) = <1, 0, aH(i+1,j)-aH(i-1,j)>
 T(i,j) = <0, 1, aH(i,j+1)-aH(i,j-1)>

其中a是一個縮放係數。normal可以由這兩個切向量得出
*N(i,j) = normalize(cross(S,T)) = cross(S,T)/length(cross(S,T)) = <-Sz, -Tz, 1> / sqrt(Sz^2 + Tz^2 +1)
然後可以把這個向量使用RGB編碼儲存到貼圖檔案中。原理是這樣,本例使用nvidia提供的工具生成normal map。

計算出Tangent space

這個計算稍微有些複雜。記得老早的時候nvidia有一個文件寫的很清楚,不過我怎麼也搜不出來了。哪位要是知道,請給我個連結。在Eric Lengyel的書裡面找到了另外一種解法,講得也很透徹。
本例子中簡單的呼叫了D3DXComputeTangent,也可以使用nvidia sdk中提供的nv_meshmender來計算。

光照計算

使用Bump map的光照計算與前面的PhongShading例子相比沒有什麼新鮮的。前面的例子中每個象素的normal只是簡單的從頂點normal插值而來,而使用bump map後,每個頂點的normal可以從貼圖中查詢;另外因為這個normal是存在與tangent space中,所以需要把light dir和view dir都轉換到tangent space再進行計算。

參考

Parallax Mapping

ParallaxMap
Parallax map通過簡單的計算可以大幅提高per-pixel光照的效果。Parallax map的想法基於一個簡單的事實,那就是當我們用一個多邊形去表現一個凹凸不平的表面的時候,事實上隨著視線的變化,看到的實際的texel並不是頂點uv座標差值出來的那個結果,而是有一定的偏移。而這個偏移可以通過是個高度圖來計算。

在本例中,經過簡化,最後單個象素uv偏移量的公式為:

offset = <viewDir.x, viewDir.y>*(height*SCALE+BIAS)

其中的height就是高度圖在這個uv的高度值,然後根據實際需要進行縮放和偏移。SCALE和BIAS可以根據所要渲染的物體的單位來估計,例如要渲染一個1平方米的磚牆,那麼我們估計每個磚的起伏在幾釐米。在本例中使用這個offset將uv進行偏移之後,再去取樣bump map和color map。

  • 程式執行時按字母鍵“P”可以開啟、關閉parallax map,用來比較效果。:)

參考

  • Terry Welsh, Parallax Mapping, ShaderX3 p89

實現類似魔獸世界的“全屏泛光”

bloom
現在很多遊戲,像WOW、激戰都有這種“全屏泛光”的畫面效果,很多人以為是使用了HDR,其實就是一個post process效果。
PostProcess是一個很簡單又有用的概念,顧名思義,就是把渲染之後的場景,在影象空間進行一些處理。它可以做出各種各樣的效果。

全屏泛光的實現步驟如下

  1. 分配三個texture,F,A,B,其中F的大小與back buffer一致,A和B為back buffer面積的四分之一;
  2. 將場景渲染到F;
  3. 將F渲染到A - 執行down sample和high-pass filter;
  4. 將A渲染到B - 執行H blur;
  5. 將B渲染到A - 執行V blur,先橫向再縱向計算,把每個象素需要的計算由n*n變為n+n,其中n為blur區域的大小;
  6. 將F和A混合,渲染到back buffer,然後present。至於混合的公式,就可以自己發揮了。:)

參考

  • nVidia FXComposer

初探Deferred shading

Deffered Shading
對於一個基於Shader的渲染系統如何來計算多個動態光源的光照呢?這是一個困擾我很久的問題。基本上有這樣三種解決方案:
1. 在單個pass中計算物體的所有光照。這種方案的主要缺點就是shader管理非常複雜。常見的思路是基於一套shader模版,自動生成n個光照shader,n往往能達到上百個;另外一個主要的缺點就是很多fragment經過複雜的光照計算之後又被z測試剔除,造成計算的浪費(可以先進行一個z only pass來優化);
2. 為每個燈光進行一個pass。這種方案也會有前者over draw的問題,另外還會造成draw call數量增加;
3. deferred shading,這種方案結構簡單,但是對顯示卡要求較高,需要multi-render targets, float point render target;還有就是對於透明面,目前還沒有很好的解決方法。

下面就介紹一下Deferred Shading的思路。它的想法非常的簡單、直接,簡單的說就是我們可以把光照計算需要的一些引數先通過一個pass渲染到一些render target中(稱為Attributes buffer或G-Buffer),然後可以為每個燈光進行一個pass來計算光照。

原始碼中演示了deferred shading的基本概念,實現了4個點光源的phong 光照渲染。為了能在Geforce 5600上跑,沒有使用multi-render targets,而是用了多個pass來生成G-Buffer,這樣也可以更清楚的演示deferred shading的概念。
首先程式建立了position,normal,diffuse三個render target,在渲染時使用3個pass,把場景的“世界座標”,“法線(世界座標系)”,“顏色”,分別渲染到這三個render target中;為了簡單起見,假設所有燈光都覆蓋整個螢幕――為每個燈光進行一個pass,渲染一個全螢幕的quad,然後為每個象素計算光照,並使用additive混合,把光照強度累加到螢幕緩衝中。

Position

POS

Normal

NORMAL

Diffuse

DIFFUSE

Deferred shading with Multiple Render Targets

MRT
今天抽空是試驗了一下使用MRT進行deferred shading。基本是在上個例子的基礎上修改的,主要有一下幾個地方:
- 使用SetRenderTarget分別將各個render target設定好;
- 在pixel shader中使用“COLOR[n]”來寫入各個render target;
- 有一點需要注意的是,MRT的各個surface的bits必須是一致的,所以這個例子使用了兩個G16R16的float - texture來儲存position。
OK,就是這樣簡單。渲染效果與上個例子一樣。這個例子程式在geforce 7600GT上測試通過,其他顯示卡沒測過。

全文結束

相關文章