ETH官方钱包

前往
大廳
主題

【程式】Direct3D與OpenGL的繪圖管線(下)

Shark | 2021-03-14 15:08:31 | 巴幣 3656 | 人氣 1603

隔幾個(gè)月才寫好下篇。繼續(xù)介紹電子妖精和顯卡少女如何聯(lián)手工作。
上篇在此

新版小屋介面有一個(gè)改變:舊版寬度是627px,新版是756px,可以放比較寬的圖和比較長(zhǎng)的程式碼了。
因?yàn)槌^寬度的圖會(huì)被縮小,用滑鼠點(diǎn)一下才能看到原尺寸,我製作圖會(huì)儘量不超過小屋寬度

另外我寫過的程式教學(xué)散落各處,可能不好找,寫了一篇目錄出來。
Shark流程式教學(xué)一覽



上篇說的那些步驟並不是呼叫一個(gè)函式就做一步,而是像這樣:


繪圖管線像顯卡少女的辦公室有一組工作檯,先呼叫一堆函式指示顯卡少女把設(shè)定值和物件放在工作檯上,然後呼叫draw call才一口氣執(zhí)行那些步驟。draw call不止一種,依不同情況使用。
上篇Input Assembler提到的頂點(diǎn)數(shù)量,以及OpenGL的幾何形狀是在draw call裡指定。

(註:雖然上圖是那樣畫,實(shí)際上套用設(shè)定值和物件的指令也是先儲(chǔ)存在驅(qū)動(dòng)程式,draw call時(shí)才一口氣傳給GPU。)

GPU讀取顯示記憶體很迅速,將資料從主記憶體傳到顯示記憶體比較慢,所以先找時(shí)間把模型、貼圖這些量大的資料傳到顯示記憶體(可以在Now Loading時(shí)做,或是遊戲進(jìn)行時(shí)在背景處理),之後每個(gè)frame只要傳送少量的資料,如「套用3號(hào)模型,5號(hào)貼圖」的指令,或是更新constant buffer。

Direct3D 11
//設(shè)定input assembler
context->IASetInputLayout(...); //頂點(diǎn)格式
context->IASetPrimitiveTopology(...); //幾何形狀
context->IASetVertexBuffers(...); //頂點(diǎn)資料
//設(shè)定vertex shader

context->VSSetShader(...);
context->Map(...); //將矩陣放入constant buffer
context->Unmap(...);
context->VSSetConstantBuffers(...); //設(shè)定要使用的constant buffer
//設(shè)定rasterizer

context->RSSetViewports(...);
context->RSSetState(...); //culling在此設(shè)定
//設(shè)定pixel shader

context->PSSetShader(...);
context->PSSetShaderResources(...); //設(shè)定貼圖
context->PSSetSamplers(...); //設(shè)定取樣器(sampler),讀取貼圖要用到這個(gè)東西
//設(shè)定depth test

context->OMSetDepthStencilState(...);
//設(shè)定blend
context->OMSetBlendState(...);
//設(shè)定畫布和depth buffer
context->OMSetRenderTargets(...);

//draw call,到這裡才開始畫
context->Draw(vertexNumber, 0); //頂點(diǎn)數(shù)量在此才設(shè)定

//填入新矩陣

context->Map(...);
context->Unmap(...);
//切換模型和貼圖
context->IASetVertexBuffers(...);
context->PSSetShaderResources(...);
//畫第二個(gè)物體
context->Draw(vertexNumber2, 0);

OpenGL
//設(shè)定input assembler
glBindVertexArray(...); //頂點(diǎn)格式和頂點(diǎn)資料
//設(shè)定shader

glUseProgram(...); //一個(gè)函式同時(shí)設(shè)定vertex shader和fragment shader
glBindTexture(...); //設(shè)定貼圖
glBindSampler(...); //設(shè)定取樣器
glBindBuffer(GL_UNIFORM_BUFFER, ...); //設(shè)定要使用的uniform buffer
glBufferSubData(GL_UNIFORM_BUFFER, ...); //將矩陣放入uniform buffer
//設(shè)定rasterizer

glViewport(...);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CW);
//設(shè)定depth test
glEnable(GL_DEPTH_TEST);
//設(shè)定blend
glEnable(GL_BLEND);
glBlendFunc(GL_ONE,GL_ZERO);
//設(shè)定畫布和depth buffer
glBindFramebuffer(...);
glFramebufferTexture2D(...);
glFramebufferRenderbuffer(...);

