[GEiv]第七章:着色器 高效的GPU渲染方案

第七章:着色器

高效的GPU渲染方案

    本章介紹着色器的基本知識以及Geiv下對其提供的支持接口,並以“漸變高斯模糊”爲線索進行實例的演示講解。

[背景信息]

    [計算機中央處理器的侷限性]

    在大學的“數字圖像處理”課程中,老師講解了高斯模糊的基本算法,並使用C#進行了基本實現。高斯模糊,簡單地說,就是使用高斯權重模板對圖像的每一個像素進行再計算、填充,以達到模糊的效果。

    在課程中,對於給定的模板與模糊度係數,對一副800X600的圖像進行模糊處理,需要計算48萬個像素點,儘管當時機房已經普及了酷睿系列CPU,但這個過程依舊耗時2~3秒。

    於是有這樣的一個問題:在遊戲中,我們經常看到畫面進行模糊到清晰的動態轉變,即使是次世代的GBA\SFC等遊戲主機依然有這樣的效果實現,而論硬件性能,我們的PC應該遠遠高於這些次世代遊戲機,而2、3秒的運算結果已經明確告訴我,想要進行更爲高效的模糊計算,顯示地挨個計算像素點是不可能達到預期效率的,想要一秒鐘完成60幅不同模糊度的圖像計算,還要另尋他路。

    爲什麼計算性能普遍較低的遊戲機卻能實現高性能CPU都無法流暢做到的運算呢?筆者認爲還是因爲CPU的結構差異導致的,PC的任務種類多,過程複雜,需要複雜的指令集系統;但遊戲機的CPU主要爲圖像而生,只需使用針對圖像的少數精簡指令系統即可。

    總而言之,爲了保證CPU在複雜任務下的通用性,計算機的CPU並不擅長進行圖形運算,並且在一些場合下,圖形運算的效率相當低下。


    [圖形處理器GPU]

    爲了彌補CPU處理圖形的缺陷,GPU便誕生了,GPU是針對圖形運算而生的處理器,如今的主流遊戲PC都會有一個高性能的GPU。具體的概念筆者就不在這裏提了,相信搞計算機的都會對其瞭解一二。


    [在編程中使用GPU運算資源]

    在大多數開發場合下,無論是C還是Java,我們敲的代碼都無疑地運行在CPU之上,那我們怎樣才能顯示地調用GPU的資源呢?在OPENGL中,除了常用的固定管線式編程外,還提供了更爲靈活的功能,它允許開發者使用着色語言(GLSL,一種類C語言)編寫名爲“着色器”的程序,經編譯運行於GPU之上並接管一部分GPU內置功能。


    [Opengl着色器簡述]

    介於筆者履歷有限,這裏只做簡單介紹,詳細概念可以參考《Opengl着色語言》一書,它對着色器和GLSL有着非常詳細的介紹。

    一個Opengl着色器(下簡稱着色器)由一個頂點着色器與片段着色器構成,它們負責的工作大部分是不同的,僅有少部分交集。

    Opengl爲着色器的校驗、編譯、連接和設置參數提供了完整的API,只要顯卡支持,在Opengl上下文中即可由源文件得到着色器程序,無需其他環境。

    編寫着色器的目的,是爲了實現諸如模糊、霧化、發光等固定管線不易敘述的功能,進而豐富圖像的表現能力。很多遊戲引擎將Opengl中與着色器相關的API包裝到了上層,我在設計時也採用了這種方案。


[內置着色器]

    在GEiv中,內置了5個系統着色器,它們是在設計之初進行着色器測試時所保留下來的,並不是由於“常用”(筆者感覺着色器的編寫往往很有針對性,一般很難有常用一說)。

    

    依次介紹:

    SD_ANTICOLOR:將目標色改爲反色

    SD_EMBOSS_MODE9:浮雕效果,後面的MODE9表示它使用的是3X3的模板,可以適應一般的需求,若要提高精確度,可以將模板擴充至4X4\5X5等。

    SD_GAUSSIAN_MODE9:高斯模糊

    SD_LAPLACIAN_MODE9:銳化

    SD_MEAN_MODE9:均值化


