Hello World,使用OpenGL ES绘制一个三角形
1.Android中创建OpenGL ES环境
为了使用OpenGL画图,我们首先需要创建OpenGL环境。
a.第一步当然要在Manifest中声明一些东西
首先要声明下使用的OpenGL ES版本,由于OpenGL ES 2.0支持Android 2.2 (API Level 8)以上版本,兼容版本比较广泛,这里使用 OpenGL ES 2.0
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
如果需要使用纹理压缩,需要以下声明来告诉移动设备,设备不支持将不能安装该APP
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
b.自定义一个GLSurfaceView
Android中使用OpenGL ES,最简单的方式就是直接使用GLSurfaceView。这里自定义了一个GLSurfaceView,其实如果不实现触摸事件
class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer mRenderer;
public MyGLSurfaceView(Context context){
super(context);
// Create an OpenGL ES 2.0 context
setEGLContextClientVersion(2);
mRenderer = new MyGLRenderer();
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(mRenderer);
}
}
其实这个View本身没有太大作用,绘图的主要任务都是在View的GLSurfaceView.Renderer中实现的。
c.创建 GLSurfaceView.Renderer
继承GLSurfaceView.Renderer,在这里控制GLSurfaceView上的绘制内容。这里主要实现三个方法。
onSurfaceCreated() - OpenGL环境注册到View调用一次
onDrawFrame() -当View内容重新绘制的时候调用(默认模式会自动刷新,可以通过setRenderMode方法设置刷新模式,setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY),设置当需要的时候刷新一次)
onSurfaceChanged() - 当View的大小发生变化(例如屏幕方向改变)被调用
public class MyGLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// Set the background frame color
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// Redraw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
}
d.创建一个Activity
把上边定义的GLSurfaceView设置到一个Activity中
public class OpenGLES20Activity extends Activity {
private GLSurfaceView mGLView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a GLSurfaceView instance and set it
// as the ContentView for this Activity.
mGLView = new MyGLSurfaceView(this);
setContentView(mGLView);
}
}
2.定义形状
OpenGL ES绘制图形,第一步要先定义图形。OpenGL ES运行使用三维空间坐标来定义形状。三角形是OpenGL ES中的基本形状,这里绘制一个三角形,需要使用坐标来定义三角形。这里使用一个数组来保存三角形的三个顶点坐标(注意 坐标按照逆时针存储在数组中。)。最后把这些坐标写入ByteBuffer, 通过OpenGL ES管线来处理。
public class Triangle {
private FloatBuffer vertexBuffer;
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = { // in counterclockwise order:
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// Set color with red, green, blue and alpha (opacity) values
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (number of coordinate values * 4 bytes per float)
triangleCoords.length * 4);
// use the device hardware's native byte order
bb.order(ByteOrder.nativeOrder());
// create a floating point buffer from the ByteBuffer
vertexBuffer = bb.asFloatBuffer();
// add the coordinates to the FloatBuffer
vertexBuffer.put(triangleCoords);
// set the buffer to read the first coordinate
vertexBuffer.position(0);
}
}
OpenGL ES坐标系中,[0,0,0] (X,Y,Z) 代表GLSurfaceView的中心,[1,1,0] 是View的右上角, [-1,-1,0] 是左下角,同时如果View不是方形坐标系也会被拉伸。如下图
3.绘制形状
a.编写着色器
OpenGL提供了大量API来控制图形渲染管线的过程。你会发现绘制一个三角形也需要大量的代码。
Vertex Shader - 顶点着色器,用于绘制坐标形状的程序代码
Fragment Shader -片段着色器,用于绘制形状颜色的程序代码
Program -包含上述两种着色器OpenGL ES程序对象
需要一个顶点着色器来绘制形状,一个片段着色器来填充颜色,然后把两个着色器源码编译并添加到OpenGL ES程序中。下面是定义三角形的着色器程序示例
都要有一个main函数,gl_Position,gl_FragColor都是OpenGL ES内部变量。
public class Triangle {
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
...
}
编写着色器使用的是GLSL(OpenGL Shading Language)。使用前必须先编译GLSL,这里封装了一个静态方法来编译GLSL。
public static int loadShader(int type, String shaderCode){
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
绘制之前我们要编译GLSL,然后添加到OpenGL ES程序对象中,最后链接程序。这是一个耗时操作,所以在对象初始化的时候执行一次就好了。所以把这一系列任务放在了构造函数中。
public class Triangle() {
...
private final int mProgram;
public Triangle() {
...
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram();
// add the vertex shader to program
GLES20.glAttachShader(mProgram, vertexShader);
// add the fragment shader to program
GLES20.glAttachShader(mProgram, fragmentShader);
// creates OpenGL ES program executables
GLES20.glLinkProgram(mProgram);
}
}
接下来是绘制工作,这里主要是获取上边定义的顶点着色器,片段着色器中的变量取出来赋值,把顶点坐标赋值给vPosition,把颜色信息赋值给vColor,Color信息由一个float数组表示 分别为R G B A颜色值,范围0到1
rivate int mPositionHandle;
private int mColorHandle;
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
// Set color with red, green, blue and alpha (opacity) values
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public void draw() {
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices 默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU,由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的属性数据。
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// Set color for drawing the triangle glUniform设置uniform类型值,后边数字表示不同的重载(c实现不支持重载),f表示float,i表示int,v表示接受相应的指针类型
//http://blog.csdn.net/wangyuchun_799/article/details/7742787
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// Draw the triangle 该函数根据顶点数组中的坐标数据和指定的模式,进行绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
写完这些代码,在自定义的Render类中 onDrawFrame() 方法调用上边的draw()方法
至此已经可以看见一个三角形了
4.使用投影
上边的三角形是变形的,我们要想看到与绘制坐标完全一样的形状,这里就用到了投影与相机视图。在OpenGL ES环境中,投影和相机视图允许您以更接近您用眼睛看物体的方式显示绘制对象,这种物理观察的模拟是通过绘制物体坐标的数学变换完成的。
Projection投影 - 基于GLSurfaceView的宽高来调整绘制对象的坐标。如果没有这个计算,对象在宽高不等比的视图中会出现变形。一个投影转换通常在视图比例被创建或者通过onSurfaceChanged()修改比例时才会被重新计算。
Camera View 摄像视图 - 基于一个虚拟的摄像点来调整绘制对象的坐标。OpenGL ES没有定义一个真实的摄像对象,只是提供一些方法来模拟一个摄像头。
a.定义投影
使用Matrix.frustumM方法计算投影矩阵
// mMVPMatrix is an abbreviation for "Model View Projection Matrix"
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
//frustumM表示生成一个透视投影矩阵,填充到mProjectionMatrix中,参数意义如下
//投影矩阵,偏移量,*面的left,*面的right ,*面的bottom ,*面的top,*面与视点的距离,远平面与视点的距离
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
b.定义相机视图
使用 Matrix.setLookAtM()方法来计算相机视图矩阵,然后使投影矩阵与相机视图矩阵相乘,然后将组合的变换矩阵传递给绘制的形状。 Matrix.multiplyMM用来矩阵乘法
@Override
public void onDrawFrame(GL10 unused) {
...
// Set the camera position (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
// Draw shape
mTriangle.draw(mMVPMatrix);
}
c.应用投影和相机矩阵变换
首先修改之前的顶点着色器,主要添加一个矩阵uMVPMatrix变量,使uMVPMatrix * vPosition赋值给最终结果。
private final String vertexShaderCode =
// This matrix member variable provides a hook to manipulate
// the coordinates of the objects that use this vertex shader
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
// the matrix must be included as a modifier of gl_Position
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = uMVPMatrix * vPosition;" +
"}";
// Use to access and set the view transformation
private int mMVPMatrixHandle;
之前的draw方法添加参数 draw(float[] mvpMatrix),然后就是添加一些传递矩阵对象给顶点着色器的操作了,draw方法中添加如下前两行代码
public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix
...
// get handle to shape's transformation matrix
@@*@*mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");@*@*
// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
应用了投影和相机视图转换后,图形的确按照正确的比例绘制出来了。如下
5.总结
Applying Projection and Camera Views
1.初始化GLSurfaceView
2.实现GLSurfaceView.Renderer
public void onSurfaceCreated(GL10 gl, javax.microedition.khronos.egl.EGLConfig config)
public void onDrawFrame(GL10 unused)
public void onSurfaceChanged(GL10 unused, int width, int height);
3.GLSurfaceView构造函数内初始化自定义GLSurfaceView.Renderer
4.定义形状
OpenGl三维坐标系,使用三个float类型表示一个顶点。然后使用数组定义颜色,分别表示RGBA。
5.定义顶点着色器片段着色器,编译GLSL,添加到OpenGL ES程序对象,链接程序对象。
6.使用投影与相机视图,计算变化矩阵
7.把定义的顶点信息,颜色信息,变换矩阵传递给OpenGL程序对象。
8.开始画图
参考
https://developer.android.com/training/graphics/opengl/index.html