//draw call,到這裡才開始畫
glDrawArrays(GL_TRIANGLES,0, vertexNumber); //頂點(diǎn)數(shù)量和幾何形狀在此才設(shè)定

//填入新矩陣

glBufferSubData(GL_UNIFORM_BUFFER, ...);
//切換模型和貼圖
glBindVertexArray(...);
glBindTexture(...);
//畫第二個(gè)物體
glDrawArrays(GL_TRIANGLES,0, vertexNumber);

這些函式實(shí)際的參數(shù)都落落長(zhǎng),本篇先略過,之後如果我有寫到那部分,實(shí)際用到這些函式再介紹。

draw call後設(shè)定值和物件仍然擺在工作檯上,下一次叫顯卡少女更換才會(huì)改變,所以如果第二個(gè)物體只要更換一部分資料,其他設(shè)定跟前一個(gè)物體相同,只要呼叫一部分函式即可。



前篇放的流程圖是簡(jiǎn)易版,那完整版又是怎麼樣呢?

(這是筆者認(rèn)為比較好講解的版本,網(wǎng)路上其他教學(xué)可能畫得不一樣,可能把某個(gè)步驟再細(xì)分,或把好幾個(gè)步驟合成一個(gè)。
步驟名稱一樣用Direct3D的,有的步驟OpenGL用不一樣的名稱。)

多了很多步驟,shader除了vertex和pixel以外還有g(shù)eometry、hull、domain三種,而且多了個(gè)Stream output,可以把前面那些shader算出的頂點(diǎn)坐標(biāo)存起來,供下次draw call使用。
很多步驟不需要時(shí)可以省略,簡(jiǎn)易版大概就是畫一個(gè)3D物體最低限度必要的工作。
有一堆用語很難懂吧。D3D和OpenGL早期版本功能很簡(jiǎn)單,大概像簡(jiǎn)易版的圖再把shader拿掉。隨著軟體對(duì)畫面的需求越來越多,D3D、OpenGL、和顯卡少女不斷追加新功能,可以做到越來越多的事,但因此變得很複雜,提高初學(xué)門檻。

上圖是D3D11和OpenGL 4版的pipeline,兩者的後繼產(chǎn)品:D3D12和Vulkan有變更一部分。然後D3D12在2018年,Vulkan在2020年新增一種技術(shù):光線追蹤(ray tracing),這是走完全不同的pipeline。



主題講到這裡,接下來講幾個(gè)有關(guān)的項(xiàng)目。

即時(shí)運(yùn)算與預(yù)運(yùn)算

繪製3D場(chǎng)景其實(shí)不只一種方法。像是描述物體形狀的方法除了本篇介紹的三角形面,還有利用曲線稱為NURBS的方法,或是用橢球組合出物體。

在「【程式】電子妖精與顯卡少女的合作——何謂GPU、Direct3D與OpenGL?」有提過標(biāo)準(zhǔn)規(guī)格很重要,如果每家廠商各自開規(guī)格,那在每個(gè)硬體和作業(yè)系統(tǒng)上程式都要重寫一次。
規(guī)格不能一味追求功能強(qiáng)大。在即時(shí)運(yùn)算的場(chǎng)合(如遊戲需要在1/60或1/30秒內(nèi)畫完整個(gè)畫面),不能採(cǎi)用計(jì)算耗時(shí)的演算法,而且功能複雜會(huì)讓軟硬體難以製造,PC、遊戲機(jī)和手機(jī)成本要低,一般使用者才負(fù)擔(dān)得起售價(jià)。在性能和成本之間取捨之後,廠商們定出的標(biāo)準(zhǔn)規(guī)格就是現(xiàn)在的繪圖管線。

相對(duì)地在動(dòng)畫、電影等應(yīng)用,工作室可以買幾臺(tái)高性能電腦讓它們計(jì)算好幾天、輸出影片檔,發(fā)佈只要發(fā)佈影片檔就行了。這時(shí)可以採(cǎi)用高畫質(zhì)但計(jì)算複雜的技術(shù),可能也不用D3D和OpenGL,而是開發(fā)一套專用API。

