文章目录

  • 为何要有纹理?
  • 纹理坐标
  • 加载图片数据到内存
    • 选用现成库
    • 使用 stb_image.h
    • 删除加载的图片数据
  • 纹理对象
    • 创建纹理对象
    • 设置为当前操作的纹理对象
    • 删除纹理对象
    • 设置纹理对象内部格式以及传入图像数据
      • Base Internal Format(内部格式)
      • Sized Internal Format
      • 也可以从纹理对象中读取数据到内存
      • 纹理单元
      • 先激活纹理单元
  • 采样器
  • 应用层程序添加顶点属性
    • 运行效果
  • 完整源码
  • 总结
  • References

LearnGL - 学习笔记目录

本人才疏学浅,如有什么错误,望不吝指出。

上一篇:LearnGL - 04.2 - 封装 ShaderProgram 类,对 Shader、Program 封装了类,便于后续使用。

这一篇:继续我们的 OpenGL 学习主线内容:纹理(Texture)。纹理的内容挺多的,这里只讲到部分的内容。


为何要有纹理?

想让我们的几何体(模型)看起来更丰富,除了之前我们说过的 添加顶点(由 三角形 到 四边形,再多一些就甚至可以是圆形,甚至是各种几何体,只要顶点数量足够多)、然后给顶点添加颜色,最终都控制如何显示 成最后的屏幕像素,让它看起来更丰富的内容。

一些色彩丰富的物体,如一些画家画的抽象画,什么鬼颜色都有,如果我们只用添加顶点的方式,让每一块相同的颜色,都弄一个几何体(一堆顶点),并添加顶点颜色来表示的话,那估计建模的同学会疯掉。

所以聪明的人类、聪明的图形先辈们想到了一个方法:纹理

纹理这玩意儿,有点像我们在一个气球上画一些图像,或是写一些字,然后我们将气球吹鼓。

再将气球捏来捏去的,改变它的形状,你会发现气球上的图案都很好的映射在你画在对应位置上的气球表面位置上;最后你将气球泄气会原型后,你会发现画在气球上的图像内容都还原样的保留着(除非你使劲儿地把画上去的颜料戳没了。-_-!!!)。

所以我们的 纹理 也是类型这么个映射。这个方法可就很好的替代了上面添加N个无穷无尽的表面顶点来丰富画面了。工作量上可是省了 N 多的量。建模同学是个幸运儿,都没经历这么多痛的假设工作量,都给图形先辈们提前想到并解决了。

纹理 的整体系统的思路很简单,先提供一些保存着图像内容的 图片纹理贴图 texture map),这些图片都有一些纹理坐标,就想我们画的一个二维的笛卡尔坐标一样,也有个坐标。然后在顶点属性上添加一个映射在纹理采样的坐标,这样不管我的顶点变化到哪去,我都让该顶点映射纹理坐标的位置不变。顶点的位置变化能引起图像映射的内容伸缩,但这正是我们想要的效果,就想上面提到的气球的例子一样。而上面的顶点坐标中映射了纹理坐标,我们通常叫:纹理映射(Texture-Mapping)。

纹理坐标

前面简单的简述了纹理的概念,也提到了 纹理坐标 的概念。

纹理系统中的 纹理贴图(texture map) 就是张图片,而这张 图片的坐标 通常是下面的方式:

但是我们的 纹理坐标 可不一样:(0,0)在左下角,即:Y轴的增量方向是朝上的。

加载图片数据到内存

选用现成库

有好几种方法

  • 一种是使用:http://www.opengl-redbook.com,(红宝书)中的源码:vglLoadImagevglUnloadIImagevglLoadTexture、等方法。(如果要全面一些的加载也可以使用这个)
  • 一种是:SOIL(Simple OpenGL Image Library,简要的 OpenGL 图像库),unsinged char* image = SIO_load_image("filename.ext", &width, &height, 0 SOIL_LOAD_RGB);(为了做学习用,可以使用这个)
  • 一种是:stb_image.h。(这个库使用比较简单,我是推荐使用这个)

我使用的是也 stb_image.h 的加载方式,使用方式可以参考:stb_image.h,除了参考文中说明的使用方式,大家也可以查查此头文件中的前面的注释,也有很详细的英文说明。

将此文件放在我们的 include path(包含目录)

在源码中包含此头文件之前,先要定义宏:

#define STB_IMAGE_IMPLEMENTATION

就像这样:

#define STB_IMAGE_IMPLEMENTATION
#include"stb_image.h"

加载图片:

int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);

使用 stb_image.h

上面确定使用了 stb_image.h ,下面我们列出我们使用到的加载方式。

要将图片数据加载到纹理中,首先我们先将图片数据加载到内存

 // loading texture here...// 加载纹理需要用的图片数据char img_path[MAX_PATH];g_GetPicturePathCallback(img_path, "my_tex.png");                // 获取图片目录int img_w, img_h, img_channels;stbi_set_flip_vertically_on_load(1);                            // 也可以在加载前设置加载时翻转的变量unsigned char* img_data = stbi_load(img_path, &img_w, &img_h, &img_channels, 4); // 加载图片数据,返回确定宽、高、通道数量、每个分量要多少字节if (img_data == NULL) {                                          // 如果加载图片失败std::cout << "Loading Image File : " << img_path << " FAILURE : " << stbi_failure_reason() << std::endl;exit(EXIT_FAILURE);}//stbi__vertical_flip(img_data, img_w, img_h, 4);                  // 如果不设置前面stbi_set_flip_vertically_on_load(1),也可以在这手动去翻转,因为图片坐标与纹理坐标的Y轴增量方向不同,所以需要翻转垂直方向的行数数据// loading texture here...// when loading complete.// free image data herestbi_image_free(img_data);
  • unsigned char* img_data = stbi_load(img_path, &img_w, &img_h, &img_channels, 0); 根据img_path路径加载图片数据,返回确定宽、高、通道数量、每个分量要多少字节,返回加载的数据
  • 注意:图像垂直方向图像翻转,前面我们介绍过,因为 图片坐标 与 纹理坐标 的垂直向量增量方向不同,所以需要翻转垂直方向行数的数据
    • stbi_set_flip_vertically_on_load(1); 也可以在加载前设置加载时翻转的变量
    • stbi__vertical_flip(img_data, img_w, img_h, 4); 如果不设置前面stbi_set_flip_vertically_on_load(1),也可以在这手动去翻转

删除加载的图片数据

上面代码最后一句,可以看到有个:回收内存函数,这样就可以回收掉之前分配的内存。

只要我们的数据上传到了GPU缓存(显存)就回收。

stbi_image_free(img_data);                                       // 纹理已经上传到了显存,内存中的数据可以删除了

纹理对象

有了图片数据我们就要开创建纹理。

就像之前介绍的:VAO(顶点缓存数组对象),VBO(顶点缓存对象),EBO/IBO(元素/索引缓存对象),Shader Object(着色器对象),Shader Program Object(着色器程序对象),等对象

所以纹理也有一个封装的对象:纹理对象(Texture Object)。

在使用纹理之前,需要先创建一个 纹理对象。

创建纹理对象

纹理对象的生成/创建可以使用: glCreateTextures。

而 glCreateTextures 是 OpenGL 4.0+ 的规范定义的API。

如果要在 OpenGL 4.0-之前的版本创建纹理对象的话,使用:glGenTextures。

OpenGL4.0+的glCreateTextures 和 OpenGL4.0-的glGenTextures 稍微有丢丢不同:

glCreateTextures

void glCreateTextures(   GLenum target,GLsizei n,GLuint *textures);

glGenTextures

void glGenTextures(  GLsizei n,GLuint * textures);

OpenGL4.0+的glCreateTextures 的多了第一个参数 GLenum target。后面两参数 n 是创建的 纹理对象数量textures 纹理对象 都一样的意思。

纹理目标类型glCreateTextures - target:是要绑定的纹理目标。它能传入的类型有:

目标(GL_TEXTURE_*) 采样器类型 维度
1D sampler1D 一维
1D_ARRAY sampler1DArray 一维数组
2D sampler2D 二维
2D_Array sampler2DArray 二维数组
2D_MULTISAMPLE sampler2DMS 二维多重采样
2D_MULTISAMPLE_ARRAY sampler2DMSArray 二维多重采样数组
3D sampler3D 三维
CUBE samplerCube 立方体映射纹理
ARRAY samplerCubeArray 立方体映射纹理数组
RECTANGLE samplerRect 二维长方形
BUFFER samplerBuffer 一维缓存

传入对应的纹理类型,对应到着色器中的 采样器类型 也是一一对应的。

glIsTexture 可以判断一个对象ID是否 纹理对象

glCreateTextures(GL_TEXTURE_2D, 1, &texture);                    // 创建纹理对象

设置为当前操作的纹理对象

//glActiveTexture(GL_TEXTURE0);                                  // 默认第 0 个纹理单元是激活的,可以不用设置
glBindTextureUnit(0, texture);                                  // 绑定第 0 索引的纹理单元,OpenGL 4.0+建议用这个,与 glCreateTextures 配对。 OpenGL4.0-可用 glBindTexture

