这一贴,我们将介绍OpenGL如何将CPU中的内存数据送到GPU的内存中,Shader又是如何找到这些数据,并进行绘制的。
我们将通过绘制三角形这一简单的例子,为大家简单的介绍下OpenGL的管线流程,以及如何渲染颜色,颜色渐变动画等知识。
#介绍OpenGL的管线
Opengl 中所有的事物,都是由点来表示的,而这些点又是由3D座标表示的。但是最终要在屏幕或窗口中显示的是2D画面,这也就是说Opengl一大部分的工作是 如何将3D 座标变换为2D座标,Opengl中 有一个 图形管线(graphics pipeline)来专门处理这一过程。 图形流程可以分为两部分:一, 将3d座标转换为2d座标;二,将2d座标变换为实际的彩色像素。
图形流程一般以 一列3D座标(物体模型的顶点)作为输入,然后生成新的顶点,并通过对顶点位置属性的空间变换,对颜色属性的灯光变换,再将其转为2D座标,最终渲染为2D彩色图形。这一过程可分为多个步骤,而每一步骤都是以上一步的输出为输入。 在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(); |
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; } |
接着,我们定义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(); } |
为了,能够获得程序在最后运行的信息,我们需要用Android的Log日志,将信息打印出来,但又不希望只打印我们程序中信息,因此定义一个Logger.java
1
2
3
|
public Logger{ public static boolean ON
= true ; } |
接着,创建一个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; |
在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的编译相似,在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); |
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); |
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颜色分量,这里为红色 |
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 ))); |
GIF图,制作时取得帧较少。 大家凑合着看吧。
记住,虽然OpenGL到目前可以为用户高度定制,但是那也只是限于在Shader中,而Shader之外只能调用GL的API, 所以OpenGL的调用顺序一定要搞清楚。 而更需记住的是,OpenGL是一个状态机,对于其操作,只需使用简单的int类型,记住其返回的标识以代表我们获取并执行了GL某个状态。
附: 本教程可能讲解的不够深入, 但是我会继续努力的,争取为大家讲清楚每一个概念(概念可以慢慢讲清,API没法讲清, 只有大家多实践了)。 欢迎大家提问,交流