我的视频课程:《Android C++ OpenGL 教程》
在前一篇博客我们知道了Android中OpenGL ES是什么,然后知道了怎么搭建一个OpenGL ES的运行环境,现在我们就来开始绘制我们自己想要的图形了(绘制图片会在后面讲解,因为绘制图形是绘制图片的基础),我们最先开始绘制一个三角形,因为三角形是很多图形的基础。
一、顶点坐标系
在绘制之前,我们需要先了解Android中OpenGL ES的顶点坐标系是怎样的,如图:
其中:中心坐标(0,0)就是我们手机屏幕的中心,然后到最左边是(-1,0)、最右边是(1,0)、最上边是(0,1)、最下边是(0,-1)这样就把我们的手机屏幕分成了一个中心坐标为(0,0)上下左右长度分别为1的矩形。不管我们的手机(具体来讲是我们的GLSurfaceView)的大小是多少,都会映射到这个矩形中。这也是OpenGL中归一化的处理方式,什么都不管,反正都必须映射到这个范围内就对了。
二、设置绘制的三角形所需要的三个顶点
因为我们绘制的是三角形,所以我们需要三个顶点来确定我们的三角形的位置,比如我们要绘制如图的三角形:
由图我们知道要绘制的三角形的三个顶点坐标分别为:(-1,0)、(0,1)和(1,0)
三、本地化三角形顶点
所谓本地化就是跳出java VM(Java虚拟机)的约束(垃圾回收)范围,使我们的顶点在程序运行时一直都有自己分配的内存地址,不会因为java的GC而把顶点内存地址给回收掉,导致顶点不存在,从而引起OpenGL找不到顶点位置等错误,所以在OpenGL中我们需要把顶点坐标给本地化。
这里我们就需要分2个步骤来完成顶点的本地化:
3.1、用float数组来存储我们的顶点坐标,因为顶点坐标范围是在(-1f~1f)之间的所有小数都可以,所以我们先创建顶点数组:
float[] vertexData = {
-1.0f, 0.0f,//三角形左下角
0.0f, 1.0f,//三角形右下角
1.0f, 0.0f//三角形顶点
};
3.2、然后根据顶点数组分配底层内存地址,因为需要本地化,所以就和c/c++一样需要我们手动分配内存地址,这里用到了ByteBuffer这个类:
FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)//分配内存空间(单位字节)
.order(ByteOrder.nativeOrder())//内存bit的排序方式和本地机器一致
.asFloatBuffer()//转换成float的buffer,因为我们是放float类型的顶点
.put(vertexData);//把数据放入内存中
vertexBuffer.position(0);//把索引指针指向开头位置
首先用allocateDirect分配内存大小,其大小为float数组长度乘以每一个float的大小,而float占4个字节,所以就是:vertexData.length * 4;然后设置其在内存中的对齐方式(分大端和小端对其)这里就和本地对齐方式一样:order(ByteOrder.nativeOrder());然后设置是存储float类型数据的内存空间:asFloatBuffer();最后再用float数组(vertexData)初始化内存中的数据:put(vertexData)。为了能从开头访问这块内存地址,还需要设置其position为0:vertexBuffer.position(0);。
这样我们的三角形的顶点内存地址就已经分配好了,并且做了本地持久化。
四、开始顶点着色器的编写(shader)
OpenGL的操作需要我们自己编写着色器(shader)程序给它,然后它会用GPU执行这个着色器程序,最终反馈执行结果给我们。我们用glsl语言来编写着色器程序,其语法方式和c语言类似,这里就不展开讲了,当学会了OpenGL编程后,可以自己学习glsl语法,然后就可以根据自己的能力编写“吊炸天”的效果了。
4.1、编写顶点着色器(vertex_shader.glsl),位置我们放在:res/raw/路径下
attribute vec4 av_Position;//用于在java代码中获取的属性
void main(){
gl_Position = av_Position;//gl_Position是内置变量,opengl绘制顶点就是根据它的值绘制的,所以我们需要把我们自己的值赋值给它。
}
这段shader很短,但是足够说明OpenGL中顶点坐标的使用方法了:
首先解释一下attribute vec4 av_Position这句的意思:
attribute是表示顶点属性的,只能用在顶点坐标里面,然后在应用程序(java代码)中可以获取其变量,然后为其赋值。vec4是一个包含4个值(x,y,z,w)的向量,x和y表示2d平面,加上z就是3d的图像了,最后的w是摄像机的距离,因为我们绘制的是2d图形,所以最后z和w的值可以不用管,OpenGL会有默认值1。所以这句话的意思就是:声明了一个名字叫av_Position的包含4个向量的attribute类型的变量,用于我们在java代码中获取并把我们的顶点(FloatBuffer vertexBuffer)值赋值给它。这样OpenGL执行这段着色器代码(程序)时,就有了具体的顶点数据,就会在相应的顶点之间绘制图形(我们定义的三角形)了。
然后void main(){}是程序中函数,和c中是一样的。
最后是gl_Position = av_Position,这里的gl_Position是glsl中内置的最终顶点变量,我们要绘制的顶点就是传递给它。这段代码就是把我们设置的顶点数据传递给gl_Position,然后OpenGL就知道在哪里绘制顶点了。
五、片元着色器程序编写(shader)
上面我们只是写了我们的三角形绘制顶点的着色器程序,而三角形是 “顶点+颜色(样式)” 组成的,所以我们就需要告诉OpenGL我们绘制的三角形是什么颜色的,这就需要片元着色器程序了。
- 编写片元着色器程序(fragment_shader.glsl)
precision mediump float;//声明用中等精度的float
uniform vec4 af_Color;//用于在java层传递颜色数据
void main(){
gl_FragColor = af_Color;//gl_FragColor内置变量,opengl渲染的颜色就是获取的它的值,这里我们把我们自己的值赋值给它。
}
这里的precision mediump float 表明用中等精度的float类型来保存变量,其他还可以设置高精度和低精度,一般中等精度就可以了,精度不同,执行的效率也会有差别。
然后这里是用了uniform这个类型来声明变量,uniform是用于应用程序(java代码中)向顶点和片元着色器传递数据,和attribute的区别在于,attribute是只能用在顶点着色器程序中,并且它里面包含的是具体的顶点的数据,每次执行时都需要从顶点内存里面获取新的值,而uniform始终都是用同一个变量。vec4 af_Color也是声明一个4个分量的变量af_Color,这个里面保存的是颜色的值了(rgba四个分量)。
最后gl_FragColor也是glsl中内置的变量,用于最终渲染颜色的赋值,这里我们就把我们自己的颜色赋值给gl_FragColor就行,是操作每一个像素的rgba。
通过第四步和第五步,我们已经设置好了顶点和颜色的着色器程序,接下来就可以让OpenGL加载这2个着色器程序,然后执行里面的代码,最终绘制出我们想要的图形(三角形)了。
六、加载并编译着色器语言
6.1、通过GLES20.glCreateShader(shaderType)创建(顶点或片元)类型的代码程序,如:
创建顶点类型的着色器代码程序:
int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)
片元传入:GLES20.GL_FRAGMENT_SHADER。
6.2、加载shader源码并编译shader
GLES20.glShaderSource(shader, source);//这里更加我们创建的类型加载相应类型的着色器(如:顶点类型)
GLES20.glCompileShader(shader);//编译我们自己写的着色器代码程序
6.3、实际创建并返回一个渲染程序(program)
int program = GLES20.glCreateProgram();//创建一个program程序
6.4、将着色器程序添加到渲染程序中
GLES20.glAttachShader(program, vertexShader);//把顶点着色器加入program程序中
GLES20.glAttachShader(program, fragmentShader);//把片元着色器加入program程序中
6.5、链接源程序
GLES20.glLinkProgram(program);//最终链接顶点和片元着色器,后面在program中就可以访问顶点和片元着色器里面的属性了。
通过上面5个步骤,我们就将用glsl写的着色器程序变成了我们可以在应用程序(java代码)中可以获取里面的变量并操作变量的具体的程序(program)了。
七、接下来就是传递顶点坐标和颜色值给着色器程序:
7.1、获取顶点变量
int aPositionHandl = GLES20.glGetAttribLocation(programId, "av_Position");//获取顶点属性,后面会给它赋值(即:把我们的顶点赋值给它)
这里的av_Position就是顶点着色器中的attribute变量,后续操作就可以用返回值aPositionHandl这个句柄了。
7.2、获取颜色变量
int afColor = GLES20.glGetUniformLocation(program, "af_Color");//获取片元变量,后面可以通过它设置片元要显示的颜色。
这里的af_Color就是片元着色器中的uniform变量。后面可以对它赋值来改变三角形的颜色。
7.3、开始执行着色器程序
GLES20.glUseProgram(programId);//开始绘制之前,先设置使用当前programId这个程序。
7.4、首先激活顶点属性
GLES20.glEnableVertexAttribArray(aPositionHandl);//激活顶点属性数组,激活后才能对它赋值
7.4、向顶点属性传递顶点数组的值
GLES20.glVertexAttribPointer(aPositionHandl, 2, GLES20.GL_FLOAT, false, 8,
vertexBuffer);//现在就是把我们的顶点vertexBuffer赋值给顶点着色器里面的变量。
第一参数就是我们的顶点属性的句柄
第二个参数是我们用的几个分量表示的一个点,这里用的(x,y)2个分量,所以就填入2
第三个参数表示顶点的数据类型,因为我们用的float类型,所以就填入GL_FLOAT类型
第四个参数是是否做归一化处理,如果我们的坐标不在(-1,1)之间,就需要,由于我们的坐标是在(-1,1)之间,所以不需要,填入false
第五个参数是每个点所占空间大小,因为是(x,y)2个点,每个点是4个字节,所以一个点占8个空间大小,这个设置好后,OpenGL才知道8个字节表示一个点,就能按照这个规则,依次取出所有的点的值。
第六个参数就是OpenGL要从哪个内存中取出这些点的数据。
7.4、最后绘制这些顶点
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);//绘制三角形,从我们的顶点数组里面第0个位置开始,绘制顶点的个数为3(因为三角形只有三个顶点)
这里是以顶点数组的方式来绘制图形
第一个参数表示绘制的方式:GLES20.GL_TRIANGLES,单个三角形的方式,还有其他方式,我们后面会讲解。
第二个参数表示从哪个位置开始绘制,因为顶点坐标里面只有3个坐标点,所以从0开始绘制。
第三个参数表示绘制多少个点,这里显然绘制三个点。
以上就是OpenGL的执行过程:
坐标点(顶点或纹理)->编写着色器程序->加载着色器程序并编译生成program->获取program中的变量->program变量赋值->最终绘制。
注:在加载着色器程序的时候还需要检查是否加载成功等结果,还有绘制图形时的清屏操作会在实例代码中给出完整的例子。
八、核心代码
8.1、加载着色器程序生成program(WlShaderUtil.java)
package com.ywl5320.opengldemo;
import android.content.Context;
import android.opengl.GLES20;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class WlShaderUtil {
public static String readRawTxt(Context context, int rawId) {
InputStream inputStream = context.getResources().openRawResource(rawId);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuffer sb = new StringBuffer();
String line;
try
{
while((line = reader.readLine()) != null)
{
sb.append(line).append("\n");
}
reader.close();
}
catch (Exception e)
{
e.printStackTrace();
}
return sb.toString();
}
public static int loadShader(int shaderType, String source)
{
int shader = GLES20.glCreateShader(shaderType);
if(shader != 0)
{
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
int[] compile = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compile, 0);
if(compile[0] != GLES20.GL_TRUE)
{
Log.d("ywl5320", "shader compile error");
GLES20.glDeleteShader(shader);
shader = 0;
}
}
return shader;
}
public static int createProgram(String vertexSource, String fragmentSource)
{
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if(vertexShader == 0)
{
return 0;
}
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if(fragmentShader == 0)
{
return 0;
}
int program = GLES20.glCreateProgram();
if(program != 0)
{
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
int[] linsStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linsStatus, 0);
if(linsStatus[0] != GLES20.GL_TRUE)
{
Log.d("ywl5320", "link program error");
GLES20.glDeleteProgram(program);
program = 0;
}
}
return program;
}
}
8.2、WlRender.java
package com.ywl5320.opengldemo;
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class WlRender implements GLSurfaceView.Renderer{
private Context context;
private final float[] vertexData ={
-1f, 0f,
0f, 1f,
1f, 0f
};
private FloatBuffer vertexBuffer;
private int program;
private int avPosition;
private int afColor;
public WlRender(Context context)
{
this.context = context;
vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
vertexBuffer.position(0);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
String vertexSource = WlShaderUtil.readRawTxt(context, R.raw.vertex_shader);
String fragmentSource = WlShaderUtil.readRawTxt(context, R.raw.fragment_shader);
program = WlShaderUtil.createProgram(vertexSource, fragmentSource);
if(program > 0)
{
avPosition = GLES20.glGetAttribLocation(program, "av_Position");
afColor = GLES20.glGetUniformLocation(program, "af_Color");
}
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
GLES20.glUseProgram(program);
GLES20.glUniform4f(afColor, 1f, 0f, 0f, 1f);
GLES20.glEnableVertexAttribArray(avPosition);
GLES20.glVertexAttribPointer(avPosition, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}
}
这里用到了通过uniform类型变量传值的方式:
GLES20.glUniform4f(afColor, 1f, 0f, 0f, 1f);//分别设置片元变量的rgba四个值(前面的glUniform4f:表示这是uniform类型的变量的4个float类型的值)
给片元着色器中的颜色变量afColor设置argb的值为:(1f, 0f, 0f,1f)——红色
然后其他的代码和上一篇博客一样。
九、最终效果如下:
十、总结
通过本篇文章,我们了解了Android中OpenGL ES的顶点坐标加载过程,在OpenGL ES中最复杂的图像就是三角形,其他任意图像都可以通过三角形来组合出来,这也为我们后续的功能打下了基础,务必好好理解里面的流程和逻辑。
GitHub:Android-OpenGL-ES