七、OpenGL中Texture(纹理)的理论与应用

一、纹理的基本概念

在 OpenGL 中,纹理通常是一个 2D 图像(但也可以是 1D、3D、立方体贴图等)。它会被映射到 3D 物体的表面,而每个顶点都通过纹理坐标(Texture Coordinates) 来确定如何从纹理图像上取颜色。

常见的 OpenGL 纹理类型:
在这里插入图片描述

二、什么是纹理坐标

在 OpenGL 中,纹理坐标(Texture Coordinates)用于在 3D 模型上正确映射 2D 纹理图像。它们定义了纹理图片的哪一部分应该映射到一个 3D 物体的某个顶点上。

1. 纹理坐标的表示
纹理坐标通常使用归一化坐标系(Normalized Coordinates),即 (s, t) 或 (u, v),它们的取值范围通常是 [0,1],但 OpenGL 也支持超出 [0,1] 的坐标(用于平铺或重复纹理)。

  • s 或 u:表示纹理的水平方向(X 轴)
  • t 或 v:表示纹理的垂直方向(Y 轴)

例如:

  • (0,0) 表示纹理的左下角
  • (1,0) 表示纹理的右下角
  • (0,1) 表示纹理的左上角
  • (1,1) 表示纹理的右上角

2. 纹理坐标与顶点关联
在 OpenGL 中,纹理坐标通常与顶点一起传递给着色器。例如,一个包含四个顶点的四边形(Quad),可以设置如下的纹理坐标:

float vertices[] = {
    // 位置(x, y)     // 纹理坐标(s, t)
    -0.5f, -0.5f,       0.0f, 0.0f,  // 左下角
     0.5f, -0.5f,       1.0f, 0.0f,  // 右下角
     0.5f,  0.5f,       1.0f, 1.0f,  // 右上角
    -0.5f,  0.5f,       0.0f, 1.0f   // 左上角
};

3. 纹理坐标在着色器中的使用
在 OpenGL 着色器中,片段着色器(Fragment Shader)会使用纹理坐标从纹理图片中采样颜色:

#version 330 core
in vec2 TexCoord;      // 从顶点着色器传入的纹理坐标
uniform sampler2D ourTexture;  // 纹理对象
out vec4 FragColor;

void main()
{
    FragColor = texture(ourTexture, TexCoord);  // 采样纹理颜色
}

4. 纹理坐标的超出范围处理
如果纹理坐标超出了 [0,1],OpenGL 提供了多种纹理环绕模式(Wrapping Mode):

  • GL_REPEAT(默认):重复纹理
  • GL_MIRRORED_REPEAT:镜像重复
  • GL_CLAMP_TO_EDGE:拉伸边缘
  • GL_CLAMP_TO_BORDER:使用边界颜色填充

示例代码:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

5. 纹理坐标的变换
如果希望对纹理进行旋转、缩放或偏移,可以使用纹理矩阵(Texture Matrix)或在着色器中对 TexCoord 进行变换。例如:

vec2 newTexCoord = TexCoord * 2.0;  // 放大两倍

三、为什么要引入纹理坐标

在 OpenGL 中,纹理坐标(Texture Coordinates) 的引入是为了在 3D 模型表面正确映射 2D 纹理,从而实现更真实的渲染效果。

1. 解决 2D 纹理映射到 3D 物体的问题
在 3D 渲染中,我们的模型通常由顶点(Vertex) 组成,而纹理(Texture) 是一个 2D 图像。要让 2D 纹理正确地贴在 3D 物体上,就需要知道哪个像素应该出现在模型的哪个位置。引入纹理坐标 (s, t)(或 u, v),就是为了建立 3D 空间顶点与 2D 纹理像素之间的映射关系。

示例: 假设有一个四边形(Quad),我们想把一个 100×120的图片贴到它上面:

  • 没有纹理坐标 ➝ OpenGL 不知道如何匹配四边形上的顶点和纹理上的像素
  • 有纹理坐标 ➝ 每个顶点都能对应到纹理上的某个点,形成正确的纹理映射

