Android显示系统(01)- 架构分析
Android显示系统(02)- OpenGL ES - 概述
Android显示系统(03)- OpenGL ES - GLSurfaceView的使用
Android显示系统(04)- OpenGL ES - Shader绘制三角形
Android显示系统(05)- OpenGL ES - Shader绘制三角形(使用glsl文件)
Android显示系统(06)- OpenGL ES - VBO和EBO和VAO
Android显示系统(07)- OpenGL ES - 纹理Texture
Android显示系统(08)- OpenGL ES - 图片拉伸
Android显示系统(09)- SurfaceFlinger的使用
Android显示系统(10)- SurfaceFlinger内部结构
Android显示系统(11)- 向SurfaceFlinger申请Surface
Android显示系统(12)- 向SurfaceFlinger申请Buffer
Android显示系统(13)- 向SurfaceFlinger提交Buffer
一、前言:
我们之前学习了绘制基本的三角形,但是,要为其着色,需要为每个像素定义颜色,并运行片元着色器。然而,在现实世界中,如果我们想要绘制一张有山有水有树林的复杂场景,手动采集每个像素的颜色显然非常繁琐。因此,为了简化这一过程,前辈们引入了纹理的概念。纹理可以被视为一幅细致的图像,我们只需将其与设备坐标系对齐,就像贴在模型表面上一样,从而使得绘制的场景变得更加逼真。
也就是说,让我们绘图更加简单了,又逼真了,我们一直在追求简单,就像以后为了简单连程序员都要省了,直接上AI了,有点残酷,容本作者先掩面哭泣一会儿!!!
二、几个重要坐标系:
先看下图:
我们发现要想将纹理贴到顶点坐标上(也就是标准设备坐标系的顶点坐标),那么顶点坐标的左上角(-1,1)就应该对应纹理坐标的左下角(0,0);
而我们屏幕坐标如下,左上角是(0, 0);
也就是说,屏幕坐标和纹理坐标是倒过来的(这指的是内部纹理,比如,图库的图片,不是摄像头那种外部纹理)。
三、纹理和Shader:
纹理是一张图片,那么,它如何和我们的着色器关联起来去渲染的呢?
- 首先,我们在
vertexShader
当中,指定顶点坐标和纹理坐标之间的对应关系; - 其次,我们使用
fragment Shader
对纹理进行采样,获得每个像素对应纹理上的颜色; - 接着,我们会通过一些技术手段得到采样的具体颜色值;
- 最后,将这个颜色绘制到片元上;
四、内部纹理和外部纹理
- 内部纹理 (Internal Textures)
内部纹理是使用 OpenGL ES 的glTexImage2D
加载图像后生成的纹理。它通常使用应用程序加载的本地图片(如 JPG 或 PNG 文件),并将其映射到 OpenGL 中的某个对象(例如正方体或四边形)。这是最常见的纹理类型。 - 外部纹理 (External Textures)
外部纹理 (GL_TEXTURE_EXTERNAL_OES
) 通常用于视频或相机画面流的映射,它允许使用操作系统层级提供的外部资源作为纹理数据。例如,Camera2 API 输出的SurfaceTexture
绑定给 OpenGL 使用时就是外部纹理。
这两种纹理的主要区别在于数据源的不同。内部纹理主要来自图片等静态资源,而外部纹理主要用于动态画面。
五、纹理使用步骤:
1、部署关系:
- Tex:纹理对象,位于GPU的显存当中,可以理解成一幅图片对应一个纹理对象,比如,我们绘制一张照片,先使用Tex0;
- Texture:纹理单元,纹理单元是用于处理纹理的单元,每个纹理单元可以存储一个纹理对象。规定Shader不能直接访问纹理对象,必须通过纹理单元去访问;
- uSmapler:指向了我们激活的纹理单元,外部可以通过uSampler给Shader提供数据(数据内容就是具体的纹理单元ID);
2、使用步骤:
-
加载纹理图片;
- 首先需要加载纹理图片,可以是PNG、JPEG等格式。在Android中,可以使用Bitmap类加载图片资源;
-
创建纹理对象:
- 通过OpenGL ES生成一个纹理对象,可以使用
glGenTextures()
函数;
- 通过OpenGL ES生成一个纹理对象,可以使用
-
绑定纹理对象:
- 和前面VAO\VBO\EBO那种操作一样,就是将这个纹理对象绑定到OpenGL ES上下文,告诉后面操作都是基于这个纹理的;
- 使用
glBindTexture()
函数绑定纹理对象,指定纹理类型和具体纹理对象。
-
设置纹理参数:
- 设置纹理的过滤方式和环绕方式,可以使用
glTexParameterf()
函数。
- 设置纹理的过滤方式和环绕方式,可以使用
-
加载纹理数据:
- 将加载的图片数据传递给纹理对象,可以使用
glTexImage2D()
函数;
至此,图片就被导出成纹理,并上传到GPU的纹理对象当中了;
- 将加载的图片数据传递给纹理对象,可以使用
-
激活纹理单元:
- 如果不调用,默认激活的就是
Texture0
;
- 如果不调用,默认激活的就是
-
绑定纹理对象和纹理单元:
- 已经准备好了纹理对象,激活了纹理单元,就将两者绑定起来,后续用纹理单元去操作纹理对象;通过
glBindTexture
完成;
- 已经准备好了纹理对象,激活了纹理单元,就将两者绑定起来,后续用纹理单元去操作纹理对象;通过
-
将纹理单元传给片元着色器:
- 通知片元着色器,我们要使用那个纹理单元进行采样颜色,因为片元着色器不能直接访问纹理;
- 通过
glUniform1i
向Shader的uSampler
提供纹理ID;
**注意:**在Shader内部通过texture2D(uSmapler, aTexCoord)
采集纹理(uSmapler是指向了我们激活的纹理单元,aTexCoord是纹理坐标);
- 其实激活之后,uSampler就像一个指针一样指向了纹理单元。
- 通过纹理单元又可以找到纹理对象Tex0,然后,结合纹理坐标aTexCoord来找到像素,这样,这个像素的颜色就被获取到了;
3、绘制矩形:
我们看到的照片是一张二维矩形图片,如何绘制这个矩形模型呢?
我们之前说过,OpenGL当中,大部分模型都是由三角形构成的,因此,我们可以使用两个三角形拼起来构成一个矩形;
但是,拼凑方式又有两种(矩形需要两个三角形即可):
GL_TRIANGLE_STRIP
和GL_TRIANGLE_FAN
都是OpenGL中用于绘制三角形的渲染模式。它们的区别在于在绘制多边形时如何连接顶点。
-
GL_TRIANGLE_STRIP
:-
定义:
GL_TRIANGLE_STRIP
以三角形带的方式逐个连接顶点来绘制图元。 -
连接方式:每个三角形的第二个和第三个顶点会与上一个三角形的两个顶点共用,形成连续的三角形结构。
-
优势:节省了定义顶点的数量,减少了数据传输和绘制调用,适用于绘制连续的表面。
-
缺点:难以控制顶点的顺序,不适合绘制非连续的图形。
-
-
GL_TRIANGLE_FAN
:-
定义:
GL_TRIANGLE_FAN
以三角形扇的方式绘制多边形。 -
连接方式:以一个公共顶点为中心,每个顶点依次与前一个顶点和中心点组成三角形。
-
优势:适用于绘制类似扇形、圆形等具有中心点的图形。
-
缺点:需要指定额外的中心点,不适合绘制线性排列的图形。
-
-
总结:
-
GL_TRIANGLE_STRIP
适合绘制连续的表面,减少顶点定义的数量。 -
GL_TRIANGLE_FAN
适合绘制以中心点为基础的图形,如扇形、圆形等。
-
显然,我们应该选择GL_TRIANGLE_STRIP
这种方式。
4、设置纹理参数:
1)主要参数:
其实可以设置的参数很多,我们主要设置下面两种;
-
纹理过滤参数(Texture Filtering Parameters):
-
放大过滤(Magnification Filter):指定当纹理被放大时如何进行采样。常见选项包括
GL_NEAREST
(最近邻采样)和GL_LINEAR
(线性插值)。 -
缩小过滤(Minification Filter):指定当纹理被缩小时如何进行采样。常见选项包括
GL_NEAREST
和GL_LINEAR
,以及GL_NEAREST_MIPMAP_NEAREST
和GL_LINEAR_MIPMAP_LINEAR
(使用Mipmaps)。
-
-
纹理环绕参数(Texture Wrapping Parameters):
- S轴和T轴的环绕方式:指定纹理坐标超出范围时如何处理。常见选项包括
GL_REPEAT
(重复纹理)、GL_CLAMP_TO_EDGE
(边界拉伸)、GL_MIRRORED_REPEAT
(镜像重复)等。
- S轴和T轴的环绕方式:指定纹理坐标超出范围时如何处理。常见选项包括
下面解释下这都是啥!
2)环绕方式:
纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但OpenGL提供了更多的选择:
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
当纹理坐标超出默认范围时,每个选项都有不同的视觉效果输出。我们来看看这些纹理图像的例子:
前面提到的每个选项都可以使用glTexParameter*函数对单独的一个坐标轴设置(s
、t
(如果是使用3D纹理那么还有一个r
)它们和x
、y
、z
是等价的):
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
第一个参数指定了纹理目标;我们使用的是2D纹理,因此纹理目标是GL_TEXTURE_2D。第二个参数需要我们指定设置的选项与应用的纹理轴。我们打算配置的是WRAP
选项,并且指定S
和T
轴。最后一个参数需要我们传递一个环绕方式(Wrapping),在这个例子中OpenGL会给当前激活的纹理设定纹理环绕方式为GL_MIRRORED_REPEAT。
如果我们选择GL_CLAMP_TO_BORDER选项,我们还需要指定一个边缘的颜色。这需要使用glTexParameter函数的fv
后缀形式,用GL_TEXTURE_BORDER_COLOR作为它的选项,并且传递一个float数组作为边缘的颜色值:
float borderColor[] = {
1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
3)过滤方式:
其他参数不管,我们主要解决图片放大或者缩小时候如何去渲染的问题,OpenGL也有对于纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:GL_NEAREST和GL_LINEAR。
GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
那么这两种纹理过滤方式有怎样的视觉效果呢?让我们看看在一个很大的物体上应用一张低分辨率的纹理会发生什么吧(纹理被放大了,每个纹理像素都能看到):
GL_NEAREST产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素。
当进行放大(Magnify)和缩小(Minify)操作的时候可以设置纹理过滤的选项,比如你可以在纹理被缩小的时候使用邻近过滤,被放大时使用线性过滤。我们需要使用glTexParameter*函数为放大和缩小指定过滤方式。这段代码看起来会和纹理环绕方式的设置很相似:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)