[爲圖元設置着色器]

    在GEiv中,使用着色器的對象是圖元,使用其setShaderProgram方法將着色器綁頂到對應圖元上,如:

Obj T =UES.creatObj(UESI.BGIndex);
T.addGLImage(0,0, "./mdls.jpg");
T.setShaderProgram(SysShader.SD_GAUSSIAN_MODE9);//將系統着色器綁定到圖元T上


[設置Uniform參數]

    Uniform修飾詞(不是數據類型)指明一個GLSL中的變量是外部傳入的,並且在整個着色器執行過程中不會改變(區別於attribute),例如對於實現模糊變化來講,“模糊度”這個變量屬於Uniform型無疑。

    在GLSL中,數組有很多存在形式,float[]可以表示一維線性浮點數組,vect2則表示了一個點對,而vec2[]則表示了一維線性浮點“數對”組,類似的還有vect3\4等等這樣在java中沒有現成的對應結構的數據,也就是說,設置Uniform參數時必須給定我們的數據源(一維線性數組)和映射到GLSL中變量的方式。

    在GEiv的圖元類中,setShaderUniform(String uniformName, Object value, intTPFLAG)方法可以設置該圖元上綁定的着色器參數,這個方法的參數依次是:

    uniformName是着色器中對於的變量名稱。

    value是數據源,目前只支持float[]、Float[]或單個的Float型。

    TPFLAG 是指定的映射方式,你可以直接從ShaderController的靜態值部分直接引用。它可以是下列值之一:

    

    AFLOAT與FLOATS分別對應單個float值與一維float數組。

    VERTXS表示由X個點對組成的數組,即該數組每個元素包含一個X維度向量。所給數據源必須是一個X整數倍大小的一維線性數組,該數組會按照順序依次填充這些X維度向量,例如使用TP_VERT2S時,數據源一維數組的前兩個元素構成了着色器向量數組的第一個向量元素。

    VERTEX表示一個X維度向量。要求給定的一維數據源數組大小必須爲X。


[自定義用戶着色器]

    除了使用SysShader中現成的着色器外,還可以使用用戶自定義的着色器程序。

    在引擎的句柄UESI中,包裝並簡化的Opengl產生着色器的API:

    UESI.createShaderProgram(String spName,String vpPath, String fpPath);

    參數介紹:

    spName是用戶爲着色器起的名字。

    vpPath是頂點着色器的源文件路徑,如”./vp.txt”。

    fpPath是片段着色器源文件路徑。

    之後我們只需要使用設置着色器的API對圖元綁定spName就行了,與之前介紹的字庫相同,spName也具有全局效應,可以爲其它上下文的圖元綁定。


[實例-漸變高斯模糊]

    您可以在[GitHub]Sample\Sample-Shader下找到這個例子及使用的資源。

Main.java

package com.geiv.test;

import geivcore.R;
import geivcore.UESI;

public class Main{
	public static void main(String[] args) {
		UESI UES = new R();
		new Guassion(UES);
	}
}
Guassion.java

package com.geiv.test;

import geivcore.SerialTask;
import geivcore.UESI;
import geivcore.engineSys.shadercontroller.ShaderController;
import geivcore.engineSys.shadercontroller.SysShader;
import geivcore.enginedata.canonical.CANExPos;
import geivcore.enginedata.obj.Obj;

import java.awt.event.KeyEvent;
import java.util.Arrays;

public class Guassion implements SerialTask{
	UESI UES;
	float[] g_aryVerticalOffset;
	float[] vertstatic;
	float bur = 600f;//bur作爲模糊度因子,使用靜態偏移量(vertstatic)除以模糊度因子得到不同的偏移量參數(g_aryVerticalOffset),由於GLSL上下文是使用的OPENGL座標式,與GEiv有所不同,因此初始因子經計算轉換後定爲600f

