OpenGL学习脚印 二维纹理映射(2D textures)_opengl对texture2d 纹理的处理

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

另外还存在其他滤波方法,例如线性滤波方法(linear filtering),它使用纹素位置(152.34,745.14)附近的一组纹素的加权平均值来确定最终的纹素值。例如使用 ( (152,745), (153,745), (152,744) and (153,744) )这四个纹素值的加权平均值。权系数通过与目标点(152.34,745.14)的距离远近反映,距离(152.34,745.14)越近,权系数越大,即对最终的纹素值影响越大。线性滤波的示意图如下图所示(来自A Textured Cube):
线性滤波
图中目标纹素位置周围的4个纹素通过加权平均计算出最终输出纹素。

还存在其他的滤波方式,如三线性滤波(Trilinear filtering)等,感兴趣的可以参考texture filtering wiki。最近邻滤波和线性滤波的对比效果如下图所示(来自Textures objects and parameters):

最近邻和线性滤波对比

可以看出最近邻方法获取的纹素看起来有明显的像素块,而线性滤波方法获取的纹素看起来比较平滑。两种方法各自有不同的应用场合,不能说线性滤波一定比最近邻滤波方法好,例如要制造8位图形效果(8 bit graphics,每个像素使用8位字节表示)需要使用最近邻滤波。作为一个兴趣了解,8位图形效果看起来也是很酷的(可以查看Welcome 8-bit, Pixel-Art Images Gallery!)获得更多8位图形),例如下面这张使用Excel制作的8位图(来自Excel is a great for making 8 bit graphics!):
这里写图片描述

另外一个问题是,纹理应用到物体上,最终要绘制在显示设备上,这里存在一个纹素到像素的转换问题。有三种情形(参考自An Introduction to Texture Filtering):

  • 一个纹素最终对应屏幕上的多个像素 这称之为放大(magnification)
  • 一个纹素对应屏幕上的一个像素 这种情况不需要滤波方法
  • 一个纹素对应少于一个像素,或者说多个纹素对应屏幕上的一个像素 这个称之为缩小(minification)
    放大和缩小的示意图如下:
    magnification and minification

在OpenGL中通过使用下面的函数,为纹理的放大和缩小滤波设置相关的控制选项:

glTexParameteri(GL\_TEXTURE\_2D, 
 GL\_TEXTURE\_MAG\_FILTER, GL\_LINEAR);
glTexParameteri(GL\_TEXTURE\_2D, 
 GL\_TEXTURE\_MIN\_FILTER, GL\_NEAREST); 

其中GL_LINEAR对应线性滤波,GL_NEAREST对应最近邻滤波方式。

使用Mipmaps

考虑一个情景:当物体在场景中离观察者很远,最终只用一个屏幕像素来显示时,这个像素该如何通过纹素确定呢?如果使用最近邻滤波来获取这个纹素,那么显示效果并不理想。需要使用纹素的均值来反映物体在场景中离我们很远这个效果,对于一个 256×256的纹理,计算平均值是一个耗时工作,不能实时计算,因此可以通过提前计算一组这样的纹理用来满足这种需求。这组提前计算的按比例缩小的纹理就是Mipmaps。Mipmaps纹理大小每级是前一等级的一半,按大小递减顺序排列为:

  • 原始纹理 256×256
  • Mip 1 = 128×128
  • Mip 2 = 64×64
  • Mip 3 = 32×32
  • Mip 4 = 16×16
  • Mip 5 = 8×8
  • Mip 6 = 4×4
  • Mip 7 = 2×2
  • Mip 8 = 1×1

OpenGL会根据物体离观察者的距离选择使用合适大小的Mipmap纹理。Mipmap纹理示意图如下所示(来自wiki Mipmap):
Mipmap
OpenGL中通过函数glGenerateMipmap(GL_TEXTURE_2D);来生成Mipmap,前提是已经指定了原始纹理。原始纹理必须自己通过读取纹理图片来加载,这个后面会介绍。
如果直接在不同等级的MipMap之间切换,会形成明显的边缘,因此对于Mipmap也可以同纹素一样使用滤波方法在不同等级的Mipmap之间滤波。要在不同等级的MipMap之间滤波,需要将之前设置的GL_TEXTURE_MIN_FILTER选项更改为以下选项之一:

  • GL_NEAREST_MIPMAP_NEAREST: 使用最接近像素大小的Mipmap,纹理内部使用最近邻滤波。
  • GL_LINEAR_MIPMAP_NEAREST: 使用最接近像素大小的Mipmap,纹理内部使用线性滤波。
  • GL_NEAREST_MIPMAP_LINEAR: 在两个最接近像素大小的Mipmap中做线性插值,纹理内部使用最近邻滤波。
  • GL_LINEAR_MIPMAP_LINEAR: 在两个最接近像素大小的Mipmap中做线性插值,纹理内部使用线性滤波。

