《Learn OpenGL》 Ch 04 Textures
本部分的实现代码,见 04_Textures
#采样
为了把纹理映射到图形上,我们需要指定三角形的每个顶点对应纹理图片的哪个地方,所以每个顶点都会关联一个 纹理坐标(Texture Coordinate)
。
如需要给一个三角形贴上砖块的纹理,可以如下设置,其中将三角形的左下角对应纹理的 点。只需要为图形的各个顶点设置纹理坐标,其余部分会自动根据顶点的纹理坐标自动采样。
#纹理映射
在采样部分中,对于每个顶点,赋值的纹理坐标都处于 的范围内。当设置的坐标处于这个范围外时,就需要用到纹理映射。
纹理映射一共有以下几种选项:
GL_REPEAT
:重复纹理(OpenGL 默认)GL_MIRRORED_REPEAT
:镜像重复GL_CLAMP_TO_EDGE
:边界拉伸GL_CLAMP_TO_BORDER
:边界填充(需要设定填充颜色)
纹理映射可以分别对图片的三个轴进行设置,图片的三个轴称为 S, T, R
,并通过函数 glTexParameter*
设置,其中 *
表示要设置的参数的类型,如 i
表示 int。设置代码如下:
1 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); |
第一个参数是表示要设置的图片类型,第二个参数是图片设置的轴,第三个参数是映射的选项。
#纹理过滤
纹理坐标只考虑了纹理如何与顶点数据结合,但并没有考虑纹理和实际渲染的分辨率。当两者分辨率不同时,就需要用到纹理过滤。
纹理会用实际渲染的分辨率进行渲染,因此纹理会进行相应的拉伸或缩小。因为纹理和实际的像素并没有一一对应,所以实际的像素颜色要经过纹理的插值计算。插值方式有 线性插值(GL_NEAREST)
和 邻近插值(GL_LINEAR)
,如下所示:
其中黑十字出现的地方是真实像素的位置,其他的大格表示纹理像素。临近插值的结果由距离最近的像素的颜色决定,线性插值的结果由附近多个像素的颜色平均值决定。
临近插值的画面会导致颗粒感,线性插值的画面虽然更加平滑,但会显得模糊。
纹理过滤同样使用 glTexParameter*
设置,如下所示:
1 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
#MipMap
当一个物体离我们很远时,其在屏幕中显示的范围就会变得很小,而若他仍然使用很大分辨率的纹理,那么 OpenGL 从高分辨率纹理上采样出正确的颜色就很困难。所以理想的状态是,显示范围大的物体使用高分辨率纹理,范围小的物体则使用低分辨率纹理。
OpenGL 采用一个叫多级渐远纹理(Mipmap)的方法来解决这一问题,即有一系列纹理图像,后一个纹理图像的宽高是前一个纹理图像宽高的 1/2。如下所示:
可以通过调用函数 glGenerateMipmaps
生成 Mipmap。
当使用 Mipmap 时,当纹理在不同级别中切换时,会产生生硬的边界,所以同样需要过滤。当使用了 Mipmap 时有以下四种过滤方式:
GL_NEAREST_MIPMAP_NEAREST
:使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样GL_LINEAR_MIPMAP_NEAREST
:使用最邻近的多级渐远纹理级别,并使用线性插值进行采样GL_NEAREST_MIPMAP_LINEAR
:在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样GL_LINEAR_MIPMAP_LINEAR
:在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样
设置采样代码如下:
1 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); |
注意,仅在实际渲染的分辨率小于图片分辨率时,mipmap 才有用,即关于 mipmap 的采样数值,只能在设置对象为 GL_TEXTURE_MIN_FILTER
时采用。
#读取图片
读取图片依赖于 Sean Barrett 开发的 stb_image 头文件,在下载头文件并放到 include 文件夹后,通过以下代码引入头文件:
1 |
下载 container 图片,并放到 resources
文件夹下。
通过 stbi_load
函数读取图片文件,如下所示:
1 | int width, height, nrChannels; |
其中获取到的 data
即为图片数据,供之后 OpenGL 加载使用。
用上述数据加载图片后,会发现图片上下颠倒,这是因为 OpenGL 认为 处于左下角,而图片定义的 点处于左上角。
可以通过设置 stbi_set_flip_vertically_on_load(true);
直接让 stb 读取的图片进行反转。
#加载纹理
OpenGL 加载纹理的流程如下
1 | unsigned int texture; |
如同生成和绑定 VBO,VAO 等,图片的加载也需要先创建图片 ID,再绑定图片 ID。
通过函数 glTexImage2D
加载图片数据
- 第一个参数为图片的类型
- 第二个参数为 Mipmap 的等级,即可以通过该函数单独的为某一 level 的 MipMap 设置
- 第三个参数为目标图片的纹理类型,这里读取的是 JPG,没有 alpha 通道,所以使用
GL_RGB
,如果读取的是 PNG 等有 alpha 通道的图片,需要使用GL_RGBA
- 第四,第五个参数为目标图片的宽高
- 第六个参数因为历史原因,始终为 0
- 第七个参数为源图片的纹理,即通过
stbi_load
加载的图片的纹理类型,这里是GL_RGB
- 第八个参数为源图片的数据类型
- 第九个参数为源图片数据
在加载完成后,可通过 glGenerateMipmap
函数创建 MipMap。
然后可以通过 glTexParameteri
函数设置纹理的过滤和映射。
1 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); |
#应用纹理
#设置 Texcoord
当使用纹理时,对于每一个顶点,都需要设置 Texcoord,所以每个顶点需要额外增加两个 float 来表示 Texcoord:
1 | constexpr float vertices[] = { |
相对应的,顶点着色器也要新增 Texcoord 的 layout 输入:
1 | layout (location=0) in vec3 pos; |
传递 layout 数据的语句也需要相应的改变:
1 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *)0); |
#纹理单元
在加载纹理部分中已经 CPP 侧将纹理读取至了新生成的纹理 ID 中,之后需要将纹理传递给着色器。纹理在着色器部分的对应类型为 sampler2D
。每一个 sampler2D
对象被称为纹理单元,通常来说 OpenGL 最少有 16 个纹理单元。如下代码,就定义了一个纹理单元
在 Fragment Shader 中,需要获取由 Vertex Shader 传递过来的 tex,
1 | in vec2 tex; |
在 CPP 部分中,通过函数 glActiveTexture
, glBindTexture
和 glUniform1i
的组合,将纹理传递给着色器。
1 | glActiveTexture(GL_TEXTURE0); |
其中的 GL_TEXTURE0
表示是第一个纹理单元,在激活了该单元后,通过 glBindTexute
将纹理 ID 与该纹理单元结合在一起。然后通过 glUniform1i
将纹理单元传递到着色器中,例子中 glUniform1i
的第二个形参,就是要传递的纹理单元,因为这里是第一个纹理单元,所以传递 0。如果有多个纹理单元,也是使用同样的传递方法,如下:
1 | glActiveTexture(GL_TEXTURE0); |
#解析纹理
GLSL 有内置的函数 texture
来解析纹理,第一个参数为类型是 sampler2D
的纹理变量,第二个参数为类型是 vec2
的 Texcoord 变量。函数的返回值是像素的颜色,因此可以直接作为着色器的输出:
1 | FragColor = texture(ourTexture, TexCoord); |
此时的效果为: