我们总是对陌生人太客气,而对亲密的人太苛刻
上一篇文章中,我们已经将OpenGL ES环境搭建完成。接下来我们就可以开始我们的绘图之旅了。该篇我们讲解最基本图形三角形的绘制,这是一切绘制的基础。在OpenGL ES的世界里一切图形都可以由三角形拼接绘制而成。
在Android官方文档中也介绍了三角形的绘制,本文案例我们参照了官方文档,但也做了进一步的改进,希望你能通过本节对OpenGL ES绘制图形有一个初步的认识。
坐标系
在绘制图形前我们要了解OpenGL ES的坐标系,我们知道Android的坐标系左上角为原点[0,0],而OpenGL ES的坐标系如下图:
OpenGL ES的坐标系中原点[0,0]在屏幕的中心,无论屏幕是正方形还是长方形,四个点的坐标都如上图所示,也就是边长都是为2的正方形
聪明的同学可能已经有疑问了,OpenGL ES坐标系明显是按照屏幕是正方形来的,那么如果屏幕为长方形,我按照比例绘制图形肯定会变形的。这里给出肯定的回答:是的!至于怎么解决先放下。
接下来我们先画一个三角形,至于遇到的问题,我们一个个解决^_^
三角形绘制
- 设置要绘制图形的坐标和颜色数据
- 定义顶点着色器和片段着色器
- 创建Shader程序并链接编译好
- 绘制图形:使用Shader程序,将顶点、颜色数据传递到显存
1. 顶点颜色数据定义
在OpenGL ES中绘制图形必须先定义好坐标,确定图形的位置才能进行绘制。我们为坐标定义浮点数的顶点数组,然后我在构造方法中将浮点数组转化为ByteBuffer
,后续我们会将它传递到OpenGL ES图像管道进行处理。
public class Triangle {
// 顶点坐标缓冲区
private FloatBuffer vertexBuffer;
// 此数组中每个顶点的坐标数
static final int COORDS_PER_VERTEX = 3;
// 三角形三个点的坐标,逆时针绘制
static float triangleCoords[] = { // 坐标逆时针顺序
0.0f, 0.616f, 0.0f, // top
-0.5f, -0.25f, 0.0f, // bottom left
0.5f, -0.25f, 0.0f // bottom right
};
// 设置颜色为白色
float color[] = {1.0f, 1.0f, 1.0f, 1.0f};
public Triangle() {
// 初始化形状坐标的顶点字节缓冲区
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 中,形状的面是由三维或更多点定义的三维表面 空间。一组三个或更多个三维点(在 OpenGL 中称为顶点)具有一个正面以及一个背面。如何知道哪一面为正面,哪一面为背面呢?这个问题问得好! 答案与环绕或者定义形状点的方向有关
。
在此示例中,三角形的点按如上顺序定义,这也决定了他们是按逆时针的方向绘制,绘制这些坐标的顺序定义了形状的环绕方向。默认情况下,在OpenGL中,逆时针绘制的面是正面,而另外一面是背面。
为什么重要的是要知道形状的哪个面是正面?答案与OpenGL的常用功能有关,称为面部剔除。面部剔除是OpenGL环境的一个选项,它允许渲染管道忽略(不计算或绘制)形状的背面,从而节省时间,内存和处理周期:
// enable face culling feature
gl.glEnable(GL10.GL_CULL_FACE);
// specify which faces to not draw
gl.glCullFace(GL10.GL_BACK);
请务必按照逆时针绘制顺序定义 OpenGL 形状的坐标
2. 创建着色器代码
public class Triangle {
// 顶点着色器代码
private final String vertexShaderCode =
"attribute vec4 vPosition;\n" +
"void main() {\n" +
" gl_Position = vPosition;\n" +
"}\n";
// 片段着色器代码
private final String fragmentShaderCode =
"precision mediump float;\n" +
"uniform vec4 vColor;\n" +
"void main() {\n" +
" gl_FragColor = vColor;\n" +
"}\n";
...
}
变量名 | 说明 | 备注 |
---|---|---|
vPosition | 顶点坐标数据,我们第一步定义的顶点数据需要传给这个变量 | 该变量名可随意修改 |
gl_Position | Shader的内置变量,就是图形的顶点位置 | 该变量名不可修改 |
vColor | 图元(像素)的颜色,我们第一步定义的颜色需要传给这个变量 | 该变量名可随意修改 |
gl_FragColor | Shader的内置变量,图元颜色 | 该变量名不可修改 |
上面先简单介绍下变量的含义,后续我们会详细讲解GLSL语言。
3. 创建Shader程序并链接
我们需要对上面的着色器语言进行编译链接后,才能在OpenGL ES环境中使用。编译此代码,我们需要一个实用的方法:
public class GLESUtils {
/**
* 加载着色器代码
*
* @param type
* @param shaderCode
* @return
*/
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;
}
}
为了匹配Renderer
生命周期方法onSurfaceCreated
,我们在Triangle
中创建surfaceCreated
方法用来编译链接Shader程序,创建surfaceChanged
来更新OpenGL ES画布大小
public class Triangle() {
...
/**
* OpenGL ES程序句柄
*/
private int mProgram;
public void surfaceCreated() {
// 加载顶点着色器代码
int vertexShader = GLESUtils.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
// 加载片段着色器代码
int fragmentShader = GLESUtils.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// 创建空的OpenGL ES程序
mProgram = GLES20.glCreateProgram();
// 将顶点着色器添加到程序中
GLES20.glAttachShader(mProgram, vertexShader);
// 将片段着色器添加到程序中
GLES20.glAttachShader(mProgram, fragmentShader);
// 链接OpenGL ES程序
GLES20.glLinkProgram(mProgram);
}
public void surfaceChanged(int width, int height) {
// 设置OpenGL ES画布大小
GLES20.glViewport(0, 0, width, height);
}
}
4. 绘制图形
此时我们已经定义好了三角形的顶点坐标数据、片元颜色值、OpenGL ES着色器程序。接下来我们只需要将坐标数据、片元颜色值传递给着色器程序,然后执行绘制我们就可以将三角形画出来了。我们定义一个单独的方法draw
用来执行绘制三角形
4.1 将Shader程序添加到OpenGL ES环境
public class Triangle() {
public void draw() {
// 将程序添加到OpenGL ES环境
GLES20.glUseProgram(mProgram);
// 重新绘制背景色为黑色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
...
}
}
4.2 传递顶点坐标和片元颜色数据
我们要从java内存将数据传递给OpenGL ES显存环境中,需要获取Shader程序属性的句柄值
注意:获取属性句柄要和Shader程序中定义的属性变量名字一样。获取属性句柄后,我们操作属性句柄就可以将数据传递给对应的变量了。
public class Triangle() {
...
/**
* Shader程序中顶点属性的句柄
*/
private int positionHandle;
/**
* Shader程序中颜色属性的句柄
*/
private int colorHandle;
public void surfaceCreated() {
// 加载Shader程序代码
...
// 获取顶点着色器vPosition成员的句柄
positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 获取片段着色器vColor成员的句柄
colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
}
}
在draw
方法中设置顶点数据和颜色值
public class Triangle() {
...
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
public void draw() {
// 将程序添加到OpenGL ES环境
...
// 为三角形顶点启用控制柄
GLES20.glEnableVertexAttribArray(positionHandle);
// 准备三角坐标数据
GLES20.glVertexAttribPointer(
positionHandle, // 执行要配置的属性句柄(编号)
COORDS_PER_VERTEX, // 指定每个顶点属性的分量数
GLES20.GL_FLOAT, // 指定每个分量的数据类型
false, // 指定是否将数据归一化到 [0,1] 或 [-1,1] 范围内
vertexStride, // (步长)指定连续两个顶点属性间的字节数。如果为 0,则表示顶点属性是紧密排列的
vertexBuffer // 指向数据缓冲对象
);
// 设置绘制三角形的颜色
GLES20.glUniform4fv(colorHandle, 1, color, 0);
// 画三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用顶点阵列
GLES20.glDisableVertexAttribArray(positionHandle);
}
}
4.3 在GLSurfaceView中绘制三角形
以上我们已经将OpenGL ES绘制三角形的流程全部讲完,接下来我们只需要在GLSurfaceView中创建Triangle
类并执行对应的方法
我们在上一篇中Android OpenGLES2.0开发(二):环境搭建已经搭建好了OpenGL ES环境,现在只需在Renderer
接口中添加Triangle
即可
public class TriangleGLSurfaceView extends GLSurfaceView {
private Context mContext;
private MyRenderer mMyRenderer;
public TriangleGLSurfaceView(Context context) {
super(context);
init(context);
}
public TriangleGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mContext = context;
mMyRenderer = new MyRenderer();
setEGLContextClientVersion(2);
setRenderer(mMyRenderer);
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
static class MyRenderer implements Renderer {
Triangle mTriangle;
public MyRenderer() {
mTriangle = new Triangle();
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mTriangle.surfaceCreated();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
mTriangle.surfaceChanged(width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
mTriangle.draw();
}
}
}
将GLSurfaceView添加到布局中,运行程序我们可以看到绘制的效果如下图所示:
经过上面坐标系的讲解,我们应该能料想到绘制的结果并不是一个正三角形,至于原因我想大家也应该知道,至于怎么解决我们后续再讲
最后
希望你根据上面的步骤一步一步将代码敲出来,相信你肯定对OpenGL ES绘制有一个全面的了解。虽然只是绘制一个三角形,但是上面的代码基本上是后续一切绘制的基础,也算是一个模板代码,后续别的绘制基本上就是对上面的代码微调即可实现。