GLSL 語法與內(nèi)建函數(shù)
GLSL 的修飾符與數(shù)據(jù)類型
GLSL 中變量的修飾符
- const:修飾不可被外界改變的常量
- attribute:修飾經(jīng)常更改的變量,只可以在頂點著色器中使用
- uniform:修飾不經(jīng)常更改的變量,可用于頂點著色器和片段著色器
- varying:修飾在頂點著色器計算,然后傳遞到片元著色器中使用的變量
GLSL 的基本數(shù)據(jù)類型
- int
- float
- bool
float 是可以再加一個修飾符的,這個修飾符用來指定精度。 - highp:32bit,一般用于頂點坐標(biāo)(vertex Coordinate)
- medium:16bit,一般用于紋理坐標(biāo)(texture Coordinate)
- lowp:8bit,一般用于顏色表示(color)
GLSL 中就是向量類型(vec)
向量類型是 Shader 中最常用的一個數(shù)據(jù)類型,因為在做數(shù)據(jù)傳遞的時候經(jīng)常要傳遞多個參數(shù),相較于寫多個基本數(shù)據(jù)類型,使用向量類型更加簡單。
比如,通過 OpenGL 接口把物體坐標(biāo)和紋理坐標(biāo)傳遞到頂點著色器中,用的就是向量類型。每個頂點都是一個四維向量,在頂點著色器中利用這兩個四維向量就能去做自己的運算。
attribute vec4 position;
矩陣類型(matrix)
矩陣類型在 GLSL 中同樣也是一個非常重要的數(shù)據(jù)類型,在某些效果器的開發(fā)中,需要開發(fā)者自己傳入一些矩陣類型的數(shù)據(jù),用于像素計算。
uniform lowp mat4 colorMatrix;
上面的代碼表示的是一個 44 的浮點矩陣,如果是 mat2 的聲明,代表的就是 22 的浮點矩陣,而 mat3 代表的就是 3*3 的浮點矩陣。
OpenGL 為開發(fā)者提供了以下接口,把內(nèi)存中的數(shù)據(jù)(mColorMatrixLocation)傳遞給著色器。
glUniformMatrix4fv(mColorMatrixLocation, 1, false, mColorMatrix);
其中,mColorMatrix 是這個變量在接口程序中的句柄。這里一定要注意,上邊的這個函數(shù)不屬于 GLSL 部分,而是屬于客戶端代碼,也就是說,我們調(diào)用這個函數(shù)來和著色器進行交互。
紋理類型
一般只在片元著色器中使用,下面 GLSL 代碼是二維紋理類型的聲明方式。
uniform sampler2D texSampler;
首先我們需要拿到這個變量的句柄,定義為 mGLUniformTexture,然后就可以給它綁定一個紋理,接口程序的代碼如下:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texId);
glUniform1i(mGLUniformTexture, 0);
注意,上述接口程序中的第一行代碼激活的是哪個紋理句柄,在第三行代碼中的第二個參數(shù)就需要傳遞對應(yīng)的 Index,就比如說代碼中激活的紋理句柄是 GL_TEXTURE0,對應(yīng)的第三行代碼中的第二個參數(shù) Index 就是 0,如果激活的紋理句柄是 GL_TEXTURE1,那對應(yīng)的 Index 就是 1,句柄的個數(shù)在不同的平臺不一樣,但是一般都會在 32 個以上。
傳遞類型
在 GLSL 中有一個特殊的修飾符就是 varying,這個修飾符修飾的變量都是用來在頂點著色器和片元著色器之間傳遞參數(shù)的。
最常見的使用場景就是在頂點著色器中修飾紋理坐標(biāo),頂點著色器會改變這個紋理坐標(biāo),然后把這個坐標(biāo)傳遞到片元著色器,代碼如下:
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main(void)
{
//計算頂點坐標(biāo)
v_texcoord = texcoord;
}
接著在片元著色器中也要聲明同名的變量,然后使用 texture2D 方法來取出二維紋理中這個紋理坐標(biāo)點上的紋理像素值,代碼如下:
varying vec2 v_texcoord;
vec4 texel = texture2D(texSampler, v_texcoord);
取出了這個坐標(biāo)點上的像素值,就可以進行像素變化操作了,比如說去提高對比度,最終將改變的像素值賦值給 gl_FragColor。
GLSL 的內(nèi)置變量與內(nèi)嵌函數(shù)
內(nèi)置變量
常見的是兩個 Shader 的輸出變量,一個是頂點著色器的內(nèi)置變量 gl_position,它用來設(shè)置頂點轉(zhuǎn)換到屏幕坐標(biāo)的位置。
vec4 gl_posotion;
另外一個內(nèi)置變量用來設(shè)置每一個粒子矩形大小,一般是在粒子效果的場景下,需要為粒子設(shè)置繪制的半徑大小時使用。
float gl_pointSize;
其次是片元著色器的內(nèi)置變量 gl_FragColor,用來指定當(dāng)前紋理坐標(biāo)所代表的像素點的最終顏色值。
vec4 gl_FragColor;
然后是 GLSL 內(nèi)嵌函數(shù)部分,我們在這里只介紹常用的幾個常用函數(shù)。
內(nèi)嵌函數(shù)
官方文檔
內(nèi)嵌函數(shù) | 說明 |
---|---|
abs(genType x) | 絕對值函數(shù) |
floor(genType x) | 向下取整函數(shù) |
ceil(genType x) | 向上取整函數(shù) |
mod(genType x, genType y) | 取模函數(shù) |
min(genType x, genType y) | 取得最小值函數(shù) |
max(genType x, genType y) | 取得最大值函數(shù) |
clamp(genType x, genType y, genType z) | 取得中間值函數(shù) |
step(genType edge, genType x) | 如果x<edge,返回0.0,否則返回1.0 |
smoothstep(genType edge0, genType edge1, genType x) | 如果x<=edge0,返回0.0,如果x>=dege1,返回1.0,如果edge0<x<edge1,則執(zhí)行0~1之間的平滑差值 |
mix(genType x, genType y, genType a) | 返回線性混合的x和y,用公式表示為x*(1-a)+y*a,這個函數(shù)在mix兩個紋理圖像的時候非常有用 |
對于一種語言的語法來講,剩下的就是控制流的部分了。 | |
GLSL 的控制流與 C 語言非常類似,既可以使用 for、while 以及 do-while 實現(xiàn)循環(huán),也可以使用 if 和 if-else 進行條件分支的操作。 |
OpenGL ES 的紋理
OpenGL 中的紋理用 GLUint 類型來表示,通常我們稱之為 Texture 或者 TextureID,可以用來表示圖像、視頻畫面等數(shù)據(jù)。
對于二維的紋理,每個二維紋理都由許多小的片元組成,每一個片元我們可以理解為一個像素點。
大多數(shù)的渲染過程,都是基于紋理進行操作的,最簡單的一種方式就是從一個圖像文件加載數(shù)據(jù),然后上傳到顯存中構(gòu)造成一個紋理。
紋理坐標(biāo)系
為了訪問到紋理中的每一個片元(像素點),OpenGL ES 構(gòu)造了紋理坐標(biāo)空間,坐標(biāo)空間的定義是從左下角的(0,0)到右上角的(1,1)。
橫軸維度稱為 S 軸,左邊是 0,右邊是 1,縱軸維度稱為 T 軸,下面是 0,上面是 1。
按照這個規(guī)則就構(gòu)成了左圖所示的坐標(biāo)系,可以看到上下左右四個頂點的坐標(biāo)位置,而中間的位置就是(0.5,0.5)。
另外在這里不得不提的是計算機系統(tǒng)里的坐標(biāo)空間,通常 X 軸稱之為橫軸,從左到右是 0~1,Y 軸稱之為縱軸,是從上到下是 0~1,如圖所示:
無論是計算機還是手機的屏幕坐標(biāo),X 軸是從左到右是 0~1,Y 軸是從上到下是 0~1,這種存儲方式是和圖片的存儲是一致的。
我們這里假設(shè)圖片(Bitmap)的存儲是把所有像素點存儲到一個大數(shù)組中,數(shù)組的第一個像素點表示的就是圖片左上角的像素點(即第一排第一列的像素點),數(shù)組中的第二個元素表示的是第一排第二列的第二個像素點,依此類推。
這樣你會發(fā)現(xiàn)這種坐標(biāo)其實是和 OpenGL 中的紋理坐標(biāo)做了一個旋轉(zhuǎn) 180 度。 因此從本地圖片中加載一張紋理并且渲染到界面上的時候,就會用到紋理坐標(biāo)和計算機系統(tǒng)的坐標(biāo)的轉(zhuǎn)換。
紋理創(chuàng)建與綁定
創(chuàng)建
加載一張圖片作為 OpenGL 中的紋理。首先要在顯卡中創(chuàng)建一個紋理對象,OpenGL ES 提供了方法原型如下:
void glGenTextures (GLsizei n, GLuint* textures)
這個方法中的第一個參數(shù)是需要創(chuàng)建幾個紋理對象,第二個參數(shù)是一個數(shù)組(指針)的形式,函數(shù)執(zhí)行之后會將創(chuàng)建好的紋理句柄放入到這個數(shù)組中。
如果僅僅需要創(chuàng)建一個紋理對象的話,只需要聲明一個 GLuint 類型的 texId,然后將這個紋理 ID 取地址作為第二個參數(shù),就可以創(chuàng)建出這個紋理對象,代碼如下:
glGenTextures(1, &texId);
執(zhí)行完上面這個指令之后,OpenGL 引擎就會在顯卡中創(chuàng)建出一個紋理對象,并且把這個紋理對象的句柄存儲到 texId 這個變量中。
綁定
那接下來我們要對這個紋理對象進行操作,OpenGL ES 提供的都是類似于狀態(tài)機的調(diào)用方式,也就是說在對某個 OpenGL ES 對象操作之前,先進行綁定操作,然后接下來所有操作的目標(biāo)都是針對這個綁定的對象進行的。對于紋理 ID 的綁定調(diào)用代碼如下:
glBindTexture(GL_TEXTURE_2D, texId);
執(zhí)行完上面這個指令之后,OpenGL ES 引擎認為這個紋理對象已經(jīng)處于綁定狀態(tài),那么接下來所有對于紋理的操作都是針對這個紋理對象的了,當(dāng)我們操作完畢之后可以調(diào)用如下代碼進行解綁:
glBindTexture(GL_TEXTURE_2D, 0);
上面這行指令執(zhí)行完畢之后,就代表我們不會對 texId 這個紋理對象做任何操作了,所以上面這行代碼一般在一個 GLProgram 執(zhí)行完成之后調(diào)用。
過濾
首先就是紋理的過濾方式,當(dāng)紋理對象被渲染到物體表面上的時候,紋理的過濾方式指定紋理的放大和縮小規(guī)則。
實際上,是 OpenGL ES 的繪制管線中將紋理的元素映射到片元這一過程中的映射規(guī)則,因為紋理(可以理解為一張圖片)大小和物體(可以理解為手機屏幕的渲染區(qū)域)大小不太可能一致,所以要指定放大和縮小的時候應(yīng)該具體確定每個片元(像素)是如何被填充的。
放大(magnification)規(guī)則的設(shè)置:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
縮?。╩inification)規(guī)則的設(shè)置:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
上述兩個指令設(shè)置的過濾方式都是 GL_LINEAR,這種過濾方式叫做雙線性過濾,底層使用雙線性插值算法來平滑像素之間的過渡部分,OpenGL 的具體實現(xiàn)會使用四個鄰接的紋理元素,并在它們之間用一個線性插值算法做插值,這種過濾方式是最常用的。
OpenGL 還提供了 GL_NEAREST 的過濾方式,GL_NEAREST 被稱為最鄰近過濾,底層為每個片段選擇最近的紋理元素進行填充,缺點就是當(dāng)放大的時候會丟失掉一些細節(jié),會有很嚴重的鋸齒效果。因為是原始的直接放大,相當(dāng)于降采樣。而當(dāng)縮小的時候,因為沒有足夠的片段來繪制所有的紋理單元,也會丟失很多細節(jié),是真正的降采樣。
其實 OpenGL 還提供了另外一種技術(shù),叫做 MIP 貼圖,但是這種技術(shù)會占用更多的內(nèi)存,優(yōu)點是渲染也會更快。當(dāng)縮小和放大到一定程度之后效果也比雙線性過濾的方式更好,但是它對紋理的尺寸以及內(nèi)存的占用是有一定限制的。不過,在處理以及渲染視頻的時候不需要放大或者縮小這么多倍,所以在這種場景下 MIP 貼圖并不適用。
綜合對比這幾種過濾方式,在使用紋理的過濾方式時我們一般都會選用雙線性過濾的過濾方式(GL_LINEAR)。
在紋理坐標(biāo)系中的 s 軸和 t 軸超出范圍的紋理處理規(guī)則,常見的代碼設(shè)置如下:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
上述代碼表示的含義就是,給這個紋理的 s 軸和 t 軸的坐標(biāo)設(shè)置為 GL_CLAMP_TO_EDGE 類型,代表所有大于 1 的像素值都按照 1 這個點的像素值來繪制,所有小于 0 的值都按照 0 這個點的像素值來繪制。
除此之外,OpenGL ES 還提供了 GL_REPEAT 和 GL_MIRRORED_REPEAT 的處理規(guī)則,從名字也可以看得出來,GL_REPEAT 代表超過 1 的會從 0 再重復(fù)一遍,也就是再平鋪一遍,而 GL_MIRRORED_REPEAT 就是完全鏡像地平鋪一遍。
紋理的上傳與下載
假設(shè)我們有一張 PNG 類型的圖片,我們需要將它解碼為內(nèi)存中 RGBA 裸數(shù)據(jù),所以首先我們需要解碼??梢圆捎每缙脚_(C++ 層)的方式,引用 libpng 這個庫來進行解碼操作,當(dāng)然也可以采用各自平臺的 API 進行解碼。無論哪一種方式,最終都可以得到 RGBA 的數(shù)據(jù)。等拿到 RGBA 的數(shù)據(jù)之后,記為 uint8_t 數(shù)組類型的 pixels。
接下來,就是要將 PNG 素材的內(nèi)容放到這個紋理對象上面去
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
GL_UNSIGNED_BYTE, pixels);
執(zhí)行上述指令的前提是我們已經(jīng)綁定了某個紋理,OpenGL 的大部分紋理一般只接受 RGBA 類型的數(shù)據(jù)。當(dāng)然在視頻場景下,考慮性能問題也會使用到 GL_LUMINANCE 類型,不過需要在片元著色器中,把 YUV420P 格式轉(zhuǎn)換成 RGBA 格式。
上述指令正確執(zhí)行之后,RGBA 的數(shù)組表示的像素內(nèi)容會上傳到顯卡里面 texId 所代表的紋理對象中,以后要使用這個圖片,直接使用這個紋理 ID 就可以了。
既然有內(nèi)存數(shù)據(jù)上傳到顯存的操作,那么一定也會有顯存的數(shù)據(jù)回傳回內(nèi)存的操作
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
執(zhí)行上述指令的前提是我們已經(jīng)綁定了某個紋理,然后將綁定的這個紋理對象代表的內(nèi)容拷貝回 pixels 這個數(shù)組中,這個拷貝會比較耗時,并且拷貝時間會和分辨率(width\height)大小成正比。
一般在實際的開發(fā)工作中要盡量避免這種內(nèi)存和顯存之間的數(shù)據(jù)拷貝與傳輸,而是使用各個平臺提供的快速映射 API 去完成內(nèi)存與顯存的拷貝工作。
物體坐標(biāo)與紋理繪制
物體坐標(biāo)系
OpenGL 規(guī)定物體坐標(biāo)系中 X 軸從左到右是從 -1 到 1 變化的,Y 軸從下到上是從 -1 到 1 變化的,物體的中心點是 (0, 0) 的位置。
接下來的任務(wù)就是將這個紋理繪制到物體(屏幕)上,首先要搭建好各自平臺的 OpenGL ES 的環(huán)境,包括上下文與窗口管理,然后創(chuàng)建顯卡可執(zhí)行程序,最終讓程序跑起來。
紋理的繪制
先來看一個最簡單的頂點著色器(Vertex Shader),代碼如下:
static char* COMMON_VERTEX_SHADER =
"attribute vec4 position; \n"
"attribute vec2 texcoord; \n"
"varying vec2 v_texcoord; \n"
" \n"
"void main(void) \n"
"{ \n"
" gl_Position = position; \n"
" v_texcoord = texcoord; \n"
"} \n";
片元著色器(Fragment Shader),代碼如下:
static char* COMMON_FRAG_SHADER =
"precision highp float; \n"
"varying highp vec2 v_texcoord; \n"
"uniform sampler2D texSampler; \n"
" \n"
"void main() { \n"
" gl_FragColor = texture2D(texSampler, v_texcoord); \n"
"} \n";
利用上面兩個 Shader 創(chuàng)建好的這個 Program,我們記為 mGLProgId。
接下來我們需要將這個 Program 中的重點屬性以及常量的句柄尋找出來,以備后續(xù)渲染過程中向頂點著色器和片元著色器傳遞數(shù)據(jù)。
mGLVertexCoords = glGetAttribLocation(mGLProgId, "position");
mGLTextureCoords = glGetAttribLocation(mGLProgId, "texcoord");
mGLUniformTexture = glGetUniformLocation(mGLProgId, "texSampler");
在這個例子里,我們要從 Program 的頂點著色器中讀取兩個 attribute,并放置到全局變量的 mGLVertexCoords 與 mGLTextureCoords 中,從 Program 的片元著色器中讀取出來的 uniform 會放置到 mGLUniformTexture 這個變量里。
所有準(zhǔn)備工作都做好了之后,接下來進行真正的繪制操作。
首先,規(guī)定窗口大?。?/p>
glViewport(0, 0, screenWidth, screenHeight);
函數(shù)中的參數(shù) screenWidth 表示繪制 View 或者目標(biāo) FBO 的寬度,screenHeight 表示繪制 View 或者目標(biāo) FBO 的高度。
然后使用顯卡繪制程序:
glUseProgram(mGLProgId);
設(shè)置物體坐標(biāo)與紋理坐標(biāo):
GLfloat vertices[] = { -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f };
glVertexAttribPointer(mGLVertexCoords, 2, GL_FLOAT, 0, 0, vertices);
glEnableVertexAttribArray(mGLVertexCoords);
設(shè)置紋理坐標(biāo):
GLfloat texCoords1[] = { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f };
GLfloat texCoords2[] = { 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f };
glVertexAttribPointer(mGLTextureCoords, 2, GL_FLOAT, 0, 0, texCoords2);
glEnableVertexAttribArray(mGLTextureCoords);
代碼中有兩個紋理坐標(biāo)數(shù)組,分別是 texCoords1 與 texCoords2,最終我們使用的是 texCoords2 這個紋理坐標(biāo)。
因為我們的紋理對象是將一個 RGBA 格式的 PNG 圖片上傳到顯卡上,其實上傳上來本身就需要轉(zhuǎn)換坐標(biāo)系,這兩個紋理坐標(biāo)恰好就是做了一個上下的翻轉(zhuǎn),從而將計算機坐標(biāo)系和 OpenGL 坐標(biāo)系進行轉(zhuǎn)換。
對于第一次上傳內(nèi)存數(shù)據(jù)的場景紋理坐標(biāo)一般都會選用 texCoords2。
但是如果這個紋理對象是 OpenGL 中的一個普通紋理對象的話,則需要使用 texCoords1。
接下來,指定我們要繪制的紋理對象,并且將紋理句柄傳遞給片元著色器中的 uniform 常量:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texId);
glUniform1i(mGLUniformTexture, 0);
執(zhí)行繪制操作:
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
上述這行指令執(zhí)行成功之后,就相當(dāng)于將最初內(nèi)存中的 PNG 圖片繪制到默認的 FBO 上去了,最終再通過各平臺的窗口管理操作(Android 平臺的 swapBuffer、iOS 平臺的 renderBuffer),就可以讓用戶在屏幕上看到了。
當(dāng)確定這個紋理對象不再使用了,則需要刪掉它,執(zhí)行代碼是:文章來源:http://www.zghlxwxcb.cn/news/detail-737880.html
glDeleteTextures(1, &texId);
除此之外,關(guān)于紋理的繪制我們還要額外注意一點:我們提交給 OpenGL 的繪圖指令并不會馬上送給圖形硬件執(zhí)行,而是會放到一個指令緩沖區(qū)中。
考慮性能的問題,等緩沖區(qū)滿了以后,這些指令會被一次性地送給圖形硬件執(zhí)行,指令比較少或比較簡單的時候,是沒辦法填滿緩沖區(qū)的,所以這些指令不能馬上執(zhí)行,也就達不到我們想要的效果。
因此每次寫完繪圖代碼,想讓它立即完成效果的時候,就需要我們自己手動調(diào)用 glFlush() 或 gLFinish() 函數(shù)。文章來源地址http://www.zghlxwxcb.cn/news/detail-737880.html
- glFlush:將緩沖區(qū)中的指令(無論是否為滿)立刻送給圖形硬件執(zhí)行,發(fā)送完立即返回;
- glFinish:將緩沖區(qū)中的指令(無論是否為滿)立刻送給圖形硬件執(zhí)行,但是要等待圖形硬件執(zhí)行完后這些指令才返回。
到了這里,關(guān)于OpenGL ES 繪制一張圖片的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!