在这里插入图片描述
在这里插入图片描述
纹理其实就是贴图系统,遵循UV坐标
在这里插入图片描述
如何将左边的图片的像素拷贝到右边的三角形内部呢?首先定义三角形会先定义其三个顶点,如图所示三角形的三个顶点的UV坐标分别是(0,0)、(0.5, 1)、(1, 0)。然后通过差值计算能够得到三角形内部的任意像素点的UV坐标。然后通过UV坐标再乘以左边图像的长或宽就能锁定到该图像上的某一个像素点。然后把这个像素点的颜色拷贝到右边的三角形内部的像素点上,然后就得到了三角形的每个像素点 的颜色。

2. 允许更灵活的贴图方式
通过调整纹理坐标,可以实现不同的纹理效果:

  • 缩放(Scaling):通过 TexCoord * scale 调整纹理大小
  • 旋转(Rotation):应用变换矩阵实现纹理旋转
  • 平移(Translation):通过 TexCoord + offset 让纹理滑动

示例:

vec2 newTexCoord = TexCoord * 2.0;  // 纹理缩放 2 倍

3. 允许不同类型的纹理映射
纹理坐标的引入让我们可以定义多种不同的纹理映射方式:

  • 平面映射(Planar Mapping):适用于地板、墙壁等
  • 球面映射(Spherical Mapping):适用于球体
  • 立方体映射(Cube Mapping):适用于天空盒(Skybox)和环境贴图(Environment Mapping)
  • UV 展开(UV Unwrapping):用于复杂 3D 模型(例如角色建模)

例如,一个 3D 角色模型,使用 UV 展开后,每个部分都可以被正确映射到纹理上,而不会导致拉伸或变形。

4. 提高渲染效率
如果没有纹理坐标,每个像素可能都需要进行复杂的计算来决定它应该显示哪部分纹理。而引入纹理坐标后,GPU 可以高效地进行纹理采样,提高渲染效率。
OpenGL 直接使用 texture() 函数,从纹理坐标查找对应的颜色值:

vec4 color = texture(myTexture, TexCoord);

这样可以充分利用 GPU 的纹理缓存和优化,加速渲染。

5. 处理超出范围的纹理映射
如果没有纹理坐标,纹理无法正确处理超出 [0,1] 范围的情况。而 OpenGL 提供了多种 环绕模式(Wrapping Mode) 让我们控制超出的纹理:

  • GL_REPEAT:纹理重复
  • GL_MIRRORED_REPEAT:镜像重复
  • GL_CLAMP_TO_EDGE:拉伸到边缘
  • GL_CLAMP_TO_BORDER:使用边界颜色填充
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

这些功能都依赖于纹理坐标的定义。

四、纹理的过滤模式

如果经过uwidth以及vheight计算出来坐标为(12.3,15.8)那么就要有一种方式进行取整,从而能够采样到图片上面的一个像素点的颜色信息。
在这里插入图片描述
在 OpenGL 中,GL_NEAREST(最近邻采样)是一种纹理过滤模式,用于决定在纹理映射过程中如何从纹理中获取颜色值。当使用 GL_NEAREST 模式时,OpenGL 会选择距离纹理坐标最近的那个纹理像素(texel)的颜色值,直接作为片段的颜色输出。

特点:

  • 性能高:由于仅需简单地选择最接近的像素,无需进行复杂的计算,因此这种方式处理速度快。
  • 像素化效果:在放大纹理时,可能会出现明显的锯齿状边缘或马赛克效果。
  • 适用场景:适用于需要保留像素化风格的场景,如某些复古风格的游戏或应用。

设置方法:
可以通过以下代码为纹理设置放大和缩小时使用 GL_NEAREST 过滤模式:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // 放大过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // 缩小过滤

需要注意的是,GL_NEAREST 过滤模式在放大纹理时可能会导致图像出现明显的像素化效果。如果希望获得更平滑的图像效果,可以考虑使用 GL_LINEAR(线性过滤)模式。

在这里插入图片描述
在 OpenGL 中,GL_LINEAR(线性插值)是一种常用的纹理过滤模式,用于在纹理映射过程中,通过对采样点周围的多个纹理像素(texel)进行加权平均计算,来获取更平滑的颜色过渡效果。

工作原理:
当需要从纹理中采样颜色时,GL_LINEAR 模式会考虑采样点周围最近的四个纹理像素,并对这四个像素的颜色值进行加权平均,以得到平滑过渡的颜色。具体而言,距离采样点越近的像素,其颜色对最终结果的贡献越大。

