vs如何写多线程_VS + PS + GS
VS + PS + GS
最近一直有人問我,我的書章節有沒有順序。我本人讀書,很不喜歡按部就班,一點一點往下看這個套路,所以我也就沒有規定章節。在我看來,除了教材按部就班以外,很多書,都應該是單獨章節的,想看什么就是什么,想看材質就看材質,想看陰影就是陰影,而不是說必須先看什么,再到什么。
但是,不得不說,如果非要論順序,本章節按道理是比較靠前的。原因無他,因為本章節,是大部分章節的基礎。如果VS,PS都不熟,看陰影章節,按道理是看不下去的。但是,我寫陰影章節的時候,預計會默認已經有了一定的圖形學基礎。
閑話少說,先來一波解釋。VS,一般叫Vertex Shader,中文有個翻譯叫做頂點著色器。PS,一般是指Pixel Shader,有時候也叫FS,有個中文翻譯叫像素著色器。GS,也就是Geometry Shader,中文一般翻譯做集合體著色器。
如果自己寫過軟光柵,其實比較好理解這些著色器的各個階段。沒寫過的話,強烈推薦寫一次。實在沒有寫過又想往下看,我勉為其難解釋一下。
假設在CPU做一個軟光柵,不考慮多線程軟光柵的話,大概是這樣:
For(int i = 0; i < 三角形數量; i++)
{
RenderTriangle(); // 渲染三角形
}
RenderTriangle這個函數大概是怎么弄呢?你渲染的時候,總得先判斷這個三角形在不在屏幕上,在屏幕的坐標啊。這部分,叫做坐標變換,也就是VS的主要工作之一。
有一個矩陣,叫做WorldViewProjection,簡稱WVP。你用WVP * Position,就能夠得到屏幕坐標。當然了,實際上要稍稍復雜一點,因為還需要轉換到屏幕坐標系。這部分內容,我記得以前應該講過,這里還是簡單復習一下,完全熟悉這塊的略過。普通的坐標系,書本上叫做平面直角坐標系,大概是這樣:
但是,屏幕坐標,你懂的,左上角是0,0,右下角是1920,1080。所以,這里有個坐標變換,而這個,是不需要你自己在VS里面做的,硬件就會幫你做了。但是,寫軟光柵,這部分需要自己做。本來想找找自己軟光柵的代碼貼一下,一下子找不到,算了。
所以,VS的核心功能,其實就是把三角形變換到屏幕上。其他都是次要功能。大多數簡單的VS,大概率就是這樣一行代碼:
那么,這么簡單的話,VS要來何用?當然了,很多時候,我們必須用到VS。例如,我們最常見的水面波動,就可以用VS來做。
怎么做?我開始折騰VS,就是練習自己寫水面。最簡單的做法,你可以傳入一個時間,然后在VS里面花式sin一下。你可以sin一下position的高度,可以sin(t * pos.y),也可以sin(t * pos.length),反正就是各種算,然后,你會慢慢摸索并且理解VS,盡管你做的效果很糟糕。
這里涉及到一個問題。無時無刻,你要切記:VS、PS、GS,都是執行在GPU中的程序,而引擎里的代碼,基本上都是執行在CPU的,所以這里涉及到CPU跟GPU的同步,例如,這個時間,GPU是不知道的,而CPU可以輕易通過系統的API獲得,例如典型的c函數time(NULL),例如GetTickCount……。所以,這里需要CPU把時間傳入到GPU。怎么傳入?不同的shader語言其實有不同的做法,一般的書里都是介紹用Uniform變量,例如這種:
我一般不這樣介紹,不同語法有不同的做法,例如典型的dx11里面,可以用const buffer。Const buffer好像最大是1024還是2*1024,可以查一下MS的文檔,我大概率不會記錯。這個必須16字節對齊,然后自己memcpy到這個里面就可以了。不同的圖形API,例如metal,GL,Vulkan等等,都可能是不同的,理解這個原理就好了,不同的API,查查文檔即可。
所以切記,幀的概念。每一幀,CPU都需要更新每一個Mesh的傳入GPU的變量。大概率,引擎里面,都會計算這個Mesh的WVP,然后傳入到GPU。模型越多,這個效率自然也就越低。
這么簡單的介紹,是不是有VS不外如是的感覺?可以有這種感覺,沒有錯。但是,當你非常純熟,爐火純青的時候,VS有很多優化,提升性能的妙用。
舉個例子,現在有一大片草地,你有1000棵草,這些草隨風而動。你可以這么干:大概計算一個風力,方向,然后計算這個草的偏移,大概可以這樣:
For(int i = 0; i < 1000; i++)
{
//計算每一棵草的頂點偏移。
GrassYaw(i);
}
這其實是用CPU來算,我沒記錯的話,早期OGRE的Grass的DEMO,就是這么干的。但是顯然,這么干效率低下,可以用GPU做優化。GPU優化聽起來高大上,無非就是把這個計算,放在VS里面干。
為什么GPU做這種事更快?強調一千遍,GPU是并行的,在CPU里,你需要循環1000次,GPU直接開1000個線程執行一次,哪個更快是顯而易見的事。
所以,對于剛入門的人來說,VS就是一個簡單的空間變換。但是,寫得多了,什么東西該在VS里面,什么東西該在CPU里做,什么時候要上CS,輕易就能找到最優方案,這就是能力的不同。
回歸正題,VS做了坐標變換之后,已知某個三角形要顯示在某個像素上,那么,現在需要計算這個像素的顏色,這部分工作,其實就是PS來做的。這部分,在很原始的階段,主要就是做一個像素采樣。所謂像素采樣,就是根據UV坐標,計算這個像素該顯示圖片上的什么顏色。這部分,我應該已經在之前的材質章節講過。不打算再詳細講了。這里,主要講講PS能做些什么東西,一般用來做什么。
這個是最簡單的PS,直接返回一個白色。無論你玩什么花樣,傳入什么參數,都沒用,直接返回白色。
這個,就是最簡單的紋理采樣,根據UV坐標,采樣紋理。有大佬說,怎么我看到的跟你這里的不一樣?當然可能不一樣,例如dx9的hlsl,大概率是這樣一個函數:
tex2D();
采樣的函數有很多,不同的圖形API,不同的版本,都可能不同,但是基本原理是一樣的。這類函數一般有一個系列,例如texCube(), tex2DLod(),隨便去搜一個hlsl說明文檔,你能看到一大堆。這類函數有區別嗎?還是有區別的,采樣cube圖,或者lod此類的,各有不同。我不打算在這里詳細講,需要的話,可以看文檔。沒記錯的話,我在紋理章節里,應該有涉及到一些,例如就是紋理LOD的,其實就是紋理的mipmap,所謂的雙線性采樣,三線性采樣,如何求偏導數,應該都有講過。
那么,PS里能干什么?但凡跟屏幕顏色相關的,都可以做。舉例:做一個簡單的漸變效果,例如一個人物,在場景里面慢慢的顯示出來或者消失,就可以通過一個alpha來做一個漸變,在PS里面實現。大概是這樣即可:
你可以在CPU里面,把Alpha的值,在3秒的時間內,從0到1,或者從1到0,就可以實現這類漸變。你還可以這里玩各種花樣,例如人物穿一身衣服,漸變切換到另外一身衣服,諸如此類的東西,都極其簡單。多寫幾次就會了,沒什么難度。
除了漸變,常見的,還可以做模糊。最簡單的模糊怎么做?例如你要采樣的坐標是UV,那么你采樣的時候,把周邊的像素都采樣了,求個平均值,這就是最簡單的模糊算法。會顯示一個虛像,整個畫面變得模糊。大概可以這樣干:
看到了嗎?其實就是采樣了周邊的像素,加權求平均,就是如此簡單。后續講陰影的時候,會有專門講到,陰影的邊緣是如何做模糊的,其實最簡單的做法,也是這么干,就能實現陰影邊緣的鋸齒變模糊。
PS還有一個非常常見的應用。這個應用,有一個比較奇怪的名字,叫“后處理”。我不知道正規的書籍是不是這么翻譯的,不同的引擎這個叫法不同,例如在OGRE里面,這個叫做Compositor。而在UE4里面,這個叫做Post Process Volume。主要做些什么呢?例如DOF(Depth Of View,景深),Blur(模糊),HDR……諸如此類的種種效果,都是用的所謂的“后處理”。說起來玄乎,其實很簡單。先正常渲染,得到一張圖片,然后對這張圖片重新處理一下,實際上就是再在屏幕上畫一個矩形,剛好滿屏,就是(-1,1)之間即可,然后在PS里實現各種效果,例如可以調色,可以理解為photoshop里面的濾鏡。很多奇奇怪怪的效果,都是這樣實現的。例如游戲里,一刀砍過去,整個屏幕一陣抖動,看起來很玄乎,都是類似的應用。這個應用有一個大硬傷,就是不支持傳統的AA(抗鋸齒),例如MSAA。這里,擴展講一下AA,AA其實可以單獨開章節講的,畢竟內容很多,方案很多,光是一個單獨的TXAA就很多內容。不過好像資料已經很多了,抄書也沒什么意思。傳統FSAA的原理,其實就是放大渲染,例如你現在渲染1920 * 1080的窗口,那么4倍抗鋸齒,就是渲染一個4 * 1920, 4 * 1080的大圖片。自己寫過軟光柵就知道,每個像素點都是需要計算光柵化的,像素點越多,渲染效率越低,這就是為什么現在4K,8K流行不起來的重要原因。所以說,AA非常的占資源。MSAA改進了一點,只把邊緣部分放大渲染,然后再縮小。這部分,是在硬件里面實現的。也就是說,你只需要開啟NV的選項即可,什么都不用干。這就帶來一個問題,你渲染到紋理的時候,是沒有抗鋸齒的,所以,早期實現什么HDR之類的,都是犧牲了抗鋸齒為代價。不過后來,有一些新式的抗鋸齒方式,例如FXAA,TXAA,這類技術,是不需要像MSAA一樣的。不過FXAA的效果,講真,一言難盡。反正我很不喜歡,基本不用。效果比較模糊不說,在一些場景,例如森林,樹葉比較多的場景,邊緣檢測實在是有點糟糕。最近兩年AA技術有沒有什么新東西我不知道,早幾年,UE4只有兩種AA可選,就是FXAA跟TXAA。主要是早幾年延遲渲染大行其道,而延遲渲染其實也是MRT(multi render target)的后處理,其實就是一次不是渲染一張圖片,而是渲染好幾張圖片,把顏色,坐標、法線等等渲染出來,統一再在PS里面處理一遍,跟后處理的區別只是渲染圖片數的不同。
PS復雜的玩法還有很多,例如光照計算,例如所謂的PBR材質計算,例如陰影的計算……基本都是在PS里面完成。這里,我不打算一一介紹。我這里只打算科普一下PS是個什么,怎么用,而不是打算寫一個《PS應用實戰案例分析》。PS的實戰,需要漫長的時間,一點一點的積累,一點一點的磨,才能有所收獲。這也是圖形學太過于枯燥跟難以掌握的原因之一。
GS,幾何體著色器。
以上,整個渲染流程都已經有了,為什么還需要GS?GS能干什么?其實,在AI大火之前,Nvidia一直很艱難。市值一直只有80億就是明證。知乎上說起來高工資,都是說FLAG,從來沒聽說AMD,NV入列。那個時候,nvidia基本上all in游戲,一直致力于解決游戲里碰到的,然后CPU不好解決的問題。我認為GS的出現,也是這個原因。
舉例:假設游戲里,你需要漫天的雪花掉落,這是不是一個很常見的需求?
漫天的花瓣掉落,是不是一個很常見的需求?
早期,你需要這么干:有一個粒子發射器,然后計算發射了多少個粒子,然后每個粒子畫一個矩形,貼上貼圖。假設某幀創建了100個花瓣,你需要這樣:
// 申請一塊顯存/內存。其實一般是寫入內存,寫好了再memcpy傳入顯存。
AllocVideoMemory();
For(int i = 0; i < 100; i++)
{
// 畫Quad
CreateQuad();
// 一般花瓣不會只有一個材質,太單調,可以隨機一下材質
RandTexture();
// 把計算結果cpy到內存,后面再統一到顯存
CpyToMemory();
}
TransferToVideoMemory();
這里,就有優化空間了。例如這個計算Quad,本身是大量并行的重復的操作,你要算vertex,index,比較麻煩。這部分工作,就可以交給GPU來做,這就是GS的用途。
也可能GS有其他用途,反正我就用來做過粒子的優化,其他的我沒用過。
一句話總結:GS就是在GPU里生成幾何體。你可以把生成幾何體,當作是一個函數,傳入一些參數,得到幾何體。那么,傳入的參數,肯定是CPU傳入。而計算部分,放到GPU,充分利用了GPU多線程的優勢。
那么GS是怎么做的呢?我的做法是:只需要傳入一個頂點,我直接在GS里面計算Quad,圓形等等。我的代碼大概是這樣的:
struct Quad
{
float4 Pos[6];
};
Quad BuildQuadFromPoint(float4 P)
{
Quad Q;
Q.Pos[0] = P;
Q.Pos[0] = Q.Pos[0] + float4(-0.2f, -0.2f, 0, 0);
Q.Pos[1] = P;
Q.Pos[1] = Q.Pos[1] + float4(-0.2f, 0.2f, 0, 0);
Q.Pos[2] = P;
Q.Pos[2] = Q.Pos[2] + float4(0.2f, 0.2f, 0, 0);
Q.Pos[3] = P;
Q.Pos[3] = Q.Pos[3] + float4(0.2f, 0.2f, 0, 0);
Q.Pos[4] = P;
Q.Pos[4] = Q.Pos[4] + float4(0.2f, -0.2f, 0, 0);
Q.Pos[5] = P;
Q.Pos[5] = Q.Pos[5] + float4(-0.2f, -0.2f, 0, 0);
return Q;
}
[maxvertexcount(18)]
void MainGS(triangle in VertexShaderOutput vertexData[3], inout TriangleStream<VertexShaderOutput> triStream)
{
for (int i = 0; i < 3; i++)
{
Quad Q = BuildQuadFromPoint(vertexData[i].Position);
VertexShaderOutput VSO[6];
for (int j = 0; j < 6; j++)
{
VSO[j].Position = Q.Pos[j];
}
triStream.Append(VSO[0]);
triStream.Append(VSO[1]);
triStream.Append(VSO[2]);
triStream.RestartStrip();
triStream.Append(VSO[3]);
triStream.Append(VSO[4]);
triStream.Append(VSO[5]);
triStream.RestartStrip();
}
}
GS里面有一些規則,例如需要定義輸入的是什么數據,輸出的是什么數據,諸如此類的東西。我這里不打算深入講解了,我當初是去MS的官網上看的,但凡理解了原理,看這類規則,都是極其簡單的事,我都是一遍過就能隨便寫了。
GS比較核心的語法,主要是兩個,一個是輸入參數,一個是輸出參數。還有一點,是GS應該是不支持Vertex,Index的套路的,也就是說,只支持頂點直接構建三角形,所以上面我的代碼,一個QUAD其實是計算了6個頂點,而不是4個。還有一個是RestartStrip()這個函數,好像一個三角形之后必須這么來一下。細節不大記得了,自己測試一下或者找找文檔即可。
摸清楚這些規則的時候,可以很簡單的測試一下。例如你傳入三個頂點,或者傳入一個三角形,每個頂點再畫一個QUAD,一天的時間足夠摸清楚這些規則。如果這點功夫都不愿意花,只希望伸手,放棄吧,搞圖形學是沒有前途的。
以上所有代碼,除了那個uniform的那個是抄的,其他全部是我手寫,并且都是經過驗證的,能用的。但是基本都是N年前的代碼了,長時間沒有測試過也沒有跑過了,不保證是不是給改壞過了。但是大概率能跑。
江湖越老,人越懶,章節越寫越短。不喜可噴。
總結
以上是生活随笔為你收集整理的vs如何写多线程_VS + PS + GS的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 5g应用场景_5G新媒体场景应用解决方案
- 下一篇: 系统业务逻辑书籍_咨询行业书籍推荐