一、开始
OpenGl主要需要实现两个类:
**GLSurfaceView
**
This class is a View where you can draw and manipulate objects using OpenGL API calls and is similar in function to a SurfaceView. You can use this class by creating an instance of GLSurfaceView and adding your Renderer to it. However, if you want to capture touch screen events, you should extend the GLSurfaceView class to implement the touch listeners, as shown in OpenGL training lesson, Responding to Touch Events.
这个是Android官方的解释,跟SurfaceView类似的可以用OpenGL去绘制的View。
**GLSurfaceView.Renderer
**
This interface defines the methods required for drawing graphics in a GLSurfaceView. You must provide an implementation of this interface as a separate class and attach it to your GLSurfaceView instance using GLSurfaceView.setRenderer().
GLSurfaceView需要设置一个GLSurfaceView.Renderer的实现类去做OpenGl绘制的处理。主要有三个方法:
-
[onSurfaceCreated()](https://developer.android.com/reference/android/opengl/GLSurfaceView.Renderer.html#onSurfaceCreated(javax.microedition.khronos.opengles.GL10, javax.microedition.khronos.egl.EGLConfig)): The system calls this method once, when creating the GLSurfaceView. Use this method to perform actions that need to happen only once, such as setting OpenGL environment parameters or initializing OpenGL graphic objects.
-
[onSurfaceChanged()](https://developer.android.com/reference/android/opengl/GLSurfaceView.Renderer.html#onSurfaceChanged(javax.microedition.khronos.opengles.GL10, int, int)): The system calls this method when the GLSurfaceView geometry changes, including changes in size of the GLSurfaceView or orientation of the device screen. For example, the system calls this method when the device changes from portrait to landscape orientation. Use this method to respond to changes in the GLSurfaceView container.
-
onDrawFrame(): The system calls this method on each redraw of the GLSurfaceView. Use this method as the primary execution point for drawing (and re-drawing) graphic objects.
从方法名字就可以看出来
- onSurfaceCreated()主要做一些初始化的操作
- onSurfaceChanged()主要做一些在view改变时候的处理
- onDrawFrame()主要做绘制处理
二、绘制简单的撞球图面
1.总览
我们最终完成的效果如下图:
效果图.png首先我们思考下我们怎么去画图,我们需要一只笔,需要知道在怎么地方画什么,所以我们的使用OpenGL去绘制的大概步骤如下:
- 定义绘图数据
- 告诉手机如何使用这些数据去绘制
2.定义绘图数据
首先我们需要知道一个概念,OpenGL的坐标系是从-1到1的,如下图:
OpenGL坐标系.png然后我们还要了解到OpenGL只能绘制三种图形,点、线、三角形,所以我们是不能直接画出来一个矩形的,但是呢,我们可以用两个三角形去拼成一个矩形。
所以我们可以这样定义我们的数据:
private float[] mData = new float[]{
//三角形
-0.5f , 0.5f,
-0.5f , -0.5f,
0.5f , 0.5f,
-0.5f , 0.5f,
0.5f , 0.5f,
0.5f , -0.5f,
//线
-0.5f , 0f,
0.5f , 0f,
//点
0f , 0.25f,
0f , -0.25f
};
我们如果用Java的话可以这样去实现,但是现在我们又需要去知道一个知识点了,OpenGL是拿不到虚拟机里面的数据的,OpenGL只能使用Native的数据,所以现在我们需要找个办法去让OpenGL能够使用我们定义的数据,我们可以这样去做:
1.定义常量
private static final int BYTE_PRE_FLOAT = 4;
2.定义全局变量
private final FloatBuffer mVertexData;
3.在构造方法中添加如下代码
mVertexData = ByteBuffer
.allocateDirect(mData.length * BYTE_PRE_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
mVertexData.put(mData);
3.使用OpenGL绘制
现在我们有了数据,而且能够使用数据了,现在我们就可以开始去做绘制的工作了。
首先我们需要知道OpenGL的绘制是通过Shader去实现的,而Shader分为两类:
1.vertex shader:主要告诉GPU哪个点要画在哪里,就是获得点绘制的位置
2.fragment shader:主要告诉GPU哪个点要用什么颜色去画,就是获得点绘制的颜色
现在我们就可以去写代码了,在res/
下建个raw
包来存放OpenGL的实现代码,然后创建simple_vertex_shader.glsl
文件,实现如下代码:
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
}
我们通过a_Position来获取位置传给gl_Position,gl_Position就是OpenGL最后用来绘制的位置。这里解释下vec4表示我们定义了一个向量,这个向量有4个参数,分别为(x,y,z,w),xyz为3D坐标,w后面的文章会讲到,默认值为1。
然后我们还需要创建一个simple_fragment_shader.glsl
文件,实现如下代码:
precision mediump float;
uniform vec4 u_Color;
void main() {
gl_FragColor = u_Color;
}
类似simple_vertex_shader.glsl
,我们也是通过u_Color获取颜色传递给gl_FragColor,gl_FragColor就是最后要绘制的颜色。
第一行代码确定所有float型的清晰度,就像是Java中double和float。我们有三个值可以选择lowp
,mediump
,highp
,分别表示低清晰度,中等清晰度,高清晰度,vertex shader对清晰度要求比较高,默认设置为highp
,所以我们不用设置。
这次我们用了uniform
,在vertex shader我们使用的是attribute
,attribute
对于每个点都是不同的,每个点都有自己的值,但是uniform
的值是不变的,除非我们再次改变它。
这个我们依然用了vec4表示我们声明了一个4个参数的向量,只不过这次内容不一样了,是(r,g,b,a),应该都猜到了,就是红、绿、蓝、透明度。
这样我们就完成了OpenGl部分代码的编写,并对OpenGL有了一定的理解。
现在问题来了,我们要怎么用这两个东西去绘制到View上呢?现在就让我们去解决这个问题,大概需要五个步骤:
1.从.glsl
文件读出代码内容
2.编译读出来的代码
3.链接到程序
4.获取参数的位置
5.使用获取到的参数的位置去绘制
现在就让我们一步一步去实现吧。
- 从
.glsl
文件读出代码内容
可能很多地方都会用到读取文件内容的功能,所以我们把该功能抽出来做一个工具类,建一个util
包,在该包下建一个TextResouceReader.java
类,并实现如下方法:
public static String readTextFileFromResource(Context context , int resourceId){
StringBuilder body = new StringBuilder();
InputStream inputStream = null;
InputStreamReader reader = null;
BufferedReader bufferedReader = null;
try {
inputStream = context.getResources().openRawResource(resourceId);
reader = new InputStreamReader(inputStream);
bufferedReader = new BufferedReader(reader);
String line = null;
while ((line = bufferedReader.readLine()) != null){
body.append(line + "\n");
}
}catch (Exception e){
Logger.debug(TAG , "read file has error");
}finally {
try {
if (bufferedReader != null){
bufferedReader.close();
}
if (reader != null){
reader.close();
}
if (inputStream != null){
inputStream.close();
}
}catch (Exception e){}
}
return body.toString();
}
然后我们第一步就可以完成了,在onSurfaceCreated()
方法中读取代码
String vertexShaderCode = TextResouceReader.readTextFileFromResource(mContext , R.raw.sample_vertex_shader);
String fragmentShaderCode = TextResouceReader.readTextFileFromResource(mContext , R.raw.simple_fragment_shader);
- 编译读出来的代码
我们在util
包下创建一个ShaderHelper.java
类,然后实现如下方法
public static int compileShaderCode(int type , String sourceCode){
final int shaderObjectId = glCreateShader(type);
if (shaderObjectId == 0){
Logger.debug(TAG , "can not create sahder");
return 0;
}
glShaderSource(shaderObjectId , sourceCode);
glCompileShader(shaderObjectId);
final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == 0){
Logger.debug(TAG , "compile fail");
}
return shaderObjectId;
}
首先我们创建了一个shader对象,type有两个值GL_VERTEX_SHADER,GL_FRAGMENT_SHADER
,返回了一个int型,这个值是我们OpeGL对象的引用,当我们以后需要应用的时候,就要用这个值,如果返回0则表示创建失败了。然后我们就需要用glShaderSource()
上传代码,glCompileShader()
编译代码,最后,我们检查了编译的状态。
为了方便使用我们在ShaderHelper.java
方法中再添加两个方法:
public static int compileVertexShader(String sourceCode){
return compileShaderCode(GL_VERTEX_SHADER , sourceCode);
}
public static int compileFragmentShader(String sourceCode){
return compileShaderCode(GL_FRAGMENT_SHADER , sourceCode);
}
- 链接到程序
接下来我们需要在ShaderHelper.java
实现如下的方法:
public static int linkProgram(int vertexShaderId , int fragmentShaderId){
int programId = glCreateProgram();
if (programId == 0){
Logger.debug(TAG , "create program fail");
return 0;
}
glAttachShader(programId , vertexShaderId);
glAttachShader(programId , fragmentShaderId);
glLinkProgram(programId);
final int[] linkStatus = new int[1];
glGetProgramiv(programId , GL_LINK_STATUS , linkStatus , 0);
if (linkStatus[0] == 0){
Logger.debug(TAG , "link fail");
return 0;
}
return programId;
}
跟编译代码类似,我们先创建一个program对象,然后把vertexShader,fragmentShader传给program,最后链接程序,检查是否链接成功。
- 获取参数的位置
在获取参数位置之前我们需要使我们链接的程序生效,还需要在ShaderHelper.java
实现如下方法:
public static boolean vaildProgram(int programId){
glValidateProgram(programId);
final int[] validateStatus = new int[1];
glGetProgramiv(programId, GL_VALIDATE_STATUS, validateStatus, 0);
return validateStatus[0] != 0;
}
我们已经在ShaderHelper.java
中完成了这些方法,我们现在可以方便的用这些方法在Renderder的onSurfaceCreated()
方法中去使用这些方法了。
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
String vertexShaderCode = TextResouceReader.readTextFileFromResource(mContext , R.raw.sample_vertex_shader);
String fragmentShaderCode = TextResouceReader.readTextFileFromResource(mContext , R.raw.simple_fragment_shader);
int vertextShaderId = ShaderHelper.compileVertexShader(vertexShaderCode);
int fragmentShaderId = ShaderHelper.compileFragmentShader(fragmentShaderCode);
mProgram = ShaderHelper.linkProgram(vertextShaderId , fragmentShaderId);
ShaderHelper.vaildProgram(mProgram);
glUseProgram(mProgram);
}
在让我们的程序生效后,我们就要开始使用它了,所以我们调用了glUseProgram()
方法。
接下来我们来获取我们定义的u_Color的位置信息,我们先做如下声明:
private static final String U_COLOR = "u_Color";
private int mUColorLocation;
然后在onSurfaceCreated()
方法中获取u_Color的位置信息
mUColorLocation = glGetUniformLocation(mProgram , U_COLOR);
类似的我们可以获取a_Position的位置信息:
private static final String A_POSITION = "a_Position";
private int mAPositionLocation;
mAPositionLocation = glGetAttribLocation(mProgram , A_POSITION);
- 使用获取到的参数的位置去绘制
最后我们就开始去绘制了,首先我们在Renderer的onSurfaceCreated()
方法中添加如下代码:
mVertexData.position(0);
glVertexAttribPointer(mAPositionLocation , POSITION_COMPOENT_COUNT , GL_FLOAT , false , 0 , mVertexData);
POSITION_COMPOENT_COUNT
为常量:
private static final int POSITION_COMPOENT_COUNT = 2;
mVertexData.position(0)
让我们可以保证从数据的第一个值开始读取,然后我们用glVertexAttribPointer()
方法去从vertexData中读取数据赋值给a_Position,该方法的各参数解释如下:
POSITION_COMPOENT_COUNT
:表示每个向量读取两个值
GL_FLOAT
:表示数据类型
false
:该值只有在用int型值的时候为true
0
:这个值只有在数据有多个参数的时候需要使用,后面会讨论,目前置为0
具体的大家可以自己去查下Api。
现在OpenGL就知道如何从vertexData中读取a_Position的值了,最后我们需要添加一行代码glEnableVertexAttribArray(aPositionLocation);
,去让a_Position能够被使用。
接下来我们就可以去绘制了,在onDrawFrame()
中添加如下代码:
//绘制两个三角形
glUniform4f(mUColorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
glDrawArrays(GL_TRIANGLES, 0, 6);
//绘制线
glUniform4f(mUColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_LINES, 6, 2);
//绘制点
glUniform4f(mUColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
glDrawArrays(GL_POINTS, 8, 1);
glUniform4f(mUColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_POINTS, 9, 1);
glUniform4f()
用来给u_Color赋值。
glDrawArrays()
来绘制,第一个参数表示要绘制什么,第二个参数表示从之前载入的数据哪里开始读取,第三个参数表示读几个值。
这个时候就算完成了我们的基本功能了,但是运行之后,你会发现绘制的点不见了,其实只是太小了,看不到而已,在simple_vertex_shader.glsl
文件中添加如下代码:gl_PointSize = 10;
再运行一遍,就能看到我们想要的效果了。
项目代码在这里:https://github.com/KevinKmoo/SimpleAirHockey
能力有限,自己读书的学习所得,有错误请指导,轻虐!
转载请注明出处。----by kmoo
网友评论