本文正在参加「金石计划」
大家好,我是小余,本篇文章是OpenGL ES系列的第一篇文章。主要通过一个基础的三角形绘制来带大家了解OpenGL ES的基础知识,作为一个入门文章。
OpenGLES是什么?
OpenGLES是OpenGL的一个子集,也就是OpenGL的一个精简指令集,主要用于嵌入式设备,如手机,平板等,本质上是一个跨编程语言,跨平台的编程接口规范。注意是规范,或者你简单可以理解为就是一个接口API。
OpenGLES如何工作的?
讲到如何工作,一定要知道OpenGl的图形渲染管线,指将一些基础数据,如顶点数据,颜色,纹理等数据作为输入,经过多个变化处理,最终输出到屏幕上一个过程。这个过程大致包括:顶点着色器,图元装备,几何着色器,光栅化,片段着色器,测试与混合这六个阶段。
对于我们GLES来说,大部分情况我们只需要和顶点着色器以及片段着色器打交道,其他的我们暂时不需要去了解。
顶点着色器
顶点着色器的主要作用是将3D坐标转换为另外一个坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。顶点着色器是几个可编程着色器之一。
输入:3D坐标数据
输出:变化后的坐标数据
编程语言:GLSL
片段着色器
片段着色器是一个给你的片段进行上色的一个步骤。假设需要对不同片段进行不同的处理,那么就需要获取当前片段的位置坐标,如何获取呢?还记得刚才说的顶点着色器么,他可以输出每个片段的位置属性信息,然后在片段着色器中作为输入属性就可以了。 输入:各种属性数据,不只限于位置属性,对于一些需要光照场景,还会添加一个物体材质,法向量,光照属性等
输出:当前片段的颜色值
编程语言:GLSL
GLSL是什么? GLSL(OpenGL Shading Language),着色器编程语言,也就是说用来编写我们着色器的。他的写法和C语言的写法很相似。
下面是一个最简单的顶点着色器的编写:
#version 300 es
layout (location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position, 1.0f);
}
-
第1行:用来指定当前使用的GLES的版本
-
第2行:in代表这个是一个输入属性,position是当前属性的name,这个自己随意指定,vec3表示当前的position属性是一个三位坐标系,layout (location = 0):表示当前属性的location值为0,在外部指定属性编号的时候会用到,后面讲demo的时候会说到。
-
第5行:main里面的gl_Position代表当前顶点位置属性的输出,这个名称不能更改,后面处理会默认使用这个名称。此处的gl_Position代表一个4分量的向量,注意最后一个值并非位置分量,而是用来处理透视划分使用到的,暂时不用去深入,默认为1.0.
既然是编程语言那就需要去编译。
编译着色器
首先编译着色器之前我们需要去创建一个着色器对象,gl api中已经给我们提供了:
GLuint vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
这里的vertexShader就是一个着色器对象的索引ID。GL_VERTEX_SHADER:表示当前创建的是一个顶点着色器。
其次我们需要将着色器源码附加到这个着色器对象中:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
第三个参数vertexShaderSource表示GLSL源码地址,最后编译:
glCompileShader(vertexShader);
对于片段着色器也是同样的编译方法。
那编译完成之后就可以使用了么?
着色器程序
着色器程序对象是多个着色器合并之后,链接的版本,前面创建的着色器对象需要使用,一定需要将他们链接到一个着色器对象上之后才能使用。如何链接?
首先我们需要创建一个着色器程序:
GLuint shaderProgram;
shaderProgram = glCreateProgram();
然后用glLinkProgram链接着色器对象:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
最后在绘制之前调用:
glUseProgram(shaderProgram);
之后所有的操作都是针对前面编写的着色器对象了。
链接顶点属性
顶点着色器其实不止会传入顶点数据作为输入,比如对颜色以及法向量等也可以使用顶点数组的形式传入。
所以就需要区分当前输入的数据对应的到底是哪一个属性。
一般我们的顶点数据会被解析成下面这种形式:
- 位置数据被储存为32-bit(4字节)浮点值
- 每个位置包含3个这样的值。每个位置代表当前x,y,z分量
- 这几个值在数组中紧密排列,没有缝隙。
- 数据中的第一个值在缓冲开始的位置。
有了上面的顶点数组信息以后,我们就可以使用OpenGl给我们提供的属性api来指定属性了。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer参数定义:
- ** 第一个参数0**:你可能还记得我们前面分析顶点着色器的时候说的那个location值,那个location值就是在这里使用的,当指定为0,着色器对象将会将当前属性分配给着色器中对应location为0的变量。
- 第二个参数3:指定了当前属性占用的长度,对于位置属性,我们一般使用vec3类型,也就是3分量x,y,z来表示,所以这里我们指定长度为3,一些其他情况,如纹理坐标,这里就可能为2了,因为只需要用到纹理坐标只需要使用到x和y分量。
- 第3个参数GL_FLOAT:指定了数据的类型,GLSL中
vec*都是由浮点数值组成的。 - 第4个参数GL_FALSE:是否希望数据被标准化,标准化后的数据都会在0和1之间或者-1和1之间。
- 第5个参数3 * sizeof(GLfloat):表示到下一个同类型的属性的内存间隙,这里我们只有一种属性,且属性的长度为3,所以间隔为3,这里说的是内存之间的距离,所以使用了3 * sizeof(GLfloat)。
- ** 第6个参数(GLvoid*)0**:表示离起始位置的偏移量,这里只有一种属性,所以偏移量为0,如果有多种属性,那么就不会只为0了。
glEnableVertexAttribArray(0):表示启用当前location为0的顶点属性,默认关闭。
VAO与VBO
VBO:顶点缓冲对象,在顶点着色器处理阶段,我们使用VBO,在GPU上提前创建一块内存,用于缓存顶点数据。这样可以避免在每次绘制的时候都需要重新发送数据给GPU,毕竟这是一个比较耗时的操作。
VAO:顶点数组对象,VAO的主要作用是用来管理VBO或者EBO, ,减少 glBindBuffer 、glEnableVertexAttribArray、 glVertexAttribPointer 这些调用操作,提高顶点数组切换步骤。
VAO 与 VBO 之间的关系:
有了以下基础之后,我们再来绘制一个三角形
绘制一个三角形
应用层我们使用GLSurfaceView作为媒介,因为其内部封装了EGL环境的搭建,所以我们只需要把它引入到我们的项目中即可。
public class MyGLSurfaceView extends GLSurfaceView {
Renderer mRenderer;
public MyGLSurfaceView(Context context) {
this(context,null);
}
public MyGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
setEGLContextClientVersion(2);
setEGLConfigChooser(8,8,8,8,16,8);
}
public void setmRenderer(MyGLSurfaceRenderer mRenderer) {
this.mRenderer = mRenderer;
setRenderer(mRenderer);
setRenderMode(mRenderer.rendererMode);
}
}
我们还自定义了个Renderer
public class MyGLSurfaceRenderer implements GLSurfaceView.Renderer {
int rendererMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY;
MyNativeRenderer nativeRenderer;
MyGLSurfaceRenderer(){
nativeRenderer = new MyNativeRenderer();
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
nativeRenderer.native_onSurfaceCreated();
}
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
nativeRenderer.native_onSurfaceChanged(width,height);
}
@Override
public void onDrawFrame(GL10 gl10) {
nativeRenderer.native_onDrawFrame();
}
public void destroy(){
nativeRenderer.native_destroy();
}
}
并搭建一个MyNativeRenderer用来和native层进行操作,直接使用的是native层的opengles接口进行开发。
其实也可以使用java层的GLES接口进行处理,但是这种方式一方面移植性不好,opengl还是有很多优秀的开源c/c++库,使用java来开发就无法移植了,所以小余选择使用了native层进行开发,另外一方面也是为了巩固自己对jni层的理解。
public class MyNativeRenderer {
static {
System.loadLibrary("opengl-es-lib");
}
public MyNativeRenderer(){
}
public native void native_onSurfaceCreated();
public native void native_onSurfaceChanged(int width,int height);
public native void native_onDrawFrame();
public native void native_destroy();
}
在jni层,我们引入GLES类库,代码如下:
MyGLRenderContext* MyGLRenderContext::context = nullptr;
//单例类
MyGLRenderContext * MyGLRenderContext::getInstance() {
if(context == nullptr){
context = new MyGLRenderContext();
}
return context;
}
void MyGLRenderContext::OnSurfaceCreated() {
glClearColor(0.1f,0.2f,0.3f,1.0f);
}
void MyGLRenderContext::OnSurfaceChanged(int width, int height) {
glViewport(0,0,width,height);
}
/**
* 绘制前操作
* 0.初始化顶点数据
* 1.创建着色器程序对象
* 2.生成VAO,VBO对象
* */
void MyGLRenderContext::beforeDraw() {
if(programObj!= 0){
return;
}
//0.初始化顶点数据
GLfloat vertices[] = {
0.0f, 0.5f, 0.0f,1.0f,0.0f,0.0f,
-0.5f,-0.5f, 0.0f,0.0f,1.0f,0.0f,
0.5f, -0.5f, 0.0f,0.0f,0.0f,1.0f
};
//1.创建着色器程序,此处将着色器程序创建封装到一个工具类中
char vShaderStr[] =
"#version 300 es \n"
"layout(location = 0) in vec4 vPosition; \n"
"layout(location = 1) in vec3 vColor; \n"
"out vec3 color; \n"
"void main() \n"
"{ \n"
" gl_Position = vPosition; \n"
" color = vColor; \n"
"} \n";
char fShaderStr[] =
"#version 300 es \n"
"precision mediump float; \n"
"in vec3 color; \n"
"out vec4 fragColor; \n"
"void main() \n"
"{ \n"
" fragColor = vec4 (color, 1.0 ); \n"
"} \n";
programObj = GLUtils::CreateProgram(vShaderStr,fShaderStr);
//2.生成VAO,VBO对象,并绑定顶点属性
GLuint VBO;
glGenVertexArrays(1,&VAO);
glGenBuffers(1,&VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
//顶点坐标属性
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(GLfloat),(GLvoid*)0);
glEnableVertexAttribArray(0);
//顶点颜色属性
glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,6*sizeof(GLfloat),(GLvoid*)(3*sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glBindVertexArray(GL_NONE);
}
/**
* 1.清除buffer
* 2.使用程序着色器对象
* 3.开始绘制
* 4.解绑
* */
void MyGLRenderContext::OnDrawFrame() {
beforeDraw();
if(programObj == 0){
return;
}
//清除buffer
glClear(GL_COLOR_BUFFER_BIT);
glClearColor(0.3f,0.5f,0.4f,1.0f);
//使用程序着色器对象
glUseProgram(programObj);
//绑定VAO
glBindVertexArray(VAO);
//开始绘制
glDrawArrays(GL_TRIANGLES,0,3);
//解绑VAO
glBindVertexArray(GL_NONE);
//解绑程序着色器对象
glUseProgram(GL_NONE);
}
void MyGLRenderContext::destroy() {
if(programObj){
programObj = GL_NONE;
}
glDeleteVertexArrays(1,&VAO);
}
我们使用一个单列类来处理三角形的初始化以及绘制操作,由于着色器的创建以及以及链接操作都是公用的,我们将其封装在一个GLUtils的类中:
GLuint GLUtils::CreateProgram(const char *pVertexShaderSource, const char *pFragShaderSource, GLuint &vertexShaderHandle, GLuint &fragShaderHandle)
{
GLuint program = 0;
FUN_BEGIN_TIME("GLUtils::CreateProgram")
vertexShaderHandle = LoadShader(GL_VERTEX_SHADER, pVertexShaderSource);
if (!vertexShaderHandle) return program;
fragShaderHandle = LoadShader(GL_FRAGMENT_SHADER, pFragShaderSource);
if (!fragShaderHandle) return program;
program = glCreateProgram();
if (program)
{
glAttachShader(program, vertexShaderHandle);
CheckGLError("glAttachShader");
glAttachShader(program, fragShaderHandle);
CheckGLError("glAttachShader");
glLinkProgram(program);
GLint linkStatus = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
glDetachShader(program, vertexShaderHandle);
glDeleteShader(vertexShaderHandle);
vertexShaderHandle = 0;
glDetachShader(program, fragShaderHandle);
glDeleteShader(fragShaderHandle);
fragShaderHandle = 0;
if (linkStatus != GL_TRUE)
{
GLint bufLength = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &bufLength);
if (bufLength)
{
char* buf = (char*) malloc((size_t)bufLength);
if (buf)
{
glGetProgramInfoLog(program, bufLength, NULL, buf);
LOGCATE("GLUtils::CreateProgram Could not link program:\n%s\n", buf);
free(buf);
}
}
glDeleteProgram(program);
program = 0;
}
}
FUN_END_TIME("GLUtils::CreateProgram")
LOGCATE("GLUtils::CreateProgram program = %d", program);
return program;
}
结合前面给的一些基础理论知识,相信你是能看懂的。 效果:
代码已经上传到github:需要的自行下载。
好了,本篇文章只是OpenGLES的入门文章,后续还是推出其他相关文章。
另外本人整理了一些关于Android开发进阶以及面试的一些指导: 关注小余,回复“资料”免费获取。