删除纹理对象

如果某个纹理对象不再使用了,可以使用 glDeleteTextures 删除这个纹理对象:

glDeleteTextures(1, &texture);                               // 删除纹理对象

设置纹理对象内部格式以及传入图像数据

上面创建好了 纹理对象 后,纹理对象里还是空的:配置是默认的,纹理数据的空的。在给纹理对象设置图像数据之前,先配置它的数据存储的是什么内部格式。

内部 格式中的 内部 指的是在 OpenGL 环境中的数据(着色器中的采样出来的数据)格式。

内部 就会有相对的 外部,外部格式是我们的图片的数据格式,代表它在 传入到 OpenGL 环境前外部 格式,后面会有说明如何指定我们纹理数据的外部格式。

设置纹理对象的内部格式有:glTextureStorage1D,glTextureStorage2D,glTextureStorage3D 三个API。

这三个 API 是 OpenGL 4.5+(包含4.5)才有的,在 glad.c OpenGL 4.5 版本的文件可以看到它的读取。

目前:docs.gl上没找到对应的 API。

在 OpenGLS 4.5-(不确定是否4.5-)之前,可以使用其他的 API :glTexImage1D、glTexImage1D、glTexImage1D。

glTextureStorage1D

void glTextureStorage1D(GLuint texture,GLsizei levels,GLenum internalformat,GLsizei width);
  • texture 是指定的纹理对象
  • leves 是指定的 mipmaps 的分层有几级
  • internalFormat 就是我们上面说的的 内部各式 下面的列表有信息,有三个表(Base/Sized/Compressed Internal Format)
  • width 用在这 1D,2D,3D 函数中的纹理的宽度

Base Internal Format(内部格式)

Base Internal Format(基础的内部格式) RGBA, Depth and Stencil Values(RGB,深度和模板值) Internal Components(内部分量)
GL_DEPTH_COMPONENT Depth D
GL_DEPTH_STENCIL Depth, Stencil D, S
GL_RED Red R
GL_RG Red, Green R, G
GL_RGB Red, Green, Blue R, G, B
GL_RGBA Red, Green, Blue, Alpha R, G, B, A

Sized Internal Format

Sized Internal Format Base Internal Format Red Bits Green Bits Blue Bits Alpha Bits Shared Bits
GL_R8 GL_RED 8
GL_R8_SNORM GL_RED s8
GL_R16 GL_RED 16
GL_R16_SNORM GL_RED s16
GL_RG8 GL_RG 8 8
GL_RG8_SNORM GL_RG s8 s8
GL_RG16 GL_RG 16 16
GL_RG16_SNORM GL_RG s16 s16
GL_R3_G3_B2 GL_RGB 3 3 2
GL_RGB4 GL_RGB 4 4 4
GL_RGB5 GL_RGB 5 5 5
GL_RGB8 GL_RGB 8 8 8
GL_RGB8_SNORM GL_RGB s8 s8 s8
GL_RGB10 GL_RGB 10 10 10
GL_RGB12 GL_RGB 12 12 12
GL_RGB16_SNORM GL_RGB 16 16 16
GL_RGBA2 GL_RGB 2 2 2 2
GL_RGBA4 GL_RGB 4 4 4 4
GL_RGB5_A1 GL_RGBA 5 5 5 1
GL_RGBA8 GL_RGBA 8 8 8 8
GL_RGBA8_SNORM GL_RGBA s8 s8 s8 s8
GL_RGB10_A2 GL_RGBA 10 10 10 2
GL_RGB10_A2UI GL_RGBA ui10 ui10 ui10 ui2
GL_RGBA12 GL_RGBA 12 12 12 12
GL_RGBA16 GL_RGBA 16 16 16 16
GL_SRGB8 GL_RGB 8 8 8
GL_SRGB8_ALPHA8 GL_RGBA 8 8 8 8
GL_R16F GL_RED f16
GL_RG16F GL_RG f16 f16
GL_RGB16F GL_RGB f16 f16 f16
GL_RGBA16F GL_RGBA f16 f16 f16 f16
GL_R32F GL_RED f32
GL_RG32F GL_RG f32 f32
GL_RGB32F GL_RGB f32 f32 f32
GL_RGBA32F GL_RGBA f32 f32 f32 f32
GL_R11F_G11F_B10F GL_RGB f11 f11 f10
GL_RGB9_E5 GL_RGB 9 9 9 5
GL_R8I GL_RED i8
GL_R8UI GL_RED ui8
GL_R16I GL_RED i16
GL_R16UI GL_RED ui16
GL_R32I GL_RED i32
GL_R32UI GL_RED ui32
GL_RG8I GL_RG i8 i8
GL_RG8UI GL_RG ui8 ui8
GL_RG16I GL_RG i16 i16
GL_RG16UI GL_RG ui16 ui16
GL_RG32I GL_RG i32 i32
GL_RG32UI GL_RG ui32 ui32
GL_RGB8I GL_RGB i8 i8 i8
GL_RGB8UI GL_RGB ui8 ui8 ui8
GL_RGB16I GL_RGB i16 i16 i16
GL_RGB16UI GL_RGB ui16 ui16 ui16
GL_RGB32I GL_RGB i32 i32 i32
GL_RGB32UI GL_RGB ui32 ui32 ui32
GL_RGBA8I GL_RGBA i8 i8 i8 i8
GL_RGBA8UI GL_RGBA ui8 ui8 ui8 ui8
GL_RGBA16I GL_RGBA i16 i16 i16 i16
GL_RGBA16UI GL_RGBA ui16 ui16 ui16 ui16
GL_RGBA32I GL_RGBA i32 i32 i32 i32
GL_RGBA32UI GL_RGBA ui32 ui32 ui32 ui32

Compressed Internal Format

Compressed Internal Format Base Internal Format Type
GL_COMPRESSED_RED GL_RED Generic
GL_COMPRESSED_RG GL_RG Generic
GL_COMPRESSED_RGB GL_RGB Generic
GL_COMPRESSED_RGBA GL_RGBA Generic
GL_COMPRESSED_SRGB GL_RGB Generic
GL_COMPRESSED_SRGB_ALPHA GL_RGBA Generic
GL_COMPRESSED_RED_RGTC1 GL_RED Specific
GL_COMPRESSED_SIGNED_RED_RGTC1 GL_RED Specific
GL_COMPRESSED_RG_RGTC2 GL_RG Specific
GL_COMPRESSED_SIGNED_RG_RGTC2 GL_RG Specific
GL_COMPRESSED_RGBA_BPTC_UNORM GL_RGBA Specific
GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM GL_RGBA Specific
GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT GL_RGB Specific
GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT GL_RGB Specific

glTextureStorage2D 比 1D 的多了个 height

void glTextureStorage2D(前面的参数与1D一样, GLsizei height);

glTextureStorage3D 比 2D 的多了个 depth

void glTextureStorage3D(前面的参数与1D一样, GLsizei depth);

一般 2D 可以当作 1D 的数组,height 作为 1D 的数据的行;3D 可以当作 2D 的数据,depth 作为 2D 的切片层数。

glTextureStorage1D 执行后就像是下面的代码处理:

for (i = 0; i < levels; i++) {glTexImage1D(target, i, internalformat, width, 0, format, type, NULL);width = max(1, (width / 2));
}

glTextureStorage2D 执行后等价于下面代码:

for (i = 0; i < levels; i++) {glTexImage2D(target, i, internalformat, width, height, 0, format, type, NULL);width = max(1, (width / 2));height = max(1, (height / 2));
}
// When target is GL_TEXTURE_CUBE_MAP, glTexStorage2D is equivalent to:
for (i = 0; i < levels; i++) {for (face in (+X, -X, +Y, -Y, +Z, -Z)) {glTexImage2D(face, i, internalformat, width, height, 0, format, type, NULL);}width = max(1, (width / 2));height = max(1, (height / 2));
}
// When target is GL_TEXTURE_1D_ARRAY or GL_PROXY_TEXTURE_1D_ARRAY, glTexStorage2D is equivalent to:
for (i = 0; i < levels; i++) {glTexImage2D(target, i, internalformat, width, height, 0, format, type, NULL);width = max(1, (width / 2));
}

glTextureStorage3D 执行后等价于下面代码:

for (i = 0; i < levels; i++) {glTexImage3D(target, i, internalformat, width, height, depth, 0, format, type, NULL);width = max(1, (width / 2));height = max(1, (height / 2));depth = max(1, (depth / 2));
}
// When target is GL_TEXTURE_2D_ARRAY, GL_PROXY_TEXTURE_2D_ARRAY, GL_TEXTURE_CUBE_MAP_ARRAY, or GL_PROXY_TEXTURE_CUBE_MAP_ARRAY, glTexStorage3D is equivalent to:for (i = 0; i < levels; i++) {glTexImage3D(target, i, internalformat, width, height, depth, 0, format, type, NULL);width = max(1, (width / 2));height = max(1, (height / 2));
}

其实就是根据要生成的 mipmaps 的 levels 值来遍历设置对应 mipmaps 层级的数据、及格式。

