本節將學習OpenGL的着色器語言GLSL。
本節效果
主要內容
- 什麼是着色器
- 如何在OpenGL中使用Shader
- 如何編寫頂點着色器和片段着色器
- 代碼實現過程
什麼是着色器
前面說了,3D編程要完成2件事。
- 物體顯示在哪?
- 物體顯示成什麼樣?
着色器,也叫shader,字面意思就是用來着色的。當然,它也順便完成了物體的空間變換。換句話說,3D渲染流水線就是在着色器中實現的,當然它要配合OpenGL的相關渲染指令來完成更加高級的操作,比如:幀緩衝區採樣、像素緩衝區的應用等。
它是一小段類似於C語言的代碼,強類型,需要編譯成GPU可執行的指令。
每個3D渲染流水線的階段對應不同的shader,完成不同的功能,我們常見的就是頂點着色器和片段着色器,這兩個着色器可以滿足我們日常絕大數需求。
如何在OpenGL中使用shader
OpenGL通過一個叫ShaderProrgam的對象來管理shader,所有的shader在使用前需要先附加到這個對象上,然後在使用的時候,使用這個對象來開啓shader的使用。
那麼shader什麼時候使用呢?當然在繪製的時候,比如執行glDrawArrays()函數之前,我們要開啓shader的使用,然後glDrawArrays()繪製的圖形纔會應用這個shader,在glDrawArrays()調用結束之後,就需要禁用當前的shader,以防止影響其他物體的繪製操作。這種編程模式可以總結爲:
- 設置某個狀態
- 執行某些操作
- 禁用前面設置的狀態
這種編程模式在OpenGL編程過程中會經常用到,因爲OpenGL本身是基於狀態機的,這也是處於性能的考慮。OpenGL每次執行特定的指令時,如果該指令依賴特定的狀態,它就會去檢查該狀態是否被設置過,如果設置過則直接使用之前的狀態,如果你不更改,則接下來所有的這些指令都會使用這個狀態,否則使用默認狀態。所以,我們要養成:設置狀態->執行操作->禁用狀態 這個編程習慣,這樣你當前設置的狀態只會對你接下來的操作有影響,而不會影響程序的其他部分。
在OpenGL中使用shader的步驟,也不是太複雜:
- 寫shader源碼
- 創建ShaderProgram對象,編譯shader源碼,附加shader源碼到ShaderProgram對象上
- 使用shader
其中,第二、三步比較固定,請查看源碼中的ShaderProgam.h/cpp的實現,主要就是shader如何寫?
如何寫Shader
寫之前,我們要了解shader的大致結構,其實很簡單,下面是一個基本的着色器的結構。
頂點着色器:
片段着色器,結構和頂點着色器是一致的。
- 版本聲明部分
聲明shader的版本,因爲不同版本的shader,其中的關鍵字、內置變量、內置函數是有差異的。而且,OpenGL es 使用的shader也pc版OpenGL使用的shader也是略有差異的。
- 輸入頂點屬性部分
頂點屬性,顧名思義就是頂點的屬性。因爲頂點不管具有空間位置,還有其他的屬性,比如:法線方向,顏色,紋理座標等。比如,一個遊戲人物角色,頂點的空間位置只是描述了人物的外形,而人物一般都是貼了紋理的,所以每個頂點還要存儲它對應的紋理座標,通過這個座標OpenGL才能正確去紋理圖片中採樣顏色,進而顯示貼了圖的人物角色。上面的着色器有2個頂點輸入屬性,頂點的位置和頂點的顏色,這裏我們沒有使用紋理, 我們留着在下一節講。
- uniform變量
有的書上翻譯爲一致變量,讀着好難受啊。顧名思義,uniform就是統一、相同、一致的意思,就是說,這個變量要作用到每個頂點上。這裏需要說明的是,每個頂點都會走一遍3D渲染管線,也就是說每個頂點都會經過頂點着色器,片段着色器,幾何着色器,計算着色器等,當然頂點着色器和片段着色器是必須的。uniform變量就是對每個頂點起相同作用的變量,比如:每個頂點都需要經過世界變換、相機變換、投影變換才能顯示到屏幕上,那麼這些變換矩陣可以聲明爲uniform變量。如果你有其他需求,當然可以增加自己的uniform變量。
- out變量
表明這個變量是輸出到下一個階段着色器中的。圖中在頂點着色器中輸出頂點的顏色到下一個着色器也就是片段着色器。有人問,你啥也沒幹,就直接輸出給下一個片段着色器了?那什麼不在片段着色器中直接寫個in color變量?
因爲流水線之所以是流水線,是因爲它是有序的。我們開始3D渲染經過的第一個階段是頂點着色器,然後是片段着色器…我們所有in變量必須在頂點着色器中設置,然後傳遞到其他的着色器中。當然,除非你聲明爲uniform變量,該變量可在3D渲染管線的任意階段進行設置。
- main函數
每個着色器都會有1個main函數, 語法類似於C語言的語法。
上面的頂點着色器和片段着色器的功能是,給定頂點的位置和顏色,根據給定的變換矩陣(前面教程介紹過)變換到標準化設備座標系中,賦值給 gl_Position,這是glsl的內置變量。
接着將頂點的顏色傳遞到片段着色器,片段着色器啥也沒幹,直接將頂點的顏色輸出到下一階段進行處理。
代碼實現過程
- 首先,我們新建一個ShaderProgram類,封裝處理shader的過程:編譯、鏈接、開啓、禁用shader等。
- 然後根據我們自己的實際需求來自定義自己的shader,比如,你繪製地形使用的shader和繪製太陽的shader肯定是不同的。太陽的shader要“發光、耀眼”,而你地形的shader主要是貼紋理,實現霧效果等。每種顯示效果的實現,都需要定義自己的shader。我們在代碼實現中,給了2個shader,一個用來渲染帶顏色的三角形basicShader,還有1個是渲染一個線框地球EarthShader。等我們講紋理的時候,我們來渲染一個帶貼圖的地球。現在,這個兩個shader很簡單,所以基本框架代碼都類似,後面我們不斷豐富EarthShader。
- 然後,我們定義了一個EarthRenderer專門來負責渲染地球。它在MasterRenderer中被調用,這樣我們就可以將渲染特定物體的代碼從MasterRenderer中分離,方便維護。
- 接着就可以開始渲染了。
首先我們需要準備渲染的數據,這裏我們是代碼生成的,當然我們可以外部加載3D建模軟件中建好的模型,這個後面我們也會介紹如何自己編碼實現加載外部模型,以及如何使用開源的庫來加載外部模型。
然後我們就可以在幀循環中渲染了。爲了演示,現在的函數接口寫得很“糙”,後面會隨着需求進行優化的。
- 此時,你應該能看到本文一開始的效果了。
本節源碼:https://ww.lanzous.com/icfc4ud
ok,本節結束。
下一節,我們繪製一個帶紋理的地球。