1. 程式人生 > >Android OpenGL ES 開發:繪製圖形

Android OpenGL ES 開發:繪製圖形

# OpenGL 繪製圖形步驟 上一篇介紹了 OpenGL 的相關概念,今天來實際操作,使用 OpenGL 繪製出圖形,對其過程有一個初步的瞭解。 OpenGL 繪製圖形主要概括成以下幾個步驟: 1. 建立程式 2. 初始化著色器 3. 將著色器加入程式 4. 連結並使用程式 6. 繪製圖形 上述每個步驟還可能會被分解成更細的步驟,對應著多個 api,下面我們來逐個看下。 ## 建立程式 使用 [glCreateProgram](https://www.khronos.org/registry/OpenGL-Refpages/es2.0/) 建立一個 program 物件並返回一個引用 ID,該物件可以附加著色器物件。注意要在OpenGL渲染執行緒中建立,否則無法渲染。 ## 初始化著色器 著色器的初始化可以細分為三個步驟: 1. 建立頂點、片元著色器物件 2. 關聯著色器程式碼與著色器物件 3. 編譯著色器程式碼 [上一篇文章](https://www.cnblogs.com/yazhidev/p/13737177.html)我們提到了頂點著色器和片元著色器都是可程式設計管道,因此著色器的初始化少不了對著色器程式碼的關聯與編譯,上面三個步驟對應的 api 為: 1. glCreateShader(int type) - type:`GLES20.GL_VERTEX_SHADER` 代表頂點著色器、`GLES20.GL_FRAGMENT_SHADER` 代表片元著色器 2. glShaderSource(int shader, String code) - shader:著色器物件 ID - code:著色器程式碼 3. glCompileShader(code) - code:著色器物件 ID 著色器程式碼使用 GLSL 語言編寫,那程式碼要怎麼儲存並使用呢?我看到過三種方式,列出供大家參考: 1. 字串變數儲存 這種應該是最直觀的寫法了,直接在對應的類中使用硬編碼儲存著色器程式碼,形如: ``` private final String vertexShaderCode = "attribute vec4 vPosition;" + "void main() {" + " gl_Position = vPosition;" + "}"; ``` 這種方式不是很建議,可讀性不好。 2. 存放於 assets 目錄 assets 資料夾下的檔案不會被編譯成二進位制檔案,因此適於存放著色器程式碼,還可以配合 AndroidStudio 外掛 **GLSL Support** 實現語法高亮: ![assets](https://yazhidev.oss-cn-hangzhou.aliyuncs.com/20201229_122040_20201229095812604_2113046260.png) 然後再封裝讀取 assets 檔案的方法: ``` private fun loadCodeFromAssets(context: Context, fileName: String): String { var result = "" try { val input = context.assets.open(name) val reader = BufferedReader(InputStreamReader(input)) val str = StringBuilder() var line: String? while ((reader.readLine().also { line = it }) != null) { str.append(line) str.append("\n") //注意結尾要新增換行符 } input.close() reader.close() result = str.toString() } catch (e: IOException) { e.stackTrace } return result } ``` 需要注意的是要在結尾新增換行符,否則最後輸出的只是一行字串,不符合 GLSL 語法,自然也就無法正常使用。 3. 存放於 raw 目錄 存放於 raw 目錄和 assets 目錄其實異曲同工,但有個好處是 raw 檔案會對映到 R 檔案,程式碼中可以通過 R.raw 的方法使用對應的著色器程式碼,但 raw 目錄下不能有目錄結構,這點需要做個取捨。 ![raw 目錄](https://yazhidev.oss-cn-hangzhou.aliyuncs.com/20201229_122040_20201229110953461_611151883.png) 同樣的,封裝讀取 raw 檔案的方法: ``` private fun loadCodeFromRaw(context: Context, fileId: Int): String { var result = "" try { val input = context.resources.openRawResource(fileId) val reader = BufferedReader(InputStreamReader(input)) val str = StringBuilder() var line: String? while ((reader.readLine().also { line = it }) != null) { str.append(line) str.append("\n") } input.close() reader.close() result = str.toString() } catch (e: IOException) { e.stackTrace } return result } ``` 著色器程式可能編譯失敗,可以使用 `glGetShaderiv` 方法獲取著色器編譯狀況: ``` var compileStatus = IntArray(1) //獲取著色器的編譯情況 GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); if (compileStatus[0] == 0) {//若編譯失敗則顯示錯誤日誌並 GLES20.glDeleteShader(shader);//刪除此shader shader = 0; } ``` ## 將著色器加入程式 初始化著色器後拿到著色器物件 ID,再使用 [glAttachShader](https://www.khronos.org/registry/OpenGL-Refpages/es2.0/) 將著色器物件附加到 program 物件上。 ``` GLES20.glAttachShader(mProgram, shader) //將頂點著色器加入到程式 GLES20.glAttachShader(mProgram, fragmentShader) //將片元著色器加入到程式中 ``` ## 連結並使用程式 使用 [glLinkProgram](https://www.khronos.org/registry/OpenGL-Refpages/es2.0/) 為附加在 program 物件上的著色器物件建立可執行檔案。連結可能失敗,可以通過 `glGetProgramiv` 查詢 program 物件狀態: ``` GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, linkStatus, 0) // 如果連線失敗,刪除這程式 if (linkStatus[0] == 0) { GLES20.glDeleteProgram(mProgram) mProgram = 0 } ``` 連結成功後,通過 `glUseProgram` 使用程式,將 program 物件的可執行檔案作為當前渲染狀態的一部分。 ## 繪製圖形 終於到最核心的繪製圖形了,前面我們初始化了 OpenGL 程式以及著色器,現在需要準備繪製相關的資料,繪製出一個圖形最基礎的兩個資料就是頂點座標和圖形顏色。 ### 定義頂點資料 嘗試畫一個三角定,定義三個頂點,每個頂點包含三個座標 x,y,z。手機螢幕中心座標系(0,0,0),左上角座標(-1, 1, 0)。 ``` private val points = floatArrayOf( 0.0f, 0.0f, 0.0f, //螢幕中心 -1.0f, -1.0f, 0.0f, //左下角 1.0f, -1.0f, 0.0f //右下角 ) private val sizePerPoint = 3 //每個頂點三個座標 private val byteSize = sizePerPoint * 4 //每個頂點之前位元組偏移量,float 四個位元組 private val pointNum = points.size / sizePerPoint //頂點數量 private var vertexBuffer: FloatBuffer? = null //頂點資料浮點緩衝區 ``` OpenGL 修改頂點屬性時接受的資料型別為緩衝區型別 Buffer,因此還需要將陣列型別轉為 Buffer: ``` fun createFloatBuffer(array: FloatArray): FloatBuffer { val bb = ByteBuffer.allocateDirect(array.size * 4);//float 四個位元組 bb.order(ByteOrder.nativeOrder()) //使用本機硬體裝置的位元組順序 val buffer = bb.asFloatBuffer() //建立浮點緩衝區 buffer.put(array) //新增資料 buffer.position(0);//從第一個座標開始讀取 return buffer } ``` ### 為頂點屬性賦值 頂點著色器程式碼: ``` attribute vec4 vPosition; void main() { gl_Position = vPosition; } ``` 頂點著色器的每個輸入變數叫頂點屬性,著色器中定義了 vPosition 用於存放頂點資料,先使用 `GLES20.glGetAttribLocation` 獲取 vPosition 控制代碼,再使用 `GLES20.glVertexAttribPointer` 為 vPosition 新增我們定義好的頂點資料。 ``` public static void glVertexAttribPointer( int indx, int size, int type, boolean normalized, int stride, java.nio.Buffer ptr ) ``` 該方法接收六個引數,分別代表: - indx:要修改的頂點屬性的控制代碼 - size:每個頂點的座標數,如果只有 x、y 兩個座標值就傳 2 - type:座標資料型別 - normalized:指定在訪問定點資料值時是應將其標準化(true)還是直接轉換為定點值(false) - stride:每個頂點之間的位元組偏移量 - ptr:頂點座標 Buffer ``` val vPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition") //獲取 vPosition 控制代碼 GLES20.glVertexAttribPointer(vPositionHandle, sizePerPoint, GLES20.GL_FLOAT, false, byteSize, vertexBuffer) //為 vPosition 新增頂點資料 ``` 如果 glGetAttribLocation 返回值為 -1 代表獲取失敗,可能 program 物件或著色器物件裡沒有對應的屬性。 還需要注意的是,為頂點屬性賦值時,`glVertexAttribPointer` 建立了 CPU 和 GPU 之前的邏輯連線,實現了 CPU 資料上傳到 GPU。但 GPU 資料是否可見,也就是頂點著色器能否讀到資料,則由是否啟用了對應的屬性決定。預設情況下頂點屬性都是關閉的,可以通過 `glEnableVertexAttribArray` 啟用屬性,允許著色器讀取 GPU 資料。 ### 定義片元顏色 OpenGL 定義色值使用 float 陣列,可以使用[色值轉換線上工具](https://tool.lu/color/)將十六進位制色值轉換為 float 值 ``` private val colors = floatArrayOf( 0.93f, 0.34f, 0.16f, 1.00f ) ``` #### 為顏色屬性賦值 片元著色器程式碼: ``` precision mediump float; uniform vec4 zColor; void main() { gl_FragColor = zColor; } ``` 顏色屬性定義為 uniform 變數,為顏色屬性賦值一樣需要先獲取屬性控制代碼,再向屬性新增資料: ``` mColorHandle = GLES20.glGetUniformLocation(mProgram, "zColor"); //獲取 zColor 控制代碼 GLES20.glUniform4fv(zColorHandle, 1, color, 0); //為 zColor 新增資料 ``` ### 繪製 ``` GLES20.glEnableVertexAttribArray(vPositionHandle) //啟用頂點控制代碼 GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, pointNum) //渲染圖元 GLES20.glDisableVertexAttribArray(vPositionHandle) //禁用頂點控制代碼 ``` ![繪製三角形](https://yazhidev.oss-cn-hangzhou.aliyuncs.com/20201229_123627_20201229123612959_1845995943.png) 噹噹噹當,三角形出現了。上次只是繪製了背景色,今天又向前邁一步繪製出圖形。但是顯而易見這並不是一個等邊三角形,和我們定義的座標有所出入,這是因為 OpenGL 螢幕座標系是一個正方形並且分佈均勻的座標系,因此將圖形繪製到非正方形螢幕上時圖形會被壓縮或者拉伸。下一篇文章我們會使用投影變換來解決這個問題。 Comming