通过学习《OpenGL ES应用开发实践指南》总结如何在安卓上创建一个OpenGL程序,并且在屏幕上绘制图案。
一、OpenGL管道
首先知道OpenGL在把本地内存中的数据绘制到屏幕所经历的步骤
- Read Vertex Data:读取顶点数据
Execute Vertex Shader:执行顶点着色器
顶点着色器:生成每个顶点的最终位置,每个顶点都会执行一次,一旦顶点的最终位置确定,OpenGL就把这些可见的顶点集合组装成点、线和三角形。
Assemble Primitives:组装图元
图元:组成图像的基本单元
Rasterize Primitives:光栅化图元
光栅化:即把每个点、直线及三角形分解成小片段
Execute Fragment Shader:执行片段着色器
片段着色器:组成点、直线或者三角形的每个片段的最终颜色,针对每个片段,它都会执行一次,一个片段是一个小的、单一的长方形区域,类似于计算机屏幕上的一个像素
- Write to Frame Buffer:写入帧缓冲区
- See It on the Screen: 显示在屏幕上
上述过程描:
1. 先读取保存在内存的顶点数据;
2. 执行顶点着色器把顶点数据转化成为点、线或者三角形,并且确定在屏幕上的位置;
3. 之后为了确定这些点、线和三角形的颜色,而光栅化它们,即把它们分解成一个个的小片段
4. 执行片段着色器,确定每一个小片段的颜色
5. 确定完位置和颜色后,写入一个叫帧缓冲区的内存区域
6. 显示在屏幕上
二、创建程序
2.1 创建一个初始化OpenGL的实例
2.1.1 布局文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<android.opengl.GLSurfaceView
android:id="@+id/surface"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>
2.1.2 初始化类
public class MainActivity extends AppCompatActivity{
private GLSurfaceView mGLSurfaceView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mGLSurfaceView = (GLSurfaceView)findViewById(R.id.surface);
//checking if the system supports OpenGL ES 2.0
final ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
&&(Build.FINGERPRINT.startsWith("generic"))
|| Build.FINGERPRINT.startsWith("unknow")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86");
if(supportsEs2){
mGLSurfaceView.setEGLContextClientVersion(2);
mGLSurfaceView.setRenderer(new MyRenderer());
}
}
@Override
protected void onPause() {
super.onPause();
mGLSurfaceView.onPause();
}
@Override
protected void onResume() {
super.onResume();
mGLSurfaceView.onResume();
}
}
创建GLSurfaceView实例
setContentView(R.layout.activity_main);
mGLSurfaceView = (GLSurfaceView)findViewById(R.id.surface);
检查系统是否支持OpenGL ES 2.0
final ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
&&(Build.FINGERPRINT.startsWith("generic"))
|| Build.FINGERPRINT.startsWith("unknow")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86");
首先,引用Android ActivityManager来获取设备的配置信息,然后取出reqGIEsVersion变量检查OpenGL ES版本号,然后检查版本号是否支持OpenGL ES 2.0
配置渲染表面
if(supportsEs2){
mGLSurfaceView.setEGLContextClientVersion(2);
mGLSurfaceView.setRenderer(new MyRenderer());
}
如果设备支持OpenGL ES 2.0,则调用setEGLContextClientVersion(2)配置这个surface视图,之后调用setRenderer传入自定义Renderer类的一个新实例
重写Android Activity生命周期事件
@Override
protected void onPause() {
super.onPause();
mGLSurfaceView.onPause();
}
@Override
protected void onResume() {
super.onResume();
mGLSurfaceView.onResume();
}
加入GLSurfaceView类的onPause()方法和onResume()方法,使得surface视图能正确暂停,释放和续用OpenGL上下文
2.2 创建Renderer类
创建一个渲染器,渲染器接口定义的方法:
- onSurfaceCreated(GL10 glUnused, EGLConfig config):当Surface被创建时,此方法被GLSurfaceView调用,此方法发生在应用程序第一次运行的时候,并且,当设备被唤醒或者用户从其它activity切换回来时也可能被调用。
- onSurfaceChanged(GL10 glUnused, int width, int height):每次Surface尺寸变化时,这个方法被GLSurfaceView调用。在横屏、竖屏来回切换的时候,Surface尺寸会发生变化
- onDrawFrame(GL10 glUnused):当绘制一帧时,这个方法会被GLSurfaceView调用。在这个方法中一定要绘制一些东西,即使是清空屏幕。
渲染器:
public class MyRenderer implements GLSurfaceView.Renderer{
private static final int POSITION_COMPONENT_COUNT = 2;
private final FloatBuffer vertexData;
private int program;
private static final String U_COLOR = "u_Color";
private int uColorLocation;
private static final String A_POSITION = "a_Position";
private int aPositionLocation;
private static final String VERTEX_SHADER =
"attribute vec4 a_Position;\n"
+"void main() {\n"
+"gl_Position = a_Position;\n"
+"gl_PointSize = 10.0;\n"
+"}";
private static final String FRAGMENT_SHADER =
"precision mediump float;\n"
+"uniform vec4 u_Color;\n"
+ "void main() {\n"
+ " gl_FragColor = u_Color;\n"
+ "}";
private static final float[] VERTEX = {
//Triangle 1
-0.5f, -0.5f,
0.5f, 0.5f,
-0.5f, 0.5f,
//Triangle 2
-0.5f, -0.5f,
0.5f, -0.5f,
0.5f, 0.5f,
//Line 1
-0.5f, 0f,
0.5f, 0f,
//Mallets
0f, -0.25f,
0f, 0.25f
};
public MyRenderer() {
vertexData = ByteBuffer.allocateDirect(VERTEX.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vertexData.put(VERTEX);
}
static int loadShader(int type, String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
GLES20.glClearColor(0.0f,0.0f,0.0f,0.0f);
program = GLES20.glCreateProgram();
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
glUseProgram(program);
uColorLocation = glGetUniformLocation(program,U_COLOR);
aPositionLocation = glGetAttribLocation(program,A_POSITION);
vertexData.position(0);
glVertexAttribPointer(aPositionLocation,POSITION_COMPONENT_COUNT,GL_FLOAT,false,0,vertexData);
glEnableVertexAttribArray(aPositionLocation);
}
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
GLES20.glViewport(0,0,width,height);
}
@Override
public void onDrawFrame(GL10 gl10) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
glUniform4f(uColorLocation,1.0f,1.0f,1.0f,1.0f);
glDrawArrays(GL_TRIANGLES,0,6);
glUniform4f(uColorLocation,1.0f,0.0f,0.0f,1.0f);
glDrawArrays(GL_LINES,6,2);
glUniform4f(uColorLocation,0.0f,0.0f,1.0f,1.0f);
glDrawArrays(GL_POINTS,8,1);
glUniform4f(uColorLocation,1.0f,0.0f,0.0f,1.0f);
glDrawArrays(GL_POINTS,9,1);
}
}
代码中定义顶点
private static final float[] VERTEX = {
//Triangle 1
-0.5f, -0.5f,
0.5f, 0.5f,
-0.5f, 0.5f,
//Triangle 2
-0.5f, -0.5f,
0.5f, -0.5f,
0.5f, 0.5f,
//Line 1
-0.5f, 0f,
0.5f, 0f,
//Mallets
0f, -0.25f,
0f, 0.25f
};
采用浮点数的顺序列表定义顶点数据,分别定义了两个三角形(组成一个矩形);一条直线,两个点
使数据可以被OpenGL存取
顶点定义完成之后,由于代码运行的环境与OpenGL运行的环境使用了不同的语言,需要将代码与OpenGL之间通信。
当调用android.opengl.GLES20包里的方法时,使用了Java本地接口,将运行在Davik虚拟机的代码与OpenGL通信。
我们把内存从Java堆复制到本地堆,使得顶点数据从Davik可以传输到OpenGL
private final FloatBuffer vertexData;
public MyRenderer() {
vertexData = ByteBuffer.allocateDirect(VERTEX.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vertexData.put(VERTEX);
}
- 定义一个FloatBuffer类型变量;
- 使用ByteBuff.allocateDirect()分配一块本地内存,这块内存不会被Davik虚拟机的垃圾回收器管理;每个顶点存储在一个浮点数组里,每个浮点数4个字节,所以内存大小是VERTEX.length * 4
- 调用order(ByteOrder.nativeOrder())保证字节缓冲区按照本地字节序组织它的内容
- 调用asFloatBuffer()得到一个FloatBuffer类的实例,即不使用单独的字节,而是使用浮点数
- 最后调用vertexData.put(VERTEX)把数据从Dalvik的内存复制到本地内存
重写三个渲染器接口定义方法
1. onSurfaceCreated
private static final String U_COLOR = "u_Color";
private int uColorLocation;
private static final String A_POSITION = "a_Position";
private int aPositionLocation;
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
GLES20.glClearColor(0.0f,0.0f,0.0f,0.0f);
program = GLES20.glCreateProgram();
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
glUseProgram(program);
uColorLocation = glGetUniformLocation(program,U_COLOR);
aPositionLocation = glGetAttribLocation(program,A_POSITION);
vertexData.position(0);
glVertexAttribPointer(aPositionLocation,POSITION_COMPONENT_COUNT,GL_FLOAT,false,0,vertexData);
glEnableVertexAttribArray(aPositionLocation);
}
- 首先执行一下清屏函数glClearColor(),四个参数分别对应红色、绿色、蓝色、透明度,此处都为0.0f,则清屏为黑色
- loadShader()是自定义的方法,用来创建着色器,并把代码上传到着色器中,并且编译
static int loadShader(int type, String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
glCreateShader(type):创建一个新的着色器对象,type为着色器对象的类型(顶点着色器和片段着色器)
GLES20.glShaderSource(shader, shaderCode):把着色器源代码上传到着色器对象里
GLES20.glCompileShader(shader):编译着色器
- 新建一个程序对象,并且把顶点着色器和片段着色器都附加到程序对象上,并链接
- program = GLES20.glCreateProgram():调用glCreateProgram()新建程序对象
- 加载着色器:创建并且上传着色器代码和编译顶点着色器和片段着色器
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
- 将顶点着色器和片段着色器都附加到程序对象上
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
- 链接程序:将两个着色器联合起来
GLES20.glLinkProgram(program);
- 获取片段着色器中定义的uniform(u_Color)的位置和位置着色器中属性(a_Position)的位置。
uColorLocation = glGetUniformLocation(program,U_COLOR);
aPositionLocation = glGetAttribLocation(program,A_POSITION);
当OpenGL 把着色器链接成一个程序的时候,实际上用位置编号编号把片段着色器中定义的每个uniform都关联起来,并且给位置着色器中的属性也分配位置编号
而OpenGL寻找所需要的数据时,使用的是uniform(u_Color)和属性(a_Position)的位置,而不是名字
- 使用vertexData.position(0);把创建到本地内存缓冲区的指针设置到数据的开头
- 调用glVertexAttribPointer(),使得OpenGL在vertexData中找到a_Position对应的数据
- 调用glEnableVertexAttribArray()使能数据属性
2. onSurfaceChanged
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
GLES20.glViewport(0,0,width,height);
}
如果Surface尺寸发生变化,则根据相应长宽调整显示大小
3. onDrawFrame
@Override
public void onDrawFrame(GL10 gl10) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
glUniform4f(uColorLocation,1.0f,1.0f,1.0f,1.0f);
glDrawArrays(GL_TRIANGLES,0,6);
glUniform4f(uColorLocation,1.0f,0.0f,0.0f,1.0f);
glDrawArrays(GL_LINES,6,2);
glUniform4f(uColorLocation,0.0f,0.0f,1.0f,1.0f);
glDrawArrays(GL_POINTS,8,1);
glUniform4f(uColorLocation,1.0f,0.0f,0.0f,1.0f);
glDrawArrays(GL_POINTS,9,1);
}
- 调用glClear(GLES20.GL_COLOR_BUFFER_BIT)清空屏幕,擦除屏幕上所有颜色,并用之前glClearColor()调用定义的颜色填充屏幕
- 绘制两个白色的三角形,即组成一个矩形
glUniform4f(uColorLocation,1.0f,1.0f,1.0f,1.0f);
glDrawArrays(GL_TRIANGLES,0,6);
- 绘制一条红色的线
glUniform4f(uColorLocation,1.0f,0.0f,0.0f,1.0f);
glDrawArrays(GL_LINES,6,2);
- 绘制一个蓝色的点和一个红色的点,点的大小由位置着色器里gl_PointSize设定
glUniform4f(uColorLocation,0.0f,0.0f,1.0f,1.0f);
glDrawArrays(GL_POINTS,8,1);
glUniform4f(uColorLocation,1.0f,0.0f,0.0f,1.0f);
glDrawArrays(GL_POINTS,9,1);