注意:但是可以看到他们的等价代码中会调用到glTexImage1D、glTexImage1D、glTexImage1D,并且最后一个 const void* data 数据传入的是 NULL,这里特别说明一下,如果该参数传入的数据为 NULL,并且当前 GL_PIXEL_UNPACK_BUFFER 绑定的像素缓存对象数据不为 NULL 的话,那么纹理对象中的数据将指向 GL_PIXEL_UNPACK_BUFFER 的缓存数据,它会在着色器程序读取数据时再取读取数据的。

OpenGL 4.5之前,上面的 mipmaps 的数据,我们也可以手动调用 glTexImage2D,再调用 glGenerateMipmap生成 mipmaps 数据:

 先填入第0 层 mipmaps 数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img_w, img_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data);
// 如果前面给这个纹理对象指定了多层级 mipmaps,那么可以使用 glGenerateMipmap
// void glGenerateMipmap(GLenum target);
// void glGenerateTextureMipmap(GLuint texture);
// 来给纹理的0层之后的其他mipmaps层生成图像数据
//glGenerateTextureMipmap(texture);// opengl 4.5 API,生成指定纹理对象的mipmaps
glGenerateMipmap(GL_TEXTURE_2D); // opengl 4.5之前的API,生成当前绑定纹理对象,且属于 GL_TEXTURE_2D 类型的纹理对象的mipmaps

glTexImage1D 原型:

void glTexImage1D(   GLenum target,GLint level,GLint internalformat,GLsizei width,GLint border,GLenum format,GLenum type,const void * data);
  • target 纹理目标类型之间有列表罗列出来了各个枚举与着色器中采样器对应。
  • level 当前要设置的 level 层级
  • internalformat 内部格式,之前也有对应的表格
  • width 单行像素的数量
  • border 必须填0
  • format 外部格式,指定需要用到的分量有哪些,下面讲有对应表格说明
  • type 外部格式,指定分量的类型
    后面的 glTexImage2D/glTextureImage2D,glTexImage3D/glTextureImage3D 都只是对二维、三维的纹理处理的。多了个 heightdepth参数而已。类似的也可以使用 glTextureSubImage2D。

这里提一下mipmaps
会每次缩小一倍的尺寸来保存另一个已经 linear 采样来缩小过的图片数据,会才图元的某个片元的像素密集度到达一定程度时就会选择使用对应的 mipmaps 数据,像素密集度越大,则使用 miplevels 中对应 level 越高的图片数据来采样。这样既可以让采样性能更高,而且显示质量也越好。

如果我们的 texture 有 mipmap 数据 OpenGL 底层实现规范有处理,OpenGL 编程指南上有非常详细的说明,然后我也在知乎上找到有人去将数中的算法实现,将不同片段确定的 mipmap 以不同的颜色输出来查看此片段使用的 mipmap level:在shader中计算贴图mipmap级别。

最终我们调用是:
使用 glTextureStorage2D 来 设置纹理对象内部格式 以及 使用 glTextureSubImage2D 传入图像数据

glTextureStorage2D(                                              // 设置 texture 纹理对象的内部格式texture,                                                 // 要设置的 texture 纹理对象1,                                                          // mipmaps 的层数,只要1层 mipmaps 即可,至少要有1层,否则有错误。需要需要多层 mipmaps ,可以指定多层GL_RGBA8,                                                 // 内部数据格式img_w,                                                     // 图像的宽img_h                                                        // 图像的高
);glTextureSubImage2D(                                          // 给 texture 纹理对象设置对应 mipmap 层级的数据texture,                                                  // 要设置的 texture 纹理对象0,                                                          // mipmaps 的层级索引,从0开始,mipmaps 的0, 0,                                                      // 要从 x,y 偏移多少开始,不要偏移所以都填0img_w, img_h,                                              // 要填入的行、列尺寸的像素数量GL_RGBA, GL_UNSIGNED_BYTE,                                 // 外部格式,指定要包含的分量数量 和 分量类型img_data                                                    // 外部图片数据
);

之前 glTextureSubImage2D API的最后一个参数的作用有说明怎么用,当 GL_PIXEL_UNPACK_BUFFER 缓存对象不为NULL时, glTextureSubImage2D 的最后一个参数将作为 GL_PIXEL_UNPACK_BUFFER 缓存对象数据的字节偏移值。

如下代码,我写了个宏,预处理分支 GET_IMG_DATA_TYPE 1 或是 2 的类型

  • 1 : 直接在 glTextureSubImage2D 的最后一个参数设置数据
  • 2 : 直接在 GL_PIXEL_UNPACK_BUFFER 中读取

1 但是没有问题的,就是 2 的时候有错误。具体暂时找不出什么原因。(按 GL 提示的错误代码是说的缓存数据提供的内容不符合纹理需要的格式,但是为何 1 就可以呢?都是原始的字节流数据,除非 GL_PIXEL_UNPACK_BUFFER 缓存对象会修改原始字节的内容?)

 // 1 : 直接在 glTextureSubImage2D 的最后一个参数设置数据// 2 : 直接在 GL_PIXEL_UNPACK_BUFFER 中读取
#define GET_IMG_DATA_TYPE 1                                         // 使用 2 类型的缓存对象方式来加载纹理对象数据会有错误#if GET_IMG_DATA_TYPE == 2                                          // 如果需要从GL_PIXEL_UNPACK_BUFFER中读取纹理数据的话glCreateBuffers(1, &pixelBufObject);                         // 创建缓存对象checkGLError();glNamedBufferStorage(                                           // 给指定缓存对象配置参数、设置数据pixelBufObject,                                              // 要配置的缓存对象sizeof(img_data),                                            // 要分配多少字节缓存大小(sizeof(img_data)的大小)img_data,                                                   // 使用 img_data 初始化字节数据,如果填入 NULL,就是不使用数据初始化0);                                                            // flag 标记位暂时填入0checkGLError();glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixelBufObject);            // 将pixelBufObject缓存对象作为 GL_PIXEL_UNPACK_BUFFER 目标缓存对象checkGLError();
#endifglTextureSubImage2D(                                          // 给 texture 纹理对象设置对应 mipmap 层级的数据texture,                                                  // 要设置的 texture 纹理对象0,                                                          // mipmaps 的层级索引,从0开始,mipmaps 的0, 0,                                                      // 要从 x,y 偏移多少开始,不要偏移所以都填0img_w, img_h,                                              // 要填入的行、列尺寸的像素数量GL_RGBA, GL_UNSIGNED_BYTE,                                 // 外部格式,指定要包含的分量数量 和 分量类型
#if GET_IMG_DATA_TYPE == 1img_data                                                    // 外部图片数据
#elseNULL                                                       // 先填充GL_PIXEL_UNPACK_BUFFER数据,这里传入的是NULL从GL_PIXEL_UNPACK_BUFFER缓存数据的字节偏移NULL就是0偏移
#endif);// 查看有无错误checkGLError();                                                    // GET_IMG_DATA_TYPE 2 时会有错误

之前我们有讲到 内部格式,上面的代码注释中有讲到 外部格式 它时告诉 OpenGL 传入的数据是什么格式,好让它转换为 内部数据

外部格式 有量个参数可以决定:formattype

format 的枚举值决定:
GL_RED, GL_RG, GL_RGB, GL_BGR, GL_RGBA, GL_BGRA, GL_DEPTH_COMPONENT, 和 GL_STENCIL_INDEX

type 的枚举值决定分量字节数据分布:
GL_UNSIGNED_BYTE, GL_BYTE, GL_UNSIGNED_SHORT, GL_SHORT, GL_UNSIGNED_INT, GL_INT, GL_FLOAT, GL_UNSIGNED_BYTE_3_3_2, GL_UNSIGNED_BYTE_2_3_3_REV, GL_UNSIGNED_SHORT_5_6_5, GL_UNSIGNED_SHORT_5_6_5_REV, GL_UNSIGNED_SHORT_4_4_4_4, GL_UNSIGNED_SHORT_4_4_4_4_REV, GL_UNSIGNED_SHORT_5_5_5_1, GL_UNSIGNED_SHORT_1_5_5_5_REV, GL_UNSIGNED_INT_8_8_8_8, GL_UNSIGNED_INT_8_8_8_8_REV, GL_UNSIGNED_INT_10_10_10_2, 和 GL_UNSIGNED_INT_2_10_10_10_REV

也可以从纹理对象中读取数据到内存

其实也可以从纹理对象中读取图像数据:

  • OpenGL4.5- 使用 glGetTexImage
  • OpenGL4.5+ 使用 glGetTexImage/glGetnTexImage/glGetTextureImage

纹理单元

我们的纹理对象的数据最终是要在着色器中被调用的,着色器调用是通过 绑定了纹理元的采样器 对,这样就可以采样到 纹理单元上绑定的纹理对象 的数据了。

纹理单元(Texture Unit),你可以理解为着色器程序中预先分配好的指向纹理指针对象。但是他们的数量是有限的。