Mipmap使用注意 使用使用glGenerateMipmap(GL_TEXTURE_2D)产生Mipmap的前提是你已经加载了原始的纹理对象。使用MipMap时设置GL_TEXTURE_MIN_FILTER选项才能起作用,设置GL_TEXTURE_MAG_FILTER的Mipmap选项将会导致无效操作,OpenGL错误码为GL_INVALID_ENUM。

设置Mipmap选项如下代码所示:

   glTexParameteri(GL\_TEXTURE\_2D, 
 GL\_TEXTURE\_MIN\_FILTER, GL\_LINEAR\_MIPMAP\_LINEAR);

加载原始纹理

从图片加载纹理这部分工作不是OpenGL函数完成的,可以通过外部库实现。这里我们使用SOIL(Simple OpenGL Image Library)库完成。下载完这个库后,你需要编译到本地平台对应版本。你可以从我的github处下载已经编译好的32位库。
使用SOIL加载纹理的代码如下:

GLubyte *imageData = NULL;
int picWidth, picHeight;
imageData = SOIL_load_image("wood.png", 
    &picWidth, &picHeight, 0, SOIL_LOAD_RGB); // 读取图片数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 
    picWidth,picHeight, 0, GL_RGB, 
    GL_UNSIGNED_BYTE, imageData); // 定义纹理图像

其中glTexImage2D函数定义纹理图像的格式,宽度和高度等信息,具体参数如下:

API void glTexImage2D( GLenum target,
GLint level,
GLint internalFormat,
GLsizei width,
GLsizei height,
GLint border,
GLenum format,
GLenum type,
const GLvoid * data);

1.target参数指定设置的纹理目标,必须是GL_TEXTURE_2D, GL_PROXY_TEXTURE_2D等参数。
2.level指定纹理等级,0代表原始纹理,其余等级对应Mipmap纹理等级。
3.internalFormat指定OpenGL存储纹理的格式,我们读取的图片格式包含RGB颜色,因此这里也是用RGB颜色。
4.width和height参数指定存储的纹理大小,我们之前利用SOIL读取图片时已经获取了图片大小,这里直接使用即可。
5. border 参数为历史遗留参数,只能设置为0.
6. 最后三个参数指定原始图片数据的格式(format)和数据类型(type,为GL_UNSIGNED_BYTE, GL_BYTE等值),以及数据的内存地址(data指针)。

使用纹理的完整过程

Step1 首先要指定纹理坐标,这个坐标和顶点位置、顶点颜色一样处理,使用索引绘制,代码如下所示:

   // 指定顶点属性数据 顶点位置 颜色 纹理
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f,0.0f, 0.0f,  // 0
        0.5f,  -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,1.0f, 0.0f,  // 1
        0.5f,  0.5f,  0.0f, 0.0f, 0.0f, 1.0f,1.0f, 1.0f,  // 2
        -0.5f, 0.5f,  0.0f, 1.0f, 1.0f, 0.0f,0.0f, 1.0f   // 3
    };
    GLushort indices[] = {
        0, 1, 2,  // 第一个三角形
        0, 2, 3   // 第二个三角形
    };

同顶点位置和颜色一样,需要指定纹理坐标的解析方式。上面的数据格式如下图所示(来自www.learnopengl.com):
data format

这个格式的说明在OpenGL学习脚印: 绘制一个三角形 已经讲过,如果不清楚,可以回过头去查看。通过查看上图,我们按照如下方式设置glVertexAttribPointer,让OpenGL知道如何解析上述数据:

    // 顶点位置属性
    glVertexAttribPointer(0, 3, GL\_FLOAT, GL\_FALSE, 
 8 \* sizeof(GL\_FLOAT), (GLvoid\*)0);
    glEnableVertexAttribArray(0);
    // 顶点颜色属性
    glVertexAttribPointer(1, 3, GL\_FLOAT, GL\_FALSE,
 8 \* sizeof(GL\_FLOAT), (GLvoid\*)(3 * sizeof(GL\_FLOAT)));
    glEnableVertexAttribArray(1);
    // 顶点纹理坐标
    glVertexAttribPointer(2, 2, GL\_FLOAT, GL\_FALSE,
 8 \* sizeof(GL\_FLOAT), (GLvoid\*)(6 * sizeof(GL\_FLOAT)));
    glEnableVertexAttribArray(2);

对应的顶点着色器如下:

#version 330

layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
layout(location = 2) in vec2 textCoord; // 纹理坐标

out vec3 VertColor;
out vec2 TextCoord;

void main()
{
    gl\_Position = vec4(position, 1.0);
    VertColor = color;
    TextCoord = textCoord;
}

Step2 :然后需要设置OpenGL纹理参数;最后通过读取纹理图片,定义纹理图像格式等信息。纹理数据最终传递到了显卡中存储。

   // Section3 准备纹理对象
    // Step1 创建并绑定纹理对象
    GLuint textureId;
    glGenTextures(1, &textureId);
    glBindTexture(GL_TEXTURE_2D, textureId);
    // Step2 设定wrap参数
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // Step3 设定filter参数
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, 
        GL_LINEAR_MIPMAP_LINEAR); // 为MipMap设定filter方法
    // Step4 加载纹理
    GLubyte *imageData = NULL;
    int picWidth, picHeight;
    imageData = SOIL_load_image("wood.png", 
        &picWidth, &picHeight, 0, SOIL_LOAD_RGB);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, picWidth, picHeight, 
        0, GL_RGB, GL_UNSIGNED_BYTE, imageData);
    glGenerateMipmap(GL_TEXTURE_2D);
    // Step5 释放纹理图片资源
    SOIL_free_image_data(imageData);
    glBindTexture(GL_TEXTURE_2D, 0);

注意 图片资源在创建完纹理后就可以释放了,使用SOIL_free_image_data完成。

Step3 着色器中使用纹理对象
在顶点着色器中我们传递了纹理坐标,有了纹理坐标,获取最终的纹素使用过在片元着色器中完成的。由于纹理对象通过使用uniform变量来像片元着色器传递,实际上这里传递的是对应纹理单元(texture unit)的索引号。纹理单元、纹理对象对应关系如下图所示:
纹理单元

着色器通过纹理单元的索引号索引纹理单元,每个纹理单元可以绑定多个纹理到不同的目标(1D,2D)。OpenGL可以支持的纹理单元数目,一般至少有16个,依次为GL_TEXTURE0 到GL_TEXTURE15,纹理单元最大支持数目可以通过查询GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS常量获取。这些常量值是按照顺序定义的,因此可以采用 GL_TEXTURE0 + i 的形式书写常量,其中整数i在[0, GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS)范围内。

作为一个了解,纹理对象不仅包含纹理数据,还包含采样参数,这些采样参数称之为采样状态(sampling state)。而采样对象(sampler object)就是只包含采样参数的对象,将它绑定到纹理单元时,它会覆盖纹理对象中的采样状态,从而重新配置采样方式。这里不再继续讨论采样对象的使用了。

要使用纹理必须在使用之前激活对应的纹理单元,默认状态下0号纹理单元是激活的,因此即使没有显式地激活也能工作。激活并使用纹理的代码如下:

  // 使用0号纹理单元
  glActiveTexture(GL\_TEXTURE0);
  glBindTexture(GL\_TEXTURE\_2D, textureId);
  glUniform1i(glGetUniformLocation(shader.programId, "tex"), 0); 

上述glUniform1i将0号纹理单元作为整数传递给片元着色器,片元着色器中使用uniform变量对应这个纹理采样器,使用变量类型为:

uniform sampler2D tex;

uniform变量与attribute变量 uniform变量与顶点着色器中使用的属性变量(attribute variables)不同,
属性变量首先进入顶点着色器,如果要传递给片元着色器,需要在顶点着色器中定义输出变量输出到片元着色器。而uniform变量则类似于全局变量,在整个着色器程序中都可见。

完整的片元着色器代码为:

#version 330

in vec3 VertColor;
in vec2 TextCoord;

uniform sampler2D tex;

out vec4 color;


void main()
{
    color = texture(tex, TextCoord);
}

其中texture函数根据纹理坐标,获取纹理对象中的纹素。
运行程序,效果如下图所示:

二维纹理

这里为绘制的矩形添加了纹理,可以从我的github下载程序完整代码。

重构代码

将上面处理纹理部分的代码整理成一个函数,放在textureHelper类里,可以从我的github查看这个类的代码。使用textureHelper类加载纹理的代码为:

GLint textureId = TextureHelper::load2DTexture("wood.png");

在上面的顶点着色器中,我们也传递了顶点颜色属性,将顶点颜色和纹理混合,修改片元着色器中代码为:

color = texture(tex, TextCoord) * vec4(VertColor, 1.0f);

二维纹理与颜色混合

使用多个纹理单元

上面介绍了一个纹理单元支持多个纹理绑定到不同的目标,一个程序中也可以使用多个纹理单元加载多个2D纹理。使用多个纹理单元的代码如下:



![img](https://img-blog.csdnimg.cn/img_convert/abf74ef7076bef477427563a576115ec.png)
![img](https://img-blog.csdnimg.cn/img_convert/ee5f4ff58cad00cfbe16f448282c6488.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

如下:



[外链图片转存中…(img-D8QqSJEK-1715839559353)]
[外链图片转存中…(img-ZBBnyp0E-1715839559353)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值