前面我已经将Android的显示原理及如何在Android如何构建OpenGL环境向你介绍清楚了,那么今天咱们再来聊聊图形渲染管线以及Shader编程。
图形渲染管线
视频、图像的渲染实际上是属于图形学的范畴,在图形学上为了提高渲染图像的效率和灵活性,提出了渲染管线。
渲染管线分为固定渲染管线和可编程渲染管线。其中固定渲染管线指的是,图像在渲染时必须按照固定的顺序一步步执行,用户在渲染开始前提供需要渲染的数据和参数,然后等待渲染结果;而可编程渲染管线指的是,在图像渲染时仍然按照固定的顺序一步步执行,但在某些阶段,如顶点着色器阶段、片元处理阶段,可以通过编写Shader程序进行控制。在Android系统下,OpenGLES1.0采用的是固定管线,而从OpenGLES2.0之后则变成了可编程管线。
如上图所示,图形渲染管线从大的方面分为:应用阶段、几何阶段和光栅化阶段。其中,应用阶段是在CPU中运行的,而几何阶段和光栅化阶段是在GPU中运行的。
应用阶段主要负责提供各种渲染数据和Shader程序;几何阶段又分为顶点着色器阶段、几何着色器、裁剪和屏幕映射四个小阶段;光栅化阶段分为连接三角形、遍历三角形、片元着色、片元操作几个小阶段。
现在我们以顶点着色器阶段为例,看看使用固定管线与使用可编程管线的区别。依然如上图所示,我们可以看到在顶点着色器阶段需要做三件事儿:模型变换、视图变换、顶点着色。如果我们想绘制一个三角形,在Android系统下采用固定渲染管线时,我们需要将顶点坐标、模型变换矩阵和视图变换矩阵通过OpenGL接口传给GPU,这样在GPU内部它就会按照管线的步骤一步步执行最终将三角形绘制出来。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ...
glVertexPointer(3, GL_FLOAT, 0, vertices); glColorPointer(4, GL_FLOAT, 0, colors); glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY);
glMatrixMode(GL_MODELVIEW);
gluLookAt(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f);
...
glDrawArrays(GL_TRIANGLES, 0, 3); ...
|
通过上面的代码我们可以看到,在使用固定管线渲染三角形时,我们只需要调用指定的API将顶点坐标、模型变换矩阵、视图变换矩阵传给GPU,然后再调用glDrawArrays方法启动绘制即可将三角形绘制出来。
而使用可编程渲染管线则与固定渲染管线不同,我们首先要编写Shader程序,然后将数据传给Shader程序,最后由Shader程序控制顶点着色器最终将三角形绘制出来。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| ...
String vertexShaderSource = "#version 300 es\n" + "layout (location = 0) in vec3 aPos;\n" + "layout (location = 1) in vec4 aColor;\n" + "uniform mat4 model;\n" + "uniform mat4 view;\n" + "uniform mat4 projection;\n" + "out vec4 vColor;\n" + "void main()\n" + "{\n" + " gl_Position = projection * view * model * vec4(aPos, 1.0);\n" + " vColor = aColor;\n" + "}\0";
String fragmentShaderSource = ...
int vertexShader; vertexShader = GL20.glCreateShader(GL20.GL_VERTEX_SHADER);
GL20.glShaderSource(vertexShader, vertexShaderSource);
GL20.glCompileShader(vertexShader);
...
int shaderProgram; shaderProgram = GL20.glCreateProgram();
GL20.glAttachShader(shaderProgram, vertexShader); GL20.glAttachShader(shaderProgram, fragmentShader); GL20.glLinkProgram(shaderProgram);
...
float[] vertices = { 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f };
...
GL20.glVertexAttribPointer(0, ...); GL20.glEnableVertexAttribArray(0);
Matrix4f model = new Matrix4f(); ... int modelLoc = GL20.glGetUniformLocation(shaderProgram, "model"); FloatBuffer modelBuffer = BufferUtils.createFloatBuffer(16); model.get(modelBuffer); GL20.glUniformMatrix4fv(modelLoc, false, modelBuffer);
Matrix4f view = new Matrix4f(); view.lookAt(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f); int viewLoc = GL20.glGetUniformLocation(shaderProgram, "view"); FloatBuffer viewBuffer = BufferUtils.createFloatBuffer(16); view.get(viewBuffer); GL20.glUniformMatrix4fv(viewLoc, false, viewBuffer);
Matrix4f projection = new Matrix4f(); projection.frustum(-1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 10.0f); int projectionLoc = GL20.glGetUniformLocation(shaderProgram, "projection"); FloatBuffer projectionBuffer = BufferUtils.createFloatBuffer(16); projection.get(projectionBuffer); GL20.glUniformMatrix4fv(projectionLoc, false, projectionBuffer); ...
GL11.glDrawArrays(...); ...
|
上面代码中,我们先实现了Shader程序,然后编译、链接该程序。当链接好程序好后就可以将它传给GPU运行了。在Shader程序中,我们定义了aPos、model、view、projection等变量用来接收用户传入的顶点数据、模型变换矩阵、视图变换矩阵、投影变换矩阵。最后,当用户调用glDrawArrays后,触发可编程管线运行,可编程管线在顶点着色器阶段运行顶点Shader,这样渲染过程就运转起来了。
至此,我们应该清楚图形渲染管线的作用(为了提高效率和灵活性)及固定管线与可编程管线的区别(都是按固定步骤对图像进行渲染,只不过可编程渲染管线中的某些阶段可通过程序进行控制,这样提高了图型渲染的能力,可以渲染出更加复杂的效果)。而渲染管线各阶段的详细作用我在课程中都做了详细介绍这里就不赘述了。
接下来我们来看看如何编写Shader程序。
Shader编程
如上面我们在渲染管线中讲到的,对于可编程渲染管线来说,有好几个阶段是可编程的,其中我接触最多的是顶点Shader和片元Shader。顶点Shader是在顶点着色阶段被调用,而片元Shader是在片元着色阶段被调用。除了这两个阶段外,几何着色阶段也可调用Shader,该阶段被调用的Shader称为几何Shader。不过由于几何Shader是在OpenGL3.2之后才实现的,所以Android下的OpenGLES是不支持的。接下来咱们就来看看如何实现顶点Shader和片元Shader。
顶点Shader
顶点Shader顾名思义,就是在顶点着色阶段调用的Shader。它的工作往往非常简单,就是确定绘制图形顶点的位置。比如我们要绘制一个三角形,它有三个顶点,那么这三个顶点在标准设备坐标系下的坐置就是由顶点Shader确定的。
我们先来看一下顶点Shader的代码,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| String vertexShaderSource = "#version 300 es\n" + "layout (location = 0) in vec3 aPos;\n" + "layout (location = 1) in vec4 aColor;\n" + "uniform mat4 model;\n" + "uniform mat4 view;\n" + "uniform mat4 projection;\n" + "out vec4 vColor;\n" + "void main()\n" + "{\n" + " gl_Position = projection * view * model * vec4(aPos, 1.0);\n" + " vColor = aColor;\n" + "}\n";
|
上述代码中,第1行指定了使用的OpenGL版本,300表示使用的是OpenGL3.0,es表示使用OpenGL的嵌入式版本,即OpenGLES;第2-3行中的in表示这两个变量都是输入变量,而前面的Layout(location=…)指明了变量的位置,通过这种方式,外面向该变量传数据时,就不再需要通过glGetAttribLocation()函数获取其位置了,可以直接使用glVertexAttribPointerb()函数对其赋值。上面的这种写法是OpenGL3.0的新语法;第4-6行没什么特别的,这里不赘述了;第7行中的out指明vColor是一个输出变量,该变量是片元Shader的输入变量,因此在片元Shader中一定有一个与该变量名一样,但前面有in修饰的变量;最后面的main()函数逻辑非常简单,计算每个顶点的位置,并将顶点位置输出给gl_Position,同时将传入的颜色值输出到vColor。
片元Shader
片元Shader的作用是对图形中每个像素的颜色进行填充。我们来看一个具体的例子:
1 2 3 4 5 6 7
| String fragmentShaderSource = "#version 300 es\n" + "in vec4 vColor;\n" + "out vec4 FragColor;\n" + "void main()\n" + "{\n" + " FragColor = vColor;\n" + "}\n";
|
上述代码中第1行代码指定了使用的OpenGLES版本为3.0;第2行指明vColor变量的数据是从顶点Shader中获得的;第3行out指定FragColor是一个输出变量;后面的main函数逻辑非常简单,就是将顶点Shader传过来的颜色直接输出给FragColor,也就是输出到屏幕上。
从上面我们可以看到其实OpenGL的Shader程序并不难,它使用的是类C语言的语法,其中顶点Shader负责处理图形的顶点位置,而片元Shader用于为图形中的像素着色。
小结
本篇我们对图形渲染管线和Shader编程进行重新梳理,在我看来,如果想学好OpenGL必须要将图形渲染管线的作用,工作机制搞清楚,只有这样你才能真正的学好、用好OpenGL。
参考资料
系统玩转OpenGL+AI,实现各种酷炫视频特效