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