如果获取你本机设备上的 OpenGL 查看的纹理单元的支持最大数量,可以使用 glGetIntegerv 使用 GL_MAX_TEXTURE_IMAGE_UNITS 参数来获取,如下:

 // 打印着色器支持最大的纹理图像单元的数量int maxTexUnit;glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTexUnit);std::cout << "Maximun number of texture image units : " << maxTexUnit << std::endl;// 打印着色器支持最大的所有组合的纹理图像单元的数量int maxCombinedTexUnit;glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxCombinedTexUnit);std::cout << "Maximun number of Combined texture image units : " << maxCombinedTexUnit << std::endl;

一般机器上都能支持 16 个纹理单元。

我的笔记本(2018年的顶配游戏本)上运行上面的代码输出:

Maximun number of texture image units : 32
Maximun number of Combined texture image units : 192

纹理单元的支持最大数量 这个结果在不同的操作系统,不同的硬件的 OpenGL 实现,不同的 OpenGL 版本,都会有可能不一样的结果。

先激活纹理单元

纹理单元要想正常使用,首先这个单元得先激活,可以使用:glActiveTexture

glActiveTexture(GL_TEXTURE0);                                    // 默认第 0 个纹理单元是激活的,可以不用设置

激活纹理单元的用法与之前的 glBindxxxx 的很类似,因为 OpenGL 底层实现就是一个状态机,多数的操作对象都先要设置为当前要操作的对象,后续的操作函数都会对之前激活、绑定的对象进行处理、配置、等。

注意:在有些代码中,可能会没看到对 GL_TEXTURE0 纹理单元的激活,因为第 0 索引的纹理单元它默认是激活的。

它的参数 GL_TEXTURE[N] 中的 N 可以是 0GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1 的范围。小于 0 或是超出最大支持数量都会出错。

有些 GLAD 的头文件的定义中,最大也就 GL_TEXTURE32 个。

但是,如果我们需要再着色器中 使用多个纹理单元 的使用,就 需要激活多个对应的纹理单元

创建纹理对象后,要在着色器中使用的话,需要绑定到 纹理单元 ,可使用 glBindTextureUnit:

glBindTextureUnit(0, texture);                                   // 绑定第 0 索引的纹理单元,OpenGL 4.0+建议用这个,与 glCreateTextures 配对。 OpenGL4.0-可用 glBindTexture

如果绑定时的 纹理对象ID 是不存在的,可使用:glGetError 来获取OpenGL 当前运行环境中的错误枚举:返回的是:GLenum。通常是:

#define GL_INVALID_ENUM 0x0500
#define GL_INVALID_VALUE 0x0501
#define GL_INVALID_OPERATION 0x0502

下面是绑定一个不存在的纹理对象ID:

glBindTextureUnit(0, 99);    // 第 0 索引的纹理单元,绑定一个不存在的纹理对象ID:99
GLenum error = glGetError();// 获取 OpenGL 运行环境的错误
if (error != 0) {std::cout << std::dec;  // 以10进制显示数值std::cout << "glError : " << error;std::cout << std::hex;   // 以16进制显示数值std::cout << "(0x" << error << ")" << std::endl;
}/* 输出:
glError : 1282(0x502)
*/

采样器

上述中,有题到 采样器类型,其中 采样器,是在着色器程序(Shader Program)中的子着色器程序(Shader)使用的。

一般来说,你在应用程序层创建的 纹理对象 的类型,要与 着色器程序中的 采样器 的类型也是要一一对应的,否则会有问题。

上述的表格有列出了他们一一对应的关系表。

早期的 OpenGL4.5 前的都是使用纹理单元对应的纹理对象,与采样器的类型匹配的方式来使用的,可读性真的很差,我也不知道这么个全球流行的开源渲染系统的设计规范,设计这么拙劣(这也是导致学习成本比较高的原因之一,更高的原因是 API 名称也不友好,很多API命名真的无语)。到了 OpenGL 4.5 的 API 设计上就好很多了,它可以创建采样器,设置采样器参数等,然后还可以绑定采样器对应的采样的纹理单元。

在 OpenGL 4.5 之前,就经过前面设置就可以在着色器添加对应的采样器就可以运作了。
在 OpenGL 4.5 之后,虽然也可以兼容,但是它可以更清晰的设置那个纹理单元使用那个采样器来采样。

我们先对 顶点着色器片元着色器 添加对应的 纹理坐标 变量,后续再给应用层的顶点数据添加 纹理坐标 数据。

首先我们对 着色器文件的目录调整了:

  • Dependencies/Shaders/ 下添加了 TestingTexture 的目录
  • 并在 TestingTexture 目录复制了之前的 Shaders/shader1.vert, Shaders/shader1.frag 的两个文件,再调整内容。

testing_tex_shader.vert 的内容为:

// jave.lin - testing_tex_shader.vert - 测试纹理的顶点着色器
#version 450 compatibility
uniform mat4 transformMat;
attribute vec3 vPos;
attribute vec3 vCol;
attribute vec2 vUV;
varying vec3 fCol;
varying vec2 fUV;
void main() {gl_Position = transformMat * vec4(vPos, 1.0);fCol = vCol;fUV = vUV;
}

注意 顶点着色器 中我们添加了:
testing_tex_shader.frag 的内容为:attribute vec2 vUV;varying vec2 fUV;
至于 attributevarying之前有讲过,前者是顶点着色器专用的属性,后者是顶点着色器的数据在光栅化插值后再传入片段着色器的。这里就不再详细说明了。

testing_tex_shader.frag 的内容为:

// jave.lin - testing_tex_shader.frag - 测试纹理的片段着色器
#version 450 compatibility
varying vec3 fCol;
varying vec2 fUV;uniform sampler2D tex;void main() {vec3 texCol = texture(tex, fUV).rgb;gl_FragColor = vec4(fCol * texCol, 1.0);
}
  • varying vec2 fUV; 是顶点着色器传过来的纹理坐标数据。
  • vec3 texCol = texture(tex, fUV).rgb; 中,textureGLSL内置函数
    • 第一个参数采样器(这个 采样器已绑定了 我们之前应用层设置的 纹理单元,而 纹理单元也绑定了 我们设置的 纹理对象所以 该采样器 最终会采样 到我们绑定的 纹理对象中的图像数据
    • 第二个参数纹理坐标,这个方法有多个重载,这里我们只用到其中一个。
    • 返回值 返回一个 vec4 类型,但是填充了数据的只会按我们之前设置的 外部格式,内部格式 来决定的,如果返回的是一个分量的 vec4 ,那么 vec4 中第一个分量中才是我们想要的数值,其他都是默认值。在这里我们返回的是 vec43 个对我们的逻辑来说才是有效的分量,所以我们采样出来的 vec4,再用 swizzle 语言来获取 rgb3 个分量,并将结果赋值给 texCol 变量。
  • gl_FragColor = vec4(fCol * texCol, 1.0); 最后我们用顶点颜色与纹理颜色相乘混合了。

texture 的各重载原型定义:

gvec4 texture(gsampler1D tex, float P[, float bias]);
gvec4 texture(gsampler2D tex, vec2 P[, float bias]);
gvec4 texture(gsampler3D tex, vec3 P[, float bias]);
gvec4 texture(gsamplerCube tex, vec3 P[, float bias]);
gvec4 texture(gsampler1DArray tex, vec2 P[, float bias]);
gvec4 texture(gsampler2DArray tex, vec3 P[, float bias]);
gvec4 texture(gsampler2DRect tex, vec2 P[, float bias]);
gvec4 texture(gsamplerCubeArray tex, vec4 P[, float bias]);

从名为 tex 的采样器中采样一个纹素,对应的纹理坐标为 P。如果对象支持 mipmap,并且设置了 bias,那么这个参数将用于 mipmap 细节层次(level-of-detail)的偏移量计算,来判断采样应当在哪一层进行。函数的返回值是一个包含了采样后的纹理数据的向量。

这里有一个专业术语上的解释:对于很多 GLSL 函数的原型,我们都可以看到 gvec4 (或者其他维度的向量)这样的定义。它实际上是一个“占位符”,表示任何类型的一个向量。它可以用来表达 vec4ivec4uvec4。同理,gsampler2D也是一个这样的占位符,它可以表达 sampler2Disampler2D或者usampler2D类型。此外,我们在书写函数参数的时候如果添加了方括号([和]),说明这个参数是可选的,可以忽略不计。

OpenGL 4.5 后,我们可以使用 glCreateSamplers 来创建我们自定义的采样器。

但是因为 OpenGL 才创建纹理对象时,会给他包含上一个默认配置的采样器,所以我们一般可以不同区设置。

但一般什么情况下会去使用呢?

因为着色器中的采样器对象/单元数量都是有限的,但你的纹理有很多个,多到超过了采样器数量的上限。

这就可以通过抽象出多个纹理对象的相同采样配置,来设置他们的采样器位同一个采样器即可。

可以通过 glBindSampler、glBindSamplers 来处理:

GLuint sampler_object;
...
glCreateSamplers(1, &sampler_object);                           // 创建采样器,OpenGL 4.5+才有的API,可读性更高,因为 OpenGL 默认的给每一个纹理对象都包含了一个默认配置的采样器对象,没必要使用,因为有默认的,除非你想多给纹理单元都使用同一个采样器来采样时,就可以通过这种方式来指定
glBindSampler(0, sampler_object);                               // 将纹理单元0 的采样器绑定为 sampler_object的

我们现在就不用这个自定义的了。先暂时用着默认的。

纹理系统要区别好这几个玩意儿:

  • 纹理对象
  • 纹理单元
  • 采样器对象/单元

这里我引用一下 《OpenGL 编程指南》第9版说的(其实这是一本很权威的数据,毕竟三个作者都是非常厉害的,都是 OpenGL 开发核心人员,OpenGL 规范制定人员),但是,我看了一些内容,说得不够透彻,该说的没有说清楚。我反正是把它引用过来了,大家能否看懂是另一回事,我个人觉得有些说明前后上是后矛盾的。

我们可以通过着色器中带有纹理单元信息的采样器变量来读取纹理,使用GLSL内置的函数从纹理图像中读取纹素。而纹素读取的方式依赖于另一个对象中的参数,名为采样器对象(sampler object)。采样器对象会绑定到采样器单元,这类似于纹理对象绑定到纹理单元。为了简便起见,我们在每一个纹理对象中包含了一个默认内置的采样器对象,如果没有把采样器对象绑定到专门的采样器单元,这个对象可以用来从纹理对象中读取数据。

OK,从上面可得知:texture object 内至少有一个类似 sampler object pointer / ID 之类的成员数据。

另一个段:

如果要在着色器中使用多重纹理,我们还需要定义多个 uniform 类型的采样器变量。每个变量都对应着一个不同的(从技术上来说,这些变量也可以关联到相同的纹理单元上。如果有两个或者更多采样器都关联到同一个纹理单元,它们将对同一个纹理进行数据的采样工作)纹理单元。从应用程序的角度来说,采样器 uniform 和一般的整数 uniform 非常类似。它们可以使用通常的 glGetActiveUniform() 函数来进行枚举,也可以使用 glUniform1i() 函数来设置数值。设置给采样器 uniform 的整数数值也就是它所关联的纹理单元的索引值。

OK,从上面可得知,sampler object 内至少有一个类似 texture unit 之类的成员数据。

从上面两端,总结出的伪代码:

// 从第一段的分析出来的 TextureObject 纹理对象的情况
class TextureObject {               // 纹理对象
public:GLuint samplerObjectPointer; // 采样器对象
};// 从第二段分析出来的 SamplerObject 采样器对象的情况
class SamplerObject {public:GLuint textureUnit;              // 纹理单元槽位
};

如果真是这样,那么我们的着色器中的采样器对象采样数据时,就采样通过设置的 GLuint textureUnit 来找到对应 glBindTextureUnit(textureUnit, textureObject) 设置后的 textureObject 的数据,那这时,class TextureObject 内的 GLuint samplerObjectPointer 就没有意义了啊。

如果你们读过《OpenGL 编程指南》第9版中的第6章,关于纹理与帧缓存的章节,就知道其中的说明迷迷糊糊的。-_-!!!

应用层程序添加顶点属性

GLfloat uvs[] = {                               // 顶点的 uv 坐标0.0f, 0.0f,                                 // 左下角1.0f, 0.0f,                                   // 右下角1.0f, 1.0f,                                   // 右上角0.0f, 1.0f,                                   // 左上角
};
...GLint vuv_location;
GLuint vertex_buffer[4]; // 原来是3的大小,现在改为4,作为uv用// shader program init 5 - 根据shader源码的相对路径(变量),加载deps下的shader
char vs_path[MAX_PATH], fs_path[MAX_PATH];
g_GetShaderPathCallback(vs_path, "TestingTexture\\testing_tex_shader.vert");
g_GetShaderPathCallback(fs_path, "TestingTexture\\testing_tex_shader.frag");
if (!shaderProgram->initByPath(vs_path, fs_path)) {std::cout << "ShaderProgram init Error: " << shaderProgram->errorLog() << std::endl; // 输出shader program错误exit(EXIT_FAILURE);
}
...
vuv_location = shaderProgram->getAttributeLoc("vUV");         // 获取 顶点着色器中的顶点 attribute 属性的 location
...
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]);                // 绑定 VBO[2]
glBufferData(GL_ARRAY_BUFFER, sizeof(uvs), uvs, GL_STATIC_DRAW); // 设置 VBO uv数据glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]);             // 绑定 VBO[3],因为后面要设置该 VBO 的uv格式
glVertexAttribPointer(vuv_location, 2, GL_FLOAT, GL_FALSE,      // 设置 顶点属性 vUV 格式sizeof(GLfloat) * 2, (GLvoid*)0);
glEnableVertexAttribArray(vuv_location);                        // 启用 顶点缓存 location uv的属性

设置好纹理坐标的顶点数据后,现在的顶点数据就变成这样的了:

第一种 也是我们之前文章中的放置,将 Pos, Color 放在同一个 VBO 不同偏移上决定的。
而我们现在使用的是 第二种:Pos, Color, UV 都是在不同 VBO 的情况。

运行效果

完整源码

// jave.lin
#include"glad/glad.h"
#include"GLFW/glfw3.h"
//#include"linmath.h"
// 把linmath.h 放在 iostream 之前include会有错误,所以放到iostream 后include就好了
// 而这个错误正式 xkeycheck.h 文件内 #error 提示的,所以可以使用 #define _XKEYCHECK_H 这个头文件的引用标记宏
// 就可以避免对 xkeycheck.h 头文件的 include 了。
#include<iostream>
#include"linmath.h"
#include"shader.h"
// 使用 stb_image.h 的加载库
// github 源码:https://github.com/nothings/stb/blob/master/stb_image.h
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
// 将之前的打印版本信息代码包含一下
#include"print_gl_version_info.h"GLfloat vertices[] = {// x, y,    z// 直接放4个顶点-0.25f, -0.25f, 0.0f,                      // 第0个顶点,左下角0.25f, -0.25f, 0.0f,                     // 第1个顶点,右下角0.25f,  0.25f, 0.0f,                     // 第2个顶点,右上角-0.25f,  0.25f, 0.0f,                        // 第3个顶点,左上角
};GLfloat colors_1[] = {                           // 顶点颜色缓存数据11.0f, 0.0f, 0.0f,                           // 第0个顶点颜色0.0f, 1.0f, 0.0f,                         // 第1个顶点颜色1.0f, 1.0f, 0.0f,                         // 第2个顶点颜色0.0f, 0.0f, 1.0f,                         // 第3个顶点颜色
};GLfloat colors_2[] = {                           // 顶点颜色缓存数据21.0f, 1.0f, 0.0f,                           // 第0个顶点颜色0.0f, 1.0f, 1.0f,                         // 第1个顶点颜色1.0f, 1.0f, 1.0f,                         // 第2个顶点颜色1.0f, 0.0f, 1.0f,                         // 第3个顶点颜色
};GLfloat uvs[] = {                                // 顶点的 uv 坐标0.0f, 0.0f,                                 // 左下角1.0f, 0.0f,                                   // 右下角1.0f, 1.0f,                                   // 右上角0.0f, 1.0f,                                   // 左上角
};GLuint indices[] = {                         // 注意索引从0开始!通过索引缓存来指定 图元 组成 用的 顶点有哪些0, 1, 3,                                    // 放置顶点的索引,第一个三角形1, 2, 3                                     // 放置顶点的索引,第二个三角形
};// 定义:获取 Shader 目录的回调函数原型
typedef char* (__stdcall * GetShaderPathCallback)(char*, const char*);
GetShaderPathCallback g_GetShaderPathCallback = NULL;
// 定义:获取 Pic 目录的回调函数原型
typedef char* (__stdcall* GetPicturePathCallback)(char*, const char*);
GetPicturePathCallback g_GetPicturePathCallback = NULL;static void error_callback(int error, const char* description) {fprintf(stderr, "ErrorCode : %d(0x%08x), Error: %s\n", error, error, description);
}static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { // 当键盘按键ESCAPE按下时,设置该window为:需要关闭if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)glfwSetWindowShouldClose(window, GLFW_TRUE);
}// 时候开启检测GL的错误
#define CHECK_GL_ERROR#ifdef CHECK_GL_ERROR
// 检测如果有GL的错误,则提示并退出程序
#define checkGLError() \
{\GLenum errorCode = glGetError(); \if (errorCode != 0) { \std::cout << "Line:" << __LINE__ << " "; \std::cout << std::dec; \std::cout << "glError : " << errorCode; \std::cout << std::hex; \std::cout << "(0x" << errorCode << ")" << std::endl; \exit(EXIT_FAILURE); \}\
}
#else
#define checkGLError()
#endifint main() {glfwSetErrorCallback(error_callback); // 安装glfw内部错误时的回调if (!glfwInit()) { // 初始化glfwstd::cout << "glfwInit FAILURE" << std::endl; // 初始化失败exit(EXIT_FAILURE);}// 设置最低的openGL 版本,major:主版本号,minor:次版本号// openGl 太低版本的话是不支持CORE Profile模式的// 会报错:ErrorCode: 65540(0x00010004), Error : Context profiles are only defined for OpenGL version 3.2 and above//glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);//glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);// 根据上面的错误提示,至少使用3.2才行,这里我们使用4.5//glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);//glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);// core profile 下运行有问题,不显示任何内容,但不会报错。// 着色器编译、着色器程序链接都没有错误日志信息。// 很有可能是因为我参考的学习网站使用的API相对比较老,使用的是3.3的。//glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// 所以这里我们不设置 major, minor的版本,默认使用本计算机能用的最高版本// 使用 compatibility profile 就有内容出现了。int width = 600;int height = 600;// 使用glfw创建窗体GLFWwindow* window = glfwCreateWindow(width, height, "jave.lin - Learning OpenGL - 05_Texture", NULL, NULL);if (window == NULL) {std::cout << "Failed to create GLFW window" << std::endl; // 构建窗体失败glfwTerminate();exit(EXIT_FAILURE);}glfwMakeContextCurrent(window);glfwSetKeyCallback(window, key_callback); // 安装glfw内部键盘按键的回调if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { // 装载OpenGL的C函数库std::cout << "Failed to initialize OpenGL context" << std::endl; // 装载报错glfwTerminate();exit(EXIT_FAILURE);}// 打印版本信息print_infos(window);// 打印支持最大的顶点支持的数量int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum number of vertex attributes supported : " << nrAttributes << std::endl;// 打印着色器支持最大的纹理图像单元的数量int maxTexUnit;glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTexUnit);std::cout << "Maximun number of texture image units : " << maxTexUnit << std::endl;// 打印着色器支持最大的所有组合的纹理图像单元的数量int maxCombinedTexUnit;glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxCombinedTexUnit);std::cout << "Maximun number of Combined texture image units : " << maxCombinedTexUnit << std::endl;GLint mat_location;GLint vpos_location, vcol_location, vuv_location;GLuint vertex_buffer[4], index_buffer;GLuint vertex_array_object[2];GLuint sampler_object;GLuint texture;GLuint pixelBufObject;GLint success, infoLogLen;//glBindTextureUnit(0, 99);  // 第 0 索引的纹理单元,绑定一个不存在的纹理对象ID:99//checkGLError()// 用 lambda 设置,获取 pic 目录的回调,后面在封装g_GetPicturePathCallback = [](char* receiveBuff, const char* file)->char* {char buf[MAX_PATH];sprintf_s(buf, "..\\..\\Dependencies\\Pic\\%s", file);strcpy_s(receiveBuff, MAX_PATH, buf);return receiveBuff;};// loading texture here...// 加载纹理需要用的图片数据char img_path[MAX_PATH];g_GetPicturePathCallback(img_path, "my_tex.png");               // 获取图片目录int img_w, img_h, img_channels;stbi_set_flip_vertically_on_load(1);                            // 也可以在加载前设置加载时翻转的变量unsigned char* img_data = stbi_load(img_path, &img_w, &img_h, &img_channels, 4); // 加载图片数据,返回确定宽、高、通道数量、每个分量要多少字节if (img_data == NULL) {                                          // 如果加载图片失败std::cout << "Loading Image File : " << img_path << " FAILURE : " << stbi_failure_reason() << std::endl;exit(EXIT_FAILURE);}//stbi__vertical_flip(img_data, img_w, img_h, 4);                  // 如果不设置前面stbi_set_flip_vertically_on_load(1),也可以在这手动去翻转,因为图片坐标与纹理坐标的Y轴增量方向不同,所以需要翻转垂直方向的行数数据glCreateTextures(GL_TEXTURE_2D, 1, &texture);                 // 创建纹理对象//glActiveTexture(GL_TEXTURE0);                                    // 默认第 0 个纹理单元是激活的,可以不用设置glBindTextureUnit(0, texture);                                  // 绑定第 0 索引的纹理单元,OpenGL 4.0+建议用这个,与 glCreateTextures 配对。 OpenGL4.0-可用 glBindTexture// 查看有无错误checkGLError();//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);   // 使用 OpenGL 4.5+ 的 API 会更清晰:glTextureParameteri,因为这个是更具 target 类型,与当前 bind 的 纹理对象来确定设置那个纹理对象的,从可读性来说 4.5+ 版本的可读性高很多//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//glTextureParameteri(texture, GL_TEXTURE_WRAP_S, GL_REPEAT);        // 设置 texture 纹理对象的 GL_TEXTURE_WRAP_S 参数,就是设置 uv 中的水平 u 坐标超出0~1范围后的数值环绕方式,GL_REPEAT 是重复的//glTextureParameteri(texture, GL_TEXTURE_WRAP_T, GL_REPEAT);     // 设置 texture 纹理对象的 GL_TEXTURE_WRAP_T 参数,就是设置 uv 中的水平 v 坐标超出0~1范围后的数值环绕方式,GL_REPEAT 是重复的//glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 设置 texture 紋理对象的 GL_TEXTURE_MIN_FILTER 在像素缩小时的滤波方式//glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  // 设置 texture 紋理对象的 GL_TEXTURE_MAG_FILTER 在像素放大时的滤波方式// 查看有无错误checkGLError();glTextureStorage2D(                                                // 设置 texture 纹理对象的内部格式texture,                                                 // 要设置的 texture 纹理对象1,                                                          // mipmaps 的层数,只要1层 mipmaps 即可,至少要有1层,否则有错误。需要需要多层 mipmaps ,可以指定多层GL_RGBA8,                                                 // 内部数据格式img_w,                                                     // 图像的宽img_h                                                        // 图像的高);// 查看有无错误checkGLError();// 1 : 直接在 glTextureSubImage2D 的最后一个参数设置数据// 2 : 直接在 GL_PIXEL_UNPACK_BUFFER 中读取
#define GET_IMG_DATA_TYPE 1                                         // 使用 2 类型的缓存对象方式来加载纹理对象数据会有错误#if GET_IMG_DATA_TYPE == 2                                          // 如果需要从GL_PIXEL_UNPACK_BUFFER中读取纹理数据的话glCreateBuffers(1, &pixelBufObject);                         // 创建缓存对象checkGLError();glNamedBufferStorage(                                           // 给指定缓存对象配置参数、设置数据pixelBufObject,                                              // 要配置的缓存对象sizeof(img_data),                                            // 要分配多少字节缓存大小(sizeof(img_data)的大小)img_data,                                                   // 使用 img_data 初始化字节数据,如果填入 NULL,就是不使用数据初始化0);                                                            // flag 标记位暂时填入0checkGLError();glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixelBufObject);            // 将pixelBufObject缓存对象作为 GL_PIXEL_UNPACK_BUFFER 目标缓存对象checkGLError();
#endifglTextureSubImage2D(                                          // 给 texture 纹理对象设置对应 mipmap 层级的数据texture,                                                  // 要设置的 texture 纹理对象0,                                                          // mipmaps 的层级索引,从0开始,mipmaps 的0, 0,                                                      // 要从 x,y 偏移多少开始,不要偏移所以都填0img_w, img_h,                                              // 要填入的行、列尺寸的像素数量GL_RGBA, GL_UNSIGNED_BYTE,                                 // 外部格式,指定要包含的分量数量 和 分量类型
#if GET_IMG_DATA_TYPE == 1img_data                                                    // 外部图片数据
#elseNULL                                                       // 先填充GL_PIXEL_UNPACK_BUFFER数据,这里传入的是NULL从GL_PIXEL_UNPACK_BUFFER缓存数据的字节偏移NULL就是0偏移
#endif);// 查看有无错误checkGLError();                                                    // GET_IMG_DATA_TYPE 2 时会有错误 先填入第 0 层 mipmaps 数据//glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img_w, img_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data);// 如果前面给这个纹理对象指定了多层级 mipmaps,那么可以使用 glGenerateMipmap // void glGenerateMipmap(GLenum target);// void glGenerateTextureMipmap(GLuint texture);// 来给纹理的0层之后的其他mipmaps层生成图像数据//glGenerateMipmap(GL_TEXTURE_2D); // opengl 4.5之前的API,生成当前绑定纹理对象,且属于 GL_TEXTURE_2D 类型的纹理对象的mipmapsglGenerateTextureMipmap(texture);// opengl 4.5 API,生成指定纹理对象的mipmaps// 查看有无错误checkGLError();// when loading complete.// free image data herestbi_image_free(img_data);                                       // 纹理已经上传到了显存,内存中的数据可以删除了//glCreateSamplers(1, &sampler_object);                         // 创建采样器,OpenGL 4.5+才有的API,可读性更高,因为 OpenGL 默认的给每一个纹理对象都包含了一个默认配置的采样器对象,没必要使用,因为有默认的,除非你想多给纹理单元都使用同一个采样器来采样时,就可以通过这种方式来指定 查看有无错误//checkGLError();//glBindSampler(0, sampler_object);                               // 将纹理单元0 的采样器绑定为 sampler_object的 查看有无错误//checkGLError();ShaderProgram* shaderProgram = new ShaderProgram; shader program init 1 - 直接加载shader源码方式//if (!shaderProgram->initBySourceCode(vertex_shader_text, fragment_shader_text)) { shader program init 2 - 加载shader源码路径方式,我真的是服了C++获取当前运行目录就这么难吗?//char exeFullPath[512];//char vs_path[512], fs_path[512];//GetCurrentDirectoryA(1000, exeFullPath);//sprintf_s(vs_path, "%s\\Debug\\%s", exeFullPath, "shader1.vert");//sprintf_s(fs_path, "%s\\Debug\\%s", exeFullPath, "shader1.frag");//if (!shaderProgram->initByPath(vs_path, fs_path)) { shader program init 3 - 加载shader源码的相对路径,方面第二种方法的是绝对路径//if (!shaderProgram->initByPath("Debug\\shader1.vert", "Debug\\shader1.frag")) {//    std::cout << "ShaderProgram init Error: " << shaderProgram->errorLog() << std::endl; // 输出shader program错误// exit(EXIT_FAILURE);//}//    // 这种宏定义只能处理常量路径,所以如果要加载动态变量的路径只能写一个方法来处理
//#define GET_SHADER(name) "..\\..\\Dependencies\\Shaders\\"#name
//  // shader program init 4 - 根据shader源码的相对路径(常量),加载deps下的shader
//  if (!shaderProgram->initByPath(GET_SHADER(shader1.vert), GET_SHADER(shader1.frag))) {//      std::cout << "ShaderProgram init Error: " << shaderProgram->errorLog() << std::endl; // 输出shader program错误
//      exit(EXIT_FAILURE);
//  }// 用 lambda 设置,获取 shader 目录的回调,后面在封装g_GetShaderPathCallback = [](char* receiveBuff, const char* file)->char* {char buf[MAX_PATH];sprintf_s(buf, "..\\..\\Dependencies\\Shaders\\%s", file);strcpy_s(receiveBuff, MAX_PATH, buf);return receiveBuff;};// shader program init 5 - 根据shader源码的相对路径(变量),加载deps下的shaderchar vs_path[MAX_PATH], fs_path[MAX_PATH];g_GetShaderPathCallback(vs_path, "TestingTexture\\testing_tex_shader.vert");g_GetShaderPathCallback(fs_path, "TestingTexture\\testing_tex_shader.frag");if (!shaderProgram->initByPath(vs_path, fs_path)) {std::cout << "ShaderProgram init Error: " << shaderProgram->errorLog() << std::endl; // 输出shader program错误exit(EXIT_FAILURE);}mat_location = shaderProgram->getUniformLoc("transformMat"); // 获取 着色器程序的 uniform 变量的 locationvpos_location = shaderProgram->getAttributeLoc("vPos");          // 获取 顶点着色器中的顶点 attribute 属性的 locationvcol_location = shaderProgram->getAttributeLoc("vCol");         // 获取 顶点着色器中的顶点 attribute 属性的 locationvuv_location = shaderProgram->getAttributeLoc("vUV");           // 获取 顶点着色器中的顶点 attribute 属性的 locationglGenVertexArrays(2, vertex_array_object);                        // 生成两个 VAOglGenBuffers(4, vertex_buffer);                                  // 创建4个 VBO,这里我们因为有一个一样的顶点坐标,一个一样的顶点UV,两个不同的顶点顔色glGenBuffers(1, &index_buffer);                                    // 创建1个 EBO,因为两个 Quad 的顶点索引顺序都是一样的//// === VAO[0] ===//glBindVertexArray(vertex_array_object[0]);                      // 绑定 VAO[0],那么之后的 vbo, ebo,的绑定指针都是指向该 VAO 中的,还有顶点格式(规范)都会保存在该 VAOglBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[0]);               // 绑定 VBO[0]glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 设置 VBO 坐标数据glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[1]);               // 绑定 VBO[1]glBufferData(GL_ARRAY_BUFFER, sizeof(colors_1), colors_1, GL_STATIC_DRAW); // 设置 VBO 颜色数据glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]);               // 绑定 VBO[2]glBufferData(GL_ARRAY_BUFFER, sizeof(uvs), uvs, GL_STATIC_DRAW); // 设置 VBO uv数据glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer);         // 绑定 EBOglBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 设置 EBO 数据glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[0]);              // 绑定 VBO[0],因为后面要设置该 VBO 的坐标格式glVertexAttribPointer(vpos_location, 3, GL_FLOAT, GL_FALSE,       // 设置 顶点属性 vPos 格式sizeof(GLfloat) * 3, (GLvoid*)0);glEnableVertexAttribArray(vpos_location);                        // 启用 顶点缓存 location 位置的属性glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[1]);               // 绑定 VBO[1],因为后面要设置该 VBO 的颜色格式glVertexAttribPointer(vcol_location, 3, GL_FLOAT, GL_FALSE,       // 设置 顶点属性 vCol 格式sizeof(GLfloat) * 3, (GLvoid*)0);glEnableVertexAttribArray(vcol_location);                        // 启用 顶点缓存 location uv的属性glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]);               // 绑定 VBO[3],因为后面要设置该 VBO 的uv格式glVertexAttribPointer(vuv_location, 2, GL_FLOAT, GL_FALSE,        // 设置 顶点属性 vUV 格式sizeof(GLfloat) * 2, (GLvoid*)0);glEnableVertexAttribArray(vuv_location);                      // 启用 顶点缓存 location uv的属性//// === VAO[1] ===//glBindVertexArray(vertex_array_object[1]);                      // 绑定 VAO[1],那么之后的 vbo, ebo,的绑定指针都是指向该 VAO 中的,还有顶点格式(规范)都会保存在该 VAOglBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[0]);               // 绑定 VBO[1]//glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 设置 VBO 坐标数据,这里不用再设置坐标,因为都一样glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[2]);               // 绑定 VBO[2]glBufferData(GL_ARRAY_BUFFER, sizeof(colors_2), colors_2, GL_STATIC_DRAW); // 设置 VBO 颜色数据,颜色就需要重新设置了,因为不一样glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]);                // 绑定 VBO[2]//glBufferData(GL_ARRAY_BUFFER, sizeof(uvs), uvs, GL_STATIC_DRAW); // 设置 VBO uv数据,这里不用再设置坐标,因为都一样glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer);         // 绑定 EBO//glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 设置 EBO 数据,这里不用再设置索引值,因为都一样glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[0]);             // 绑定 VBO[0],因为后面要设置该 VBO 的坐标格式glVertexAttribPointer(vpos_location, 3, GL_FLOAT, GL_FALSE,       // 设置 顶点属性 vPos 格式sizeof(GLfloat) * 3, (GLvoid*)0);glEnableVertexAttribArray(vpos_location);                        // 启用 顶点缓存 location 位置的属性glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[2]);               // 绑定 VBO[2],因为后面要设置该 VBO 的颜色格式glVertexAttribPointer(vcol_location, 3, GL_FLOAT, GL_FALSE,       // 设置 顶点属性 vCol 格式sizeof(GLfloat) * 3, (GLvoid*)0);glEnableVertexAttribArray(vcol_location);                        // 启用 顶点缓存 location 位置的属性glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer[3]);               // 绑定 VBO[3],因为后面要设置该 VBO 的uv格式glVertexAttribPointer(vuv_location, 2, GL_FLOAT, GL_FALSE,        // 设置 顶点属性 vUV 格式sizeof(GLfloat) * 2, (GLvoid*)0);glEnableVertexAttribArray(vuv_location);                      // 启用 顶点缓存 location uv的属性//glEnable(GL_CULL_FACE);                                      // 开启面向剔除//glCullFace(GL_BACK);                                     // 设置剔除背面GLboolean cf = glIsEnabled(GL_CULL_FACE);                 // 查看是否启用面向剔除std::cout << "cull face enabled : " << (cf ? "true" : "false") << std::endl;//glFrontFace(GL_CW);                                          // 顺时针//glFrontFace(GL_CCW);                                        // 逆时针(默认的)ClockWiseGLint facing;glGetIntegerv(GL_FRONT_FACE, &facing);                       // 获取正面的顺逆时针 : CW(ClockWise - 顺时针), CCW(Counter ClockWise - 逆时针)std::cout << "facing : " << (facing == GL_CW ? "CW" : "CCW") << std::endl;mat4x4 rMat, tMat, tranformMat;                                         // 声明定义一个 mat4x4 用的旋转矩阵while (!glfwWindowShouldClose(window)) {                 // 检测是否需要关闭窗体glfwGetFramebufferSize(window, &width, &height);       // 获取窗口大小glViewport(0, 0, width, height);                       // 设置ViewportglClearColor(0.1f, 0.2f, 0.1f, 0.f);                   // 设置清理颜色缓存时,填充颜色值glClear(GL_COLOR_BUFFER_BIT);                          // 清理颜色缓存//glUseProgram(program);                                   // 使用此着色器程序,两个 VAO 的着色都一样,设置一些 uniform 不一样shaderProgram->use();glBindVertexArray(vertex_array_object[0]);              // 先绘制 VAO[0] 的 VBO,EBO,VAF,ENABLEDmat4x4_identity(tMat);                                   // 给矩阵单位化,消除之前的所有变换mat4x4_translate(tMat, -0.5, 0.0f, 0.0f);             // x轴位移-0.5,注意是NDC下的坐标//glUniformMatrix4fv(mat_location, 1, GL_FALSE, (const GLfloat*)tMat); // 设置, 着色器中 uniform mat4 rMat; 的矩阵数据shaderProgram->setMatrix4x4(mat_location, (const GLfloat*)tMat); // 设置, 着色器中 uniform mat4 rMat; 的矩阵数据glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (GLvoid*)0); // 参数1:绘制三角图元;参数2:取6个索引来绘制三角图元(每个三角图元需要3个,所以可以画两个三角图元);参数3:将 GL_ELEMENT_ARRAY_BUFFER 每个元素视为 uint 类型;参数4:设置索引缓存的字节偏移量。也可以设置为另一个 缓存数据的指针,即:使用另一个数据。glBindVertexArray(vertex_array_object[1]);               // 先绘制 VAO[1] 的 VBO,EBO,VAF,ENABLEDmat4x4_identity(rMat);                                   // 给矩阵单位化,消除之前的所有变换mat4x4_rotate_Z(rMat, rMat, (float)glfwGetTime());        // 先旋转,沿着 z 轴旋转,旋转量为当前 glfw 启用到现在的时间点(秒)mat4x4_translate(tMat, +0.5, 0.0f, 0.0f);              // 再位移mat4x4_mul(tranformMat, tMat, rMat);                  // 将旋转与位移的变换合并//glUniformMatrix4fv(mat_location, 1, GL_FALSE, (const GLfloat*)tranformMat); // 设置, 着色器中 uniform mat4 rMat; 的矩阵数据shaderProgram->setMatrix4x4(mat_location, (const GLfloat*)tranformMat); // 设置, 着色器中 uniform mat4 rMat; 的矩阵数据glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (GLvoid*)0); // 参数1:绘制三角图元;参数2:取6个索引来绘制三角图元(每个三角图元需要3个,所以可以画两个三角图元);参数3:将 GL_ELEMENT_ARRAY_BUFFER 每个元素视为 uint 类型;参数4:设置索引缓存的字节偏移量。也可以设置为另一个 缓存数据的指针,即:使用另一个数据。glfwSwapBuffers(window);                              // swap buffer, from backbuffer to front bufferglfwPollEvents();                                        // 处理其他的系统消息}glDeleteSamplers(1, &sampler_object);                      // 测试删除采样器对象 SOglDeleteBuffers(1, &pixelBufObject);                     // 测试删除 BOglDeleteBuffers(4, vertex_buffer);                            // 测试删除 VBOglDeleteBuffers(1, &index_buffer);                           // 测试删除 EBOglDeleteBuffers(2, vertex_array_object);                 // 测试删除 VAOglDeleteTextures(1, &texture);                               // 删除纹理对象 TOdelete shaderProgram;                                       // 销毁 shader programcheckGLError();                                             // 最后再看看GL还有什么错误glfwDestroyWindow(window);                                  // 销毁之前创建的window对象glfwTerminate();                                          // 清理glfw之前申请的资源//checkGLError();                                               // 最后再看看GL还有什么错误,从这句一直报错可以看出:glfw 的销毁与退出是有问题的return 0;
} // int main() {

