OpenGL纹理映射

纹理的作用

在渲染的时候,为了让图像看起来真实,我们就必须由足够多的定点,指定足够多的定点颜色,但是这样会导致每个模型都会有巨大的开销。为了解决这个问题,我们通常使用2D的图片来代替更多的端点,由于图片上有很多细节,所以在渲染时就可以直接使用图片覆盖在3D物体的表面,让渲染出来的物体看起来更加精致,这就是纹理的作用。

纹理的创建与加载

1. 加载纹理图片

我们知道纹理来源于2D的图片,所以第一步我们需要从原始纹理的图片文件中读出图像的原始数据。有如果使用了opencv的话,可以很方便读取常见格式的图片到mat对象中,但是在一般OpenGL工程中也不会为读取图片特别去配置opencv的环境,一般可以使用一些比较小巧的库。当前比较常用的是std_image.h,它是个单头文件图像加载库,它能够加载大部分流程的图像格式。也正是由于它只有一个头文件,所以将它加载到工程是非常方便的。

std_image.h文件下载地址为:https://github.com/nothings/stb/blob/master/stb_image.h

加载图像文件代码如下:

1
2
3
4
5
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

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

由于在load图像的时候申请了内存,所以在使用完成后,需要记得释放内存空间:

1
stbi_image_free(data);

2. 创建纹理

在载获取到了图像的数据后,需要使用OpenGL创建绑定纹理、载入图像数据,除此之外一般会要设置纹理的一些配置属性如:“纹理环绕方式”、“纹理过滤”、“多级渐远纹理”。

  • 纹理环绕方式,可以设置当纹理坐标超过(0, 0)到(1, 1)这个限定范围后的操作,重复这个纹理图像,s、t时其两个方向
  • 纹理过滤,可以指定在超过原始图像分辨率时怎么插值,有放大(Magnify)和缩小(Minify)两种情况

以下为这些操作的基本流程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
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);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}

纹理映射

为了将2D的图片贴附在3D的物体上,有一个非常关键的过程就是要确定纹理图的哪部分对应3D物体的那部分,这就是纹理映射要解决的问题。

1. 纹理坐标

首先要规定的是纹理坐标(Texture Coordinate),由于纹理实际上是一张2D的图像,所以纹理坐标是一个二维坐标,左下角为坐标原点,右上角坐标规定为(1,1),这就是整个纹理坐标系的范围;而另外由于原始纹理图片是有分辨率大小的,所以在映射时需要插值来获取超过了分辨的像素点。

2. 纹理属性

在确定了纹理坐标后,我们只需要将端点坐标与纹理坐标一一对应起来就可以了,使用OpenGL的顶点数组可以规定格式如下:

1
2
3
4
5
6
7
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};

如上代码,在vertices数组中每8个数据代表一个端点,前三个为三维的位置坐标,中间3个为端点的RGB颜色属性,最后两个为其对应纹理贴图中的位置的纹理坐标,在内存中分布如下图:

vertices格式

有了以上端点数组后,就可以指定顶点属性,告诉OpenGL如何解析。前面两个属性不再写了,下面是添加纹理坐标属性的代码:

1
2
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

3. 在Shader中的处理

一般在顶点Shader中,会将传入的顶点数组,依照设置的顶点属性进行解析,然后传入片元Shader中处理;在片元Shader中需要使用到GLSL供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,在这里我们使用sampler2D;另外我们使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。

片元Shader的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main()
{
FragColor = texture(ourTexture, TexCoord);
}

绘制纹理

以上纹理操作全部完成后,就可以绘制了:

1
2
3
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

参考

  1. LearnOpenGL CN - 纹理
Compartir