首先当然是创建Android项目,你可以选择最新的Android Studio也可以选择eclipse都是一样的。我们重点讲解开发OpenGL ES的流程
1.定义顶点着色器和片段着色器
第一节我们讲解的已经很细致了,为了便于理解在这里在详细的说明一下。并且换一种方式定义着色器。
我们知道第一篇定义的顶点的坐标和颜色是分开的,这样可以但如果把它们放在一起会方便许多。
假设我们要绘制一个长方形和两条直线,二个定点,我们就需要这样来定义这个数组。
float[] tableVerticesWithTriangles = { //两个三角形和三角形的颜色分量 0f, 0f, 1f, 1f, 1f, -0.5f, -0.5f, 0.7f, 0.7f, 0.7f, 0.5f, -0.5f, 0.7f, 0.7f, 0.7f, 0.5f, 0.5f, 0.7f, 0.7f, 0.7f, -0.5f, 0.5f, 0.7f, 0.7f, 0.7f, -0.5f, -0.5f, 0.7f, 0.7f, 0.7f, //两条直线和直线的颜色分量 -0.5f, 0f, 1f, 0f, 0f, 0.5f, 0f, 1f, 0f, 0f, //两个顶点和顶点的颜色分量 0f, -0.25f, 0f, 0f, 1f, 0f, 0.25f, 1f, 0f, 0f };
前面的两个顶点代表坐标,后面三个顶点代表颜色分别为:红色,绿色和蓝色。
接着必须对应的建立对应的顶点着色器,假设raw文件夹下的顶点着色器的文件名是
simple_vertex_shader.glsl: attribute vec4 a_Position; attribute vec4 a_Color; varying vec4 v_Color; void main() { v_Color=a_Color; gl_Position = a_Position; gl_PointSize = 10.0; }
我们加入了一个新的属性a_Color,也加入了一个叫做v_Color的新varying。上一篇已经讲过varying是一个特殊的变量类型,它把给它的值进行混合并把这些混合的值发送给片段着色器。
我们把varying也加入片段着色器,在raw文件夹下创建simple_fragment_shader.glsl:
precision mediump float; varying vec4 v_Color; void main() { gl_FragColor = v_Color; }
我们用varying变量v_Color替换了原来代码中的uniform。如果这个片段属于一条直线,那个OpenGL就会用构成那条直线的两个顶点计算其混合后的颜色。
2.加载着色器
在项目中创建一个新的Java源代码包,命名为util把,至于前缀得看你项目的名称或者你自己的爱好。
在这个包下创建一个名为“TextResourceReader”的新类
public class TextResourceReader { public static String readTextFileFromResource(Context context, int resourceId) { StringBuilder body = new StringBuilder(); try { InputStream is = context.getResources().openRawResource(resourceId); InputStreamReader reader = new InputStreamReader(is); BufferedReader bufferedReader = new BufferedReader(reader); String nextLine; while ((nextLine = bufferedReader.readLine()) != null) { body.append(nextLine); body.append("\n"); } } catch (IOException e) { throw new RuntimeException( "Could not open resource: " + resourceId, e); } catch (Resources.NotFoundException nfe) { throw new RuntimeException("Resource not found: " + resourceId, nfe); } return body.toString(); } }
至于这段代码我就不过多的解释了这属于JAVA基础也可以说是Android基础,本文重点讲解OpenGL,这段代码的作用是加载着色器。
3.初始化OpenGL
定义两个成员变量:
private GLSurfaceView glSurfaceView; private boolean rendererSet=false;
在Activity的OnCreate()方法里面初始化glSurfaceView:
this.glSurfaceView = new GLSurfaceView(this);
检查系统是否支持OpenGL ES 2.0:
final ActivityManager activityManager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE); final ConfigurationInfo configurationInfo=activityManager.getDeviceConfigurationInfo(); final boolean supportsEs2=configurationInfo.reqGlEsVersion>=0x20000;
首先我们需要Android ActivityManager的一个引用,用它来获取设备的配置信息,然后,取出reqGlEsVersion变量检查OpenGL ES版本号。如果版本号为0*20000或后续版本,我们就可以使用OpenGL ES2.0的API了。
为OpenGL ES2.0配置渲染表面
if(supportsEs2){ this.glSurfaceView.setEGLContextClientVersion(2); this.glSurfaceView.setRenderer(new LYJRenderer(this)); this.rendererSet=true; }else{ Toast.makeText(this,"bu zhi chi gai banben ",Toast.LENGTH_SHORT).show(); return; }
如果设备支持OpenGL ES2.0,我们就通过调用setEGLContextClientVersion(2)配置这个surface视图,然后调用setReader()传进自定义的Renderer类的一个新实例,其实如果设备不支持OpenGL ES2.0,公开发布的应用在这个设备的应用程序市场中被隐藏起来,至于隐藏,后续讲到,当然这也是清单文件的基础知识。rendererSet记住GLSurfaceView是否处于有效状态。
setContentView(this.glSurfaceView);
相信大家都知道上面的作用,就是把GLSurfaceView加入到Activity中。并把它显示到屏幕上。
当然我们还需要利用Activity生命周期释放资源,如果没有下面的代码,应用程序可能会崩溃。
@Override protected void onResume() { super.onResume(); if(this.rendererSet){ this.glSurfaceView.onResume(); } } @Override protected void onPause() { super.onPause(); if(this.rendererSet){ this.glSurfaceView.onPause(); } }
创建Renderer类
让我们看一下这个接口的方法:
onSurfaceCreated(GL10 glUnused,EGLConfig config)
当Surface被创建的时候,GLSurfaceView会调用这个方法;这发生在应用程序第一次运行的时候,并且,当设备被唤醒或者用户从其他Activity切换回来时,这个方法可能会被调用。在实践中,这意味着,当应用程序运行时,本方法可能会被调用多次。
onSurfaceChanged(GL10 glUnused,int width,int height)
在Surface被创建后,每次Surface尺寸变化时,这个方法都会被GLSurfaceView调用到,在横屏,竖屏来回切换的时候,Surface尺寸会发生变化。
onDrawFrame(GL10 glUnused)
当绘制一帧时,这个方法会被GLSurfaceView调用。在这个方法中,我们一定要绘制一些东西,即使只是清空屏幕;因为,在这个方法返回后,渲染缓冲区会被交换并显示在屏幕上,如果什么都没画,可以会看到糟糕的闪烁效果。
观察这些方法,可能细心的会发现都有一个GL10参数,这是OpenGL1.0遗留下来的,如果在1.0的设备上就会用到,当时对于OpenGL ES2.0,GLES20类提供了静态方法存取。
新建渲染器LYJRenderer:
public class LYJRenderer implements GLSurfaceView.Renderer { public LYJRenderer(Context context) { this.context = context; this.vertexData = ByteBuffer.allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer(); this.vertexData.put(tableVerticesWithTriangles); } public void onSurfaceCreated(GL10 gl, EGLConfig config) { GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); } public void onSurfaceChanged(GL10 gl, int width, int height) { GLES20.glViewport(0, 0, width, height); } public void onDrawFrame(GL10 gl) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); } }
首先调用在onSurfaceCreated调用glClearColor设置清空屏幕,前三个参数对应颜色,后一个参数是透明度。这里设置为黑色。
下一步就是设置视口的尺寸了也就是glViewport,这就是告诉OpenGL可以用来渲染的surface的大小。
在onDrawFrame中调用glClear清空屏幕,这会擦除屏幕上所有颜色,并调用之前的glClearColor()调用定义的颜色填充整个屏幕。
在Renderer类中读入着色器,在onSurfaceCreated()的结尾除加入如下代码:
String vertexShaderSource = TextResourceReader.readTextFileFromResource(this.context, R.raw.simple_vertex_shader); String fragmentShaderSource = TextResourceReader.readTextFileFromResource(this.context, R.raw.simple_fragment_shader);
4.编译着色器
我们把着色器源码从文件中读取出来,下一步就是编译每个着色器了。我们要创建一个新的辅助类,它可以创建新的OpenGL着色器对象,编译着色器并且返回代表那段着色器的对象。一旦写出这个样板代码,在未来的项目中可以重用了。
创建一个名为ShaderHelper类,并添加如下代码:
public class ShaderHelper { public static final String TAG = "ShaderHelper"; public static int compileVertexShader(String shaderCode) { return compileShader(GLES20.GL_VERTEX_SHADER, shaderCode); } public static int compileFragmentShader(String shaderCode) { return compileShader(GLES20.GL_FRAGMENT_SHADER, shaderCode); } public static int compileShader(int type, String shaderCode) { } }
这些代码作为辅助类的基础。下面就要创建新的着色器对象并检查创建是否成功,下面都是在构建compileShader方法。
final int shaderObjectId = GLES20.glCreateShader(type); if (shaderObjectId == 0) { if (LoggerConfig.ON) { Log.w(TAG, "Counld not create new shader"); } return 0; }
这里,用glCreateShader()调用创建了一个新的着色器对象,并把这个对象的ID存入变量shaderObjectId。这个type可以代表定点着色器的GL_VERTEX_SHADER,或者是代表片段着色器的GL_FRAGMENT_SHADER。剩下的代码也用同样的方式。
记住我们是如何创建对象并检查它是否有效的;这个模式将在OpenGL里广泛使用:
1.首先使用一个如glCreateShader()一样的调用创建一个对象,这个调用会返回一个整型值。
2.这个整型值就是OpenGL对象的一个引用。无论后面什么时候要引用这个对象,就要把这个整型值传回 OpenGL。
3.返回值0表示这个对象创建失败,它类似于Java代码返回的null值。
上传和编译着色器源代码
GLES20.glShaderSource(shaderObjectId, shaderCode);
这个调用是告诉OpenGL读入字符串shaderCode定义的源代码,并把它与shaderObjectId所引用的着色器对象关联起来。然后调用下面方法编译着色器:
GLES20.glCompileShader(shaderObjectId);
这个调用告诉OpenGL编译上传到shaderObjectId的源代码。
取出编译状态,加入如下代码:
final int[] compileStatus = new int[1]; GLES20.glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
为了检查编译失败还是成功,首先要创建新的长度为1的int数组,称为compileStatus;然后调用glGetShaderiv。这就告诉OpenGL读取与shaderObjectId关联的编译状态,并把它写入compileStatus的第0个元素。
这是Android平台上的OpenGL的另一个通用模式。 为了取出一个值,我们通常会使用一个长度为1的数组,并把这个数组传进OpenGL调用。在一个调用中,我们告诉OpenGL把结果存进数组的第一个元素。
验证编译状态并返回着色器对象ID,代码如下:
if (compileStatus[0] == 0) { GLES20.glDeleteShader(shaderObjectId); if (LoggerConfig.ON) { Log.w(TAG, "Compilation of shader failed"); } return 0; }
如果编译成功返回shaderObjectId:
return shaderObjectId;
在Renderer类中编译着色器,在OnSurfaceView()结尾处加入如下代码:
int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource); int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);