Android OpenGL ES 開發:繪製圖形
阿新 • • 發佈:2020-12-29
# 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