	Obj T;
	public Guassion(UESI UES) {
		this.UES = UES;

		T = UES.creatObj(UESI.BGIndex);
		T.addGLImage(0, 0, "./mdls.jpg");
		
		T.setShaderProgram(SysShader.SD_GAUSSIAN_MODE9);//設置着色器
		T.setShaderUniform(SysShader.NA_WEIGHTARGS,SysShader.BR_GAUSSIAN_MODE9, ShaderController.TP_FLOATS);//設置Unfiorm參數,其中規格化後的權重模板及其在GLSL上下文中的名稱已經給定,它們可以由SysShader直接引用。


		vertstatic = new float[]{-1,-1, 0,-1, 1,-1
				  -1, 0, 0, 0, 1, 0
				  -1, 1, 0, 1, 1, 1};//偏移量模板,看不懂的話可以參考下面的圖

		g_aryVerticalOffset = Arrays.copyOf(vertstatic, vertstatic.length);
		
		for(int i = 0;i < g_aryVerticalOffset.length;i++)
		{
			g_aryVerticalOffset[i] = vertstatic[i]/bur;
		}
		
		T.setShaderUniform(SysShader.NA_OFFSETARGS, g_aryVerticalOffset,ShaderController.TP_VERT2S);//設置Uniform偏移量
		T.setPosition(CANExPos.POS_CENTER);
		T.show();
		
		UES.addSerialTask(this);
	}

	@Override
	public void Serial(int clock) {//掛載一個掃描式的鍵盤輸入,當Z鍵按下時bur增加,重置偏移量並重設Uniform。當X鍵按下時則相反。 
		if(UES.getKeyStatus(KeyEvent.VK_Z))
		{
			T.setShaderUniform(SysShader.NA_OFFSETARGS,g_aryVerticalOffset,ShaderController.TP_VERT2S);
			bur+=4f;
			for(int i = 0;i < g_aryVerticalOffset.length;i++)
			{
				g_aryVerticalOffset[i] = vertstatic[i]/bur;
			}
		}
		else if(UES.getKeyStatus(KeyEvent.VK_X))
		{
			T.setShaderUniform(SysShader.NA_OFFSETARGS,g_aryVerticalOffset,ShaderController.TP_VERT2S);
			bur-=4f;
			for(int i = 0;i < g_aryVerticalOffset.length;i++)
			{
				g_aryVerticalOffset[i] = vertstatic[i]/bur;
			}
		}
	}
}

關於偏移量模板的映射,實際上是這樣的↓:

    

    除此之外,還有高斯模糊着色器的兩部分源代碼

頂點着色器:

attribute float sys_pIndex;
void main(void)
{
	gl_TexCoord[0] = gl_MultiTexCoord0;
	gl_Position = ftransform();
}

↑這個頂點着色器沒有寫任何實質性的功能,它被很多片段着色器共用。

高斯-片段着色器:
const int g_iWeightNumber = 9;//權重模板大小
uniform sampler2D g_FilterTexture;//我們的紋理
uniform float g_aryWeight[g_iWeightNumber];//權重數組
uniform vec2 g_aryOffset[g_iWeightNumber];//偏移量-即使使用9個格子,也沒有規定必須是相鄰的格子;偏移量敘述着權重模板格子間的實際像素距離,取值越高,模糊度越高。
void main(void)
{
	vec4 vec4Sum = vec4(0.0);
	for(int i = 0; i < g_iWeightNumber; ++i)
	{
		vec4Sum += texture2D(g_FilterTexture, gl_TexCoord[0].st + g_aryOffset[i])*g_aryWeight[i];//這裏進行實質的計算過程,也就是替換原先我們讓CPU挨個計算像素點的部分。gl_TexCoord[0]是綁定的0號紋理(Opengl中圖形是可以綁定多個紋理的),它的st就是位置,一個vec2類型量;位置與偏移量進行加和,得到偏移位置。之後根據紋理和偏移位置取出對應的rgba四維向量,它是一個vec4類型。進一步,將我們的vec4乘以規格化後的權重,並加和到vec4Sum上,由於是規格化後的權重,因此RGBA都不會越界。
	}
	gl_FragColor = vec4Sum;// 最後,我們將這個計算完畢的RGBA結果填入紋理中。
}

最後,執行結果(按住X鍵時):


↑可以觀察到由清晰到模糊的動態過程。

[總結]

    本章介紹了着色器的使用場合以及GEiv下對應的API。

    使用着色器是爲了實現固定管線難以敘述的效果。

    着色器運行於GPU,在對圖像渲染時效率比直接使用CPU運算高出很多。



發佈了42 篇原創文章 · 獲贊 50 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章