Android 自從2.2 版本之后之后開始支持OpenGL,在沒有支持OpenGL 的 GPU的情況下,也可以使用(通過軟件來模擬)。在Android上使用Opengl操作的對象是GLSurfaceView,這是一個繼承自View的擴展。
在Android上Opengl是通過Vertex Shader 和 Fragment Shader 這兩種定點着色器程序來實現圖片的加載和渲染的,中文稱為定點着色器和片段着色器。一個完整的Opengl程序需要創建定點着色器和片段着色器並將他們Link起來組成一個完整的OpenGL程序。
頂點着色器的作用是為每一個頂點生成坐標,因此每個頂點都要運行一遍頂點着色器程序,一旦頂點坐標計算出來之后,OpenGL就能夠使用這些頂點來組成點,線,和三角形。所有任意的圖形都是由這三種基本元素來描述的。下圖是頂點着色器進行坐標轉換的過程(稍微有點復雜):
這個過程包含了從原始的對象坐標經過模型視圖轉換生成眼坐標,再經過投影轉換生成裁剪坐標,再通過w坐標的歸一化轉換成為NDC(頂點坐標由(x,y,z,w)構成,在shader程序中一般用一個四維向量vec4來描述),最終通過視口變換生成屏幕坐標顯示在屏幕上。這一系列的轉換都是通過矩陣來完成的,轉換過程和原理是Opengl的精華內容,對於需要進入3D世界的同學需要掌握。在2D世界中我們只需要了解NDC就行了,這里就把NDC叫做OpenGL 坐標,加上Normalized 的修飾是因為這些坐標的值都在(-1,1)之間,OpenGL坐標表示見下圖:
OpenGL坐標原點在屏幕中央,左右坐標范圍為[-1,1],在2d環境中坐標值只存在(x,y),並且坐標范圍都只能在-1 到 1之間,屏幕中心坐標為(0,0)。因此如果需要指定一張圖片的顯示位置,指定坐標需要根據這個坐標系來,此外為了保證圖片顯示比例(長寬比例),在portrait 到 landscape 之間變換的時候一般需要乘以一個aspectRatio (width / height) 來重新設定坐標值。
了解了Opengl坐標,在來了解一下屏幕坐標(屏幕坐標的坐標原點在左上角)和紋理坐標(紋理坐標的坐標原點在左下角):
屏幕坐標系
紋理坐標系
片段着色器的作用是為點,線或者三角形的每一個頂點的片段(Fragment)生成渲染后的最終顏色。片段就是一個小的單色矩形區域,可以簡單的認為是屏幕上的一個像素點。
以上基本知識基本上可以處理Opengl實現2D圖像的繪制和處理。下面來簡單看一下Shader的寫法,在Android平台上Shader程序一般以字符串的形式出現,或者在res/raw/目錄下以*.glsl的格式出現,如果是以單獨的文件出現,需要定義專門的文件讀入接口來加載Shader程序。這里就介紹一下字符串的形式的Shader程序,來看下面簡單的Vertex Shader 和 Fragment Shader:
private static final String VERTEX_SHADER = "attribute vec4 a_position;\n" + "attribute vec2 a_texcoord;\n" + "varying vec2 v_texcoord;\n" + "void main() {\n" + " gl_Position = a_position;\n" + " v_texcoord = a_texcoord;\n" + "}\n"; private static final String FRAGMENT_SHADER = "precision mediump float;\n" + "uniform sampler2D tex_sampler;\n" + "varying vec2 v_texcoord;\n" + "void main() {\n" + " gl_FragColor = texture2D(tex_sampler, v_texcoord);\n" + "}\n";
先看一下VERTEX_SHADER,定義了兩種類型的變量 atrribute 和 varying。
其中 attribute變量是只能在vertex shader中使用的變量。(它不能在fragment shader中聲明attribute變量,也不能被fragment shader中使用),一般用attribute變量來表示一些頂點的數據,這些頂點數據包含了頂點坐標,法線,紋理坐標,頂點顏色等。應用中一般用函數glBindAttribLocation()來綁定每個attribute變量的位置,然后用函數glVertexAttribPointer()為每個attribute變量賦值。
varying被稱為易變量,一般用於從Vertex Shader 向 Fragment Shader傳遞數據,上面例子中在VertexShader中定義了attribute 類型的二維向量a_texcoord,並將該值賦值給varying類型的二維向量 v_texcoord。此外對於Vertex Shader 在main() 中必須將頂點坐標賦值給系統變量gl_Position。
看一下FRAGMENT_SHADER,定義了兩種類型的變量,uniform 和 varying。此外還多了一句 precision mediump float,這句話用於定義數據精度,Opengl中可以設置三種類型的精度(lowp,medium 和 highp),對於Vertex Shader來說,Opengl使用的是默認最高精度級別(highp),因此沒有定義。
uniform變量是APP序傳遞給(vertex和fragment)shader的變量。通過函數glUniform**()函數賦值的。 在(vertex和fragment)shader程序內部,uniform變量就像是C語言里面的常量(const ),它不能被shader程序修改(shader只能用,不能改)。如果uniform變量在vertex和fragment兩者之間聲明方式完全一樣,則它可以在vertex和fragment共享使用。 (相當於一個被vertex和fragment shader共享的全局變量)uniform變量一般用來表示:變換矩陣,材質,光照參數和顏色等信息。
對於Fragment Shader也需對gl_FragColor賦值。
現在對基本的Shader有了了解,來看一下Android怎么使用Shader程序。先說明一下Opengl中的資源一般都是用一個句柄(handle)來引用,句柄一般由gl***接口返回,代表一個特定的資源。
在Android上使用gl,需要用到一些列接口,這里按照一般的調用順序來列一下基本的接口(暫不包含錯誤處理)
1. 和創建Shader相關的:glCreateShader -> glShaderSource -> glCompileShader , 通過這幾個接口的調用最終返回一個Shader的句柄
2. 和創建Shader程序相關的(每個shader程序都必須包含Vertex Shader 和 Fragment Shader兩部分):glCreateProgram -> glAttachShader -> glLinkProgram,通過這些接口的調用最終返回個Program的句柄。
3. 獲取Shader內部變量賦值和傳遞數據到gl,對於不同類型的數據使用不同的接口:GLES20.glGetUniformLocation(mProgram, "tex_sampler"),GLES20.glGetAttribLocation(mProgram, "a_texcoord"),以上兩個接口中mProgram 代表glCreateProgram返回的 Shader程序句柄,“a_texcoord”和"tex_sampler"是 Vertex Shader和 Fragment Shader中定義的變量,通過這兩個接口獲取了變量的句柄,便於向這些變量中傳入值。獲得變量的句柄之后就要向其中傳值,一般通過glVertexAttribPointer()來完成,詳細參數這里沒有列出,在實例代碼中可以學習一下,傳值之后需要使glEnableVertexAttribArray才能將數據傳入到gl中,傳入數據使用glDrawArrays() 。
這里介紹了Opengl的基本知識,下一篇結合實例介紹一下Opengl在Android上的使用。