上一节我们简单的搭建了一个OpenGL的运行窗口,也简单的对这个窗口进行绘制。
来源: [转载]一步一步教你写OpenGLES之画三角形 – OpenGL – ARSchool
上一节我们简单的搭建了一个OpenGL的运行窗口,也简单的对这个窗口进行绘制。
这一节,我们将介绍OpenGL如何将CPU中的内存数据送到GPU的内存中,Shader又是如何找到这些数据,并进行绘制的。
我们将通过绘制三角形这一简单的例子,为大家介绍OpenGL的管线流程,以及如何渲染颜色,颜色渐变动画等知识。
#介绍OpenGL的管线
Opengl 中所有的事物,都是由点来表示的,而这些点又是由3D坐标表示的。但是最终要在屏幕或窗口中显示的是2D画面,这也就是说Opengl一大部分的工作是 如何将3D 坐标变换为2D坐标,Opengl中 有一个 图形管线(graphics pipeline)来专门处理这一过程。 图形流程可以分为两部分:一, 将3d坐标转换为2d坐标;二,将2d坐标变换为实际的彩色像素。
图形流程一般以 一列3D坐标(物体模型的顶点)作为输入,然后将其转为2D坐标,再渲染为2D彩色图形。
其中将3D的点集渲染为彩色图形,又可分为多个步骤,而每一步骤都是以上一步的输出为输入。 在Opengl中,每一步都被高度定制(简单的由一个API表示)。 这所有的步骤之间可以并行执行。多数显卡都有数千个小的处理单元(processing cores),通过在GPU中运行小的工程从而使得图形流程可以很快处理数据 。这些小的工程被称为着色器(shaders)。
有些着色器是可以供开发者配置,通过编写 这些着色器可以替换opengl里默认的着色器, 从而可以使得开发者细致的掌控流程中一些特定的部分。 OpenGL为我们提供了GLSL语言,该语言除了简单的基本类型(类C)外,都是一些抽象的函数实现,而这些函数实现的算法都集成到了GPU中,从而大 大的节省了CPU的运行时间。
下图简单的描述了图形管线的处理步骤:
如何由顶点再到最后的渲染成像一目了然。 这里面,最重要的两个就是顶点着色(Vertex Shader) 和 片段着色(Fragment Shader), 由于OpenGL 2.0以后,接口编程的开放,这两个就需要用户自己定制,而其他的在一般情况下可保持默认。
# 顶点由CPU到GPU
在MyGLRenderer里, 创建一个构造函数,定义三角形的顶点数据:
复制代码
1
2
3
4
5
6
|
float [] verticesWithTriangle = { 0f, 0f, 5f, 5f, 10f, 0f } |
一般在Java中的数据,是由虚拟机为其分配好内存的,因此这一步还不能让CPU真正获取我们所定义的数据,并将数据传递给GPU,
好在Java为我们提供了Buffer这样的对象, 它可以直接在Native层分配内存,以让CPU获取。 在Java中,一个浮点型是4字节,因此,我们可定义BYTES_PER_FLOAT = 4,并有
复制代码
1
2
|
vertexData = ByteBuffer.allocateDirect(tableVerticesWithTriangles.length*BYTES_PER_FLOAT) .order(ByteOrder.nativeOrder()).asFloatBuffer(); |
在这里,我么将分配好的字节按nativeOrder进行排序,这样在大小端机器上都能适用。
CPU的数据是要送到GPU供Shader使用的,因此,我们需要在Shader中制定顶点的属性
首先,在Android工程目录, res下面创建一个raw文件夹,并创建vertexShader.glsl
复制代码
1
2
3
4
5
6
7
|
// vertex shader attribute vec4 a_Position; void main() { gl_Position = a_Position; } |
上面我们只定义了顶点的位置属性, 因此,我们只声明 一个位置属性。 vec4是一个四维数组,如果我们没有为其分配数据,它将自动填充0, 0, 0, 1。 在vertexShader, 最终OpenGL 会将输入的顶点位置赋值给gl_Position 进行输出。
接着,我们定义fragmentShader.glsl
复制代码
1
2
3
4
5
6
7
8
|
//fragShader precision mediump float ; uniform vec4 u_Color; void main() { gl_FragColor = u_Color; } |
第一句定义了数据的精度,就像java语言的double, float类型一样。 highp 就像double代表了高精度,因其效率不高,只用于有限的几个操作。我们只需用mediump即可。 然后,定义了一个 uniform颜色变量, 一般uniform变量在shader中不会有太大变化。 最后,将颜色赋值给gl_FragColor, 完成最后的颜色输出。
gl_Position 和 gl_FragColor 都是OpenGL的内置变量。
# 链接着色器到工程中
为了能够让Opengl能够使用这些着色器,还需要对这些着色器进行编译,以便能够在运行时使用它们。
第一件事,就是创建一个 着色器工程(Shader Program)
首先,我们需要将Shader的代码,读到一个字符串中。在我们package下,创建一个TexResourceRender.java
里面写上如下函数:
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
|
public static String readTextFromResource(Context context, int resourceId) { StringBuilder body = new StringBuilder(); try { InputStream inputStream = context.getResources().openRawResource(resourceId); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String nextLine; while ((nextLine = bufferedReader.readLine())!= null ) { body.append(nextLine); body.append( "\n" ); } } catch (IOException e) { throw new RuntimeException( "Could not open resource: " + resourceId, e); } catch (Resources.NotFoundException nfe) { throw new RuntimeException( "Resource not found: " +resourceId, nfe); } return body.toString(); } |
以上代码表示: 我们从res/raw读取了glsl的内容到字符串中,为了覆盖所有的执行可能,我们用try catch包含了文件读取异常和读取不到的情况。
为了,能够获得程序在最后运行的信息,我们需要用Android的Log日志,将信息打印出来,但又不希望只打印我们程序中信息,因此定义一个Logger.java
1
2
3
|
public Logger{ public static boolean ON = true ; } |
这样就可以用ON来判断,是否要打印我们的日志
接着,创建一个ShaderUtils.java,用于完成shader工程的编译,链接:
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public static int compileVertexShader(String shaderCode) { return compileShader(GL_VERTEX_SHADER,shaderCode); } public static int compileFragShader(String shaderCode) { return compileShader(GL_FRAGMENT_SHADER, shaderCode); } public static int compileShader( int type, String shaderCode) { } |
这里,我们将重点介绍compileShader, OpenGL的Shader编译流程大体如下:1 创建一个Shader 2 将Shader源码赋值给Shader 3 编译 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
|
final int shaderObjectId = glCreateShader(type); // 1 if (shaderObjectId == 0 ) { if (LoggerConfig.ON) { Log.w(TAG, "Could not create new shader." ); } return 0 ; } glShaderSource(shaderObjectId, shaderCode); // 2 glCompileShader(shaderObjectId); // 3 final int [] compileStatus = new int [ 1 ]; glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0 ); if (LoggerConfig.ON) { Log.v(TAG, "Results of compiling source: " + "\n" + shaderCode + "\n:" + glGetShaderInfoLog(shaderObjectId)); } if (compileStatus[ 0 ] == 0 ) { glDeleteShader(shaderObjectId); if (LoggerConfig.ON) { Log.w(TAG, "Compliation of shader failed" ); } return 0 ; } return shaderObjectId; |
为了检测编译的状态,一般在Java平台上,都是创建一个数组,然后获得的编译信息 都是存在该数组的第一个元素中。 如果,编译的状态有错误,删除创建的Shader标识, 如果没错,就返回该标识。
在MyGLRenderer.java的 onSurfaceChanged 下的glClear后读取 glsl的shader文件源码, 之后编译它们
1
2
3
4
5
|
String vertexShaderSource = TextResourceRender.readTextFromResource(mContext, R.raw.simple_vertex_shader); String fragShaderSource = TextResourceRender.readTextFromResource(mContext, R.raw.simple_frag_shader); int vertexShader = ShaderUtils.compileVertexShader(vertexShaderSource); int fragShader = ShaderUtils.compileFragShader(fragShaderSource); |
Shader编译以后,需要链接到工程中 ,链接工程的步骤大体如下: 1 创建一个program 2 将Shader依附在工程上 3 链接工程
它的步骤与 Shader的编译相似,在ShaderUtils.java添加如下代码:
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
|
public static int LinkProgram( int vertexShaderId, int fragShaderId) { final int programObjectId = glCreateProgram(); //1 if (programObjectId == 0 ) { if (LoggerConfig.ON) { Log.w( "TAG" , "Could not create new program" ); } return 0 ; } glAttachShader(programObjectId, vertexShaderId); //2 glAttachShader(programObjectId, fragShaderId); glLinkProgram(programObjectId); //3 //check any error final int [] linkStatus = new int [ 1 ]; glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0 ); if (LoggerConfig.ON) { Log.v(TAG, "Results of linking program:\n" + glGetProgramInfoLog(programObjectId)); } if (linkStatus[ 0 ] == 0 ) { glDeleteProgram(programObjectId); if (LoggerConfig.ON) { Log.w(TAG, "Linking of program valid" ); } return 0 ; } return programObjectId; } |
然后,在MyGLRenderer.java的 onSurfaceCreated 下创建program 工程
复制代码
1
|
int program = ShaderUtills.LinkProgram(vertexShader, fragShader); |
在使用创建的program之前,我们很想知道,创建的工程是否符合opengl当前上下文的状态, 知道为什么它有时候会运行失效,根据OpenGLES2.0的官方文档, 我们需在ShaderUtils.java下创建如下方法:
1
2
3
4
5
6
7
8
9
|
public static boolean validateProgram( int programObjected) { glValidateProgram(programObjected); final int [] validateStatus = new int [ 1 ]; glGetProgramiv(programObjected, GL_VALIDATE_STATUS, validateStatus, 0 ); Log.v(TAG, "Results of validating program: " + validateStatus[ 0 ] + "\nLog: " +glGetProgramInfoLog(programObjected)); // 打印日志信息 return validateStatus[ 0 ] != 0 ; } |
接着,我们就可以使用Shader工程了,
1
2
3
4
5
6
|
if (LoggerConfig.ON) { ShaderUtils.validateProgram(program); } glUseProgram(program); |
有了program标识后,我们可以使用它将数据送到Shader里了,首先在MyGLRenderer.java的Filed域中定义
1
2
3
4
5
|
private static final String U_COLOR = “u_Color”; // 标识fragment shader里 uniform变量 private static final String A_POSITION = "a_Position" ; private int uColorLocation; // 存放shader里的变量位置 private int aPositionLocation; |
然后,在onSurfaceCreated里
1
2
3
4
5
6
|
uColorLocation = glGetUniformLocation(program, U_COLOR); aPositionLocation = glGetAttribLocation(program, A_POSITION); vertexData.position( 0 ); glVertexAttribPointer(aPositionLocation, 2 , GL_FLOAT, false , 0 , vertexData); glEnableVertexAttribArray(aPositionLocation); |
首先找到 shader里,变量的位置, 然后将vertexData的数据用glVertexAttribPointer 传递给Shader, 该函数的6个参数分别代表为:
1 shader中顶点变量的位置
2 顶点的坐标属性分量个数
3 指明了顶点的数据类型
4 顶点是否归一化(是否是整数)
5 代表,每个顶点的总数据大小(包含了所有属性的内存), 由于这里,我们只定义了顶点属性一种,因此可以赋值为0,(后面我们会介绍,顶点有多个属性的情况,再来着重介绍该函数)
6 数据的首地址
最后,用glEnableVertexAttribArray 激活这一顶点属性即可。
到这里,我们将重要的顶点数据由CPU送到GPU,可供VertexShader适用。 关于uniform变量,它相当于图像管线中的全局变量,即vertex shader和fragment shader能对其共享,它传入的值一般不会被管线流程改变。
在onDrawFrame方法的glClear(GL_CLOR_BUFFER_BIT)后面加上:
1
|
glUniform4f(uColorLocation, 0 , 1 .0f, 0f, 0f); // 第二个参数表示offset, 后面是rgb颜色分量,这里为红色 |
最后,glDrawArrays(GL_TRIANGLE, 0, 3); //
GL_TRIANGLE 是以三角形的形式去着色,同理还有点(GL_POINT), 线(GL_LINE)的方式。
看一下效果:
哎呦, 哪里不对劲, 为什么是在右上角,没显示全? 这事因为坐标系没有对,在opengl里,坐标系是图像的中心点,即我们定义的(0, 0)点。 而(10, 0)点跑到屏幕外面去了的原因是, opengl的坐标范围一般都在(-1, 1)之间,也就是设备归一化。 所以,我们如果想让自己的三角形显示在中间需要对之前定义的顶点做以下处理:
1
2
3
4
5
6
|
float [] verticesWithTriangle = { -1f, -1f, 0f, 1f, 1f, 1f } |
这样就可以显示在中间了。
接下来,通过系统时间来改变每次传入的颜色, 可以实现以下动画效果:
1
|
glUniform4f(aColorPosition, 0f, ( float )(Math.abs(Math.sin(System.currentTimeMiilus()/ 1000 ))); |
由于颜色的最小值为0, 为了不让动画有较长的黑屏现象,可对sin的值去绝对值,这样由黄到黑的不断交替变换的三角形就出来了
GIF图,制作时取得帧较少。 大家凑合着看吧。
记住,虽然OpenGL到目前可以为用户高度定制,但是那也只是限于在Shader中,而Shader 之外只能调用GL的API, 所以OpenGL的调用顺序一定要搞清楚。 而更需记住的是,OpenGL是一个状态机,对于其操作,只需使用简单的int类型,记住其返回的标识以代表我们获取并执行了GL某个状态。
附: 本教程可能讲解的不够深入, 但是我会继续努力的,争取为大家讲清楚每一个概念(概念可以慢慢讲清,API没法讲清, 只有大家多实践了)。 欢迎大家提问,交流