以上兩種分別是「即時(shí)運(yùn)算」和「預(yù)運(yùn)算」,即時(shí)運(yùn)算容易看情況更改內(nèi)容,但不能產(chǎn)生很精緻或擬真的畫面,預(yù)運(yùn)算則相反。可以觀察遊戲裡的過場(chǎng)動(dòng)畫,奴果精緻度比遊戲其他部分高很多那應(yīng)該是預(yù)運(yùn)算,如果角色變更裝備會(huì)反映在動(dòng)畫裡那大概是即時(shí)運(yùn)算。
(如果精緻度跟其他部分一樣,且角色變更裝備不會(huì)反映在動(dòng)畫裡,……那就無法判斷。)

還有3D軟體像3ds Max和Blender為了讓使用者操作後可以即時(shí)在畫面上反映,編輯時(shí)是用D3D和OpenGL畫出比較簡(jiǎn)單的畫面,render時(shí)才用精確的演算法慢慢算。
下圖是Blender說明書裡的範(fàn)例,左邊的沒有表面紋路、光柱半透明、影子,右邊才有畫出來。




如何用D3D和OpenGL畫2D畫面?

從名稱看起來D3D和OpenGL是專門畫3D的,但現(xiàn)在2D也是用D3D和OpenGL繪製了。
如果要畫一張矩形點(diǎn)陣圖,把傳給顯卡少女的資料改一下:

  1. CPU將矩形四個(gè)頂點(diǎn)傳給GPU,位置坐標(biāo)只有XY,沒有Z分量。
  2. CPU和vertex shader裡使用2D數(shù)學(xué)來計(jì)算坐標(biāo)。
    只有4個(gè)頂點(diǎn),移動(dòng)和變形可以乾脆完全由CPU算,不靠vertex shader。
  3. vertex shader輸出的W值設(shè)為1,讓這個(gè)值不起作用。
  4. 關(guān)閉depth test改用手動(dòng)排序,或者乾脆不建立Z buffer,因?yàn)?D很常用到半透明。

就可以畫2D畫面。2D、3D統(tǒng)一使用D3D和OpenGL繪製可以簡(jiǎn)化規(guī)格和軟硬體,廠商只要做一套系統(tǒng)就可以兩者通用。
DirectDraw本來是在顯示晶片沒有3D的時(shí)代使用,現(xiàn)在已停止增加新功能和改進(jìn)效能,只是為了讓舊程式能繼續(xù)執(zhí)行而保留。



D3D和OpenGL哪個(gè)比較好?

當(dāng)然兩者最大差異是哪個(gè)作業(yè)系統(tǒng)能用,大概只有Windows桌機(jī)版可以同時(shí)用兩者,這裡根據(jù)筆者經(jīng)驗(yàn)講一下性能方面。

以Direct3D 10和OpenGL 3.2以後來說,最底層建立物件的步驟差很多,但從高階一點(diǎn)的需求來看兩者能做的事就差不多,大部分功能都能找到對(duì)應(yīng)。例如都可以建立vertex buffer和framebuffer物件,但呼叫的函式和傳入的參數(shù)差很多。
以前的版本就不一定,有一段時(shí)間是OpenGL落後於D3D,D3D持續(xù)推出新版但OpenGL遲遲沒有跟進(jìn)。

API設(shè)計(jì)方面,OpenGL的風(fēng)格比較古老,寫程式比較容易出錯(cuò)。
舉個(gè)例子,操作物件的時(shí)候,D3D11是呼叫物件的method,或把物件作為參數(shù)傳入函式,每一行都明確指出操作的物件。
//貼圖物件為ID3D11Texture2D* textureObj1;
context->UpdateSubresource(textureObj1, 0, NULL, ……);

ID3D11ShaderResourceView* shaderResourceView;
device->CreateShaderResourceView(textureObj1, NULL, &shaderResourceView);
textureObj1->Release();

OpenGL通常是在內(nèi)部設(shè)定一個(gè)全域變數(shù),之後的函式讀取這個(gè)全域變數(shù)。
//設(shè)定「GL_TEXTURE_2D」這個(gè)全域變數(shù)
glBindTexture(GL_TEXTURE_2D, textureObj1);

  ……

//這些函式讀取GL_TEXTURE_2D得知要操作的貼圖是textureObj1
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, ……);
如果glBindTexture()和下面函式距離有些遠(yuǎn),看程式碼有時(shí)會(huì)看不懂目前在操作哪個(gè)物件。

