先看看我们的整个流程:
理解坐标系:
左侧是Opengl默认的坐标系,右边是典型的android设备屏幕的坐标系。左侧的瘦瘦的三角形映射到android屏幕上就变成了胖胖的三角形(屏幕横向的时候),我们可以使用
camera和投影解决这个问题,具体怎么解决这里就先不累述了。这里我们只需要知道屏幕的左上角是(-1.0,1.0)横向向右为X轴正向,纵向向下为Y轴
负向,其范围都是从 -1到 +1。
定义三角形顶点:
我们在第一个android的小demo的工程里新建一个包shape,然后新建一个类Triangle 。然后我们在该类中定义三角形的三个顶点的数据:
此时该类如下(Triangle.java):
package com.cumt.shape;
import android.content.Context;
public class Triangle {
private Context context;
// 数组中每个顶点的坐标数
static final int COORDS_PER_VERTEX = 2;
// 每个顶点的坐标数 X , Y
static float triangleCoords[] = { 0.0f, 0.5f , // top
-0.5f, -0.5f , // bottom left
0.5f, -0.5f }; // bottom right
public Triangle(Context context){
this.context = context;
}
}
我们在该类中定义的float类型的数据并不能直接被opengl使用,float[ ]是属于虚拟机环境的,而Opengl作为本地系统库直接运行在硬件上,所以我们需要将float[ ] 转化为
FloatBuffer以使数据可以被opengl使用,此时该类如下(Triangle.java ):
package com.cumt.shape;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import android.content.Context;
public class Triangle {
private Context context;
private static final int BYTES_PER_FLOAT = 4;
private FloatBuffer vertexBuffer;
// 数组中每个顶点的坐标数
static final int COORDS_PER_VERTEX = 2;
// 每个顶点的坐标数 X , Y
static float triangleCoords[] = { 0.0f, 0.5f , // top
-0.5f, -0.5f , // bottom left
0.5f, -0.5f }; // bottom right
public Triangle(Context context){
this.context = context;
vertexBuffer = ByteBuffer
.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
// 把坐标们加入FloatBuffer中
vertexBuffer.put(triangleCoords);
// 设置buffer,从第一个坐标开始读
vertexBuffer.position(0);
}
}
编写着色器:
着色器语言(shading language)是opengl es2.0 比opengl es 1.X 新增的内容,使我们更加自由的实现自己想要的效果,该语言基于C/C++。该语言的具体类型等等这里不详细说明。
我们需要两个着色器——顶点着色器和片段着色器,顾名思义顶点着色器是用于处理顶点坐标的(不要与屏幕的像素点混淆),而片段着色器用于处理片段,所谓片段简单理解就是顶点着色器处理后的一堆顶点形成的片段。
现在我们来编写这两个顶点着色器:在 res 目录下新建一个文件夹 raw 在该文件夹下新建一个文件simple_vertex_shader.glsl ,其内容如下:
attribute vec4 a_Position;
void main()
{
gl_Position = a_Position;
}
gl_Position即opengl定义的顶点的坐标,我们目的就是通过这个来告诉opengl我们的顶点数据。vec4是着色器语言中的向量类型的一种,包含了四个浮点数的向量。
接下来在raw文件夹内新建文件simple_fragment_shader.glsl ,其内容如下:
precision mediump float;
uniform vec4 u_Color;
void main()
{
gl_FragColor = u_Color;
}
这里我们只传入一个颜色信息。这里注意一下 上面顶点着色器的 限定符attribute 和uniform 。attribute 一般用于每个顶点各不相同的量,如顶点位置等,后者一般用于
同一组顶点组成的相同的量,如光源位置,一组颜色等。
编译着色器及绘制:
接下来我们进行最后一步,编译着色器以及绘制三角形,这里我们来写一个工具类用于加载着色器程序以及编译着色器。
首先新建一个包utils ,此时目录结构如下图所示:
在utils包下新建类 LoggerConfig 用于管理我们的日志(是否输出Logcat的信息),代码如下(LoggerConfig.java):
package com.cumt.utils;
public class LoggerConfig {
public static final boolean ON = true;
}
再在该包下新建类ShaderHelper用于加载着色器程序以及编译着色器:
package com.cumt.utils;
import static android.opengl.GLES20.GL_COMPILE_STATUS;
import static android.opengl.GLES20.GL_FRAGMENT_SHADER;
import static android.opengl.GLES20.GL_LINK_STATUS;
import static android.opengl.GLES20.GL_VALIDATE_STATUS;
import static android.opengl.GLES20.GL_VERTEX_SHADER;
import static android.opengl.GLES20.glAttachShader;
import static android.opengl.GLES20.glCompileShader;
import static android.opengl.GLES20.glCreateProgram;
import static android.opengl.GLES20.glCreateShader;
import static android.opengl.GLES20.glDeleteProgram;
import static android.opengl.GLES20.glDeleteShader;
import static android.opengl.GLES20.glGetProgramInfoLog;
import static android.opengl.GLES20.glGetProgramiv;
import static android.opengl.GLES20.glGetShaderInfoLog;
import static android.opengl.GLES20.glGetShaderiv;
import static android.opengl.GLES20.glLinkProgram;
import static android.opengl.GLES20.glShaderSource;
import static android.opengl.GLES20.glValidateProgram;
import android.util.Log;
public class ShaderHelper {
private static final String TAG = "ShaderHelper";
/**
* 加载并编译顶点着色器,返回得到的opengl id
* @param shaderCode
* @return
*/
public static int compileVertexShader(String shaderCode) {
return compileShader(GL_VERTEX_SHADER, shaderCode);
}
/**
* 加载并编译片段着色器,返回opengl id
* @param shaderCode
* @return
*/
public static int compileFragmentShader(String shaderCode) {
return compileShader(GL_FRAGMENT_SHADER, shaderCode);
}
/**
* 加载并编译着色器,返回opengl id
* @param type
* @param shaderCode
* @return
*/
private static int compileShader(int type, String shaderCode) {
// 建立新的着色器对象
final int shaderObjectId = glCreateShader(type);
if (shaderObjectId == 0) {
if (LoggerConfig.ON) {
Log.w(TAG, "不能创建新的着色器.");
}
return 0;
}
// 传递着色器资源代码.
glShaderSource(shaderObjectId, shaderCode);
//编译着色器
glCompileShader(shaderObjectId);
// 获取编译的状态
final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS,
compileStatus, 0);
if (LoggerConfig.ON) {
//打印log
Log.v(TAG, "代码编译结果:" + "\n" + shaderCode
+ "\n:" + glGetShaderInfoLog(shaderObjectId));
}
// 确认编译的状态
if (compileStatus[0] == 0) {
// 如果编译失败,则删除该对象
glDeleteShader(shaderObjectId);
if (LoggerConfig.ON) {
Log.w(TAG, "编译失败!.");
}
return 0;
}
// 返回着色器的opengl id
return shaderObjectId;
}
/**
* 链接顶点着色器和片段着色器成一个program
* 并返回这个pragram的opengl id
* @param vertexShaderId
* @param fragmentShaderId
* @return
*/
public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
// 新建一个program对象
final int programObjectId = glCreateProgram();
if (programObjectId == 0) {
if (LoggerConfig.ON) {
Log.w(TAG, "不能新建一个 program");
}
return 0;
}
// Attach the vertex shader to the program.
glAttachShader(programObjectId, vertexShaderId);
// Attach the fragment shader to the program.
glAttachShader(programObjectId, fragmentShaderId);
// 将两个着色器连接成一个program
glLinkProgram(programObjectId);
// 获取连接状态
final int[] linkStatus = new int[1];
glGetProgramiv(programObjectId, GL_LINK_STATUS,
linkStatus, 0);
if (LoggerConfig.ON) {
// Print the program info log to the Android log output.
Log.v(
TAG,
"Results of linking program:\n"
+ glGetProgramInfoLog(programObjectId));
}
// 验证连接状态
if (linkStatus[0] == 0) {
// If it failed, delete the program object.
glDeleteProgram(programObjectId);
if (LoggerConfig.ON) {
Log.w(TAG, "连接 program 失败!.");
}
return 0;
}
// Return the program object ID.
return programObjectId;
}
/**
* Validates an OpenGL program. Should only be called when developing the
* application.
*/
public static boolean validateProgram(int programObjectId) {
glValidateProgram(programObjectId);
final int[] validateStatus = new int[1];
glGetProgramiv(programObjectId, GL_VALIDATE_STATUS,
validateStatus, 0);
Log.v(TAG, "Results of validating program: " + validateStatus[0]
+ "\nLog:" + glGetProgramInfoLog(programObjectId));
return validateStatus[0] != 0;
}
/**
* /**
* 编译,连接 ,返回 program 的 ID
* @param vertexShaderSource
* @param fragmentShaderSource
* @return
*/
public static int buildProgram(String vertexShaderSource,
String fragmentShaderSource) {
int program;
// Compile the shaders.
int vertexShader = compileVertexShader(vertexShaderSource);
int fragmentShader = compileFragmentShader(fragmentShaderSource);
// Link them into a shader program.
program = linkProgram(vertexShader, fragmentShader);
if (LoggerConfig.ON) {
validateProgram(program);
}
return program;
}
}
上面整个过程如图所示:

以后我们就可以很方便的一直使用该工具类来加载编译着色器了。只需要调用buildProgram,传入两个着色器的String文本就可以获得program的id是不是很方便。
我们似乎遗忘了什么,对的我们的两个着色器的文本内容该怎么活的呢,我们再在该目录下定义一个工具类TextResourceReader类来获得文本的内容,返回String类型
代码如下(TextResourceReader.java):
package com.cumt.utils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import android.content.Context;
import android.content.res.Resources;
public class TextResourceReader {
/**
* Reads in text from a resource file and returns a String containing the
* text.
*/
public static String readTextFileFromResource(Context context,
int resourceId) {
StringBuilder body = new StringBuilder();
try {
InputStream inputStream =
context.getResources().openRawResource(resourceId);
InputStreamReader inputStreamReader =
new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
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();
}
}
OK ,接下来就让我们来使用者两个工具类获得program的id吧,先回到 前面定义的Triangle 类:
我们来定义一个私有方法getProgram 并在构造函数中调用 ,此时代码如下 (Triangle.java):
package com.cumt.shape;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import com.cumt.openglestwo_test_one.R;
import com.cumt.utils.ShaderHelper;
import com.cumt.utils.TextResourceReader;
import android.content.Context;
import android.opengl.GLES20;
public class Triangle {
private Context context;
private static final int BYTES_PER_FLOAT = 4;
private FloatBuffer vertexBuffer;
// 数组中每个顶点的坐标数
static final int COORDS_PER_VERTEX = 2;
// 每个顶点的坐标数 X , Y
static float triangleCoords[] = { 0.0f, 0.5f , // top
-0.5f, -0.5f , // bottom left
0.5f, -0.5f }; // bottom right
private int program;
public Triangle(Context context){
this.context = context;
vertexBuffer = ByteBuffer
.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
// 把坐标们加入FloatBuffer中
vertexBuffer.put(triangleCoords);
// 设置buffer,从第一个坐标开始读
vertexBuffer.position(0);
getProgram();
}
//获取program的id
private void getProgram(){
//获取顶点着色器文本
String vertexShaderSource = TextResourceReader
.readTextFileFromResource(context, R.raw.simple_vertex_shader);
//获取片段着色器文本
String fragmentShaderSource = TextResourceReader
.readTextFileFromResource(context, R.raw.simple_fragment_shader);
//获取program的id
program = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource);
GLES20.glUseProgram(program);
}
}
至此我们已经胜利在望了,接下来思考一下,我们应该做哪些工作?显然我们需要将定义的数据传入着色器中来使用。详细的步骤见下面的代码(Triangle.java):
package com.cumt.shape;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import com.cumt.openglestwo_test_one.R;
import com.cumt.utils.ShaderHelper;
import com.cumt.utils.TextResourceReader;
import android.content.Context;
import android.opengl.GLES20;
public class Triangle {
private Context context;
private static final int BYTES_PER_FLOAT = 4;
private FloatBuffer vertexBuffer;
//---------第四步:定义坐标元素的个数,这里有三个顶点
private static final int POSITION_COMPONENT_COUNT = 3;
// 数组中每个顶点的坐标数
static final int COORDS_PER_VERTEX = 2;
// 每个顶点的坐标数 X , Y
static float triangleCoords[] = { 0.0f, 0.5f , // top
-0.5f, -0.5f , // bottom left
0.5f, -0.5f }; // bottom right
private int program;
//------------第一步 : 定义两个标签,分别于着色器代码中的变量名相同,
//------------第一个是顶点着色器的变量名,第二个是片段着色器的变量名
private static final String A_POSITION = "a_Position";
private static final String U_COLOR = "u_Color";
//------------第二步: 定义两个ID,我们就是通ID来实现数据的传递的,这个与前面
//------------获得program的ID的含义类似的
private int uColorLocation;
private int aPositionLocation;
public Triangle(Context context){
this.context = context;
vertexBuffer = ByteBuffer
.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
// 把坐标们加入FloatBuffer中
vertexBuffer.put(triangleCoords);
// 设置buffer,从第一个坐标开始读
vertexBuffer.position(0);
getProgram();
//----------第三步: 获取这两个ID ,是通过前面定义的标签获得的
uColorLocation = GLES20.glGetUniformLocation(program, U_COLOR);
aPositionLocation = GLES20.glGetAttribLocation(program, A_POSITION);
//---------第五步: 传入数据
GLES20.glVertexAttribPointer(aPositionLocation, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false, 0, vertexBuffer);
GLES20.glEnableVertexAttribArray(aPositionLocation);
}
//获取program
private void getProgram(){
//获取顶点着色器文本
String vertexShaderSource = TextResourceReader
.readTextFileFromResource(context, R.raw.simple_vertex_shader);
//获取片段着色器文本
String fragmentShaderSource = TextResourceReader
.readTextFileFromResource(context, R.raw.simple_fragment_shader);
//获取program的id
program = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource);
GLES20.glUseProgram(program);
}
//----------第七步:绘制
public void draw(){
GLES20.glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, POSITION_COMPONENT_COUNT);
}
}
最后我们只需要在我们前面定义的MyRender中使用即可,此时MyRender代码(这里在构造函数中引入了Context上下文环境,因为需要我们的Triangle对象的构造函数需要Context),
如下(MyRender.java ):
package com.cumt.render;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import com.cumt.shape.Triangle;
import android.content.Context;
import android.opengl.GLSurfaceView.Renderer;
import android.util.Log;
import static android.opengl.GLES20.glClear;
import static android.opengl.GLES20.glClearColor;
import static android.opengl.GLES20.glViewport;
import static android.opengl.GLES20.GL_COLOR_BUFFER_BIT;
public class MyRender implements Renderer {
private Context context;
public MyRender(Context context){
this.context = context;
}
//定义三角形对象
Triangle triangle;
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.w("MyRender","onSurfaceCreated");
// TODO Auto-generated method stub
//First:设置清空屏幕用的颜色,前三个参数对应红绿蓝,最后一个对应alpha
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
triangle = new Triangle(context);
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.w("MyRender","onSurfaceChanged");
// TODO Auto-generated method stub
//Second:设置视口尺寸,即告诉opengl可以用来渲染的surface大小
glViewport(0,0,width,height);
}
public void onDrawFrame(GL10 gl) {
Log.w("MyRender","onDrawFrame");
// TODO Auto-generated method stub
//Third:清空屏幕,擦除屏幕上所有的颜色,并用之前glClearColor定义的颜色填充整个屏幕
glClear(GL_COLOR_BUFFER_BIT);
//绘制三角形
triangle.draw();
}
}
运行结果如下: