这篇文章将从demo开始介绍 GLSurfaceView 和 Renderer的使用。
如果对OpenGL的一些基本概念不清楚可以第一篇文章
《OpenGL从入门到放弃01 》一些基本概念
1、GLSurfaceView
GlSurfaceView继承自SurfaceView。并增加了Renderer接口,提供三个回调方法
先看下一般使用方法
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GLSurfaceView glSurfaceView = new GLSurfaceView(this);
glSurfaceView.setRenderer(new GLSurfaceView.Renderer() {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
}
@Override
public void onDrawFrame(GL10 gl) {
}
});
setContentView(glSurfaceView);
}
- 创建 GLSurfaceView
- 调用glSurfaceView.setRenderer,为GLSurfaceView设置一个Renderer,并重写三个方法
2、GlSurfaceView.Renderer
GlSurfaceView.Renderer 提供和三个渲染回调方法
public interface Renderer {
void onSurfaceCreated(GL10 gl, EGLConfig config);
void onSurfaceChanged(GL10 gl, int width, int height);
void onDrawFrame(GL10 gl);
}
- onSurfaceCreated: GlSurfaceView 创建的时候回调,可以做一些参数初始化操作
- onSurfaceChanged:GlSurfaceView尺寸发送变化时回调,例如横竖屏切换
- onDrawFrame:此方法频繁回调,我们可以在这个方法里面进行绘制操作
怎么知道 onDrawFrame 会频繁回调?来,上源码
GLSurfaceView 是一个View对象,在onAttachedToWindow方法启动一个渲染线程
protected void onAttachedToWindow() {
super.onAttachedToWindow();
...
mGLThread = new GLThread(mThisWeakRef);
if (renderMode != RENDERMODE_CONTINUOUSLY) {
mGLThread.setRenderMode(renderMode);
}
mGLThread.start();
}
GLThread 继承自Thread,run方法里调用了guardedRun 方法,重点来了
private void guardedRun() throws InterruptedException {
...
while (true) {
// 1 onSurfaceCreated 只会调用一次,调用之后createEglContext就为false了
if (createEglContext) {
GLSurfaceView view = mGLSurfaceViewWeakRef.get();
if (view != null) {
try {
view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
}
}
// 赋值为false,说明onSurfaceCreated只执行一次
createEglContext = false;
}
...
// 2 大小改变的时候调用onSurfaceChanged
if (sizeChanged) {
GLSurfaceView view = mGLSurfaceViewWeakRef.get();
if (view != null) {
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onSurfaceChanged");
view.mRenderer.onSurfaceChanged(gl, w, h);
}
}
sizeChanged = false;
}
...
// 3 每次都调用 onDrawFrame
{
GLSurfaceView view = mGLSurfaceViewWeakRef.get();
if (view != null) {
try { view.mRenderer.onDrawFrame(gl);
}
}
}
}
}
从注释1、2、3处我们可以验证 Renderer接口 三个方法的调用时机。
上面这些貌似理解起来没啥问题,但是绘制图形就复杂一点了。
3、先来简单的,画一个背景
3.1 声明OpenGL版本
在使用OpenGL之前,需要在AndroidManifest.xml中设置OpenGL的版本:这里我们使用的是OpenGl ES 2.0,所以需要添加如下说明:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
3.2 GLSufaceView 准备
在Activity onCreate中创建 GLSufaceView 和设置Renderer。GLSufaceView可以写在xml中,一样的。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GLSurfaceView glSurfaceView = new GLSurfaceView(this);
glSurfaceView.setRenderer(new DemoRenderer());
setContentView(glSurfaceView);
}
DemoRenderer 实现了GLSurfaceView.Renderer接口,并且重写三个方法,继续看
3.3 GlSurfaceView.Renderer中的绘制步骤
- 设置展示窗口(viewport):
GLES20.glViewport(0,0,width,height);
- 创建图形类,确定好顶点位置和图形颜色,将顶点和颜色数据转换为OpenGl使用的数据格式
- 加载顶点着色器和片元着色器用来修改图形的颜色,纹理,坐标等属性
- 创建投影和相机视图来显示视图的显示状态,并将投影和相机视图的转换传递给着色器。
- 创建项目(Program),连接顶点着色器片段着色器
- 将坐标数据传入到OpenGl ES程序中
绘制步骤大概是这些,接下来上代码了。
3.4 画个背景色看看效果
public class DemoRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// 设置个红色背景
GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// Redraw background color 重绘背景
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
// 设置绘图的窗口(可以理解成在画布上划出一块区域来画图)
GLES20.glViewport(100,100,width,height);
}
}
image.png
很简单,就画一个背景色而已。
4、画一个三角形
创建一个几何图形(这里列举三角形),需要注意一点,我们设置图形的顶点坐标后,需要将顶点坐标转为ByteBuffer,这样OpenGL才能进行图形处理。
4.1 定义一个三角形View
网上很多demo画三角形都是在Renderer里面,这里我们将三角形的绘制流程抽取到一个单独的类,定义为 GLTriangle,在构造方法里面初始化数据,然后定义一个draw方法,在onDrawFrame()中调用draw方法进行绘制操作。
public class GLTriangle{
// 顶点着色器的脚本
String vertexShaderCode =
" attribute vec4 vPosition;" + // 应用程序传入顶点着色器的顶点位置
" void main() {" +
" gl_Position = vPosition;" + // 此次绘制此顶点位置
" }";
// 片元着色器的脚本
String fragmentShaderCode =
" precision mediump float;" + // 设置工作精度
" uniform vec4 vColor;" + // 接收从顶点着色器过来的顶点颜色数据
" void main() {" +
" gl_FragColor = vColor;" + // 给此片元的填充色
" }";
private FloatBuffer vertexBuffer; //顶点坐标数据要转化成FloatBuffer格式
// 数组中每3个值作为一个坐标点
static final int COORDS_PER_VERTEX = 3;
//三角形的坐标数组
static float triangleCoords[] = {
0.0f, 0.5f, 0.0f, // top
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f // bottom right
};
//顶点个数,计算得出
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
//一个顶点有3个float,一个float是4个字节,所以一个顶点要12字节
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
//三角形的颜色数组,rgba
private float[] mColor = {
0.0f, 1.0f, 0.0f, 1.0f,
};
//当前绘制的顶点位置句柄
private int vPosition;
//片元着色器颜色句柄
private int vColor;
//这个可以理解为一个OpenGL程序句柄
private final int mProgram;
public GLTriangle() {
/** 1、数据转换,顶点坐标数据float类型转换成OpenGL格式FloatBuffer,int和short同理*/
vertexBuffer = GLUtil.floatArray2FloatBuffer(triangleCoords);
/** 2、加载编译顶点着色器和片元着色器*/
int vertexShader = GLUtil.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = GLUtil.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
/** 3、创建空的OpenGL ES程序,并把着色器添加进去*/
mProgram = GLES20.glCreateProgram();
// 添加顶点着色器到程序中
GLES20.glAttachShader(mProgram, vertexShader);
// 添加片段着色器到程序中
GLES20.glAttachShader(mProgram, fragmentShader);
/** 4、链接程序*/
GLES20.glLinkProgram(mProgram);
}
public void draw() {
// 将程序添加到OpenGL ES环境
GLES20.glUseProgram(mProgram);
/***在什么位置显示什么颜色*/
// 获取顶点着色器的位置的句柄(这里可以理解为当前绘制的顶点位置)
vPosition = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 启用顶点属性
GLES20.glEnableVertexAttribArray(vPosition);
//准备三角形坐标数据
GLES20.glVertexAttribPointer(vPosition, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// 获取片段着色器的vColor属性
vColor = GLES20.glGetUniformLocation(mProgram, "vColor");
// 设置绘制三角形的颜色
GLES20.glUniform4fv(vColor, 1, mColor, 0);
// 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用顶点数组
GLES20.glDisableVertexAttribArray(vPosition);
}
}
代码基本都加了注释,在构造函数中主要做的事是:
- 顶点数据格式转换,转成成OpenGL能识别的数据格式
为什么数据需要转换格式呢?主要是因为Java的缓冲区数据存储结构为大端字节序(BigEdian),而OpenGl的数据为小端字节序(LittleEdian),因为数据存储结构的差异,所以,在Android中使用OpenGl的时候必须要进行下转换。
/**
* float 数组转换成FloatBuffer,OpenGL才能使用
* @param arr
* @return
*/
public static FloatBuffer floatArray2FloatBuffer(float[] arr)
{
FloatBuffer mBuffer;
// 初始化ByteBuffer,长度为arr数组的长度*4,因为一个int占4个字节
ByteBuffer qbb = ByteBuffer.allocateDirect(arr.length * 4);
// 数组排列用nativeOrder
qbb.order(ByteOrder.nativeOrder());
mBuffer = qbb.asFloatBuffer();
mBuffer.put(arr);
mBuffer.position(0);
return mBuffer;
}
- 加载和编译定义好的顶点着色器和片元着色器代码
这里面有两个知识点,一个是着色器语言,一个是编译过程。
对于着色器代码,加了注释,大概意思能看懂就行,后面会写一篇专门讲解着色器语言。
着色器语言需要经过加载和编译之后,链接到OpenGL ES程序中
public static int loadShader(int shaderType, String source) {
// 创造顶点着色器类型(GLES20.GL_VERTEX_SHADER)
// 或者是片段着色器类型 (GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(shaderType);
// 添加上面编写的着色器代码并编译它
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
return shader;
}
加载和编译,这些都是固定的步骤
- 创建空的 OpenGL ES程序,并把着色器句柄添加进去(着色器句柄可以理解为这个着色器的id)
- 链接程序
初始化OpenGL ES程序4个步骤基本是固定的,为OpenGL绘制做准备
接下来看下draw方法:
- 构造方法中已经把程序(mProgram)准备好了,还需要将程序添加到OpenGL ES环境:
GLES20.glUseProgram(mProgram);
- 准备三角形的坐标数据
// 获取顶点着色器的位置的句柄(这里可以理解为当前绘制的顶点位置)
vPosition = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 启用顶点属性
GLES20.glEnableVertexAttribArray(vPosition);
//准备三角形坐标数据(这里可以理解为将数据传到顶点着色器的vPosition变量)
GLES20.glVertexAttribPointer(vPosition, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
- 设置绘制三角形的颜色
// 获取片段着色器的vColor句柄
vColor = GLES20.glGetUniformLocation(mProgram, "vColor");
// 设置绘制三角形的颜色
GLES20.glUniform4fv(vColor, 1, mColor, 0);
- 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
对于三角形的封装,代码不多,步骤也还算清晰,那怎么使用应该能猜到吧
public class DemoRenderer implements GLSurfaceView.Renderer {
private GLTriangle mGlTriangle;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mGlTriangle = new GLTriangle();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 设置绘图的窗口(可以理解成在画布上划出一块区域来画图)
GLES20.glViewport(100,100,width,height);
}
@Override
public void onDrawFrame(GL10 gl) {
mGlTriangle.draw();
}
}
image.png
图片偏右,这是因为GLES20.glViewport(100,100,width,height);
,xy值不为0,
至此,一个简单的三角形就绘制好了,
对于习惯使用Android 原生控件的看官来说,OpenGL可能是完全陌生的,需要时间慢慢消化才行,这一节的内容也就到此为止。
另外,大家有没有发现这个三角形形状有点怪怪的,坐标是
static float triangleCoords[] = {
0.0f, 0.5f, 0.0f, // top
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f // bottom right
};
我们要的是等边的,为什么会显示成这样呢?
第一节介绍概念时有说到OpenGL的坐标系,没错,就是因为坐标问题啦,下一节将介绍投影和相机视图来解决这个问题。
网友评论