const float CUBE[]={ 0,0,0, 1,1,0, 1,0,0, //三角形a 0,0,0, 0,1,0, 1,1,0, 0,0,0, 0,1,1, 0,1,0, 0,0,0, 0,0,1, 0,1,1, 0,0,0, 1,0,1, 0,0,1, //三角形b 0,0,0, 1,0,0, 1,0,1, 1,1,1, 1,1,0, 0,1,0, 1,1,1, 0,1,0, 0,1,1, //三角形c 1,1,1, 0,1,1, 0,0,1, 1,1,1, 0,0,1, 1,0,1, 1,1,1, 1,0,1, 1,0,0, 1,1,1, 1,0,0, 1,1,0, }; |
我們?nèi)祟悘那懊娴某淌酱a可以看出頂點格式是怎麼樣:不過寫程式有個重要的觀念是「程式編譯成binary之後就沒有變數(shù)名稱和資料型態(tài)了,所有資料都是一堆byte」,把頂點資料傳到顯示記憶體後,顯卡少女看到的是這樣:
- 12個面,每個面3個頂點,共36個頂點。
- 每個頂點坐標(biāo)由三個浮點數(shù)(float)組成。
- 一個float是4 byte。
0000803f0000803f0000803f0000803f0000803f00000000000000000000803f……
必須呼叫一些函式告訴顯卡少女頂點是什麼格式,她才能正確解讀這些binary資料。
--Direct3D 11
D3D11_INPUT_ELEMENT_DESC layoutDesc[] = {
{"P",0, DXGI_FORMAT_R32G32B32_FLOAT, 0,0,
D3D11_INPUT_PER_VERTEX_DATA,0},
};
device->CreateInputLayout(layoutDesc, ...);
--OpenGL
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, 0, sizeof(float)*3, 0);
此外要設(shè)定幾何形狀(primitive),D3D和OpenGL支援點、線段、三角形,三角形也有獨立三角形、strip(長條)和fan(扇形)好幾種,要告訴GPU是哪一種。但是「36個頂點」的資訊不是在這一步告訴GPU,OpenGL的幾何形狀也不是在這一步設(shè)定,在下篇會說明。
--Direct3D 11
context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
繪圖管線最終要把立體物畫在平面的畫面上,要計算各頂點應(yīng)該放在畫面的哪裡,才能正確呈現(xiàn)立體感。
想像一下正方體畫好後的樣子,量一下頂點在畫面上的坐標(biāo)(以其中四個頂點為例):
D3D和OpenGL的規(guī)格裡畫面坐標(biāo)不是以像素為單位,而是-1~1的浮點數(shù),(0,0)是畫面正中間。這一步要根據(jù)物體位置和鏡頭位置,從左邊的值算出右邊的值。如流程圖所述這一步是shader,要自己寫程式計算,如何算就得運用數(shù)學(xué)知識了。會用到3D圖學(xué)常提到的「矩陣」,詳細(xì)算法很長,必須另寫一篇介紹。
模型裡的資料 畫面坐標(biāo) 0,0,0 -0.119, -0.783 1,0,0 0.567, -0.518 1,1,0 0.598, 0.365 0,1,0 -0.126, 0.173
除了Input Assembler傳來的頂點以外,如果顯卡少女計算時有需要其他資料,放在一個叫constant buffer的地方傳過去,物體位置和鏡頭位置要用這個方法告訴顯卡少女。
(OpenGL裡稱為uniform buffer)
每個頂點用shader處理後會輸出四個分量:X,Y,Z,W,很明顯X和Y是畫面上的坐標(biāo),Z與W跟頂點與鏡頭的距離有關(guān)但兩者用途不一樣,Z用在後述的depth test,W用來產(chǎn)生近大遠(yuǎn)小的效果。
字面意思是「點陣化」。把頂點連接畫出三角形,並找出三角形覆蓋畫面上哪些pixel。下圖以兩個三角形為例,事實上12個三角形都會做相同處理。
此時會套用一個設(shè)定:viewport,可以在視窗或螢?zāi)簧锨幸粔K矩形區(qū)域,只畫在這個範(fàn)圍。像二分割畫面是將viewport設(shè)成畫面的一半畫出場景,然後把viewport設(shè)成另一半,修改鏡頭位置把整個場景再畫一次,畫出從另一個角度看到的場景。
此外會做兩個工作,第一個是back-face culling(消除背面),實際應(yīng)用裡大部分模型都是封閉無開口的,背對鏡頭的面一定會被面對鏡頭的面擋住,如果畫出物體背面之後也會被正面蓋掉。雖然背面也可以用之後的depth test檢查出來,但早一點把不必畫的部分排除可減少之後的工作。
從一開始的陣列取出兩個三角形來看。判斷正背面的方法是三角形三個頂點的順序。看下面圖1,將三角形a三個頂點依序繞一圈:(0,0,0)、(1,1,0)、(1,0,0)、再回到(0,0,0),是順時針。
0,0,0, 1,1,0, 1,0,0, //三角形a
1,1,1, 1,1,0, 0,1,0, //三角形g
再繞三角形g:(1,1,1)、(1,1,0)、(0,1,0)、回到(1,1,1),也是順時針,這兩個三角形都會被畫出來。
如果從N的位置看正方體則會變成圖2,同樣依序繞三角形a一圈,變成逆時針,就不會被畫出來。從這個角度看三角形g仍然是順時針,會被畫出來。
因此送給GPU的頂點資料要有一定順序,不能任意把三角形的兩個點互換。
順時針和逆時針哪個代表背面可以用程式控制,也可以設(shè)成不消除背面。每個3D軟體、遊戲引擎、檔案格式可能會用不同設(shè)定,讀寫模型檔的時候要注意。
因為有culling的存在,一般3D遊戲裡的polygon都是單方向透明的,如果鏡頭移到物體內(nèi)部可以從內(nèi)部看到外面,但從外面看不到內(nèi)部。
第二個是內(nèi)插。vertex shader求出的Z值需要內(nèi)插,三角形三個頂點作為端點,算出內(nèi)部各個像素的Z值是多少。
圖中的Z值筆者沒有精確計算,是用Blender大概建個模型求出來的,只是用來介紹內(nèi)插的概念。
有需要也可以叫GPU內(nèi)插其他數(shù)值,如下面介紹的貼圖坐標(biāo)。
這個步驟的計算顯卡少女會自己做,人類只要呼叫函式把設(shè)定值告訴顯卡少女就行了,不用自己寫算式。
(D3D把這一步稱為pixel shader,OpenGL稱為fragment shader,本篇稱為pixel shader)
每個被覆蓋的像素執(zhí)行一次pixel shader算出顏色,要設(shè)法模擬現(xiàn)實中的各種現(xiàn)象,例如:這一步也是shader,必須運用數(shù)學(xué)理論自己寫程式計算。上面列的現(xiàn)象可以自己決定要做到多少,實作越多會越有真實感,但程式也越難寫,也消耗越多效能。
- 物體面對光的部分亮,背光的部分暗
- 光被其他物體擋住形成影子
- 貼圖
- 物體表面凹凸
- 其他物體反光,讓光線照在這個物體上
跟Vertex Shader一樣,如果有什麼額外資料要告訴顯卡少女,放在constant buffer。
pixel shader必須執(zhí)行成千上萬次(例如畫一張640×480的圖,需要執(zhí)行640×480=307200次),而且現(xiàn)在的pixel shader常會寫得很複雜,因此pixel shader是效能的重點,減少pixel shader的複雜度和執(zhí)行次數(shù)是tune效能的主要方法。
如下圖從M的位置看,按照常理B的一部分會被A擋住,電腦裡要如何實作這個現(xiàn)像?
也許有人想到只要先畫B再畫A,後畫的自然會把先畫的蓋掉。但在一個主角可任意走動的遊戲,如果鏡頭移到N的位置那前後順序就反過來了。場景裡有眾多物體且有可能一個嵌入另一個的時候,一個一個物體檢查遠(yuǎn)近順序難以做到。
目前標(biāo)準(zhǔn)做法是:另外建立一個點陣圖記錄各個像素與鏡頭的距離(不一定要真實距離,只要與距離是正相關(guān)或負(fù)相關(guān)就行了),每個像素有一個分量,數(shù)值範(fàn)圍是0.0~1.0的浮點數(shù),稱為depth buffer或Z buffer。
畫場景之前先把所有像素都設(shè)為1.0,代表能看到的最遠(yuǎn)距離。
Z值在這裡派上用場了,畫polygon時除了更新color buffer,把各像素的Z值也寫進(jìn)Z buffer。
更新各個像素時把Z值跟目前Z buffer裡的值比對,如果新的Z值較大就代表被較近的物體擋住,這個像素不需要畫出。
也可以初值設(shè)為0.0,數(shù)字越大代表越近,寫程式時可以設(shè)定要用哪一種。
每個像素都要記一個值,消耗一大塊記憶體的方法看起來很原始,但是有用。
但Z buffer的做法只能用在完全不透明的物體,半透明物體背後的物體也必須畫出來,且要先畫遠(yuǎn)後畫近才會正確,這時還是得用手動排遠(yuǎn)近的方法。
流程圖裡還有一個Early Depth Test,這又是什麼?D3D和OpenGL規(guī)格允許在pixel shader裡修改Z值,必須算完pixel shader才能確定Z值是什麼,所以depth test得排在pixel shader後面。但如果pixel shader並沒有修改Z值,那在pixel shader之前和之後做depth test結(jié)果會一樣。因此大部分GPU有做一個優(yōu)化:如果你寫的pixel shader沒有修改Z值就把depth test移到pixel shader之前做,提早把不用畫的像素排除。
由於有depth test的存在,3D繪圖先畫近再畫遠(yuǎn)會比較有效率,因為可以及早排除被擋住的像素,減少pixel shader執(zhí)行次數(shù)。
題外話:PS1並沒有Z buffer的功能,聽說做PS1的3D遊戲時polygon排序得自己做,如果遇到兩個面交差的情況就很麻煩,N64才開始有Z buffer的功能。但N64的弱點是卡帶的容量比光碟小很多,放不下大檔案,因此模型和貼圖品質(zhì)很受限。
pixel shader算出的BGRA值和畫面上目前的BGRA值,要用什麼算式計算、混合。
類似繪圖軟體的圖層模式:物體完全不透明,直接覆蓋舊的值;物體是半透明,要看得到背後的物體;或是火焰特效,要用加法混色做出發(fā)光的感覺,這些效果是在這一步設(shè)定。
公式怎麼設(shè)定可參考繪圖軟體的演算法。這一步不能寫shader,能用的算式很有限,不是所有圖層模式都能做到。
繪圖管線最終目的是把3D場景畫在點陣圖上,一般要準(zhǔn)備兩張圖作為輸出,其中color buffer記錄顏色,Z buffer的用途如上面depth test所述。OpenGL把兩張圖合稱為framebuffer object,D3D則沒有取特別的名稱。
這兩張圖是由顯卡少女建立的,電子妖精要做的是把顏色格式、尺寸等資訊告訴她,叫她建立物件。
D3D和OpenGL初始化完成後,就會有一張framebuffer代表人類看到的畫面,有需要也可以自行建立其他framebuffer,將圖畫在記憶體裡而不直接畫在畫面上。
同樣是點陣圖,把它當(dāng)作輸出時稱為framebuffer,當(dāng)作輸入時就稱為貼圖。可以用兩階段繪圖的技巧:先把場景畫在一張framebuffer,再把它作為貼圖畫到另一張framebuffer上。
(有的文章翻譯成紋理。)
照以上的方法只能畫出素色物體,但現(xiàn)在的3D畫面幾乎沒有不用貼圖的,使用貼圖要多做哪些工作?
首先當(dāng)然要把一張點陣圖放在顯示記憶體。
然後頂點資料要包含「貼圖坐標(biāo)」。
上圖標(biāo)出8個頂點和其中兩個三角形。自己想像一下把左邊正方體和右邊展開圖的坐標(biāo)對應(yīng)起來,折疊後B與B'會重合,其餘C'、D'、F'、G'、H'依此類推。
下列紅色字是貼圖坐標(biāo)。貼圖坐標(biāo)不是以像素為單位,而是0~1的浮點數(shù),D3D跟多數(shù)繪圖軟體一樣以左上角為(0,0)。
const float CUBE[]={
0,0,0,0.25,0.5, 1,1,0,0.5,0.25, 1,0,0,0.5,0.5,
//↑頂點A ↑頂點F ↑頂點B
0,0,0,0.25,0.5, 0,1,0,0.25,0.25, 1,1,0,0.5,0.5,
0,0,0,0.25,0.5, 0,1,1,0,0.25, 0,1,0,0.25,0.25,
0,0,0,0.25,0.5, 0,0,1,0,0.5, 0,1,1,0,0.25,
0,0,0,0.25,0.5, 1,0,1,0,0.75, 0,0,1,0,0.5,
0,0,0,0.25,0.5, 1,0,0,0.25,0.75, 1,0,1,0,0.75,
1,1,1,0,0, 1,1,0,0.25,0, 0,1,0,0.25,0.25,
1,1,1,0,0, 0,1,0,0.25,0.25, 0,1,1,0,0.25,
//↑頂點G' ↑頂點E ↑頂點H'
//以下四個面在背後
1,1,1,0.75,0.25, 0,1,1,1,0.25, 0,0,1,1,0.5,
1,1,1,0.75,0.25, 0,0,1,1,0.5, 1,0,1,0.75,0.5,
1,1,1,0.75,0.25, 1,0,1,0.75,0.5, 1,0,0,0.5,0.5,
1,1,1,0.75,0.25, 1,0,0,0.5,0.5, 1,1,0,0.5,0.25,
};
OpenGL比較特別,以左下角為(0,0),但是將貼圖傳到顯示記憶體的函式glTexImage2D()也假設(shè)給它的指標(biāo)以左下角為原點,所以讀取圖檔時不用刻意把圖上下顛倒。但是把framebuffer當(dāng)成貼圖使用時就會出問題,以前有寫過一篇介紹:【程式】倒立的OpenGL貼圖坐標(biāo)。
Input Assembler設(shè)定頂點格式時,要告訴顯卡少女有貼圖坐標(biāo)的存在。
Rasterizer除了內(nèi)插Z值以外也要內(nèi)插貼圖坐標(biāo),計算出每個像素的貼圖坐標(biāo)。
讀取貼圖會用到一個叫取樣器(sampler)的物件,要填好設(shè)定值告訴顯卡少女。
最後在pixel shader裡下一行指令讀取貼圖,取得貼圖裡某一點的BGRA值,用它計算最終的顏色。
--HLSL
float4 texColor=texture1.Sample(sampler, texCoord);
--GLSL
vec4 texColor=texture(sampler, texCoord);
同標(biāo)籤作品搜尋:程式|遊戲製作|Direct3D|DirectX|OpenGL
留言共 5 篇留言
前一篇:FF36遊戲攤位一覽... 後一篇:【程式】Visual C...
活動與參展 (0)
└活動與參展資訊 (1)
└活動與製作後記 (11)
└販?zhǔn)蹠[戲團(tuán)調(diào)查 (14)
遊戲團(tuán)隊「電子妖精實驗室」 (0)
└重要消息 (4)
└Cyber Sprite遊戲秘密 (2)
└製作進(jìn)度 (26)
創(chuàng)作 (0)
└繪圖 (24)
└程式 (48)
└故事、劇本 (3)