Android OpenGL ES 開發(二):繪製圖形
- 2020 年 12 月 29 日
- 筆記
OpenGL 繪製圖形步驟
上一篇介紹了 OpenGL 的相關概念,今天來實際操作,使用 OpenGL 繪製出圖形,對其過程有一個初步的了解。
OpenGL 繪製圖形主要概括成以下幾個步驟:
- 創建程序
- 初始化着色器
- 將着色器加入程序
- 鏈接並使用程序
- 繪製圖形
上述每個步驟還可能會被分解成更細的步驟,對應着多個 api,下面我們來逐個看下。
創建程序
使用 glCreateProgram 創建一個 program 對象並返回一個引用 ID,該對象可以附加着色器對象。注意要在OpenGL渲染線程中創建,否則無法渲染。
初始化着色器
着色器的初始化可以細分為三個步驟:
- 創建頂點、片元着色器對象
- 關聯着色器代碼與着色器對象
- 編譯着色器代碼
上一篇文章我們提到了頂點着色器和片元着色器都是可編程管道,因此着色器的初始化少不了對着色器代碼的關聯與編譯,上面三個步驟對應的 api 為:
- glCreateShader(int type)
- type:
GLES20.GL_VERTEX_SHADER
代表頂點着色器、GLES20.GL_FRAGMENT_SHADER
代表片元着色器
- type:
- glShaderSource(int shader, String code)
- shader:着色器對象 ID
- code:着色器代碼
- glCompileShader(code)
- code:着色器對象 ID
着色器代碼使用 GLSL 語言編寫,那代碼要怎麼保存並使用呢?我看到過三種方式,列出供大家參考:
- 字符串變量保存
這種應該是最直觀的寫法了,直接在對應的類中使用硬編碼存儲着色器代碼,形如:
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}";
這種方式不是很建議,可讀性不好。
- 存放於 assets 目錄
assets 文件夾下的文件不會被編譯成二進制文件,因此適於存放着色器代碼,還可以配合 AndroidStudio 插件 GLSL Support 實現語法高亮:
然後再封裝讀取 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 語法,自然也就無法正常使用。
- 存放於 raw 目錄
存放於 raw 目錄和 assets 目錄其實異曲同工,但有個好處是 raw 文件會映射到 R 文件,代碼中可以通過 R.raw 的方法使用對應的着色器代碼,但 raw 目錄下不能有目錄結構,這點需要做個取捨。
同樣的,封裝讀取 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 將着色器對象附加到 program 對象上。
GLES20.glAttachShader(mProgram, shader) //將頂點着色器加入到程序
GLES20.glAttachShader(mProgram, fragmentShader) //將片元着色器加入到程序中
鏈接並使用程序
使用 glLinkProgram 為附加在 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 數組,可以使用色值轉換在線工具將十六進制色值轉換為 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) //禁用頂點句柄
噹噹噹噹,三角形出現了。上次只是繪製了背景色,今天又向前邁一步繪製出圖形。但是顯而易見這並不是一個等邊三角形,和我們定義的坐標有所出入,這是因為 OpenGL 屏幕坐標系是一個正方形並且分佈均勻的坐標系,因此將圖形繪製到非正方形屏幕上時圖形會被壓縮或者拉伸。下一篇文章我們會使用投影變換來解決這個問題。
Comming soon 😛