效能的話根據(jù)筆者經(jīng)驗(yàn),關(guān)鍵是「晶片廠商有沒有認(rèn)真寫驅(qū)動(dòng)程式」,跟API本身關(guān)係不大。
電子妖精送指令給顯卡少女實(shí)際要經(jīng)過以下途徑。

一個(gè)功能即使晶片做得到,作業(yè)系統(tǒng)和驅(qū)動(dòng)程式?jīng)]有配合製作的話,也不能使用。


以下是筆者用2012左右的硬體測(cè)試,有的問題在新版本可能已經(jīng)解決。

筆者有一臺(tái)電腦是Intel Sandy Bridge微架構(gòu),在Windows上如果把相同功能用D3D和OpenGL寫,它的內(nèi)顯在D3D的效能比較好,而且有些功能只有D3D才能用。
可能因?yàn)閃indows的遊戲幾乎都用Direct3D,廠商就花比較多心力在D3D驅(qū)動(dòng)程式,OpenGL的只求有,沒認(rèn)真debug或改進(jìn)效能。
AMD和nVidia的Windows OpenGL驅(qū)動(dòng)程式比較沒這個(gè)問題。

Linux驅(qū)動(dòng)程式的情況是這樣:AMD和nVidia是廠商自行開發(fā),closed-source。這兩家另外有非官方的driver,不過是逆向工程做出來的,性能遠(yuǎn)不及官方版本。
Intel則是把規(guī)格公開,讓網(wǎng)路上的開發(fā)者以open-source的方式開發(fā)。
同一臺(tái)電腦裝Windows和Linux來比較的話,AMD和nVidia晶片在Linux表現(xiàn)比較差,可能因?yàn)楸容^少人做Linux的遊戲,Linux版驅(qū)動(dòng)程式就沒有認(rèn)真寫。有時(shí)候裝了原廠驅(qū)動(dòng)程式還會(huì)沒辦法開GUI,只剩黑底白字的命令列可以用。
Intel的由於有很多Linux使用者改良,驅(qū)動(dòng)程式品質(zhì)比較好,Intel晶片在Linux的表現(xiàn)比在Windows好。
2015年AMD把驅(qū)動(dòng)程式改用open-source的方式開發(fā),品質(zhì)可能有改善,但筆者手上沒有夠新的硬體可試用。

最近一個(gè)經(jīng)驗(yàn)也跟這有關(guān)。筆者的Linux開發(fā)機(jī)有Intel內(nèi)顯和nVidia獨(dú)顯,上個(gè)月在這臺(tái)安裝Mint 19.3,剛灌好只有open source版驅(qū)動(dòng)程式時(shí)還可以開GUI,安裝nVidia官方driver以後就無法開GUI了,照網(wǎng)路上的說明用命令列模式移除driver也不能復(fù)原,只好重灌。
但較早的18.2版沒這個(gè)問題。

查了一下資料,一般桌機(jī)裝兩個(gè)GPU是這樣,顯卡和主機(jī)板各有一組插座,要使用哪個(gè)GPU就把螢?zāi)痪€插到哪裡。


筆電的Optimus技術(shù)是這樣,只有內(nèi)顯輸出到螢?zāi)唬锚?dú)顯繪製時(shí)會(huì)先在獨(dú)顯畫出整個(gè)畫面,把畫面複製到內(nèi)顯記憶體,再傳給螢?zāi)弧?br>
驅(qū)動(dòng)程式要特別寫才做得到這個(gè)功能,可能nVidia的Linux版驅(qū)動(dòng)程式?jīng)]有好好寫,不是每個(gè)發(fā)行版都能正常運(yùn)作。
如果使用非官方驅(qū)動(dòng)程式,不但不能用獨(dú)顯,而且獨(dú)顯閒置時(shí)也不能節(jié)省電力,電腦會(huì)變得很燙。所以我寧願(yuàn)放棄獨(dú)顯了,修改BIOS設(shè)定調(diào)成只用內(nèi)顯。

有聽說nVidia本來就對(duì)Linux社群比較不友善。目前不急著買新電腦,這臺(tái)先將就著用,但以後Linux開發(fā)機(jī)我要用Intel或AMD的晶片。



偽3D是什麼?

這個(gè)詞沒有明確的定義,只是玩家間的俗稱,所以如果看到兩個(gè)人爭(zhēng)論某個(gè)東西是不是偽3D,吵半天得不出共識(shí),這很正常。

