首先当然是创建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);
5.把着色器一起链接进OpenGL的程序
既然我们已经加载并编译了一个顶点着色器和一个片段着色器,下一步就是把它们绑定在一起放入一个单个的程序里。
5.1理解OpenGL的程序:
简单来说,一个OpenGL程序就是把一个顶点着色器和一个片段着色器链接在一起变成单个对象。顶点着色器和片段着色器总是一起工作的。没有片段着色器,OpenGL就不知道怎么绘制那些组成的每个点,直线和三角形片段;如果没有顶点着色器,OpenGL就不知道在哪里绘制这些片段。
虽然顶点着色器和片段着色器总是要一起工作的,但并不意味着它们必须是一对一匹配的,我们可以同时在多个程序中使用同一个着色器。
让我们打开ShaderHelper,并在类的末尾加入如下代码:
public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
}
下面我们将构建这个方法。
新建程序并附着上着色器,我们要做的第一件事就是调用glCreateProgram()新建程序对象,并把那个对象的ID存进programObjectId。如下:
final int programObjectId = GLES20.glCreateProgram();
if (programObjectId == 0) {
if (LoggerConfig.ON) {
Log.w(TAG, "Could not create new Program");
}
return 0;
}
和上面的代码类似就不过多的解释了,下一步就是附上着色器:
GLES20.glAttachShader(programObjectId, vertexShaderId);
GLES20.glAttachShader(programObjectId, fragmentShaderId);
使用glAttachShader()方法把顶点着色器和片段着色器附加到程序对象上。
5.2链接程序
现在准备把这些着色器联合起来了,为此,将调用glLickProgram(programObjectId):
GLES20.glLinkProgram(programObjectId);
为了检查这个链接是成功还是失败,我们遵循编译着色器时所使用的步骤:
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linkStatus, 0);
最后验证链接状态并返回程序对象ID,代码如下:
if (linkStatus[0]==0) {
GLES20.glDeleteProgram(programObjectId);
if(LoggerConfig.ON){
Log.w(TAG,"linking of program failed");
}
return 0;
}
return programObjectId;
给渲染类LYJRenderer加入代码,先定义成员变量:
private int program;
然后在onSurfaceCreated()结尾处加入如下代码把着色器链接起来:
program = ShaderHelper.linkProgram(vertexShader, fragmentShader);
6.最后的拼接
6.1验证OpenGL程序的对象
在开始使用OpenGL的程序之前,我们首先应该验证一下它,看看这个程序对于当前的OpenGL状态是不是有效的。根据OpenGL ES2.0的文档,它也给OpenGL提供了一种方法让我们知道为什么当前程序可能是低效率的,无法运行的,等等。
让我们在ShaderHelper加入如下代码:
public static boolean validateProgram(int programObjectId){
GLES20.glValidateProgram(programObjectId);
final int[] validateStatus=new int[1];
GLES20.glGetProgramiv(programObjectId,GLES20.GL_VALIDATE_STATUS,validateStatus,0);
Log.v(TAG,GLES20.glGetProgramInfoLog(programObjectId));
return validateStatus[0]!=0;
}
这段代码与上面验证类似就不过多的阐述了,然后在onSurfaceView()结尾处加入如下代码:
ShaderHelper.validateProgram(program);
然后调用glUseProgram(program);
调用glUseProgram()告诉OpenGL在绘制任何东西到屏幕上的时候要使用这个定义的程序。
获取属性的位置
在使用属性之前我们要获取它们的位置。我们可以让OpenGL自动给这些属性分配位置编号,或者在着色器被链接到一起之前,可以通过调用glBindAttrribLocation()由我们自己给它们分配位置编号。我们要让OpenGL自动分配这些位置,因为它使代码容易管理。
在LYJRenderer顶部加入如下定义:
private static final String A_POSITION="a_Position";
private int aPositionLocation;
private static final int POSITION_COMPONENT_COUNT = 2;
一旦着色器被链接起来了,我们就只需要加入一些代码去获取属性位置。在onSurfaceCreated() 结尾处加入如下代码:
this.aPositionLocation = GLES20.glGetAttribLocation(program, A_POSITION);
调用glGetAttribLocation()获取属性的位置。 有了这个位置,就能告诉OpenGL到哪里去找到这个属性对应的数据了。
6.2关联属性与顶点数据的数组
下一部就是要告诉OpenGL到哪里找到属性a_Position对应的数据。
this.vertexData.position(0);
GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT, false, STRIDE, this.vertexData);
在我们告诉OpenGL从这个缓冲区中读取数据之前,需要确保它会从开头处开始读取数据,而不是中间或者结尾处。每个缓冲区都有一个内部指针,可以通过调用position(int)移动它,并且当OpenGL从缓冲区读取时,它会从这个位置开始读取。
下面是参数的解析:
aPositionLocation:这个是属性位置。
POSITION_COMPONENT_COUNT:这个属性有多少分量
GLES20.GL_FLOAT:这是数据的类型
false:只有使用整型数组时候,这个数据才有意义。
STRIDE:多于一个属性时候,就要告诉取下个数据要跳过多少分量。
vertexData:这个参数告诉OpenGL去哪里读取数据。
尽管我们已经把数据属性链接起来了,在开始绘制之前,我们还需要调用glEnableVertexAttribArray()使用这个属性。代码如下:
GLES20.glEnableVertexAttribArray(aPositionLocation);
同理颜色分量的代码如下:
private static final int COLOR_COMPONENT_COUNT=3;
private static final String A_COLOR = "a_Color";
private int aColorLocation;
private static final int STRIDE=(POSITION_COMPONENT_COUNT+COLOR_COMPONENT_COUNT)*BYTES_PER_FLOAT;
this.vertexData.position(POSITION_COMPONENT_COUNT);
GLES20.glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GLES20.GL_FLOAT, false, STRIDE, this.vertexData);
GLES20.glEnableVertexAttribArray(aColorLocation);
7.在屏幕上绘制
在onDrawFrame() 方法的结尾添加如下代码:
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 6);
GLES20.glDrawArrays(GLES20.GL_LINES, 6, 2);
GLES20.glDrawArrays(GLES20.GL_POINTS, 8, 1);
GLES20.glDrawArrays(GLES20.GL_POINTS, 9, 1);
第一个语句告诉OpenGL绘制三角扇形,什么是三角扇形后面讲解,第二个语句告诉OpenGL绘制直线,第三,四条语句告诉OpenGL绘制点。
这七个步骤就是开发OpenGL程序的基本流程。如图