总结

纹理的使用简述步骤为:

  • 创建纹理对象
  • 设置纹理对象内部格式
  • 加载图片的图像数据
  • 设置外部格式数据加载到纹理对象
  • 回收图像数据
  • 激活纹理单元(默认有激活第0个)
  • 绑定纹理对象到指定的纹理单元上
  • 添加顶点数据中的:纹理坐标数据
    • 创建纹理坐标 vbo,设置指向顶点数据缓存对象
    • 设置纹理坐标 vbo 的格式(规范)
  • 创建采样器对象(这步可以不需要,默认纹理对象上有默认配置的采样器)
  • 绑定采样器对象到指定的纹理单元(这步可以不需要,默认纹理对象上有默认配置的采样器)
  • 着色器添加采样器(注意采样器维度类型要与应用程序创建的纹理对象一直)
  • 顶点着色器添加纹理坐标 attribute、varying 变量
    • 将 attribute 设置到 varying
  • 片段着色器添加纹理坐标 varying 变量
    • 使用 纹理坐标 varying 变量,来传入 texture 内置函数来采样纹理对象的数据(texture(sampler_tex_xxx, uv)

References

  • 纹理

LearnGL - 05 - Texture相关推荐

  1. Reading a paper of Texture'05 submission #050 Hole Filling Throng Photomontage

    今天读了篇文章,Hole Filling Throng Photomontage.英国人写的,关于图像(大部分是照片)的特定区域填充,可能这么说不大准确,应该叫graph cuts.老师说这篇文章的理 ...

  2. opengl地球贴纹理_一文看懂材质/纹理 Material, Texture, Shading, Shader 的区别

    在计算机图形学和三维设计中,有几个容易混淆的概念.今天我们来一举拿下. 概念整理 可以这么总结: Material 是表现 Shading 的数据集.其他几个概念都是生成这一数据集的资源或者工具. 这 ...

  3. Q96:PT(3.5):木纹纹理(Wood Texture)

    标题中的"PT"表示:Procedural Texture(过程纹理).表示该章节属于"过程纹理"的内容. 当前章节在"过程纹理"内容中的位 ...

  4. 问题六十七:ray tracing学习总结(2016.11.13, 2017.02.05)

    从2016.11.13开始接触ray tracing到今天2017.02.05,差不多80天的时间.截至当前,学习ray tracing的过程,也是我重新找回自己或者说是"find what ...

  5. LearnGL - 03 - DrawQuad - VBO/EBO - 理解 CW, CCW 的正背面

    文章目录 如何绘制? 先画第一个直角三角形 绘制效果 再画第二个直角三角形 绘制效果 查看正面的顺/逆时针 使用 索引缓存 EBO/IBO 来节省显存 重新计算显存顶点+索引用量 索引缓存 EBO/I ...

  6. 【论文阅读32】《Texture Defragmentation for Photo-Reconstructed 3D Models》

    目录 1 introduction 2 overview 3 Related work 3.1 Single-patch Mesh Parametrization 3.2 Global Mesh Pa ...

  7. LearnGL - 11.1 - 实现简单的Gouraud光照模型 dot 点积/点乘的作用

    文章目录 Gouraud dot - 点积的作用 图形了解顶点点积的作用 漫反射 纯漫反射效果 Diffuse - Shader GLSL 的中奇怪的问题 高光 reflect - 反射高光方向 GL ...

  8. 3D mark 05 测试

    买了台新电脑,这两天一直在考机 ,用3Dmark05测了一下4177(NoAA,MaxAF,No催化剂,No超频)分,中规中矩. 下面是测试数据: File Name Benchmark   Widt ...

  9. http://bbs.859e.com/forum.php,[15.04.05][战团1.161+][因斯维尔的抉择][1.5032]

    1.代码: 二次分裂制作组的module_item借鉴: R大的领军者module_system借鉴: 战风奇幻纪元module_system借鉴: 2.装备模型: 直接或修改后引用的OSP或其作者: ...

最新文章

  1. 创建DLL动态链接库——声明导出法
  2. git 提交代码命令_Git命令可视化展示,代码管理再也不愁了,建议收藏!
  3. html文字依次显示,利用定时器和css3动画制作文字依次渐变显示的效果
  4. 工科学生考研能选择计算机专业么,这8个“工科专业”考研后发展会更好,毕业生紧缺度高,前途很好!...
  5. NOIP2013普及组复赛试题_计数问题
  6. 看到大家在讨论阿拉伯数字-》汉字数字的转换,拿出我去年写的C版本
  7. 动软代码生成器的具体使用方法步骤
  8. hp/博科光纤交换机配置小记
  9. 2021-10-25
  10. Eclipse使用技巧--设置编辑器背景护眼色和设置字体
  11. Apache虚拟主机配置之基于IP的虚拟主机实践
  12. 「冰狐智能辅助」如何在线实时调试?
  13. 关于 电子护照 的基本小常识问答
  14. QT 5.14 高仿 Win10 计算器(标准、科学、程序员、货币、容量)
  15. 关于单体应用的简单讲解
  16. 消息中间件之rabbitMQ实战-死信队列
  17. [转帖] “王者对战”之 MySQL 8 vs PostgreSQL 10
  18. 什么是驱动?驱动程序的工作原理?
  19. Kafka配置动态SASL_SCRAM认证
  20. Spring In Action读书笔记

热门文章

  1. matlab特征值是空集,MATLAB中矩阵方程求解的实现
  2. umi build打包之后部署报错
  3. firstvalue函数mysql_MySQL 窗口函数之头尾函数
  4. 一个简单的微分对策问题求解及其Matlab实现
  5. Android开发(四):在标题栏右上角实现菜单(三个点)
  6. 简单的模拟京东商城购买过程-pymysql
  7. 电脑键盘错乱完美解决
  8. linux开发板通过网线连接电脑(win10)连接网络问题
  9. openstack搭建教程
  10. 使用Python解数学方程