目录
1.GLSurfaceView和主线程,渲染线程的关系
渲染线程与主线程之间的通信可以用如下方法:
在主线程中的 GLSurfaceView 实例可以调用queueEvent( )方法传递一个 Runnable 给后台渲染线程,渲染线程可以调用 Activity 的 runOnUIThread()来传递事件 (event) 给主线程。
GLSurfaceView和主线程,渲染线程的关系如下:
渲染线程
- 定义:
GLSurfaceView
内部维护了一个专门的渲染线程(通常称为GL线程或EGL线程),该线程专门用于OpenGL ES的渲染工作。 - 职责:
- 执行OpenGL ES的渲染命令,绘制图形和图像。
- 管理OpenGL ES的上下文(Context)和表面(Surface)。
- 在需要时,根据
Renderer
的指示重新绘制场景。
GLSurfaceView
- 定义:
GLSurfaceView
是Android提供的一个特殊View,用于在Android应用中嵌入OpenGL ES内容。它继承自SurfaceView
,并实现了OpenGL ES的渲染功能。 - 与主线程的关系:
GLSurfaceView
的实例化、布局和生命周期管理(如onResume
、onPause
)通常在主线程中进行。- 主线程可以通过调用
GLSurfaceView
的queueEvent
等方法,将需要在渲染线程中执行的任务放入队列中。
- 与渲染线程的关系:
GLSurfaceView
内部维护了一个渲染线程,专门用于OpenGL ES的渲染工作。- 开发者通过实现
GLSurfaceView.Renderer
接口,并调用GLSurfaceView
的setRenderer
方法,将自定义的渲染器设置给GLSurfaceView
。 - 渲染线程会不断调用渲染器的
onSurfaceCreated
、onSurfaceChanged
和onDrawFrame
等方法,以执行OpenGL ES的渲染命令。
关系总结
- 主线程与GLSurfaceView:主线程负责
GLSurfaceView
的实例化、布局和生命周期管理,并通过特定方法(如queueEvent
)与渲染线程进行通信。 - 渲染线程与GLSurfaceView:渲染线程是
GLSurfaceView
内部维护的,专门用于执行OpenGL ES渲染命令的线程。渲染线程根据渲染器的指示,在需要时重新绘制场景。 - 主线程与渲染线程:两者通过
GLSurfaceView
提供的机制(如queueEvent
)进行通信,确保UI更新和渲染工作能够协调进行,避免阻塞主线程或造成渲染延迟。
通过这种方式,GLSurfaceView
有效地将OpenGL ES的渲染工作与Android应用的UI绘制和用户交互任务分离开来,提高了应用的性能和响应速度。
2.顶点数组中元素排列顺序
当定义三角形的时候,总是以逆时针的顺序排列项点; 这称为卷曲顺序 ( winding order)。因为在任何地方都使用这种一致的卷曲顺序,可以优化性能: 使用卷曲顺序可以指出一个三角形属于任何给定物体的前面或者后面,OpenGL 可以忽略那些无论如何都无法被看到的后面的三角形。
3.OpenGL中物体构成
无论何时,如果想表示一个 OpenGL 中的物体,都要考虑如何用点、直线及三角形把它组合出来。
4.使Java层数据可以被OpenGL存取
在java层完成顶点的定义后,但是,在OpenGL可以存取它们之前,我们仍然需要完成另外一步。主要的问题是这些代码运行的环境与OpenGL运行的环境使用了不同的语言,我们需要理解如下两个主要的概念。
(1)当我们在模拟器或者设备上编译和运行Java代码的时候,它并不是直接运行在硬件上的;相反,它运行在一个特殊的环境上,即Dalvik虚拟机(Dalvik virtualmachine);运行在虚拟机上的代码不能直接访问本地环境(nativeenvironment),除非通过特定的API。
(2)Davik虚拟机还使用了垃圾回收(garbage collection)机制。这意味着,当虚拟机检测到一个变量、对象或者其他内存片段不再被使用时,就会把这些内存释放掉以备重用;它也能腾挪内存以提高空间使用效率。本地环境并不是这样工作的,它不期望内存块会被移来移去或者被自动释放。Android之所以这样设计,是因为开发者在开发程序的时候不必关心特定的CPU或者机器架构,也不必关心底层的内存管理。这通常都能工作得很好,除非要与本地系统交互比如OpenGL。OpenGL作为本地系统库直接运行在硬件上;没有虚拟机,也没有垃圾回收或内存压缩。
从 Java 调用本地代码
Dalvik方案是Android的主要特点之一,但是,如果代码运行在虚拟机内部,那它怎么与OpenGL通信呢?有两种技术,第一种技术是使用Java本地接口(JNI),这个技术已经由 Android 软件开发包提供了;当调用android.opengl.GLES20包里的方法时,软件开发包实际上就是在后台使用JNI调用本地系统库的。
把内存从 Java 堆复制到本地堆
第二种技术是改变内存分配的方式,Java 有一个特殊的类集合,它们可以分配本地内存块,并且把 Java 的数据复制到本地内存。本地内存可以被本地环境存取,而不受垃圾回收器的管控。
FloatBuffer用来在本地内存中存储数据。
FloatBuffer vertexData =ByteBuffer
.allocateDirect(tableVerticesWithTriangles.length * BYTES PER FLOAT).order(Byte0rder.native0rder()).asFloatBuffer();
让我们看一下代码的每一个部分。首先,我们使用ByteBuffer.allocateDirect()分配了一块本地内存,这块内存不会被垃圾回收器管理。这个方法需要知道要分配多少字节的内存块;因为顶点都存储在一个浮点数组里,并且每个浮点数有4个字节,所以这块内存的大小应该是tableVerticesWithTriangles.length* BYTES PER FLOAT。
5.顶点数据显示到屏幕上的过程
6.为什么使用着色器
在着色器出现之前,OpenGL只能使用一个固定的方法集合控制很少而有限的事情比如场景里有多少光线或者加多少雾;这些固定的 API很容易使用,但是它们很难扩展。你只能实现API提供的效果,而且仅此而已;几乎不能添加如卡通着色一样的自定义效果。
随着时间的推移,底层的硬件有了很大提高;设计OpenGL的人意识到这些API需要演进,并跟上这些变化。在OpenGLES2.0里,他们使用着色器加入了可编程API;为了保持简洁,他们把那些固定的API完全删除了,因此,用户必须使用着色器。现在用着色器控制每个顶点应该如何画到屏幕上,也控制所有点、直线和三角形上的每个片段应该如何绘制;这打开了一个新的、充满了无限可能的新世界。现在可以按每个像素实现光照和其他优美的效果,如卡通着色。只要可以用着色器语言表达出来,就可以加入任何理想的自定义效果。
7. OpenGL 怎样平滑地从一个点向另外一个点混合颜色
线性插值
直线:长度做线性插值
三角形:面积做线性插值
直线颜色线性插值,示例如下
直线起始点坐标(-0.5,0),终点坐标(0.5,0)
片段着色器代码如下:
precision mediump float;
varying vec4 v_Color;
void main(){
if(gl_FragCoord.x>-0.5 && gl_FragCoord.x < 0.5){
gl_FragColor.r = v_Color.r*(gl_FragCoord.x +0.5);
gl_FragColor.g = v_Color.g*(gl_FragCoord.x +0.5);
gl_FragColor.b = v_Color.b*(gl_FragCoord.x +0.5);
} else {
gl_FragColor =v_Color;
}
}
绘制直线如下:
glDrawArrays(GL_LINES, 18, 2)
8.GL_TRIANGLE_FAN三角形扇的原理介绍
GL_TRIANGLE_FAN三角形扇
是OpenGL中用于绘制三角形的一种模式。在这种模式下,第一个顶点被作为公共中心点,之后每个新加入的顶点与前一次添加的最后两个顶点以及公共中心点一起定义一个新的三角形。也就是说,每增加一个新顶点,都会生成一个新的扇形区域,并且所有三角形都共享这个公共中心点。
具体来说,当你定义了一系列的顶点,OpenGL会按照以下方式绘制三角形:第一个三角形由公共中心点、第二个顶点和第三个顶点构成;第二个三角形由公共中心点、第三个顶点和第四个顶点构成;依此类推,每增加一个顶点,都会与公共中心点和前一个顶点形成一个新的三角形。
这种模式非常适合用于绘制扇面或圆盘等具有公共中心区域的图形。例如,你可以使用GL_TRIANGLE_FAN
来绘制一个围绕某一点的旋转效果,或者创建一个以某点为中心的扇形区域。
需要注意的是,GL_TRIANGLE_FAN
模式的绘制效果会受到顶点顺序的影响,因此你需要确保顶点的顺序是正确的,以便生成你想要的图形。
总的来说,GL_TRIANGLE_FAN
原理基于公共中心点和新增顶点来定义并绘制一系列的三角形,实现具有公共中心区域的图形绘制。
代码示例如下:
private var tabUeVerticesWithTriangles = floatArrayOf(
//x,y,r,g,b
//桌面三角形
0.0f, 0.0f, 1.0f, 1.0f, 1.0f,//公共中心点
-0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
-0.25f, -0.5f, 0.0f, 1.0f, 0.0f,
0.0f, -0.5f, 1.0f, 0.0f, 0.0f,
0.25f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
0.5f, -0.25f, 0.0f, 1.0f, 0.0f,
0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, 0.25f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.7f, 0.7f, 0.7f,
0.25f, 0.5f, 0.0f, 1.0f, 0.0f,
0.0f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.25f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.7f, 0.7f, 0.7f,
-0.5f, 0.25f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.25f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
//桌面分割线
-0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
//木槌
0.0f, -0.25f, 0.0f, 0.0f, 1.0f,
0.0f, 0.25f, 1.0f, 0.0f, 0.0f,
//冰球
0.0f, 0.0f,0.0f,1.0f,1.0f
)
override fun onDrawFrame(gl: GL10?) {
glClear(GL_COLOR_BUFFER_BIT)
glDrawArrays(GL_TRIANGLE_FAN, 0, 18)
glDrawArrays(GL_LINES, 18, 2)
glDrawArrays(GL_POINTS, 20, 1)
glDrawArrays(GL_POINTS, 21, 1)
glDrawArrays(GL_POINTS, 22, 1)
}
运行截图如下:
三角形扇和三角形带
三角形扇(Triangle Fan)
三角形扇是一种特殊的三角形绘制模式,它从一个中心点开始,将后续的点依次与中心点和前一个点相连,形成一系列的三角形。具体来说,假设顶点序列为V0, V1, V2, ..., Vn,则三角形扇的绘制方式如下:
- 第一个顶点V0作为中心点。
- 第二个顶点V1与V0相连,形成第一个三角形的第一条边。
- 第三个顶点V2与V0和V1相连,形成第一个完整的三角形V0-V1-V2。
- 接着,V3与V0和V2相连,形成第二个三角形V0-V2-V3,以此类推,直到Vn。
这种绘制方式的特点是,所有三角形都共享同一个顶点(即中心点),且每个新添加的顶点都会与中心点和前一个顶点形成一个新的三角形。三角形扇适用于绘制具有中心对称或辐射状图案的图形。三角形扇,如下图
三角形带(Triangle Strip)
三角形带是另一种高效的三角形绘制模式,它通过连续添加顶点来形成一系列相连的三角形。假设顶点序列为V0, V1, V2, ..., Vn,则三角形带的绘制方式如下:
- 第一个和第二个顶点V0和V1形成第一个三角形的两个顶点。
- 第三个顶点V2与V0和V1相连,形成第一个完整的三角形V0-V1-V2。
- 然后,V3与V1和V2相连,形成第二个三角形V1-V2-V3(注意这里V0不再参与)。
- 以此类推,每增加一个顶点,就与前两个顶点形成一个新的三角形,直到Vn。
三角形带的特点是,每增加一个顶点,就形成一个新的三角形,且这些三角形在顶点序列上是相连的。这种绘制方式适用于绘制一系列相邻的三角形,如网格或地形等。三角形带,如下图:
9.OpenGL 如何把坐标映射到屏幕
归一化设备坐标
无论是x还是y坐标,OpenG 都会把屏幕映射到[-1,1]的范围内。这就意味着屏幕的左边对应x轴的-1,而屏幕的右边对应 +1;屏幕的底边会对应y轴的-1,而屏幕的顶边就对应 +1,如图所示。
不管屏幕是什么形状和大小,这个坐标范围都是一样的,如果我们需要在屏幕上显示任何东西,都需要在这个范围内绘制它们。
10.正交投影矩阵——弥补屏幕的宽高比
概念
在Android OpenGL中,正交投影矩阵的主要作用是定义一个视景体(Viewing Volume),即一个三维空间中的长方体区域,它决定了哪些物体应该被渲染到屏幕上。这个长方体区域被称为正交投影体。
正交投影的一个关键特性是,无论物体距离观察点的远近如何,它们在投影后的二维图像中都会保持相同的尺寸和形状。这意味着,在正交投影下,物体的大小不会因为距离的远近而改变,这与透视投影不同,透视投影中物体的大小会随着距离的远近而发生变化。
在OpenGL中,正交投影矩阵通常用于渲染那些不需要考虑透视效果的场景,比如2D游戏、UI界面等。在这些场景中,我们更关心的是物体在屏幕上的精确位置和大小,而不是它们因为距离观察点远近而产生的视觉变化。
在OpenGL中设置正交投影矩阵时,通常需要指定一个近裁剪面和一个远裁剪面,以及投影体的左右、上下边界。这些参数定义了投影体的范围和形状,从而决定了哪些物体应该被包含在渲染结果中。
总之,正交投影矩阵在Android OpenGL中扮演着定义渲染区域和保持物体形状尺寸不变的重要角色。
使用正交投影,不管多远或多近,所有的物体看上去大小总是相同的。
从虚拟坐标回到归一化设备坐标
当我们使用正交投影把虚拟坐标变换回归一化设备坐标时,实际上定义了三维世界内部的一个区域。在这个区域内的所有东西都会显示在屏幕上,而区域外的所有东西都会被裁剪掉。在下面的图像中,我们能在一个封闭的立方体内看到一个简单的场景(见图1)。当我们使用一个正交投影矩阵把这个立方体映射到屏幕上时,就会看到如图2所示的正交投影。
正交投影矩阵会把所有在左右之间、上下之间和远近之间的事物映射到归一化设备坐标中从 -1到1的范围,在这个范围内的所有事物在屏幕上都是可见的。
利用正交投影矩阵改变立方体的大小,以使我们可以在屏幕上看到或多或少的场景我们也能改变立方体的形状弥补屏幕的宽高比的影响。
正交投影矩阵方法
orthoM(float[] m, int mOffset, float left, float right, float bottom, float top, float near, float far)
参数介绍
foat[]m:目标数组,这个数组的长度至少有16个元素,这样它才能存储正交投影矩阵;
int mOffset:结果矩阵起始的偏移值;
float left;x轴的最小范围;
float right:x轴的最大范围;
float bottom:y轴的最小范围;
float top:y轴的最大范围;
floatnear:z轴的最小范围;
float far:z轴的最大范围。
x,y轴放大或缩小实现:left,right,bottom,top乘以缩放比例即可。
x,y轴平移实现:left,right,bottom,top加减平移数值。
例如:
var aspectRatio: Float = 0.0f
if (width > height) {//landscape
aspectRatio = (width.toFloat() / height.toFloat())
} else {//portrait
aspectRatio = (height.toFloat() / width.toFloat())
}
if (width > height) {//landscape水平放大
orthoM(projectionMatrix, 0, -aspectRatio*0.2f, aspectRatio*0.2f, -1f, 1f, -1f, 1f);
} else {//portrait垂直方向平移
orthoM(projectionMatrix, 0, -1f, 1f, -aspectRatio+0.5f, aspectRatio+0.5f, -1f, 1f);
}
11.透视投影矩阵
Android OpenGL的透视投影是3D图形渲染中的关键技术之一,它模拟了人类视觉系统中物体远近大小变化的感知。将现实3D模型转到2D屏幕上显示,下面是对Android OpenGL透视投影的详细解析:
基本原理
透视投影是从一个虚拟的观察点(或称为相机位置)出发,将3D空间中的物体投影到2D平面上。这个过程中,物体距离观察点越近,其在投影平面上显得越大;距离越远,则显得越小。这种投影方式能够产生强烈的空间感和立体感。
投影矩阵
实现透视投影的关键是设置透视投影矩阵。这个矩阵定义了从3D世界坐标到2D裁剪坐标的映射关系。
对宽高比和视野进行调整
如下通用的投影矩阵,它允许我们调整视野以及屏幕的宽高比。
参数说明:
a:如果我们想象一个相机拍摄的场景,这个变量就代表那个相机的焦距。焦距是由1/tan(视野/2)(tan表示正切函数)计算得到的。这个视野必须小于180度。比如,一个90度的视野,它的焦距会被设置为1/tan(90°/2),也就是1/1或者1
aspect:屏幕的宽高比,它等于宽度/高度
f:到远处平面的距离,必须是正值且大于到近处平面的距离
n:到近处平面的距离,必须是正值。比如,如果此值被设为1,那近处平面就位于一个z值为-1处
参数设置
透视投影矩阵的设置通常涉及以下几个关键参数:
- 视场角(Field of View, FOV):决定了观察者视野的大小,通常是以度为单位的夹角。较小的视场角会产生较大的放大效果,使得场景看起来更加紧凑;较大的视场角则会产生类似缩小的效果,是场景有较宽的视野。A为垂直视场角,B为水平视场角。
fov数值变化,引起场景变化。
- 宽高比(Aspect Ratio):通常是视口的宽度与高度的比值。这个参数对于保持场景的比例正确性非常重要。
- 近裁剪面(Near Clipping Plane)和远裁剪面(Far Clipping Plane):定义了视椎体的前后边界。位于这两个平面之间的物体才会被投影并渲染到屏幕上。
投影过程
- 顶点变换:首先,3D物体的顶点坐标会经过模型视图变换,从局部坐标系转换到世界坐标系,再进一步转换到相机坐标系(或称为观察坐标系)。
- 透视除法:在透视投影中,经过变换后的顶点坐标会进行透视除法。这一步是透视投影的关键,它确保了近处的物体在屏幕上显得大,远处的物体显得小。
- 裁剪与归一化:经过透视除法的坐标会进入裁剪空间,并进行裁剪测试。在裁剪过程中,位于视椎体之外的部分会被裁剪掉。剩余的坐标会经过归一化处理,最终映射到标准设备坐标系(NDC)上,准备进行后续的光栅化过程。
注意事项
- 深度测试:在透视投影中,深度测试是确保正确渲染物体顺序的关键。通过比较片元的深度值,可以确定哪些物体在前面,哪些在后面,从而避免错误的遮挡关系。
- 优化性能:透视投影的计算相对复杂,可能会对性能产生影响。因此,在开发过程中需要注意优化算法和减少不必要的计算。
综上所述,Android OpenGL的透视投影通过模拟人类视觉系统的特点,实现了对3D场景的逼真渲染。掌握透视投影的原理和技巧对于创建高质量的3D图形应用至关重要。
12.模型投影矩阵
利用模型矩阵移动物体。
private val modelMatrix = FloatArray(16)
setIdentityM(modelMatrix,0);
translateM(modelMatrix,0,0f,0f,-2.5f);//平移物体
val temp = FloatArray(16)
multiplyMM(temp,0,projectionMatrix,0,modelMatrix,0);//透视投影矩阵乘以模型矩阵
System.arraycopy(temp,0,projectionMatrix,0,temp.size);
13.旋转矩阵
利用旋转矩阵,基于某个角度或者向量(如,围绕x,y,z轴))旋转物体。
private val modelMatrix = FloatArray(16)
setIdentityM(modelMatrix,0);
rotateM(modelMatrix,0,-60f,1f,0f,0f)//围绕x轴,旋转物体-60度
val temp = FloatArray(16)
multiplyMM(temp,0,projectionMatrix,0,modelMatrix,0);//透视投影矩阵乘以模型矩阵
System.arraycopy(temp,0,projectionMatrix,0,temp.size);
14.纹理
为什么使用纹理?
用顶点着色器和片段着色器完成了简单的图形和颜色工作。但是还缺了点什么:如果想在这些图形上绘画并加人精致的细节呢?像艺术家一样,可以从基本的图形和颜色开始,再使用纹理(texture)在其表面上加人额外的细节。简单来说,纹理就是一个图像或照片,它们可以被加载进 OpenGL 中。我们可以用纹理加入大量令人难以置信的细节。想想你最近可能玩过的一个精美的三维游戏。与任何其他三维程序一样,那个游戏的本质也只是使用了点、直线和三角形。然而,通过纹理添加的细节和一个熟练的艺术家的加工,这些三角形可以用纹理构建一个精美的三维场景。
如何将纹理加载入OpenGL
加载图片纹理到openGL,示例代码如下:
private var textureId = 0
textureId = loadTexture(mContext!!, R.mipmap.air_hockey_surface)
fun loadTexture(context: Context, resourceId: Int): Int {
val textureObjectIds = IntArray(1)
glGenTextures(1, textureObjectIds, 0)
if (textureObjectIds[0] == 0) return 0
var options = BitmapFactory.Options();
options.inScaled = false
var bitmap = BitmapFactory.decodeResource(context.resources, resourceId, options)
if (bitmap == null) {
Log.d(TAG, "resourceId could not be decoded.")
glDeleteTextures(1, textureObjectIds, 0)
return 0
}
glBindTexture(GL_TEXTURE_2D, textureObjectIds[0])
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
texImage2D(GL_TEXTURE_2D, 0, bitmap, 0)
glGenerateMipmap(GL_TEXTURE_2D)
bitmap.recycle()
glBindTexture(GL_TEXTURE_2D, 0)
return textureObjectIds[0]
}
如何显示纹理
创建纹理着色器->链接openGL程序program->设置uniform变量数值->绑定着色器属性值与顶点数组数据->设置顶点属性数据可用->绘制纹理数据,显示纹理。
15.屏幕、窗口、视口、裁剪区域概念区分
- 屏幕:即计算机的整个屏幕大小。
- 窗口:指的就是现实的界面,即屏幕中的某一个窗口,可放大放小和移动关闭。
- 视口:通过glViewport()函数设置大小,视口指的就是窗口中用来显示图形的一块区域,它可以和窗口等大,也可以比窗口大或者小,不过只有绘制在视口区域中的图形才会被显示,如果图形其中一部分超出了视口区域,那么超出的部分是看不到的。如下图,不同大小的视口:
- 裁剪区域:即在视口中让你看到的图形,即显示出来的那部分。指的就是视口矩形区域的最小最大X坐标(left,right)和最小最大Y坐标(bottom,top),而不是窗口的最小最大X坐标和Y坐标,通过glOrtho()函数设置,这个函数还需要指定最近最远Z坐标,形成一个立体的裁剪区域。如下图,裁剪区域:
关系:当要把图形绘制在屏幕之前,首先要建立图形的几何描述,即建模,这往往是在世界坐标系内进行的。而图形最终是要显示在屏幕上,即屏幕坐标系。在模型的二维区域内选择一个区域,映射到屏幕坐标系的指定区域(窗口)中。在这个映射过程中包括平移、旋转、缩放操作及删除位于显示区域范围以外的图形部分。也就是OpenGL绘制图形时,并不是把整个模型直接绘制在整个屏幕上,而是把模型的部分绘制在窗口内。窗口还可以分为若干个区域,称为视口。模型在裁剪窗口内的部分映射到显示窗口中的指定视口中。窗口选择显示模型的那个部分,而视口指定显示在窗口的什么位置。