特点:

  • 平滑过渡:由于采用了加权平均的方式,GL_LINEAR 能够有效减少锯齿和像素化现象,使纹理在放大或缩小时呈现出更平滑的过渡效果。
  • 计算开销:相比于 GL_NEAREST(最近邻采样),GL_LINEAR 需要对多个像素进行插值计算,因此在性能上可能略有影响,但在现代硬件上,这种开销通常可以忽略不计。

适用场景:
GL_LINEAR 适用于对视觉质量要求较高的应用场景,例如高清图形渲染、照片编辑软件、3D 建模等。对于需要平滑过渡、细节丰富的纹理,GL_LINEAR 通常能提供更好的视觉效果。

设置方法:
可以通过以下代码为纹理设置放大和缩小时使用 GL_LINEAR 过滤模式:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 放大过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 缩小过滤

注意事项:
虽然 GL_LINEAR 能够提供更平滑的纹理效果,但在某些情况下,可能会引入轻微的模糊,特别是在大幅度缩放时。这种模糊是由于插值过程混合了多个像素的颜色造成的。因此,需要根据具体应用场景选择合适的过滤模式,以在性能和视觉效果之间取得平衡。

在这里插入图片描述

五、纹理接口预览

在C++当中创建Texture,并且绑定到系统当中进行渲染以及数据传递。
在这里插入图片描述
在FragmentShader当中进行调取,并且利用纹理坐标找到像素,对输出颜色赋值
在这里插入图片描述
其实OpenGL给我们提供了很多的纹理Texture锚定点,比如GL_TEXTURE0-GL_TEXTURE15。
为什么我们需要这么多的Texture锚定点呢? 请看下图:
在这里插入图片描述
在 OpenGL 中使用纹理(Texture)通常包括以下几个关键步骤:
1. 创建纹理对象
首先,需要生成一个纹理对象并获取其ID。

GLuint textureID;
glGenTextures(1, &textureID);

2. 绑定纹理
将纹理绑定到指定的纹理目标(如GL_TEXTURE_2D)。

glBindTexture(GL_TEXTURE_2D, textureID);

3. 设置纹理参数
配置纹理的过滤和环绕方式。

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);

4. 加载纹理数据
将图像数据加载到纹理中。

int width, height, nrChannels;
unsigned char *data = stbi_load("texture.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::cerr << "Failed to load texture" << std::endl;
}
stbi_image_free(data);

5. 使用纹理
在渲染时激活并绑定纹理。

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureID);

6. 在着色器中使用纹理
顶点着色器:vertex_shader.vert

#version 330 core
layout (location = 0) in vec3 aPos; //顶点位置锚点
layout (location = 1) in vec3 aColor; //顶点颜色锚点
layout (location = 2) in vec2 aUV; //纹理锚点

out vec4 outColor;
out vec2 outUV;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    outColor = vec4(aColor, 1.0f);
    outUV = aUV;
}

在片段着色器中采样纹理:fragment_shader.frag

#version 330 core
out vec4 FragColor;

in vec4 outColor;
in vec2 outUV;

/*
sampler2d:是OpenGL内建的变量
ourTexture:本身是由C++传输告诉shader这个值是多少,如果没有传输,则默认为0
*/
uniform sampler2D  ourTexture;

void main()
{
    FragColor = texture(ourTexture, outUV) * outColor;
}

六、完整示例

#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include "glad/glad.h"
#include "GLFW/glfw3.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

GLuint VBO = 0;
GLuint VAO = 0;
GLuint _texture = 0;
GLuint shaderProgram = 0;

void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window) {
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
		glfwSetWindowShouldClose(window, true);
	}
}

void rend() {
	glBindTexture(GL_TEXTURE, _texture);
	glUseProgram(shaderProgram);
	glBindVertexArray(VAO);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
	glBindVertexArray(0);
	glUseProgram(0);
}

