首先收藏这个
OpenGL of CSDN
建立OpenGL ES环境
为了能在你的Android应用中使用OpenGLES绘画,你必须创建一个view作为容器。而最直接的方式就是从
GLSurfaceView和
GLSurfaceView.Renderer分别派生一个类。
GLSurfaceView作为OpenGL绘制所在的容器,而实际的绘图动作都是在
GLSurfaceView.Renderer里面发生的。
使用
GLSurfaceView几乎是整合OpenGLES到你的应用的唯一方式。对于一个全屏或近全屏的graphicsview,它是最好的选择。如果只是在某个小部分显示OpenGLES图形则可以考虑
TextureView。当然如果你比较变态,你完全可以使用
SurfaceView创建一个OpenGLES view。
本教程演示如何完成一个最小实现的OpenGLES2.0应用。
在Manifest中声明使用OpenGLES
为了能使用OpenGLES 2.0 API,你必须在你的manifest中添加以下声明:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
如果你的应用要使用纹理压缩功能,你必须还要声明设备需要支持什么样的压缩格式:
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" /><supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
为OpenGLES Graphics创建一个Activity
这个Activity与其它类型应用的Activity并无不同,要说不同,也仅仅是放到Activity的layout的View不一样,你需要放入的是一个
GLSurfaceView。
下面的代码演示了使用
GLSurfaceView作为主视图的Acitivity的最少代码实现:
public class OpenGLES20 extends Activity {
private GLSurfaceView mGLView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//创建一个GLSurfaceView实例然后设置为activity的ContentView.
mGLView = new MyGLSurfaceView(this);
setContentView(mGLView);
}
}
注:OpenGL ES 2.0需要Android2.2 (API Level 8) 及以上版本。
构键一个GLSurfaceView对象
GLSurfaceView中其实不需要做太多工作,实际的绘制任务都在
GLSurfaceView.Renderer中了。所以
GLSurfaceView中的代码也非常少,你甚至可以直接使用
GLSurfaceView,但是,然而,别这样做。因为你需要扩展这个类来响应触摸事件啊孩子。
扩展
GLSurfaceView的类像这样写就可以了:
class MyGLSurfaceView extends GLSurfaceView {
public MyGLSurfaceView(Context context) {
super(context);
//设置Renderer到GLSurfaceView
setRenderer(new MyRenderer());
}
}
当使用OpenGLES 2.0时,你必须在
GLSurfaceView构造器中调用另外一个函数,它说明了你将要使用2.0版的API:
// 创建一个OpenGL ES 2.0 contextsetEGLContextClientVersion(2);
另一个可以添加的你的
GLSurfaceView实现的可选的操作是设置render模式为只在绘制数据发生改变时才绘制view。使用
GLSurfaceView.RENDERMODE_WHEN_DIRTY:
// 只有在绘制数据改变时才绘制viewsetRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
此设置会阻止绘制
GLSurfaceView的帧,直到你调用了
requestRender(),这样会非常高效。
构建一个Renderer类
此类控制向
GLSurfaceView的绘制工作。它有三个方法被Android系统调用来计算在
GLSurfaceView上画什么以及如何画。
·
onSurfaceCreated()- 仅调用一次,用于设置view的OpenGLES环境。
·
·
onDrawFrame()- 每次View被重绘时被调用。
·
·
onSurfaceChanged()- 如果view的几和形状发生变化了就调用,例如当竖屏变为横屏时。
·
下面是一个OpenGLES renderer的最基本的实现,它仅在
GLSurfaceView上画了一个灰色的背景:
public class MyGL20Renderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
//设置背景的颜色
GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// 重绘背景色
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
}
以上就是所有需要做的东西!上面的代码们创建了一个简单的Android应用,它使用OpenGL显示了一个灰色的屏幕。但这段代码并没有做什么有趣的事情,只是为使用OpenGL绘图做好了准备。
注:你可以不明白为什么这些方法们都具有一个
GL10参数,但你使用的却是OpengGLES 2.0 API们。这其实是为了使Android框架能简单的兼容各OpenGLES版本而做的。
如果你对OpenGLES API很熟悉,你其实现在已经可以开始进行绘图了。然而,如果你熟悉,就继续学习下一章吧。
定义形状
会定义在OpenGLES view上所绘制的形状,是你创建高端图形应用杰作的第一步。如果你不懂OpenGLES定义图形对象的一些基本知识,使用OpenGLES可能有一点棘手。
本文解释OpenGLES相对于Android设备屏幕的坐标系统、定义一个形状的基础知识、形状的外观、以及如何定义三角形和正方形。
定义一个三角形
OpenGLEs允许你使用坐本在三个维度上定义绘制对象。所以,在你可以绘制一个三角形之前,你必须定义它的坐标。在OpenGL中,典型的方式是为坐标定义一个浮点类型的顶点数组。为了最高效,你应把这些坐标都写进一个
ByteBuffer,它会被传到OpenGLES图形管线以进行处理。
class Triangle {
private FloatBuffer vertexBuffer;
// 数组中每个顶点的坐标数
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = {
// 按逆时针方向顺序: 0.0f, 0.622008459f, 0.0f,
// top -0.5f, -0.311004243f, 0.0f,
// bottom left 0.5f, -0.311004243f, 0.0f
// bottom right
// 设置颜色,分别为red, green, blue 和alpha (opacity)
float color[] = {0.63671875f, 0.76953125f, 0.22265625f, 1.0f};
} ;
public Triangle() {
// 为存放形状的坐标,初始化顶点字节缓冲
ByteBuffer bb = ByteBuffer.allocateDirect(
// (坐标数 * 4)float占四字节
triangleCoords.length * 4);
// 设用设备的本点字节序
bb.order(ByteOrder.nativeOrder());
// 从ByteBuffer创建一个浮点缓冲
vertexBuffer = bb.asFloatBuffer();
// 把坐标们加入FloatBuffer中
vertexBuffer.put(triangleCoords);
// 设置buffer,从第一个坐标开始读
vertexBuffer.position(0);
}
}
缺省情况下,OpenGLES 假定[0,0,0](X,Y,Z) 是
GLSurfaceView 帧的中心,[1,1,0]是右上角,[-1,-1,0]是左下角。
注意此形状的坐标是按逆时针方向定义的。绘制顺序很重要,因为它定义了哪面是形状的正面,哪面是反面,使用OpenGLES 的cullface特性,你可以只画正面而不画反面。
定义一个正方形
在OpenGL中定义正方形是十分容易的,有很多方法能做的,但是典型的做法是使用两个三角形
你要为两个三角形都按逆时针方向定义顶点们,并且将这些坐标值们放入一个
ByteBuffer中。为了避免分别为两个三角形定义两个坐标数组,我们使用一个绘制列表来告诉OpenGLES图形管线如果画这些顶点们。下面就是这个形状的代码:
class Square {
private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;
// 每个顶点的坐标数
static final int COORDS_PER_VERTEX = 3;
static float squareCoords[] = {-0.5f, 0.5f, 0.0f,
// top left -0.5f, -0.5f, 0.0f,
// bottom left 0.5f, -0.5f, 0.0f,
// bottom right 0.5f, 0.5f, 0.0f };
// top right
private short drawOrder[] = {0, 1, 2, 0, 2, 3};
// 顶点的绘制顺序
public Square() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (坐标数 * 4)
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// 为绘制列表初始化字节缓冲
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (对应顺序的坐标数 * 2)short是2字节
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
本例让你见识了用OpenGL如何创建更复杂的形状。通常,你都是使用一群小三(三角形)来绘制对象。下一章,你将学会如何将这些形状画到屏幕上。
绘制形状
你定义了要绘制的形状后,你就要画它们了。使用OpenGLES 2.0会形状会有一点点复杂,因为API提供了大量的对渲染管线的控制能力。
本文讲解如何绘制你在前文中定义的那些形状们。
初始化形状
在你做任何绘制之前,你必须初始化形状然后加载它。除非形状的结构(指原始的坐标们)在执行过程中发生改变,你都应该在你的Renderer的方法
onSurfaceCreated()中进行内存和效率方面的初始化工作。
public void onSurfaceCreated (GL10 unused, EGLConfig config){
// 初始化一个三角形
mTriangle = new Triangle();
// 初始化一个正方形
mSquare = new Square();
}
绘制一个形状
使用OpenGLES 2.0画一个定义好的形状需要一大坨代码,因为你必须为图形渲染管线提供一大堆信息。典型的,你必须定义以下几个东西:
·
VertexShader-用于渲染形状的顶点的OpenGLES 图形代码。
·
·
FragmentShader-用于渲染形状的外观(颜色或纹理)的OpenGLES 代码。
·
·
Program-一个OpenGLES对象,包含了你想要用来绘制一个或多个形状的shader。
·
你至少需要一个vertexshader来绘制一个形状和一个fragmentshader来为形状上色。这些形状必须被编译然后被添加到一个OpenGLES program中,program之后被用来绘制形状。下面是一个展示如何定义一个可以用来绘制形状的基本shader的例子:
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;" + "}";
//Shader们包含了OpenGLShading Language (GLSL)代码,必须在使用前编译。要编译这些代码,在你的Renderer类中创建一个工具类方法:
public static int loadShader ( int type, String shaderCode){
// 创建一个vertex shader类型(GLES20.GL_VERTEX_SHADER)
// 或fragment shader类型(GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// 将源码添加到shader并编译之
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
为了绘制你的形状,你必须编译shader代码,添加它们到一个OpenGLES program 对象然后链接这个program。在renderer对象的构造器中做这些事情,从而只需做一次即可。
注:编译OpenGLES shader们和链接linkingprogram们是很耗CPU的,所以你应该避免多次做这些事。如果在运行时你不知道shader的内容,你应该只创建一次code然后缓存它们以避免多次创建。
public Triangle() {
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
mProgram = GLES20.glCreateProgram();
// 创建一个空的OpenGL ES Program
GLES20.glAttachShader(mProgram, vertexShader);
// 将vertex shader添加到program
GLES20.glAttachShader(mProgram, fragmentShader);
// 将fragment shader添加到program
GLES20.glLinkProgram(mProgram);
// 创建可执行的 OpenGL ES program
}
此时,你已经准备好增加真正的绘制调用了。需要为渲染管线指定很多参数来告诉它你想画什么以及如何画。因为绘制操作因形状而异,让你的形状类包含自己的绘制逻辑是个很好主意。
创建一个draw()方法负责绘制形状。下面的代码设置位置和颜色值到形状的vertexshader和fragmentshader,然后执行绘制功能。
public void draw() {
// 将program加入OpenGL ES环境中
GLES20.glUseProgram(mProgram);
// 获取指向vertex shader的成员vPosition的 handle
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 启用一个指向三角形的顶点数组的handle
GLES20.glEnableVertexAttribArray(mPositionHandle);
// 准备三角形的坐标数据
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);
// 获取指向fragment shader的成员vColor的handle
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// 设置三角形的颜色
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// 画三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用指向三角形的顶点数组
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
一旦你完成这些代码,画这个对象只需要在Renderer的
onDrawFrame()调用draw()方法。当你运行应用时,它应该看起来这样:
图.没有投射和视口时画三角形
此例子中的代码还有很多问题。首先,它不会给人留下印像。其次,三角形会在你从竖屏变为横屏时被压扁。三角形变形的原因是其顶点们没有跟据屏幕的宽高比进行修正。你可以使用投射和视口更正这个问题。那在下一讲了。
应用投影和相机视口
在OpenGLES环境中,投影和相机视口使你绘制的对象以更接近物理对象的样子显示。这是通过对坐标精确的数学变换实现的。
·
投影-这种变换跟据所在GLSurfaceView的宽和高调整对象的坐标。如果没有此变换,对象会被不规则的视口扭曲。投射变换一般只需要在OpenGLview创建或发生变化时调用,代码写在renderer的onSurfaceChanged()方法中。
·
·
相机视口-此变换基于一个虚拟相机的位置调整对象的坐标。注意OpenGLES并没有定义一个真的相机对象,而是提供了一些工具方法变换绘制对象的显示来模拟一个相机。一个相机视口的变换可能只在创建GLSurfaceView时调用,或跟据用户动作动态调用。
·
本文讲解了如何创建一个投影和一个相机视口然后应用到GLSurfaceView的形状绘制过程。
定义一个投影
投影变换的数据是在GLSurfaceView.Renderer 类的 onSurfaceChanged() 方法中计算。下面的例子跟据GLSurfaceView 的宽和高,使用Matrix.frustumM()方法计算出了一个投影变换Matrix:
@Override
public void onSurfaceChanged (GL10 unused,int width, int height){
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// 此投影矩阵在onDrawFrame()中将应用到对象的坐标
Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
以下代码产生了一个投影矩阵mProjMatrix ,你可以把它在 onDrawFrame() 方法中与一个相机视口变换结合。
注: 只对你的对象应用一个投影变换一般会导制什么也看不到。通常,你必须也对其应用一个视口变换才能看到东西。
定义一个相机视口
再定义一个相机视口变换以使对绘制对象的变换处理变得完整。在下面的例子中,使用方法Matrix.setLookAtM()计算相机视口变换,然后结合前面所计算的投影矩阵。结合后的变换矩阵之后传给要绘制的对象。
@Override
public void onDrawFrame (GL10 unused){
// 设置相机的位置(视口矩阵)
Matrix.setLookAtM(mVMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// 计算投影和视口变换
Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);
// 绘制形状
mTriangle.draw(mMVPMatrix);
}
应用投影和相机视口变换
为了使用前面的合并后的投影和相机视口变换矩阵,修改你的图形对象的方法draw(),接受结果矩阵并应用到你的形状上:
public void draw(float[] mvpMatrix) {
// 传递计算出的变换矩阵 ...
// 获得形状的变换矩阵的handle
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// 应用投影和视口变换
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
}
一旦你正确的计算和应用了投影和视口变换,你的图像将出现在正确的位置,看起来像下面这样:
图.应用了投影和视口变换后绘制的三角形
现在你拥有了一个正确显示你的形状的应用了,是让你的图形动起来的时候了...嘿嘿...
添加运动
在屏幕上绘制是OpenGL的基础能力,但是你也可以用其它的Android图形框架类来做,包括Canvas和Drawable。 但是OpenGL ES提供了另外的能力,可以在三维上移动和变换对象。总之它能创造很牛B的用户体验。在本文中,你将学会如何使用OpenGL ES为形状添加旋转功能。
转动一个形状
使用OpenGL ES 2.0旋转一个对象也是十分简单地。你创建另外一个变换矩阵(一个旋转矩阵)然后把它合并到你的投影和相机视口变换矩阵就行了:
private float[] mRotationMatrix = new float[16];
public void onDrawFrame (GL10 gl){
...
// 为三角形创建一个旋转变换
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);
// 把旋转矩阵合并到投影和相机矩阵
Matrix.multiplyMM(mMVPMatrix, 0, mRotationMatrix, 0, mMVPMatrix, 0);
// 画三角形
mTriangle.draw(mMVPMatrix);
}
如果你的三角形在此新后转不起来,则要查看是否把GLSurfaceView.RENDERMODE_WHEN_DIRTY 设置注释了,下面马上就讲到。
启用持续渲染
到现在,你应在代码中注释掉设置只在数据改变时才渲染的代码,否则,OpenGL 只有转一次然后等待直到GLSurfaceView 的包含者调用requestRender():
public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
// 注释掉以自动旋转
}
除非你不让对象与用户有交互,否则启用这个设置是一个好做法。要准备解除这句的注释了,因为下一讲会用到它。
响应触摸事件
使你的OpenGL ES应用能响应触摸的关键是扩展你实现的GLSurfaceView 代码,覆写onTouchEvent() 方法来监听触摸事件。
本文向你展示如何监听用户的触摸事件以使用户可以旋转某个OpenGL ES对象。
设置一个触摸监听器
为了使你的OpenGL Es应用响应触摸事件,你必须在你的GLSurfaceView 类中实现onTouchEvent()事件。下面的例子演示了如何监听MotionEvent.ACTION_MOVE 事件然后把它们转换成一个形状的旋转角度。
@Override
public boolean onTouchEvent (MotionEvent e){
// MotionEvent带有从触摸屏幕来的输入的详细信息以及其它输入控制
// 此处,你只需对触摸位置的改变感兴趣即可。
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mPreviousX;
float dy = y - mPreviousY;
// reverse direction of rotation above the mid-line
if (y > getHeight() / 2) {
dx = dx * -1;
}
// reverse direction of rotation to left of the mid-line
if (x < getWidth() / 2) {
dy = dy * -1;
}
mRenderer.mAngle += (dx + dy) * TOUCH_SCALE_FACTOR; // = 180.0f / 320
requestRender();
}
mPreviousX = x;
mPreviousY = y;
return true;
}
注意在计算完旋转角度之后,本方法调用requestRender() 来告诉renderer要渲染帧了。这样做是很高效的,因为在没有发生旋转时不需要重画帧。然而,在你没有要求只在数据发生改变才重画之前,还不能达到最高效,即别忘了解除这一句的注释:
public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
曝露出旋转角度
上面的例子要求你向其它类曝露出你的旋转角度,所以你要为你的renderer添加一个public成员。既然renderer的代码运行于主界面之外的单独线程中,你必须声明这个公开变量为volatile. 。下面的代码就是这样做的:
public class MyGLRenderer implements GLSurfaceView.Renderer {
public volatile float mAngle;
}
应用旋转
要应用触摸所产生的旋转, 注释掉产生角度的代码并且添加mAngle,它包活了触摸所产生的角度:
public void onDrawFrame (GL10 gl){
// Create a rotation for the triangle
// long time = SystemClock.uptimeMillis() % 4000L;
// float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);
// 合并旋转矩阵到投影和相机视口矩阵
Matrix.multiplyMM(mMVPMatrix, 0, mRotationMatrix, 0, mMVPMatrix, 0);
// 画一个角度
mTriangle.draw(mMVPMatrix);
}
当你完成了上述的几步,运行程序然后在陪同幕上拖动你的手指头,你会看到下面这样:
Figure . 跟据触摸输入的转动的三角形(圈圈显示了触摸的位置)。