頂點(diǎn)緩沖區(qū)和著色器
頂點(diǎn)緩沖區(qū)
使用OpenGL畫一個(gè)三角形,我們需要?jiǎng)?chuàng)建一個(gè)vertex buffer(頂點(diǎn)緩沖區(qū))和一個(gè)shader(著色器),vertex buffer本質(zhì)上就是一個(gè)內(nèi)存緩沖區(qū),簡單來說,就是一塊用來存字節(jié)的內(nèi)存,它是OpenGL中的內(nèi)存緩沖區(qū),存在于顯卡上,在我們的VRAM中(VideoRAM,顯存的一種形式,允許CPU、GPU同時(shí)訪問);著色器基本上就是一個(gè)在GPU上運(yùn)行的程序,意思是它就是我們寫的一堆代碼,被傳遞給顯卡,然后像別的程序一樣進(jìn)行編譯,鏈接,然后運(yùn)行,與其他程序的不同之處只在于它運(yùn)行在GPU上,而不是像C++程序一樣運(yùn)行在CPU上。
畫一個(gè)三角形的基本思想是這樣的,首先我們用C++寫的東西是在CPU上運(yùn)行的,我們定義一些數(shù)據(jù)來表示三角形,把這些數(shù)據(jù)放到顯卡的VRAM中,然后CPU發(fā)出DrawCall指令,這是一個(gè)繪制命令,它告訴顯卡去讀取顯存中的這些數(shù)據(jù),并且把它畫在屏幕上,如何讀取、解釋這些數(shù)據(jù)以及如何把它畫到屏幕上也是我們需要告訴顯卡的,這就是著色器的作用。
我們要做的就是把之前這些頂點(diǎn)數(shù)據(jù)放進(jìn)一個(gè)緩沖區(qū),傳到OpenGL的VRAM,然后發(fā)出一個(gè)DrawCall指令,所以我們先定義一個(gè)緩沖區(qū):
創(chuàng)建了緩沖區(qū)后,我們要使用這個(gè)緩沖區(qū),就要先綁定它:
下一步就是指定數(shù)據(jù),先創(chuàng)建一個(gè)數(shù)組來儲存頂點(diǎn)數(shù)據(jù),這里可以簡單地定義數(shù)組([]中空著),也可以像這里一樣更具體地定義,glBufferData()函數(shù)的第三個(gè)參數(shù)需要的指針就指向positons數(shù)組
這樣我們就準(zhǔn)備好了一個(gè)緩沖區(qū),現(xiàn)在只要發(fā)出一個(gè)DrawCall指令,由兩種方式:一個(gè)是glDrawArrays()函數(shù),這是一個(gè)沒有索引緩沖區(qū)時(shí)可以用的方法,現(xiàn)在我們還沒有索引緩沖區(qū),所以就用這個(gè)方法。另一個(gè)是glDrawElements()函數(shù),這個(gè)是有索引緩沖區(qū)時(shí)使用的函數(shù),
現(xiàn)在你可能注意到我們告訴了OpenGL要畫一個(gè)三角形,從0開始,渲染3個(gè)點(diǎn),但并沒有指明要畫的是哪個(gè)緩沖區(qū)里的數(shù)據(jù),這是因?yàn)镺penGL是一個(gè)狀態(tài)機(jī),當(dāng)我們創(chuàng)建了緩沖區(qū)buffer后,用glBindBuffer(GL_ARRAY_BUFFER, buffer)函數(shù)綁定了這個(gè)緩沖區(qū),之后調(diào)用DrawCall指令,畫的就是綁定的這個(gè)緩沖區(qū),如果在綁定之前調(diào)用DrawCall指令或者綁定的是其他緩沖區(qū),就會(huì)出現(xiàn)問題。
著色器
目前簡單的來說,OpenGL為我們的顯卡提供了我們想要畫的三角形的數(shù)據(jù),現(xiàn)在我們要用到一個(gè)著色器,它會(huì)在GPU上執(zhí)行,去讀取這些頂點(diǎn)數(shù)據(jù),然后在屏幕上顯示。著色器讀取的數(shù)據(jù)是一大堆浮點(diǎn)數(shù),這些浮點(diǎn)數(shù)用來表示每個(gè)頂點(diǎn)的位置,還包括坐標(biāo)、紋理、法線等,但我們需要告訴OpenGL它們是如何布局的以及這些內(nèi)存到底是什么東西,我們把這些布局方式定義在CPU里。
我們在這里需要了解vertex attributes(頂點(diǎn)屬性),頂點(diǎn)不只是位置,它是一個(gè)在幾何上的點(diǎn),它可以包含位置、紋理、坐標(biāo)、法線、顏色等屬性。
調(diào)用glVertexAttribPointer()前,需要先調(diào)用glEnableVertexAttribArray(),就像綁定緩存一樣,這樣一來,這兩行代碼就能告訴OpenGL我們的緩存數(shù)據(jù)的布局,此時(shí)不帶任何著色器去運(yùn)行,也能看到一個(gè)白色的三角形了,因?yàn)槿绻覀儧]有提供自己的著色器的話,GPU驅(qū)動(dòng)會(huì)向我們提供默認(rèn)的著色器。
我們想要給GPU編程,利用GPU的性能在屏幕上畫出圖形,但這并不意味著我們需要在GPU上做所有的事,有些東西在CPU上是更快的,某些情況可能只是發(fā)送結(jié)果數(shù)據(jù)給GPU,但過程是在CPU上運(yùn)行的,但不可否認(rèn),有大量和圖形有關(guān)的事情,GPU運(yùn)行起來是更快的,這就是著色器派上用場的地方?,F(xiàn)在如果我們著手寫一個(gè)自己的著色器,我們要考慮到,即使只是畫一個(gè)簡單的三角形,我們?nèi)匀幌M芨嬖VGPU如何去畫這個(gè)三角形,比如頂點(diǎn)位置在哪,這個(gè)三角形的顏色應(yīng)該是什么,應(yīng)該怎么畫等等,特別是當(dāng)我們在一個(gè)更加復(fù)雜的3D場景時(shí),光照是一個(gè)很典型的需要被考慮的例子,我們需要告訴GPU,我們發(fā)給它的這些數(shù)據(jù)要做什么,這就是著色器的本質(zhì)。
對于大多數(shù)圖形編程來說,要注意兩種類型的著色器,Vertex Shader(頂點(diǎn)著色器)和fragment Shader(片段著色器),片段著色器在有些地方也叫作Pixel Shader(像素著色器),這兩種著色器是目前為止最流行的兩種,可能90%的時(shí)間都會(huì)用到,其它還有很多不同類型的著色器,我們暫時(shí)不去考慮那些。
現(xiàn)在我們討論一下頂點(diǎn)著色器和片段著色器,我們粗略地說明圖像渲染管線是如何工作的,我們在CPU上寫了一大堆數(shù)據(jù),把數(shù)據(jù)發(fā)給GPU,再綁定一些狀態(tài),就可以發(fā)出一個(gè)調(diào)用,最后就進(jìn)入了著色階段,確切地說,GPU此時(shí)開始處理調(diào)用并在屏幕上繪制一些東西,先是頂點(diǎn)著色器,再到片段著色器,然后就能夠在屏幕上看到圖形,這里頂點(diǎn)著色器做的就是獲取每一個(gè)我們想要渲染的頂點(diǎn),在這個(gè)例子里,我們有3個(gè)頂點(diǎn),頂點(diǎn)著色器會(huì)調(diào)用3次,每個(gè)頂點(diǎn)各1次,目的就是告訴OpenGL我們想要那些頂點(diǎn)顯示在顯示器的哪里,或者說在窗口的哪里,總之頂點(diǎn)著色器指定了所有我們寫在緩沖里面的頂點(diǎn)的位置,同時(shí)頂點(diǎn)著色器會(huì)帶著每個(gè)頂點(diǎn)的屬性,所有的這些屬性我們可以在頂點(diǎn)著色器里訪問到,glVertexAttribPointer()函數(shù)的第一個(gè)參數(shù)index與我們定義在頂點(diǎn)著色器里面的index是相符合的,我們可以通過這個(gè)格式訪問頂點(diǎn)屬性,在當(dāng)前的例子,我們的頂點(diǎn)只有位置屬性;下一個(gè)階段就是片段著色器,片段著色器會(huì)為每個(gè)像素運(yùn)行一次光柵化,我們的窗口實(shí)際上是由像素組成的,比如1920?1080的顯示屏,就可以簡單的理解為屏幕上有1920?1080個(gè)像素,畫一個(gè)三角形實(shí)際上就是去充滿一部分像素,這就是光柵化做的事情,片段著色器的基本目的就是決定像素的顏色應(yīng)該是什么;現(xiàn)在我們發(fā)現(xiàn),頂點(diǎn)著色器調(diào)用3次,片段著色器可能調(diào)用成百上千次,這取決于這個(gè)三角形在我們的屏幕上占用了多大空間,當(dāng)涉及到性能優(yōu)化時(shí),片段著色器里面的東西往往花銷更大,因?yàn)槠沃魇且o每個(gè)像素運(yùn)行一次的,一個(gè)很好的例子,就是在光照下進(jìn)行計(jì)算時(shí),每個(gè)像素都會(huì)有一個(gè)顏色值,這個(gè)值由光照、環(huán)境、紋理、材質(zhì)等所有東西共同決定的,而每個(gè)像素都需要由片段著色器去一個(gè)個(gè)計(jì)算清楚,這就是片段著色器需做的事情。有了頂點(diǎn)著色器和片段著色器,我們可以做出90%的圖形程序,目前我們在游戲中看到的所有東西,都可能用這兩個(gè)著色器完成了80%到90%,很多游戲引擎,會(huì)根據(jù)游戲中發(fā)生的事情,根據(jù)我們的設(shè)置,動(dòng)態(tài)的生成著色器,這在游戲引擎中是十分常見的。最后需要提一下的是,著色器在OpenGL中的工作也是基于機(jī)器的狀態(tài),比如,我們實(shí)現(xiàn)了某個(gè)著色器,當(dāng)我們想要使用這個(gè)著色器時(shí),我們也會(huì)發(fā)送數(shù)據(jù)到這個(gè)著色器,就像我們在一個(gè)頂點(diǎn)緩沖調(diào)用前從CPU給GPU發(fā)送頂點(diǎn)數(shù)據(jù)一樣。