void initModel() {
	GLfloat vertices[] = {
		// 位置(x, y, z)     颜色(r, g, b)    纹理(u, v)
		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, 0.0f, 0.1f, 1.0f, 0.0f, 1.0f,
	};

	GLuint indices[] =
	{
		0, 1, 3,
		1, 2, 3,
	};

	glGenVertexArrays(1, &VAO);
	glBindVertexArray(VAO);

	GLuint EBO = 0;
	glGenBuffers(1, &EBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

	glGenBuffers(1, &VBO); //获取vbo的index
	glBindBuffer(GL_ARRAY_BUFFER, VBO); //绑定vbo的index,给vbo分配显存空间,传输数据
	//告诉shader数据解析格式
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 
	
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); //顶点位置锚点
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(sizeof(float)*3)); //顶点颜色锚点
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(sizeof(float)*6)); //纹理锚点
	
	glEnableVertexAttribArray(0); //启用顶点位置锚点
	glEnableVertexAttribArray(1); //启用顶点颜色锚点
	glEnableVertexAttribArray(2); //启动纹理锚点
	
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);
}

void initTexture() {
	int _picType = 0;
	int _width = 0;
	int _height = 0;

	//stbimage读入的图片是反过来的
	stbi_set_flip_vertically_on_load(true);
	unsigned char* bits = stbi_load("MultiTexture/BrushStroke_Coloured_Variant_C.png", &_width, &_height, &_picType, STBI_rgb_alpha);

	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_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	if (bits) {
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, _width, _height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bits);
	}

	stbi_image_free(bits);
}

void initShader(const char* _vertexPath, const char* _fragmPath) {
	std::string _vertexCode("");
	std::string _fragCode("");

	std::ifstream _vShaderFile;
	std::ifstream _fShaderFile;

	_vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
	_fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);

	try {
		_vShaderFile.open(_vertexPath);
		_fShaderFile.open(_fragmPath);

		std::stringstream _vShaderStream, _fShaderStream;
		_vShaderStream << _vShaderFile.rdbuf();
		_fShaderStream << _fShaderFile.rdbuf();

		_vertexCode = _vShaderStream.str();
		_fragCode = _fShaderStream.str();
	} catch (const std::exception&) {
		std::string errStr = "read shader fail";
		std::cout << errStr << std::endl;
	}

	const char* _vShaderStr = _vertexCode.c_str();
	const char* _fShaderStr = _fragCode.c_str();

	//shader的编译链接
	unsigned int _vertexID = 0, _fragID = 0;
	char _inforLog[512] = { 0 };
	int _successFlag = 0;

	
	//编译
	_vertexID = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(_vertexID, 1, &_vShaderStr, NULL);
	glCompileShader(_vertexID);

	glGetShaderiv(_vertexID, GL_COMPILE_STATUS, &_successFlag);
	if (_successFlag) {
		glGetShaderInfoLog(_vertexID, 512, NULL, _inforLog);
		std::string errstr(_inforLog);
		std::cout << _inforLog << std::endl;
	}

	_fragID = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(_fragID, 1, &_fShaderStr, NULL);
	glCompileShader(_fragID);

	glGetShaderiv(_fragID, GL_COMPILE_STATUS, &_successFlag);
	if (_successFlag) {
		glGetShaderInfoLog(_vertexID, 512, NULL, _inforLog);
		std::string errstr(_inforLog);
		std::cout << _inforLog << std::endl;
	}

	//链接
	shaderProgram = glCreateProgram();
	glAttachShader(shaderProgram, _vertexID);
	glAttachShader(shaderProgram, _fragID);
	glLinkProgram(shaderProgram);

	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &_successFlag);
	if (!_successFlag) {
		glGetProgramInfoLog(shaderProgram, 512, NULL, _inforLog);
		std::string errStr(_inforLog);
		std::cout << _inforLog << std::endl;
	}
	glDeleteShader(_vertexID);
	glDeleteShader(_fragID);
}

int main() {
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	GLFWwindow* window = glfwCreateWindow(800, 600, "OPenGL Core", NULL, NULL);
	if (window == NULL) {
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);

	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	glViewport(0, 0, 800, 600);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	initModel();
	initTexture();
	initShader("vertex_shader.vert", "fragment_shader.frag");

	while (!glfwWindowShouldClose(window)) {
		processInput(window);

		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		rend();

		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	// 删除 VAO 和 VBO
	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);

	// 清理
	glfwTerminate();
	
	return 0;
}

效果展示:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值