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 - 图片拉伸
一、前言:
我们之前学习了绘制基本的三角形,但是,要为其着色,需要为每个像素定义颜色,并运行片元着色器。然而,在现实世界中,如果我们想要绘制一张有山有水有树林的复杂场景,手动采集每个像素的颜色显然非常繁琐。因此,为了简化这一过程,前辈们引入了纹理的概念。纹理可以被视为一幅细致的图像,我们只需将其与设备坐标系对齐,就像贴在模型表面上一样,从而使得绘制的场景变得更加逼真。
也就是说,让我们绘图更加简单了,又逼真了,我们一直在追求简单,就像以后为了简单连程序员都要省了,直接上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);
4)多级渐远纹理(Mipmap):
主要使用在缩小的场景中!
想象一下,假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。
OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。让我们看一下多级渐远纹理是什么样子的:
手工为每个纹理图像创建一系列多级渐远纹理很麻烦,幸好OpenGL有一个glGenerateMipmap函数,在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作了。后面的教程中你会看到该如何使用它。
在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用NEAREST和LINEAR过滤。为了指定不同多级渐远纹理级别之间的过滤方式,你可以使用下面四个选项中的一个代替原有的过滤方式:
过滤方式 | 描述 |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 |
就像纹理过滤一样,我们可以使用glTexParameteri将过滤方式设置为前面四种提到的方法之一:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
一个常见的错误是,将放大过滤的选项设置为多级渐远纹理过滤选项之一。这样没有任何效果,因为多级渐远纹理主要是使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。
六、关键API:
glGenTextures:
原型:
void glGenTextures(int n, int[] textures, int offset);
功能: 该函数用于生成一个或多个纹理对象并返回它们的纹理 ID。生成的 ID 是 OpenGL 管理纹理的唯一标识符,通常是在开始使用纹理前调用。
参数:
n
:要生成的纹理对象的数量。textures
:用于存储生成的纹理 ID 的数组。数组的大小至少要与要生成的纹理数量n
相同。offset
:数组中的起始位置,指定从哪个位置开始填充纹理 ID。
使用示例:
int[] textureIds = new int[1];
GLES30.glGenTextures(1, textureIds, 0); // 生成一个纹理对象
glBindTexture:
原型:
void glBindTexture(int target, int texture);
功能: 该函数将纹理对象绑定到当前 OpenGL 上下文中的指定目标。只有与当前绑定的纹理才能进行修改或使用,因此在设置纹理参数或上传纹理数据之前,必须先绑定纹理。
参数:
target
:纹理目标类型,通常使用GLES30.GL_TEXTURE_2D
,表示目标是二维纹理。texture
:要绑定的纹理对象的 ID,这个 ID 是之前通过glGenTextures
生成的。
使用示例:
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]); // 绑定指定的纹理对象
glTexParameteri:
原型:
void glTexParameteri(int target, int pname, int param);
功能: 该函数用于设置当前绑定纹理的参数,比如过滤模式、包裹模式等。不同的参数会影响纹理如何被使用和显示。
参数:
target
:指定纹理目标,通常是GLES30.GL_TEXTURE_2D
。pname
: 指定要设置的参数名,如:GLES30.GL_TEXTURE_MIN_FILTER
:纹理缩小时的过滤模式。GLES30.GL_TEXTURE_MAG_FILTER
:纹理放大时的过滤模式。GLES30.GL_TEXTURE_WRAP_S
:横向纹理包裹模式。GLES30.GL_TEXTURE_WRAP_T
:纵向纹理包裹模式。
param
:设置参数的值,通常是过滤的方式(例如,GLES30.GL_LINEAR
或GLES30.GL_NEAREST
)。
使用示例:
GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR);
GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
GLUtils.texImage2D:
原型:
void texImage2D(int target, int level, int internalformat, Bitmap bitmap, int border);
功能:用于将位图图像上传到当前绑定的纹理对象中,为纹理提供图像数据。在创建纹理时,通常会使用此函数将实际图像转移到 OpenGL 的纹理内存中。
参数:
target
:指定纹理目标,通常是GLES30.GL_TEXTURE_2D
。level
:纹理的多级渐进级别,0 表示基础纹理。internalformat
:纹理的内部格式,如GLES30.GL_RGBA
,表明存储时所用的颜色格式。bitmap
:要上传的位图对象,包含了纹理的实际像素数据。border
:边框宽度,通常设置为 0。
使用示例:
GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, bitmap, 0);
七、开始编码:
步骤如下:
- 导入图片;
- 设置对应纹理参数;
- 映射好顶点坐标和纹理坐标;
- 根据导入的图片,创建纹理,并上传纹理数据;
- 修改Shader程序;
- 激活纹理并渲染图片;
1、导入图片:
在res/drawable
目录下新增图片android_logo.png
:
新建类com/example/glsurfaceviewdemo/TextureRender.java
用于渲染纹理
// 加载图片private Bitmap loadImage() {BitmapFactory.Options options = new BitmapFactory.Options();options.inScaled = false;return BitmapFactory.decodeResource(mContext.getResources(), R.drawable.android_logo, options);}
2、上传纹理到GPU:
接下来要做的就是创建纹理,并为纹理设置参数,后面才可以,将图片上传到纹理中,将纹理上传到GPU中;
// 上传纹理到GPUprivate int uploadTexture() {Bitmap bitmap = loadImage();int[] textureIds = new int[1];GLES30.glGenTextures(1, textureIds, 0); // 创建纹理GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]); // 绑定纹理GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR); // 设置缩小策略GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR); // 设置放大策略GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, bitmap, 0); // 纹理上传到GPUbitmap.recycle(); // 回收bitmapGLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0); // 解绑纹理,避免后续误操作return textureIds[0];}
3、初始化坐标数据:
private float[] mCoordData = {// 顶点坐标 纹理坐标-1.0f, 1.0f, 0.0f, 0.0f, 0.0f, // 左上角-1.0f, -1.0f, 0.0f, 0.0f, 1.0f, // 左下角1.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右上角1.0f, -1.0f, 0.0f, 1.0f, 1.0f // 右下角};// 初始化坐标数据private void initVertexBuffer() {ByteBuffer byteBuffer = ByteBuffer.allocateDirect(mCoordData.length * 4);byteBuffer.order(ByteOrder.nativeOrder());mCoordBuffer = byteBuffer.asFloatBuffer();mCoordBuffer.put(mCoordData);mCoordBuffer.position(0);}
注意,我们将顶点坐标的左上角对应了纹理坐标的(0, 0),这个对内部纹理坐标来说其实是纹理的左下角,我们给映射起来了;
4、加载并编译着色器:
// 加载并编译着色器private void initShaders(Context context) {String vertexShaderCode = ShaderController.loadShaderCodeFromFile("texture_vertex_shader.glsl", context);String fragmentShaderCode = ShaderController.loadShaderCodeFromFile("texture_fragment_shader.glsl", context);mProgram = ShaderController.createGLProgram(vertexShaderCode, fragmentShaderCode);if (mProgram == 0) {Log.e("TextureRender", "Failed to create OpenGL program.");}}
和之前一样,没啥好分析的;
5、初始化 VBO:
注意,为了尽快完成编码,这儿我没有使用EBO和VAO,读者可以编码完成了自己改改,方法我之前文章都详细写了。
// 初始化 VBOprivate void initVbo() {int[] vbos = new int[1];GLES30.glGenBuffers(1, vbos, 0);mVboId = vbos[0];GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId);mCoordBuffer.position(0);GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, mCoordBuffer.capacity() * 4, mCoordBuffer, GLES30.GL_STATIC_DRAW);}
6、初始化变换矩阵:
变换矩阵是在draw函数中控制顶点位置的,我们初始化成单位矩阵相当于清空之前移动的位置了。
// 初始化变换矩阵private void initMatrix() {Matrix.setIdentityM(mTranslateMatrix, 0); // 设置为单位矩阵}
1)什么是单位矩阵?
单位矩阵(Identity Matrix)是一种特殊的矩阵,其主要特点是:
- 矩阵乘以单位矩阵时,任何矩阵都不会发生改变。
- 例如,对于一个二维或者三维的变换,单位矩阵是一个不做任何变换的基础矩阵。
在三维空间中,具体的单位矩阵形式如下:
对于 4x4 的单位矩阵:
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
2)为什么使用 setIdentityM
?
Matrix.setIdentityM(translateMatrix, 0);
这行代码的含义是将 translateMatrix
重置为单位矩阵。这样做的目的是为了在进行后续的变换(如旋转、缩放、平移)之前,确保矩阵没有之前操作的残留状态。这样可以避免因矩阵状态混乱而导致的渲染错误。
3)举个例子:
假设你正在创建一个 2D 游戏,一个角色在屏幕上移动,你希望用矩阵变换来控制角色的位置和方向。
- 初始状态:
你可能会在开始渲染时用以下方法初始化你的变换矩阵:
Matrix.setIdentityM(translateMatrix, 0); // 初始化为单位矩阵
- 添加变换:
然后你希望角色向右移动 1 个单位,可以通过如下方式进行平移:
Matrix.translateM(translateMatrix, 0, 1.0f, 0.0f, 0.0f); // 向右移动1个单位
此时,translateMatrix
会变成一个表示“向右移动 1 个单位”的矩阵,而不是单位矩阵。
- 继续其他变换:
如果你想在此基础上再向上移动 2 个单位:
Matrix.translateM(translateMatrix, 0, 0.0f, 2.0f, 0.0f); // 向上移动2个单位
- 如果你不重置矩阵:
如果你在没有重置矩阵的情况下继续进行其他变换,translateMatrix
会包含多个变换的影响,最终结果可能不是你预期的。而使用 setIdentityM
重置矩阵后可以确保下次变换时其基于单一状态进行,非常重要:
Matrix.setIdentityM(translateMatrix, 0); // 再次重置为单位矩阵
Matrix.translateM(translateMatrix, 0, 3.0f, 0.0f, 0.0f); // 向右移动3个单位
4)小结:
使用 Matrix.setIdentityM(translateMatrix, 0);
将矩阵设置为单位矩阵,确保在每次变换前,矩阵处于一个已知的起始状态。这是为了避免残留的变化影响你接下来的操作,可以对位置、旋转、缩放等变换进行精确控制。
7、获取GPU和Shader的一些操作接口:
首先得说明下GPU的一些关键接口,我们可以想象成一个芯片的管脚,比如,芯片规定我们,必须通过读写管脚1来控制led灯,通过管脚2来控制蜂鸣器。
aPosition
用于接收顶点坐标数据;aTexCoord
用于接收纹理坐标数据;uTMatrix
用于接收变换矩阵;uSampler
是Shader里面的一个接口,用于指向激活的纹理单元,Shader只有通过纹理单元才能操作纹理对象;
// 获取GPU和Shader的一些操作接口private void initHandles() {// 获取顶点坐标操作接口的句柄mPositionHandle = GLES30.glGetAttribLocation(mProgram, "aPosition");validateAttributeLocation(mPositionHandle, "aPosition");GLES30.glEnableVertexAttribArray(mPositionHandle); // 启用位置属性数组// 获取纹理坐标操作接口的句柄mTexCoordHandle = GLES30.glGetAttribLocation(mProgram, "aTexCoord");validateAttributeLocation(mTexCoordHandle, "aTexCoord");GLES30.glEnableVertexAttribArray(mTexCoordHandle); // 启用纹理坐标属性数组// 获取变换矩阵操作接口的句柄mTMatrixHandle = GLES30.glGetUniformLocation(mProgram, "uTMatrix");// 用于获取Shader当中纹理采样器的操作接口的句柄mSamplerHandle = GLES30.glGetUniformLocation(mProgram, "uSampler");}
8、绘图:
上面都初始化好了,下面就可以周期性的绘图了.
// 绘制纹理public void draw() {// 激活着色器程序GLES30.glUseProgram(mProgram);// 绑定VBOGLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mVboId);// 设置顶点坐标和纹理坐标给OpenGLsetupVertexAttribPointer();// 上传变换矩阵GLES30.glUniformMatrix4fv(mTMatrixHandle, 1, false, mTranslateMatrix, 0);// 绑定纹理并设置采样器bindTexture();// 绘制矩形GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);// 解绑当前的 VBO,避免后续操作意外影响到这个缓冲GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);// 检查 OpenGL 错误checkOpenGLError();}
9、资源释放:
当然,你不想绘制了需要释放资源
public void release() {GLES30.glDeleteBuffers(1, new int[]{mVboId}, 0); // 删除 VBOGLES30.glDeleteProgram(mProgram); // 删除 shader program}
10、编写着色器:
顶点着色器:
文件路径:app/src/main/assets/texture_vertex_shader.glsl
#version 300 es // 指定 GLSL 版本
precision mediump float; // 定义浮点数精度// 统一变量
uniform mat4 uTMatrix; // 变换矩阵// 属性变量
in vec4 aPosition; // 顶点位置
in vec2 aTexCoord; // 纹理坐标// 输出变量
out vec2 vTexCoord; // 传递给片段着色器的纹理坐标void main() {// 应用变换矩阵并设置顶点位置gl_Position = uTMatrix * aPosition;// 将纹理坐标传递给片段着色器vTexCoord = aTexCoord;
}
片元着色器:
文件路径:app/src/main/assets/texture_fragment_shader.glsl
#version 300 es // 指定 GLSL 版本
precision mediump float; // 定义浮点数精度// 统一变量
uniform sampler2D uSampler; // 纹理采样器// 输入变量
in vec2 vTexCoord; // 从顶点着色器传递的纹理坐标// 输出变量
out vec4 fragColor; // 片段着色器的输出颜色void main() {// 通过纹理采样器获取最终颜色fragColor = texture(uSampler, vTexCoord);
}
11、调用:
上面都是修改的com/example/glsurfaceviewdemo/TextureRender.java
文件,调用它的地方也得改改
其实主要改渲染器,其他不用动,我们都隔离的比较好。
文件路径:com/example/glsurfaceviewdemo/GLRenderTest.java
public class GLRenderTest implements GLSurfaceView.Renderer {private Triangle mTriangle;private TextureRender mTextureRender;private Context mContext;public GLRenderTest(Context context) {this.mContext = context;}@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);// mTriangle = new Triangle(mContext);mTextureRender = new TextureRender(mContext);}@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {GLES30.glViewport(0, 0, width, height);}@Overridepublic void onDrawFrame(GL10 gl){GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);// mTriangle.draw();mTextureRender.draw();}public void onDestroy() {// mTriangle.release();mTextureRender.release();}
}
主要是创建TextureRender
对象,并在周期性的回调函数onDrawFrame
当中调用draw()
渲染纹理;
八、运行结果:
发现没有,变形了,我的原图是个土肥圆,编程高富帅了,关于怎么处理拉伸,又比较复杂,且听下回分解!
九、总结:
本章主要讲解了内部纹理如何渲染,你可以简单理解成如何显示出你手机图库里面某张照片。需要掌握的内容非常多,主要涉及GPU内部结构和关键接口的了解,纹理的概念,并且得了解一些纹理参数,每个参数又和一些业务场景相关,只要做图形学相关,纹理是必须掌握的,大家加油压!