如果真要問筆者的看法,按照上篇「polygon或曲線 → 坐標(biāo)轉(zhuǎn)換 → rasterizer → 計(jì)算顏色 → 輸出」的流程繪製的是真3D,用其他方法產(chǎn)生遠(yuǎn)近感的是偽3D。

很多SFC和Mega Drive遊戲可見到一種效果:讓背景用較慢速率捲動(dòng)產(chǎn)生背景比較遠(yuǎn)的感覺。由於背景只是單張圖片,並沒有建模型用3D數(shù)學(xué)計(jì)算,這是一種偽3D。

紅白機(jī)的兩個(gè)遊戲:Mach Rider和Rad Racer,紅白機(jī)並沒有處理polygon的功能,也是用偽3D做出遠(yuǎn)近感。



當(dāng)時(shí)繪製畫面的方法是畫面由很多橫向掃瞄線組成,逐條畫出,每畫一條掃瞄線之後把下一條橫向偏移,就能讓圖彎曲,紅白機(jī)有這個(gè)功能。

這個(gè)技巧在日本叫raster scroll(ラスタースクロール),英文好像叫l(wèi)ine scrolling。
因?yàn)槭沁@種方法,遊戲畫面表現(xiàn)不出交差或距離很近,照理說可以看到另一條路的情況。也只能把圖橫向變形,紅白機(jī)看不到用這個(gè)技巧把圖縱向變形的遊戲。
找到這篇舉了幾個(gè)Mega Drive的例子:Line Scrolling - Raster Scroll Books

鐵桶之類的障礙物也不是建個(gè)圓柱形的模型,而是準(zhǔn)備數(shù)張大小不同的圖片看時(shí)機(jī)切換(紅白機(jī)是沒有縮放功能的)。



關(guān)於插圖:

很多教學(xué)裡畫暗處的常用方法是用乘法(multiply)疊上顏色,這張圖改用線性加深(linear burn)試試看。

乘法的算式:A×B。
A、B代表RGB分量,範(fàn)圍是0~1。
假如底色是(1, 0.75, 0.5),max-min=0.5
疊上灰色(0.6, 0.6, 0.6)
會(huì)變成(0.6, 0.45, 0.3),max-min=0.3
RGB差異減少代表彩度下降,所以乘法模式容易讓整張圖變得灰暗,避免的方法是針對(duì)每個(gè)底色的特性各別選擇重疊的顏色。

線性加深的算式:A+B-1,如果<0就設(shè)為0。
底色(1, 0.75, 0.5),max-min=0.5
疊上灰色(0.6, 0.6, 0.6)
變成(0.6, 0.35, 0.1),max-min=0.5
彩度比較不易下降,所以試試看這個(gè)模式,看能不能畫暗處一個(gè)顏色用到底,只有少數(shù)部位各別選色。
但這個(gè)模式可能讓分量變成0,容易讓整張圖一片黑,還是免不了要看底色各別選色。試一試覺得還是乘法模式比較易用。

另外試用一下GIMP 2.99。這版把依存的GTK 2和Python 2換成GTK 3和Python 3,Inkscape之前就已經(jīng)把這兩個(gè)函式庫(kù)換新,GIMP也更新之後,我的電腦上就不再需要GTK 2和Python 2了。
不過發(fā)現(xiàn)2.99版的Python API改變,script必須重寫,所以暫時(shí)還是用2.10,等這張圖畫完再改用2.99。

右下的遊戲畫面放大

因?yàn)榈谒母竦倪[戲邏輯寫的是RPG的戰(zhàn)鬥,這一格就這樣畫了。
角色是把目前設(shè)計(jì)出的顯卡少女挑三個(gè)換服裝,但角色在圖中很小,不容易看出誰是誰。

電子妖精和顯卡少女的工作室長(zhǎng)怎麼樣、工作時(shí)用什麼工具等等的,這張圖畫得還比較隨便,但如果之後要?jiǎng)?chuàng)作一系列作品,要把這些背景設(shè)定定個(gè)規(guī)格,以免不同作品之間有矛盾。

創(chuàng)作回應(yīng)

樂小呈
感謝解說!
2021-03-14 15:22:07
阿修
讚讚 好文章[e22]
2021-03-14 19:11:34
Sunwen
好棒的文章,長(zhǎng)知識(shí)了
2021-03-14 19:48:38

相關(guān)創(chuàng)作

更多創(chuàng)作