Android OpenGL ES(三)-平面图形
上一章Android OpenGL ES(二)-正交投影
的学习,我们已经能够画正常的图片图形了,这章我们会继续来绘制正方形和圆的这样的平面图形和绘制纹理。
平面图形
前两章,其实我们已经明白了绘制平面图形的套路了。
接下来我们按照套路继续画其他的图形。
正方形
因为OpenGL只提供给我们画三角形的方式,所以想要正方形的话,其实就是画两个三角形拼在一起。
一:使用GL_TRIANGLE_STRIP
的方式
- 绘制方式-三角形带
GL_TRIANGLE_STRIP.png
如上图。使用GLES20.GL_TRIANGLE_STRIP
可以在定义3个点的确定三角形的情况下,每多一个点,就多绘制一个三角形。这种方式需要注意数组中点的顺序。
1. 修改矩阵的数组。将其改成正方形
按照上图中的顺序定制我们的矩阵数组。
private static float SQUARE_COLOR_COORDS[] = {
//Order of coordinates: X, Y, Z, R,G,B,
-0.5f, 0.5f, 0.0f, 1.f, 0f, 0f, // 0.top left RED
-0.5f, -0.5f, 0.0f, 0.f, 0f, 1f, // 1.bottom left Blue
0.5f, 0.5f, 0.0f, 1f, 1f, 1f, // 3.top right WHITE
0.5f, -0.5f, 0.0f, 0.f, 1f, 0f, // 2.bottom right GREEN
};
2. 调用onDrawFrame
内调用我们的代码
//在OnDrawFrame中进行绘制
@Override
public void onDrawFrame(GL10 gl) {
super.onDrawFrame(gl);
//传递给着色器
GLES20.glUniformMatrix4fv(uMatrix, 1, false, mProjectionMatrix, 0);
//1.使用三角形带的方式
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0, VERTEX_COUNT);
}
3. 结果
二:使用GL_TRIANGLES_FAN
的方式
将传入的顶点作为扇面绘制,ABCDEF绘制ABC、ACD、ADE、AEF四个三角形.
1. 更新正方形的矩阵数组
//0,1,2 是一个三角形 //0,2,3 是一个三角形
private static float SQUARE_COLOR_COORDS[] = {
//Order of coordinates: X, Y, Z, R,G,B,
-0.5f, 0.5f, 0.0f, 1.f, 0f, 0f, // 0.top left RED
0.5f, 0.5f, 0.0f, 1f, 1f, 1f, // 1.top right WHITE
0.5f, -0.5f, 0.0f, 0.f, 1f, 0f, // 2.bottom right GREEN
-0.5f, -0.5f, 0.0f, 0.f, 0f, 1f, // 3.bottom left Blue
};
2. 更新绘制的方法
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, VERTEX_COUNT);
3. 结果
三:使用GL_TRIANGLES
和顶点矩阵数组加位置矩阵数组的方式
这种方法就是根据我们的数组,自己来定义绘制的顺序来,完成绘制两个三角形完成正方形的任务。
1. 更新数组的数据
//正方形的点1
private static float SQUARE_COLOR_COORDS[] = {
//Order of coordinates: X, Y, Z, R,G,B,
-0.5f, 0.5f, 0.0f, 1.f, 0f, 0f, // 0.top left RED
-0.5f, -0.5f, 0.0f, 0.f, 0f, 1f, // 1.bottom right Blue
0.5f, 0.5f, 0.0f, 1f, 1f, 1f, // 3.top right WHITE
0.5f, -0.5f, 0.0f, 0.f, 1f, 0f, // 2.bottom left GREEN
};
/*
创建一个遍历的点的顺序.
1,0,2,1 一个三角形
1,2,3,1 另一个三角
*/
private static short SQUARE_INDEX[] = {
1 , 0, 2, 1, 2, 3
};
2. 添加indexBuffer
我们同样需要为我们新添加的位置数组的分配内存,让OpenGL来读取。
// /*
// 新增-为位置添加内存空间
// */
mIndexBuffer = ByteBuffer
.allocateDirect(SQUARE_INDEX.length * Constant.BYTES_PER_SHORT)
.order(ByteOrder.nativeOrder())
.asShortBuffer()
.put(SQUARE_INDEX);
mIndexBuffer.position(0);
3. 修改绘制的方法
//使用indexBuffer的方式
GLES20.glDrawElements(GLES20.GL_TRIANGLES, SQUARE_INDEX.length, GLES20.GL_UNSIGNED_SHORT, mIndexBuffer);
4. 结果
正方形小节
这里我们一共使用三种方式进行绘制
-
GL_TRIANGLES
将传入的顶点作为单独的三角形绘制,ABCDEF绘制ABC,DEF两个三角形 -
GL_TRIANGLE_FAN
将传入的顶点作为扇面绘制,ABCDEF绘制ABC、ACD、ADE、AEF四个三角形 -
GL_TRIANGLE_STRIP
将传入的顶点作为三角条带绘制,ABCDEF绘制ABC,BCD,CDE,DEF四个三角形
圆
绘制圆形。是通过绘制切分的三角形来形成的。三角形切分的越细,越接近圆。
1.更新代表圆形的矩阵数组
-
计算绘制圆所需要的点
因为是通过切分的方式来构成圆。所以我们需要先确定需要一个圆需要多少个点来绘制。/* 需要的点的个数等于 1(圆心)+切分圆的点数+1(为了闭合,切分圆的起点和终点,需要重复一次) */ private int getCircleVertexNum(int numbersRoundCircle) { return +1 + numbersRoundCircle + 1; }
通过上面画正方形的经历,我们知道了
GL_TRIANGLE_FAN
绘制扇形的顺序。所以我们需要
a. 先传入一个圆形。
b. 然后按照我们的切分点开始绘制若干个三角形。最后一个三角形闭合,
c. 还需要重复一次起点和终点。 -
计算绘制圆上切分点的坐标
image.png
按照上图和我们的切分点,计算每一个点的坐标,放到数组里面。构造出来的数组属性是X,Y,Z,R,G,B
。
private float[] createCircleCoords(Circle circle, int numbersRoundCircle) {
//先计算总共需要多少个点
int needNumber = getCircleVertexNum(numbersRoundCircle);
//创建数组
float[] circleColorCoord = new float[needNumber * TOTAL_COMPONENT_COUNT];
//接下来给每个点分配数据
//对每一组点进行赋值
for (int numberIndex = 0; numberIndex < needNumber; numberIndex++) {
int indexOffset = numberIndex * TOTAL_COMPONENT_COUNT;
if (numberIndex == 0) { //第一个点。就是圆心
//位置
circleColorCoord[indexOffset] = circle.center.x;
circleColorCoord[indexOffset + 1] = circle.center.y;
circleColorCoord[indexOffset + 2] = circle.center.z;
//下面是颜色。给一个白色
circleColorCoord[indexOffset + 3] = 1.f;
circleColorCoord[indexOffset + 4] = 1.f;
circleColorCoord[indexOffset + 5] = 1.f;
} else if (numberIndex < needNumber - 1) { //切分圆的点
//需要根据半径。中心点。来结算
int angleIndex = numberIndex - 1;
float angleRadius = (float) (((float) angleIndex / (float) numbersRoundCircle) * Math.PI * 2f);
float centerX = circle.center.x;
float centerY = circle.center.y;
float centerZ = circle.center.z;
float radius = circle.radius;
float tempX = (float) (centerX + radius * Math.cos(angleRadius));
float tempY = (float) (centerY + radius * Math.sin(angleRadius));
float temp = centerZ + 0;
//位置
circleColorCoord[indexOffset] = tempX;
circleColorCoord[indexOffset + 1] = tempY;
circleColorCoord[indexOffset + 2] = temp;
//下面是颜色。给一个白色
circleColorCoord[indexOffset + 3] = (float) (1.f* Math.cos(angleRadius));
circleColorCoord[indexOffset + 4] = (float) (1.f* Math.sin(angleRadius));
circleColorCoord[indexOffset + 5] = 1.f;
} else { //最后一个点了。重复数据中的二组的位置
//位置.index为1的点
int copyTargetIndex = 1;
//复制点
circleColorCoord[indexOffset] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT];
circleColorCoord[indexOffset + 1] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT + 1];
circleColorCoord[indexOffset + 2] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT + 2];
circleColorCoord[indexOffset + 3] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT + 3];
circleColorCoord[indexOffset + 4] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT + 4];
circleColorCoord[indexOffset + 5] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT + 5];
}
}
return circleColorCoord;
}
这样就更新好我们的矩阵数组了。
2. 绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, VERTEX_COUNT);
3. 结果
小节
画圆就是熟练的应用了正方形的经验。
纹理
除了平面图形。我们还能绘制我们自己的2D纹理。
OpenGL中的纹理可以用来表示图像。照片甚至数学算法生成的分形数据。
每个二维的纹理都由许多小的纹理元素(text1)组成。它们是小块的数据。
理解纹理的坐标
每个二维的纹理都有自己的坐标空间。其范围是从一个拐角(0,0)到另外一个拐角(1,1)。一个纬度叫做S,而另一个拐角叫做T.
二维纹理坐标.png对比Android系统的Y轴
android系统中的y轴也是向下的。但是纹理坐标是向上的。
纹理的大小
在标准的OpenGL ES 2.0中,纹理不必是正方形。但是每个纬度都应该是2的幂。POT纹理适用于各种情况。
纹理也有一个最大值,但是会根据不同的实现而变化。
理解纹理过滤模式
当我们渲染表面上绘制一个纹理时,那个纹理的纹理元素可能无法精确的映射到OpenGL生成的片段上。由两种情况:缩小或者放大。通过纹理过滤(texture filtering),来控制产生的效果。
- 放大
一个纹理放到多个片段时,就会放大。 - 缩小
当我们尽力把几个纹理元素放到一个片段时,缩小发生
最近邻过滤
最近邻过滤.png- 缺点
放大时,锯齿相当的明显。
双线性过滤
双线性过滤会进行插值。
双线性过滤.png
- 优点
适合于放大的情况 - 缺点
不适合缩小。因为OpenGL的双线性过滤只给每个片段使用四个纹理元素。我们会失去很多细节。
MIP 贴图
可以生成一组优化过的不同大小的纹理。当生成这组纹理的时候。OpenGL会使用所有的纹理元素生成每个级别的纹理,当过滤纹理时,还要确保所有的纹理元素能被使用。在渲染时,会更具每个片段的纹理元素数量为每个片段选择最合适的级别。
- 缺点
会占用很多内存,但是有点 - 优点
同时渲染也会更快。是因为在较小的界别的纹理在GPU的纹理缓存中占用较少的空间。
三线性过滤
如果OpenGL在不同的MIP贴图级别中来回切换。当我们用双线性过滤使用MIP贴图时,再起渲染的场景中,在不同级别的切换时,就会看到明显的跳跃。我们可以切换到三线性过滤。告诉OpenGL 两个最邻近的MIP贴图级别之间也要插值。这样每个片段总共要使用8个纹理元素插值。有助于消除每个MIP贴图级别中间的过渡。得到一个更平滑的图像。
过滤模式总结
过滤模式.png纹理绘制代码
1. 更新着色器
- 顶点着色器
在顶点着色器中添加attribute
的locationa_TextureCoordinates
(纹理的坐标)属性和varying
型的变量v_TextureCoordinates
attribute vec4 a_Position; //添加了一个 a_TextureCoordinates ,因为他有两个分量。S坐标和T 坐标,所以定义为vec2. attribute vec2 a_TextureCoordinates; uniform mat4 u_Matrix; //然后把坐标传递给被插值的varying varying vec2 v_TextureCoordinates; void main(){ gl_Position=u_Matrix*a_Position; v_TextureCoordinates=a_TextureCoordinates; }
- 片段着色器
在片段着色器需要添加sampler2D
采样器u_TextureUnit
,并应用v_TextureCoordinates
纹理坐标系precision mediump float; //在片元着色器这里添加这个 sampler2D 表示我们要添加2D贴图 uniform sampler2D u_TextureUnit; varying vec2 v_TextureCoordinates; void main(){ //渲染2D纹理,交给fragColor gl_FragColor=texture2D(u_TextureUnit,v_TextureCoordinates); }
2. 更新代码
-
更新矩阵数组
在这里,我们把OpenGL代表屏幕的X,y坐标和代表纹理的S.T坐标都放到数组中。
这里需要注意的是,我们从上面知道。Android屏幕的Y坐标
是向下的和而纹理中的T坐标
是向上的,所以表达同一个点的Y坐标
和T坐标
是相反的!//顶点的坐标系 private static float TEXTURE_COORDS[] = { //Order of coordinates: X, Y,S,T -1.0f, 1.0f, 0.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, //bottom left 1.0f, 1.0f, 1.0f, 0.0f, // top right 1.0f, -1.0f, 1.0f, 1.0f, // bottom right };
-
将数组传递给OpenGL
private static final String A_COORDINATE = "a_TextureCoordinates"; private static final String U_TEXTURE = "u_TextureUnit"; //取到这个属性值和应用。偏移到我们的ST坐标使用。 int aCoordinate = GLES20.glGetAttribLocation(mProgramObjectId, A_COORDINATE); mVertexFloatBuffer.position(COORDS_PER_VERTEX); GLES20.glVertexAttribPointer( aCoordinate, COORDS_PER_ST, GLES20.GL_FLOAT, false, STRIDE, mVertexFloatBuffer); GLES20.glEnableVertexAttribArray(aCoordinate); uTexture = GLES20.glGetUniformLocation(mProgramObjectId, U_TEXTURE); //生成纹理ID mTextureId = createTexture();
-
生成纹理ID
纹理Id就相当于纹理内容在内存中索引。我们后面可以通过这个id,继续操作我们绑定的纹理。
这里需要注意的是,图片的解码,需要将scale属性为false。如果不做这个操作的话,解码出来的图片,是放大了很多倍的。而我们的ST坐标,只能对应到一个很小的部分。
这里的代码也是经典的流程。注意注释中提到的调用的流程。//使用mip贴图来生成纹理,相当于将图片复制到openGL里面? private int createTexture() { final Bitmap mBitmap; BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = false; //加载Bitmap mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher, options); //保存到textureObjectId int[] textureObjectId = new int[1]; if (mBitmap != null && !mBitmap.isRecycled()) { //生成一个纹理,保存到这个数组中 GLES20.glGenTextures(1, textureObjectId, 0); //绑定GL_TEXTURE_2D GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectId[0]); //设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色 GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); //设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色 GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); //设置环绕方向S,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合 GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); //设置环绕方向T,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合 GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); //根据以上指定的参数,生成一个2D纹理 GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0); //回收释放 mBitmap.recycle(); //因为我们已经复制成功了。所以就进行解除绑定。防止修改 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); return textureObjectId[0]; } return 0; }
-
绘制
在onDrawFrame
方法中,重新激活和绑定。然后调用画出定义的矩阵就可以了//绑定和激活纹理 //因为我们生成了MIP,放到了GL_TEXTURE0 中,所以重新激活纹理 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); //重新去半丁纹理 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId); //设置纹理的坐标 GLES20.glUniform1i(uTexture, 0); //绘制三角形. //draw arrays的几种方式 GL_TRIANGLES三角形 GL_TRIANGLE_STRIP三角形带的方式(开始的3个点描述一个三角形,后面每多一个点,多一个三角形) GL_TRIANGLE_FAN扇形(可以描述圆形) GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT);
-
结果
image.png
纹理小节
- 这里需要注意的还是纹理的整体过程。后续的使用中,还是会重复使用这一的流程。
- 纹理的Id,在这里就是代表我们复制到内存中的
bitmap
- 绘制的时候,重新绑定绘制就可以了
总结
总结一下,我们从这第一章节的内容了解到了下面这些使用的知识点:
- 绘制正方形的多种方式和绘制圆的方式。熟悉了GL的绘制方法。
- 纹理的基础概念
- 绘制一个纹理的基本套路
下一章开始,我们会进入Android的相机和OpenGL的结合。
相机部分结束之后,才会到三维图形的部分。
整体的代码位置:https://github.com/deepsadness/OpenGLDemo5
系列文章地址
Android OpenGL ES(一)-开始描绘一个平面三角形
Android OpenGL ES(二)-正交投影
Android OpenGL ES(三)-平面图形
Android OpenGL ES(四)-为平面图添加滤镜
Android OpenGL ES(五)-结合相机进行预览/录制及添加滤镜
Android OpenGL ES(六) - 将输入源换成视频
Android OpenGL ES(七) - 生成抖音照片电影