背景
?繪制 3D 圖,總覺得是一件很炫酷的事。雖然在項(xiàng)目中一直沒有用到過,但是還是想找個(gè)時(shí)間,實(shí)踐一下。
?繪制二維圖形,盡管使用 OpenGL 有它的優(yōu)勢(shì),但是還是感覺有點(diǎn)殺雞用牛刀的意 思。這里主要是借助對(duì) 二維圖形 的繪制過程,解釋相關(guān)概念。
什么是 OpenGL ES
?首先,來說明一下 OpenGL ES for Embedded Systems(OpenGL ES)。它是 OpenGL 的子集,用以渲染 2D 、3D 矢量圖的跨語言、跨平臺(tái)的API,這個(gè) API 通常會(huì)和 GPU 交互,完成硬件加速渲染。
?Android 平臺(tái)支持不同版本 OpenGL ES 的 API。其中:
- OpenGL ES 1.0 and 1.1 - 該 API 被Android 1.0 及更高版本支持.
- OpenGL ES 2.0 - 該 API 被 Android 2.2 (API level 8) 及更高版本支持.
- OpenGL ES 3.0 - 該 API 被 Android 4.3 (API level 18) 及更高版本支持.
- OpenGL ES 3.1 - 該 API 被 Android 5.0 (API level 21) 及更高版本支持.
?為了獲取更廣泛的設(shè)備支持,通常會(huì)基于 OpenGL ES 2.0 做開發(fā)。本文也是基于該版本展開。
Android 平臺(tái)提供的基礎(chǔ)
?Android 框架層提供了兩個(gè)對(duì)象以使用 OpenGL ES API 操作圖像:GLSurfaceView 和 GLSurfaceView.Renderer。
GLSurfaceView
?GLSurfaceView 繼承自 SurfaceView,擁有專用的 surface 以展示 OpenGL 渲染。它提供了一下特性:
- 管理 surface,同時(shí)使得 OpenGL 可以在 surface 上渲染;
- 可以使用用戶自定義的 Renderer 對(duì)象進(jìn)行實(shí)際的渲染工作;
- 渲染線程獨(dú)立于 UI 線程之外 ;
- 支持在需要時(shí)才進(jìn)行的被動(dòng)渲染 和 不間斷自動(dòng)進(jìn)行的主動(dòng)渲染兩種渲染方式;
//設(shè)置一下模式,為被動(dòng)刷新
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
- 調(diào)試渲染調(diào)用
簡單的使用方式如下所示:
//直接創(chuàng)建一個(gè) GLSurfaceView,當(dāng)然,也可以通過布局文件創(chuàng)建
glSurfaceView = new GLSurfaceView(this) ;
//使用 OpenGL ES 2.0 context.
glSurfaceView.setEGLContextClientVersion(2);
//設(shè)置我們自定義的 renderer
glSurfaceView.setRenderer(new MyRenderer());
setContentView(glSurfaceView);
GLSurfaceView.Renderer
?負(fù)責(zé)進(jìn)行幀渲染。提供的回調(diào)函數(shù)有:
- onDrawFrame:負(fù)責(zé)對(duì)當(dāng)前幀的繪制;
- onSurfaceChanged:當(dāng) surface 的大小發(fā)生變化時(shí)候的回調(diào),比如剛創(chuàng)建 surface 的時(shí)候,繪制屏幕發(fā)生旋轉(zhuǎn);
- onSurfaceCreated:當(dāng) surface 創(chuàng)建或者重建的時(shí)候被調(diào)用。調(diào)用發(fā)生在渲染線程開始的時(shí)候,或者當(dāng) EGL context 丟失的時(shí)候(該 context 通常會(huì)在設(shè)備從睡眠中喚醒的時(shí)候丟失)。
注意,當(dāng) EGL context 丟失,和 context 關(guān)聯(lián)的所有 OpenGL 自愿將會(huì)被自動(dòng)刪除。
管線渲染過程
?要明白這個(gè)過程,首先要知道什么是管線。所謂管線,就是在顯卡上執(zhí)行的將數(shù)據(jù)源轉(zhuǎn)換投射到屏幕像素點(diǎn)上的過程。也就是將我們通過頂點(diǎn)定義的形狀,顯示到屏幕上。
如上圖所示,
- 定義頂點(diǎn);
- 通過 vertexShader著色器 告知 GPU 頂點(diǎn)的位置等屬性;
- 通過圖元裝配,生成要繪制的形狀;
- 光柵化處理,將所有的點(diǎn)轉(zhuǎn)化為片元(fragment);
- 通過 fragmentShader著色器 為片元上色;
- 將片元投射到屏幕上的像素上。
更加形象一點(diǎn)的過程如下所示:
?注意,其中 vertexShader 和 fragmentShader 是通過 GLSL 語言定義的,并直接運(yùn)行在 GPU 上。
VertexShader
頂點(diǎn)著色器,主要用于確定頂點(diǎn)位置,由 GLSL 語言定義,對(duì)于每個(gè)頂點(diǎn)都會(huì)執(zhí)行該程序。通常用法如下:
//通過 GLSL 定義 VertexShader
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"uniform mat4 u_Matrix;"+
"void main() {" +
// "gl_Position = vPosition;" +
"gl_Position = u_Matrix * vPosition;" +
"gl_PointSize = 10.0;"+
"}";
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
...
//取出位置索引
aPositionLocation = GLES20.glGetAttribLocation(program,"vPosition") ;
//將位置索引和我們定義的數(shù)據(jù)源 vertexes 進(jìn)行綁定,將 vertexes 中的每個(gè)頂點(diǎn)拿出來賦值到 vPosition ,并分別執(zhí)行上面定義的 GLSL 程序
GLES20.glVertexAttribPointer(aPositionLocation,2,GLES20.GL_FLOAT,false,0,vertexes);
GLES20.glEnableVertexAttribArray(aPositionLocation);
}
FragmentShader
片元著色器,目的就是告訴 GPU 每個(gè)片段的最終顏色應(yīng)該是什么。對(duì)于基于圖元的每個(gè)片段,片段著色器都會(huì)被調(diào)用一次。因此,如果一個(gè)三角形被映射到 10000 個(gè)片段,片段著色器就會(huì)被調(diào)用 10000次。
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//取出顏色索引
aColorLocation = GLES20.glGetUniformLocation(program,"vColor") ;
}
@Override
public void onDrawFrame(GL10 gl) {
//給顏色賦值
GLES20.glUniform4f(aColorLocation, 0f,1,1, 1.0f);
//繪制
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}
完整代碼如下:
public class MyRenderer implements GLSurfaceView.Renderer {
private FloatBuffer vertexes ;
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"uniform mat4 u_Matrix;"+
"void main() {" +
"gl_Position = u_Matrix * vPosition;" +
"gl_PointSize = 10.0;"+
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
private int aPositionLocation ;
private int aColorLocation ;
private int uMatrixLocation;
private final float[] projectionMatrix = new float[16];
private void createVertexes(){
float [] vertexesArray = new float[]{
0,1,
-1,-1,
1,-1
} ;
vertexes.clear();
vertexes.put(vertexesArray) ;
}
private void init(){
//創(chuàng)建本地內(nèi)存,以便將我們定義的頂點(diǎn)放進(jìn)去,供設(shè)備訪問
//堆內(nèi)存上的數(shù)據(jù),GPU是無法直接訪問的
vertexes = ByteBuffer.allocateDirect(6*4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer() ;
//創(chuàng)建 頂點(diǎn) 坐標(biāo)
createVertexes();
}
public MyRenderer(){
init();
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(1f,1f,0f,0f);
int vertexShader = ShaderHelper.compileVertexShader(vertexShaderCode) ;
int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderCode) ;
int program = ShaderHelper.linkProgram(vertexShader, fragmentShader);
if (LoggerConfig.ON) {
ShaderHelper.validateProgram(program);
}
GLES20.glUseProgram(program);
aPositionLocation = GLES20.glGetAttribLocation(program,"vPosition") ;
aColorLocation = GLES20.glGetUniformLocation(program,"vColor") ;
uMatrixLocation = GLES20.glGetUniformLocation(program, "u_Matrix");
vertexes.position(0) ;
GLES20.glVertexAttribPointer(aPositionLocation,2,GLES20.GL_FLOAT,false,0,vertexes);
GLES20.glEnableVertexAttribArray(aPositionLocation);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
// 根據(jù)屏幕方向設(shè)置投影矩陣
float ratio= width > height ? (float)width / height : (float)height / width;
if (width > height) {
// 橫屏
Matrix.orthoM(projectionMatrix, 0, -ratio, ratio, -1, 1, 0, 5);
} else {
Matrix.orthoM(projectionMatrix, 0, -1, 1, -ratio, ratio, 0, 5);
}
}
@Override
public void onDrawFrame(GL10 gl) {
// Clear the rendering surface.
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// Assign the matrix
GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0);
GLES20.glUniform4f(aColorLocation, 0f,1,1, 1.0f);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}
}
下面對(duì)以上代碼塊做幾點(diǎn)說明:
- 在使用 OpenGL ES 的時(shí)候,我們一般會(huì)做以下處理:1. 編譯頂點(diǎn)著色器;2. 編譯出片元著色器;3. 創(chuàng)建程序;4.通過程序鏈接所有著色器;5. 使用程序,并通過程序,取出位置、顏色等索引,以便后期繪制。
- 上面代碼塊中,回調(diào)方法 onDrawFrame 的調(diào)用時(shí)機(jī)需要注意。默認(rèn)情況下,會(huì)按照屏幕刷新的周期來調(diào)用該方法,即每秒執(zhí)行 60 次。當(dāng)然,我們可以通過在代碼中設(shè)置以改變這種默認(rèn)行為。
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
- 注意本地內(nèi)存和虛擬機(jī)中堆內(nèi)存的使用。后者是無法直接被設(shè)備訪問的,后者可以,同時(shí)后者不受 GC 的影響。
- GLES20 中的所有方法,實(shí)際上都是本地方法,即通過 JNI 調(diào)用實(shí)現(xiàn)的,底層是由 C 語言實(shí)現(xiàn)。
小結(jié)
?本節(jié)主要借用二維圖形的繪制,講解了 OpenGL ES 在 Android 應(yīng)用中使用的相關(guān)概念。下面想講述一下坐標(biāo)變換。
參考鏈接:
https://developer.android.com/training/graphics/opengl/environment.html
http://www.cs.ucr.edu/~shinar/courses/cs130-spring-2012/schedule.html
https://www.zhihu.com/question/29163054
https://en.wikibooks.org/wiki/GLSL_Programming/OpenGL_ES_2.0_Pipeline