上一次我讲述了OpenGL的作用了,这次我使用了OpenGL来绘制一张桌子,其实我是将一个冰球桌拆分成几块来讲述,现在就来绘制冰球桌的一些基本元素。在绘制的同时我顺便来介绍下一些基础知识。
一、OpenGL中顶点的作用
顶点:代表几何对象的拐角的点,其中最主要的属性就是其位置,代表其在空间中的位置,另外,OpenGL只能够绘制点、直线、三角形。
点和直线我们可以理解,使用三角形是因为三角形由于其稳定的结构可以来绘制复杂的对象和纹理场景。
OpenGL在绘制的时候采用逆时针绘制比较好,这种顺序叫做卷曲顺序,可以优化性能。
1.使数据可以被OpenGL存取
OpenGL作为本地系统库是直接运行在硬件上的,而我们的代码是运行在Dalvik上,导致OpenGL无法去读取我们的数据,所以有两种方案解决上述问题:
- 从Java调用本地代码
- 通过使用JNI技术,一般我们调用GLES20包里的技术,就是在后台使用JNI
- 把内存从Java堆复制到本地堆:意思就是改变内存分配的方式,Java有个特殊的类集合,把Java数据复制到本地内存中。
private static final int BYTES_PER_FLOAT = 4;
private final FloatBuffer vertexData;
public AirHockeyRenderer() {
float[] tableVerticesWithTriangles = {
// Order of coordinates: X, Y, R, G, B
// Triangle Fan
0f, 0f, 1f, 1f, 1f,
-0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
0.5f, 0.5f, 0.7f, 0.7f, 0.7f,
-0.5f, 0.5f, 0.7f, 0.7f, 0.7f,
-0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
// Line 1
-0.5f, 0f, 1f, 0f, 0f,
0.5f, 0f, 1f, 0f, 0f,
// Mallets
0f, -0.25f, 0f, 0f, 1f,
0f, 0.25f, 1f, 0f, 0f
};
vertexData = ByteBuffer
ByteBuffer//分配了一块本地内存,这块内存不会被垃圾回收器管理,这里需要知道具体分配多少内存块
.allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT)
//告诉字节缓冲区读取的的内容序列,最重要的是保持同样的顺序
.order(ByteOrder.nativeOrder())
//得到一个可以放映底层字节的FloatBuffer类实例,可以避免操作单独字节的麻烦
.asFloatBuffer();
vertexData.put(tableVerticesWithTriangles);
}
FloatBuffer用来在本地内存中存储数据
vertexData = ByteBuffer
.allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
vertexData.put(tableVerticesWithTriangles);
- allocateDirect:分配了一块指定大小的本地内存,并且这块本地内存不会受到gc管理,需要指定字节大小
- order:告诉字节缓冲区按照本地字节序组织内容。
- asFloatBuffer:表示我们不愿单独操作字节,而是一整块的操作,通过pu可以实现将数据从Dalvik的内存复制到本地内存。
二、着色器
一般我们定义好物体的顶点,被读取到本地内存中,在绘制到屏幕的时候,需要通过管道进行传输,这类管道其实也成为着色器。
着色器:会告诉GPU如何处理绘制数据
在OpenGL中,总共有两种类型的着色器:
(1)顶点着色器生成每个顶点的最终位置,一旦 最终位置确定了,OpenGL就可以把这些点集合组装成点、直线、以及三角形
(2)片段着色器为组成的点、直线、三角形的每个片段生成最终的颜色,针对每个片段,都会执行一次,类似于计算机屏幕上的一个像素,一个长方形片段。
一旦最后颜色生成后,OpenGl会把他们写到帧缓冲区的内存块中,然后Android会把这块帧缓冲区显示到屏幕上。
1.创建一个顶点的着色器
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
}
着色器采用了GLSL定义,这种语法同C语言类似,当我们定义每一个单一的顶点,顶点着色器都会被调用一次,它被调用的时候,他会在a_Position属性里接受当前顶点的位置,这个属性被定义为vec4属性。
vec4是包含4个分量的向量,可以认为有x,y,z,w,其中x、y、z表示三维位置,w比较特殊,目前默认为1。
attribute表示顶点的所有属性集合
main方法主要是着色器的入口,他所做的就是把前面定义过的位置复制到制定的输出变量gl_Position,OpenGL会把gl_Position中存储的值作为当前顶点的最终位置,并组装成点。线和三角形。
2.创建第一个片段着色器
(1)光栅化技术
显示屏成千上万色彩原理:主要是每个像素由3个子元件构成,分别发出红、蓝、绿三种光,利用人眼把光混在一起,从而创造出成千上万的色彩。
光栅化:把每个点、直线和三角形分解为大量的小片段,然后映射到屏幕上,通常上一个片段对应一个像素点,但是在高分辨率下存在使用较大片段。
(2)编写代码
片段着色器的目的:告诉GPU每个片段最终的颜色是什么,对于每个片段的着色器都会被调用一次。
precision mediump float;
uniform vec4 u_Color;
void main(){
gl_FragColor = u_Color;
}
(3)精度定位符
precision mediump float;
uniform vec4 u_Color;
void main(){
gl_FragColor = u_Color;
}
在这个片段渲染器中,precision mediump float这句代码定义了所有的浮点类型数据的默认精度。
另外可以选择lowp、mediump和highp,他们分别对应低精度、中精度和高精度,一般只有某些硬件支持高精度。
这点和顶点着色器不一样,顶点着色器由于位置的精确度,一般默认为高精度,所以不需要再去怎么修改,而片段着色器则采用中等精度,主要是考虑到性能和兼容性。
(4)生成片段的颜色
颜色的传递使用一个叫uniform,每个顶点都需要设置一个,uniform也是一个四分量向量,主要面对红、绿、蓝、alpha。
main方法是片段中着色器的入口,着色器的颜色一定要给gl_GragColor赋值,OpenGL会使用这个颜色作为当前片段的最终颜色。
前面我们讲述完原理,接着我们要开始编写着色器程序,通过其来驱动OpenGL进行绘制
四、加载着色器
我们需要写一个可以从资源文件中读取那些代码的方法
1.从资源中加载文本
/**
* 读取着色器代码
* @param context 上下文
* @param resourceId 资源ID
* @return 代码字符串
*/
public static String readTextFileFromResource(Context context,int resourceId){
StringBuilder sb = new StringBuilder();
try {
InputStream is = context.getResources().openRawResource(resourceId);
InputStreamReader inputStreamReader = new InputStreamReader(is);
BufferedReader br = new BufferedReader(inputStreamReader);
String nextLine;
while((nextLine = br.readLine())!=null){
sb.append(nextLine);
sb.append('\n');
}
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
2.读入着色器代码
String vertexShaderSource = TextResourceReader
.readTextFileFromResource(mContext, R.raw.simple_vertex_shader);
String fragmentShaderSource = TextResourceReader
.readTextFileFromResource(mContext,R.raw.simple_fragment_shader);
二、编译着色器
通过一个辅助类来返回着色其对象
/**
* author: machenshuang
* <p>
* Date: 2017-11-15 15:07
* <p>
* 描述:着色器对象生成辅助类
*/
public class ShaderHelper {
private static final String TAG = "ShaderHelper";
public static int compileVertexShader(String shaderCode) {
return compileShader(GL_VERTEX_SHADER, shaderCode);
}
public static int compileFragmentShader(String shaderCode) {
return compileShader(GL_FRAGMENT_SHADER, shaderCode);
}
public static int compileShader(int type, String shaderCode) {
return 0;
}
}
1.创建一个新的着色器对象
创建一个新的着色器对象,并且检查是否成功。
public static int compileShader(int type, String shaderCode) {
final int shaderObjectId = glCreateShader(type);
if (shaderObjectId == 0){
if (LoggerConfig.ON){
Log.d(TAG,"Could not create new shader.");
}
return 0;
}
return shaderObjectId;
}
这里的glCreateShader调用创建一个新的着色器对象,并把id存入到shaderObjectId中。
OpenGL创建shader对象并且进行检查有效性的总结:
- 首先使用一个glCreateShader()一样来创建一个对象,会返回一个int型值
- 这个整形值就是OpenGL对象的引用,想要使用就必须把整型值传回给OpenGL
- 返回值返回0,则表示创建失败。
2.上传和编译着色器代码
glShaderSource(shaderObjectId,shaderCode);
3.取出编译状态
final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId,GL_COMPILE_STATUS,compileStatus,0);
为了检查编译是否成功还是失败,首先创建一个大小为1的数组,接着调用glGetShaderiv,告诉OpenGL读取与shaderObjectId关联的编译状态,并写入到数组中去。
4.取出着色器信息日志
if (LoggerConfig.ON){
//Log.d(TAG,"Could not create new shader.");
Log.d(TAG,"Results of compiling source:"+"\n"+shaderCode+"\n:"
+ glGetShaderInfoLog(shaderObjectId));
}
return 0;
通过glGetShaderInfoLog可以获取到一个关于着色器的有用内容
5.验证编译状态并返回着色器对象ID
if (compileStatus[0]==0){
glDeleteShader(shaderObjectId);
if (LoggerConfig.ON){
Log.d(TAG,"Compilation of shader failed.");
}
return 0;
}
6.在Renderer类中编译着色器
int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource);
int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);
回顾下流程,我们创建了ShaderHelper,并加入一个用来创建、编译新着色器对象的方法,同时创建LoggerConfig,一个用来在单一代码行打开或者关闭日志的类,对于ShaderHelper:
- compileShader:这个compileShader(int type, String shaderCode)方法使用了着色器源代码和类型,typ可以代表顶点着色器或者片段着色器。如果OpenGL编译成功,就会返回着色器对象的Id,否则就会返回0。
- compileVertexShader():这个方法调用compileShader(int type, String shaderCode),使用GL_VERTEX_SHADER作为着色器类型
- compileFragmentShader:这个方法也调用了compileShader(int type, String shaderCode),使用了GL_FRAGMENT_SHADER作为着色器类型
三、将着色器链接进入OpenGL的程序
1.理解OpenGL的程序
OpenGL程序就是把一个顶点着色器和片段着色器放在一起组成单个对象,但是顶点着色器和片段着色器不一定是一对一的。
2.新建程序对象并附上着色器
final int programObjectId = glCreateProgram();
if (programObjectId == 0){
if (LoggerConfig.ON == true){
Log.d(TAG,"Could not crate new program");
}
return 0;
}
上述代码创建程序对象,接着附上着色器
//把顶点着色器和片段着色器都附加到程序对象上
glAttachShader(programObjectId,vertexShaderId);
glAttachShader(programObjectId,fragmentShaderId);
3.链接程序
glLinkProgram(programObjectId);
final int[] linkStatus = new int[1];
glGetProgramiv(programObjectId,GL_LINK_STATUS,linkStatus,0);
if (LoggerConfig.ON){
Log.d(TAG,"Results of linking program:\n"
+ glGetProgramInfoLog(programObjectId));
}
通过glLinkProgram将这些着色器联合起来,同时通过glGetProgramiv检查这个链接是成功还是错误。同时借助glGetProgramInfoLog来查看错误日志
4.检测链接状态并返回程序对象ID
if (linkStatus[0] == 0){
glDeleteProgram(programObjectId);
if (LoggerConfig.ON){
Log.d(TAG,"Linking of program failed");
}
return 0;
}
四、最后拼接
1.验证OpenGL程序的对象
/**
* 验证programObjectId是否有效
* @param programObjectId OpenGL对象的id
* @return boolean
*/
public static boolean validateProgram(int programObjectId) {
glValidateProgram(programObjectId);
final int[] validateStatus = new int[1];
glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0);
Log.d(TAG, "Results of validating program:" + validateStatus[0]
+ "\nLog:" + glGetProgramInfoLog(programObjectId));
return validateStatus[0] != 0;
}
2.获得一个uniform的位置
当OpenGL程序把着色器都链接成一个程序的时候,它实际上用一个位置编号把片段着色器中定义的每一个uniform都关联起来,利用位置编号来给着色器发送数据。
private static final String U_COLOR = "u_Color";
private int uColorLocation;
//获取uniform的位置
uColorLocation = glGetUniformLocation(program,U_COLOR);
通过为我们为定义的uniform的名字创建一个常量和一个用来容纳它在OpenGL程序对象中的位置变量,uniform的位置变量只有当程序链接成功后才能捕获到,并且位置是固定的,及时有同名的uniform,也不意味着他们有相同的位置。
我们通过glGetUniformLocation来获取它的位置。
3.获取属性的位置
//获取attr需要的名字和变量
private static final String A_POSITION = "a_Position";
private int aPostionLocation;
//获取attr的位置
aPostionLocation = glGetAttribLocation(program, A_POSITION);
4.关联属性与顶点数据的数组
//4.关联属性与顶点数据的数组
vertexData.position(0);
glVertexAttribPointer(aPostionLocation,POSITION_COPOMENT_COUNT,GL_FLOAT,
false,0,vertexData);
我们在前面创建了一个本地缓冲区,称为vertexData,并且已经对这个缓冲区赋予一些数据了,在我们告诉OpenGL ES从这个缓冲区读取数据前,要确保它从开头处读取数据,我们通过调用position(0)来保证,然后调用glVertexAttribPointer告诉OpenGL,它可以在缓冲区vertexData中找到a_Position的对应的数据,详见如下:
假如使用错误的参数,会导致严重的后果,所以需要传递一些正确的参数。
5.使能顶点数组
//5.使能顶点数组
glEnableVertexAttribArray(aPostionLocation);
五、在屏幕上绘制
glUniform4f(uColorLocation,1.0f,1.0f,1.0f,1.0f);
glDrawArrays(GL_TRIANGLES,0,6);
首先通过glUniform4f更新u_Color的值,并且为它4个分量设置好值,设置完了之后调用glDrawArrays进行绘制,第一个参数告诉OpenGL要绘制三角形,第二个参数告诉绘制的数组起点,第三个告诉OpenGL一共读取六个顶点。
六、总结:
这篇文章主要是展示怎样去定义顶点坐标,并将其复制到本地内存中,让OpenGL去读取它,同时了解了着色器的作用,如何用代码去实现着色器,包括着色器的创建、编译以及同OpenGL程序对象链接起来,顶点着色器内部的属性变量同顶点属性数组关联起来,从而在屏幕上显示出东西,基本上完成OpenGL的绘制过程,当然还有其他复杂的东西,下期再讲。