最近由于项目需要,所以开始学习OpenGL,网络上的东西零零散散,所以就想写一系列博客来记录学习OpenGL。
首先我们要对其有一个简单的认识
什么是OpenGL?
官方描述:OpenGL是一个跨平台的图形API,用于指定3D图形处理硬件中的标准软件接口。
OpenGL的优势?
OpenGl是用来做图像处理的,那我们为什么不使用Canvas呢?费劲学这个干嘛?
答案很简单,为了效率,使用Canvas画图使用的是CPU,那我们都知道手机上还有一个专门的图形处理单元叫GPU,它可以并行的做浮点运算,OpenGL使用的就是GPU,这样的话可以用GPU分担CPU的工作量,提高图形渲染效率。
概念
顶点着色器:它的作用就是为每一个顶点生成坐标,因此每个顶点都要运行一遍顶点着色器程序,一旦顶点坐标计算出来之后,OpenGL就能够使用这些顶点来组成点,线,面。
片段着色器:它的作用就是为每一个顶点的片段渲染颜色。
坐标系:原点在中心,范围为[-1,1],2d环境只有(x,y),3d环境有(x,y,z)
了解了基础概念之后我们就开始上代码,要使用OpenGL进行绘制就必须要有一个地方来供它绘制,它既然是使用GPU进行计算的,那这块地方就肯定与Canvas有所不同,这个地方就是GLSurfaceView,它是android为我们提供的一个类,继承于SurfaceView,也就是说它管理着一块Surface。
要创建SurfaceView可以使用new来创建,也可以在xml中书写
GLSurfaceView glSurfaceView=new GLSurfaceView(context);
setContentView(glSurfaceView);
或者
GLSurfaceView glSurfaceView = (GLSurfaceView) findViewById(R.id.glSurfaceView);
创建完成后还要为其设定要使用的OpenGL版本,一般来说我们为其设置版本好为2
glSurfaceView.setEGLContextClientVersion(2);
为其设置Render,这个才是控制渲染的地方
glSurfaceView.setRenderer(renderer);
Render是一个接口,存在于GLSurfaceView类中,你需要将它里面的三个重要的方法实现
public interface Renderer {
//当GlSurfaceView创建时被调用
void onSurfaceCreated(GL10 var1, EGLConfig var2);
//当GlSurfaceView尺寸发生变化时被调用
void onSurfaceChanged(GL10 var1, int var2, int var3);
//当画每一帧的时候都会被调用
void onDrawFrame(GL10 var1);
}
一般来说我们在onSurfaceCreated中绘制背景,在onSurfaceChanged中设置视图大小,在onDrawFrame中进行真正的绘画,不过要在绘画前清空视图,下面给一个简单的示例:
public class MyRender implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
GLES20.glClearColor(0.5f,0.5f,0.5f,1.0f);
}
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
GLES20.glViewport(0,0,width,height);
}
@Override
public void onDrawFrame(GL10 gl10) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT|GLES20.GL_DEPTH_BUFFER_BIT);
}
}
该Render绘制出来就是一个灰色的背景,上面没有任何图形,要绘制图形的话还要使用Shader,比如说我们要绘制三角形,那么它的ShaderCode就应该这么写
private final String vertexShaderCode=
"attribute vec4 vPosition;"+
"void main(){"+
" gl_Position=vPosition;"+
"}";
当然还要给图形上色了,涂颜色也需要使用ShaderCode
private final String fragmentShaderCode=
"precision mediump float;"+
"uniform vec4 vColor;"+
"void main(){"+
" gl_FragColor=vColor;"+
"}";
现在你先不用着急去研究Shader该怎么写,先熟悉整个流程
现在有了画图的程序和上色的程序还缺什么呢,缺少坐标,你不给坐标程序怎么知道你要画在哪,怎么画
//用于装载坐标点
private FloatBuffer vertexBuffer;
//编译ShaderCode后程序的指针
private int mProgram;
//一个坐标要用三个数表示,x,y,z
private final int COORDS_PER_VERTEX=3;
//坐标数组
private float triangleCoords[]={
0.5f,0.5f,0.0f,
-0.5f,-0.5f,0.0f,
0.5f,-0.5f,0.0f
};
//顶点句柄
private int mPositionHandle;
//颜色句柄
private int mColorHandle;
//坐标点数量
private final int vertexCount=triangleCoords.length/COORDS_PER_VERTEX;
//一个float占四位,一共占坐标点数量*4位的空间
private final int vertexStride=COORDS_PER_VERTEX*4;
private float[] color={1.0f,1.0f,1.0f,1.0f};
接下来就是真正的编译程序和绘画了
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
//设置背景
GLES20.glClearColor(0.5f,0.5f,0.5f,1.0f);
//初始化ByteBuffer,为其分配空间
ByteBuffer byteBuffer=ByteBuffer.allocateDirect(triangleCoords.length*4);
//设置排列顺序为nativeOrder
byteBuffer.order(ByteOrder.nativeOrder());
//将ByteBuffer转换为FloatBuffer
vertexBuffer=byteBuffer.asFloatBuffer();
//将坐标点放入FloatBuffer
vertexBuffer.put(triangleCoords);
//设置起点
vertexBuffer.position(0);
//获得编译后的顶点程序句柄
int vertexShader=loadShader(GLES20.GL_VERTEX_SHADER,vertexShaderCode);
//获得编译后的颜色程序句柄
int fragmentShader=loadShader(GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);
//创建一个OpenGLES程序
mProgram=GLES20.glCreateProgram();
//将编译后的顶点程序加入其中
GLES20.glAttachShader(mProgram,vertexShader);
//将编译后的颜色程序加入其中
GLES20.glAttachShader(mProgram,fragmentShader);
//连接程序
GLES20.glLinkProgram(mProgram);
//声明一个数组,用于存放连接程序结果
int[] linkStatus = new int[1];
//获取连接程序结果,将其存入linkStatus的第0个位置
GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, linkStatus, 0);
//对连接程序结果的处理
if (linkStatus[0] != GLES20.GL_TRUE) {
Log.e("ES20_ERROR", "Could not link program: ");
Log.e("ES20_ERROR", GLES20.glGetProgramInfoLog(mProgram));
GLES20.glDeleteProgram(mProgram);
mProgram = 0;
}
}
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
//设置视图大小
GLES20.glViewport(0,0,width,height);
}
@Override
public void onDrawFrame(GL10 gl10) {
//清空视图
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT|GLES20.GL_DEPTH_BUFFER_BIT);
//将程序加入到OpenGLES环境
GLES20.glUseProgram(mProgram);
//获取顶点着色器的句柄
mPositionHandle=GLES20.glGetAttribLocation(mProgram,"vPosition");
//启用句柄
GLES20.glEnableVertexAttribArray(mPositionHandle);
//填入数据
GLES20.glVertexAttribPointer(mPositionHandle,COORDS_PER_VERTEX,
GLES20.GL_FLOAT,false,vertexStride,vertexBuffer);
//获取片元着色器的句柄
mColorHandle=GLES20.glGetUniformLocation(mProgram,"vColor");
//填入数据
GLES20.glUniform4fv(mColorHandle,1,color,0);
//开始绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLES,0,vertexCount);
//禁用顶点着色器的句柄
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
//根据类型加载ShaderCode程序
protected int loadShader(int type, String shaderCode){
//根据类型创建着色器
int shader= GLES20.glCreateShader(type);
//加入代码
GLES20.glShaderSource(shader,shaderCode);
//开始编译
GLES20.glCompileShader(shader);
//声明一个数组,用于存放编译结果
int[] compiled = new int[1];
//获取编译结果,将其存入到compiled的第0个位置中
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
//根据编译结果做处理
if (compiled[0] == 0) {
Log.e("ES20_ERROR", "Could not compile shader " + type + " : "+shaderCode);
Log.e("ES20_ERROR", GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = 0;
}
return shader;
}
这样一个三角就画好了,但是你会发现画出来的三角形和填入的坐标并不相符,明明应该是一个等腰直角三角形,为什么被拉长了呢?因为你的屏幕不是方的,你的屏幕要是方的它就是一个等腰直角三角形了,所以怎么才能把它转换一下呢?这就涉及到了投影和相机视图了,下篇我会讲一下。
项目地址:https://github.com/s15603333319/AndroidOpenGL