OpenGl之纹理坐标及纹理映射

news2024/10/5 14:00:19

文章目录

  • 纹理坐标
  • 纹理映射
  • 代码

纹理坐标

 为了实现纹理贴图我们需要做三件事:将一张贴图加载到OpenGL中,提供纹理坐标和顶点(将纹理对应匹配到顶点上),并使用纹理坐标从纹理中进行取样操作取得像素颜色。由于三角形会被缩放、旋转、平移变换导致最后会以不同的结果投影显示到屏幕上,而且由于camera的不同操作看上去也会很不一样。GPU要做的就是让纹理紧跟三角形图元顶点的移动使其看上去真实(如果纹理看上去明显游离在三角形上产生错位就不真实了)。为实现这个效果开发者需要为每个顶点提供一系列纹理坐标。在GPU光栅化三角形阶段,会对纹理坐标进行插值计算并覆盖到整个三角形面上,并且在片段着色器中开发者要将这些坐标跟纹理进行匹配。这个操作叫做‘取样’,取样的结果叫做‘纹素’(纹理中的一个像素)。纹素通常包含一个颜色值用于画屏幕上对应的一个像素。后面的教程中我们会看到纹素可以包含不同的数据类型来产生不同的效果。
 OpenGL支持几种不同类型的纹理:1D,2D,3D,立方体等等,可以应用于不同的技术中。现在这里就先只使用2D纹理。一张2D纹理可以在某些特殊限制下有任意宽度和高度的,宽度和高度相乘可以计算得到纹素的数量。那么如何确定纹理坐标和顶点呢?事实上不是的,坐标并不是在纹理中纹素的坐标,那样局限性太大了,因为这样如果要用一张宽度高度不一样的纹理替换一张纹理的话我们得更新所有顶点的坐标来匹配新的纹理图片。理想的方案是要能够在不改变纹理坐标的情况下随意更换纹理贴图。因此,纹理坐标是定义在‘纹理空间’的,也就是定义在单位化的[0,1]范围内。也就是说纹理坐标事实上是个分数,纹理的宽度高度乘以相应的比例分数就可以算出纹素在纹理中的坐标。例如:如果纹理坐标是[0.5,0.1]并且纹理的宽度为320,高度为200,那么纹素在纹理中的坐标为(160,20)(0.5 * 320 = 160 和 0.1 * 200 = 20)。

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  // 左上 
};

我们知道纹理坐标的范围是在0-1的,我们先正常设置纹理坐标,看看得到的结果。
在这里插入图片描述
这是我们的纹理。
在这里插入图片描述
 这是正常将纹理坐标设置在0-1之间的时候,这时候我们修改下纹理坐标,将0.0修改为0.5。我们看看这时候的变化:
在这里插入图片描述
 我们可以看到只有纹理图片的四分之一。由于设置了最小是从0.5开始的,所以采样的时候,是从图片的中间开始采样的。
 当我们把最大坐标设置成2.0的时候,由于OpenGL的默认采样方式是超出范围即进行重复。
在这里插入图片描述
所以最后的效果就是这样。
对于这个结果我们可以这么理解,我们可以分几部分来看就很容易理解了
在这里插入图片描述

纹理映射

 一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的激活纹理单元,当我们只有一张纹理的时候,这时候是不需要考虑纹理单元和采样器之间的对应关系的。
 当我们有多个纹理的时候就需要考虑他们之间的对应关系并进行设置,否则会出现错误的结果。
 纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像glBindTexture一样,我们可以使用glActiveTexture激活纹理单元,传入我们需要使用的纹理单元:

glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);

读取纹理

	int width, height, nrChannels;
	unsigned char* data = stbi_load("../pics/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);

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

		//float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
		//glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	}
	else 
	{
			std::cout << "Failed to load texture" << std::endl;
	}
	stbi_image_free(data);

 我们还要通过使用glUniform1i设置每个采样器的方式告诉OpenGL每个着色器采样器属于哪个纹理单元。

ourShader.use(); // 不要忘记在设置uniform变量之前激活着色器程序!
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手动设置
ourShader.setInt("texture2", 1); // 或者使用着色器类设置

while(...) 
{
    [...]
}

我们看一下如果没有正常设置的话会出现什么情况。(第一次绑定的是木箱,第二次绑定的是笑脸)
第一种情况:
在这里插入图片描述
两个纹理单元都没有激活,由于OpenGL默认只开启第一个纹理单元,所以这时候的第二个纹理单元是无法使用的,且这时候的第一个纹理单元是最后一次绑定的那个纹理,所以最后的结果为:
在这里插入图片描述

第二种情况:
在这里插入图片描述
激活了第二个纹理单元且绑定第二张图片
在这里插入图片描述
我们可以发现激活纹理单元的主要作用是让纹理单元上有对象,OpenGL默认开启第一个纹理单元
第三种情况:

在这里插入图片描述
没有告诉着色器去哪个纹理单元上进行读取,这时候默认都是从第一个纹理单元上进行读取
在这里插入图片描述
正确的结果
在这里插入图片描述

代码

main.cpp

#include <glad/glad.h> 
#include <GLFW/glfw3.h>
#include <iostream>
#include <cmath> 
#include "../shader.h"
#include "../stb_image.h"

float vertices[] = {
	// 位置              // 颜色           // 纹理坐标
	 0.5f,  0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 2.0f, 2.0f, // 右上
	 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 2.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, 2.0f  // 左上 
};

unsigned int indices[] = { // 注意,我们从零开始算! 
	0, 1, 3, // 第一个三角形 
	1, 2, 3 // 第二个三角形 
};

float ratio = 0.5;
void processInput(GLFWwindow* window);
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
int main() {
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__ 
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	if (window == NULL) {
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	//GLFW将窗口的上下文设置为当前线程的上下文
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	//GLAD
	// glad: 加载所有OpenGL函数指针
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	Shader ourShader("shaders/shader.vs","shaders/shader.fs");

	//创建VBO和VAO对象,并赋予ID
	unsigned int VBO, VAO, EBO;
	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);
	//绑定VBO和VAO对象
	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	//为当前绑定到target的缓冲区对象创建一个新的数据存储。
	//如果data不是NULL,则使用来自此指针的数据初始化数据存储
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	//告知Shader如何解析缓冲里的属性值
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
	//开启VAO管理的第一个属性值
	glEnableVertexAttribArray(0);

	//告知Shader如何解析缓冲里的属性值
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
	//开启VAO管理的第一个属性值
	glEnableVertexAttribArray(1);

	//告知Shader如何解析缓冲里的属性值
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
	//开启VAO管理的第一个属性值
	glEnableVertexAttribArray(2);

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

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);

	stbi_set_flip_vertically_on_load(true);
	unsigned int texture, texture1;
	glGenTextures(1, &texture);
	glBindTexture(GL_TEXTURE_2D, texture);

	// 加载并生成纹理 
	int width, height, nrChannels;
	unsigned char* data = stbi_load("../pics/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);

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

		//float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
		//glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	}
	else 
	{
			std::cout << "Failed to load texture" << std::endl;
	}
	stbi_image_free(data);

	glGenTextures(1, &texture1);
	glBindTexture(GL_TEXTURE_2D, texture1);
	data = stbi_load("../pics/awesomeface.png", &width, &height, &nrChannels, 0);
	if (data)
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);

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

		//float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
		//glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	}
	else
	{
		std::cout << "Failed to load texture" << std::endl;
	}
	stbi_image_free(data);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, texture);
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, texture1);

	ourShader.use();
	ourShader.setInt("texture1", 0);
	ourShader.setInt("texture2", 1);
	ourShader.setFloat("ratio", ratio);

	// 渲染循环
	while (!glfwWindowShouldClose(window)) {
		processInput(window);
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置
		glClear(GL_COLOR_BUFFER_BIT); //状态使用

		ourShader.use();
		ourShader.setFloat("ratio", ratio);

		glBindVertexArray(VAO);
		// glfw: 交换缓冲区和轮询IO事件(按键按下/释放、鼠标移动等)
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	// glfw: 回收前面分配的GLFW先关资源. 
	glfwTerminate();
	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	glDeleteProgram(ourShader.ID);

	return 0;
}

void processInput(GLFWwindow* window) {
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);
	if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS)
	{
		ratio += 0.001;
		printf("%lf\n", ratio);
		if (ratio > 1.0)ratio = 1.0;
	}
	if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS)
	{
		ratio -= 0.001;
		if (ratio < 0.0)ratio = 0.0;
	}
}

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

shader.vs

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec2 TexCoord;
uniform float offsetX;

void main()
{
    gl_Position = vec4(aPos.x,aPos.y,aPos.z, 1.0);
    TexCoord = aTexCoord;
}

shader.fs

#version 330 core
out vec4 FragColor;
in vec2 TexCoord;

uniform sampler2D texture1; 
uniform sampler2D texture2; 
uniform float ratio;

void main()
{
    FragColor = mix(texture(texture1, TexCoord/2.0), texture(texture2, vec2(1.0-TexCoord.x,TexCoord.y)), ratio); 
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/633644.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue源码解析--更新中

【尚硅谷】Vue源码解析之虚拟DOM和diff算法 【Vue源码】图解 diff算法 与 虚拟DOM-snabbdom-最小量更新原理解析-手写源码-updateChildren] 文章目录 2. snabbdom 简介 及 准备工作2.1 简介2.2 搭建初始环境1. 安装snabbdom2. 安装webpack5并配置3. 复制官方demo Example 3. …

如何把歌曲里的伴奏音乐提取出来,分享几个方法给大家!

对于一首歌&#xff0c;我们都知道&#xff0c;它有两部分组成&#xff1a;背景音乐人声。这两者合在一起&#xff0c;便是我们经常听的歌。部分用户想要直接获取歌曲伴奏&#xff0c;那么可以在UU伴奏网上下载。 操作方法比较简单&#xff0c;直接搜索想要的歌曲名称就可以了…

【分布式系统与一致性协议】

分布式系统与一致性协议 CAP原理APCPCA总结BASE理论 一致性拜占庭将军问题 分布式系统是一个硬件或软件组件分布在不同的网络计算机上&#xff0c;彼此之间仅仅通过消息传递进行通信和协调的系统。 分布式系统的设计目标一般包含如下&#xff1a; 可用性&#xff1a;可用性是分…

C++14中binary literals的使用

一个形如42的值被称作字面值常量(literal),这样的值一望而知。每个字面值常量都对应一种数据类型&#xff0c;字面值常量的形式和值决定了它的数据类型。 我们可以将整型字面值写作十进制(基数为10)、八进制(基数为8)或十六进制(基数为16)数的形式。以0开头的整数代表八进制数&…

Dijkstra算法求最短路

Dijkstra算法是单源最短路算法&#xff0c;是用来求一个点到其他所有点点最短距离&#xff0c;使用小根堆优化后时间复杂度大概为 O m l o g n Omlogn Omlogn 注意&#xff1a;不可以解决存在负权边的问题 【模板】单源最短路径&#xff08;标准版&#xff09; 链接&#xff1…

使用CRM系统通过四点快速收集客户信息

CRM是客户关系管理的缩写&#xff0c;它可以帮助企业管理销售、营销和客户服务&#xff0c;提升管理能力&#xff0c;获得更多收益。 那么&#xff0c;在CRM系统中客户是什么&#xff1f;如何快速收集客户信息呢&#xff1f;下面我们来说一说。 一、CRM中的客户是什么&#xf…

Java012——String引用数据类型的简单学习

回顾Java数据类型 本次要学习的是Java引用数据类型String 一、对String类简单说明 说明&#xff1a;String是Java中的一个类 二、String类的作用 作用&#xff1a;主要用来创建和操作字符串。 三、使用String类 3.1、创建字符串 注意&#xff1a; 1、字符串使用双引号&qu…

shell脚本:文本三剑客awk

awk-命令讲解&#xff1a; awk-命令讲解 一、awk&#xff1a;1.定义&#xff1a;2.格式&#xff1a;3.工作过程&#xff1a;4.工作原理&#xff1a;5.常用选项&#xff1a; 二、内置函数&#xff1a;1.getline的工作过程&#xff1a;2.打印&#xff1a;3.文件内容匹配过滤打印…

软考A计划-系统架构师-学习笔记-第四弹

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff…

信道编码的基本概念

本专栏包含信息论与编码的核心知识&#xff0c;按知识点组织&#xff0c;可作为教学或学习的参考。markdown版本已归档至【Github仓库&#xff1a;https://github.com/timerring/information-theory 】或者公众号【AIShareLab】回复 信息论 获取。 文章目录 信道编码信道编码的…

[迁移学习]预训练和微调

一、概述 一般的有监督迁移学习分为以下三种&#xff1a; ①将训练好的模型作为特征抽取模块&#xff08;例如以resnet进行特征提取&#xff09; ②在一个相关的任务中训练后直接后直接使用(例如gpt) ③在训练好的模型基础上进行微调 此外还有无监督学习的方式 zero-shot&#…

jmeter004:察看结果树

元件添加路径&#xff1a;线程组>监听器>察看结果树 取样器结果&#xff1a; ps&#xff1a;取样器是可以显示变量所对应的值的 Thread Name&#xff1a;线程组名称 Sample Start&#xff1a;运行的开始时间 Load time&#xff1a;加载的时间、持续的时间 Connect Time&a…

从零玩转系列之微信支付开篇

一、前言 halo各位大佬很久没更新了最近在搞微信支付,因商户号审核了我半个月和小程序认证也找了资料并且将商户号和小程序进行关联,至此微信支付Native支付完成.此篇文章过长我将分几个阶段的文章发布(项目源码都有,小程序和PC端) 二、演示 微信支付Native案例 微信支付JSAPI案…

【MySQL】数据表的基本操作

目录 1. 创建表 2. 创建表案例 2.1 创建一个users表 2.2 查看表结构 2.3 修改表 3. 删除表 MySQL&#x1f337; 1. 创建表 语法&#xff1a; CREATE TABLE table_name (field1 datatype,field2 datatype,field3 datatype ) character set 字符集 collate 校验规则 engine 存储…

Blogger空闲短域名搜索代码

Blogger是一个由Google提供的&#xff08;收购的公司&#xff09;免费博客平台&#xff0c;旨在使个人博客创建变得容易。用户可以使用Blogger创建自己的博客并将其托管在Google的服务器上。它不需要任何费用&#xff0c;并提供简单易用的编辑器和主题来帮助用户轻松地创建博客…

网络安全学术顶会——SP 2023 议题清单、摘要与总结(下)

注&#xff1a;本文由ChatGPT与Claude联合生成 121、QueryX: Symbolic Query on Decompiled Code for Finding Bugs in COTS Binaries 可扩展的静态检查工具&#xff0c;如Sys和CodeQL&#xff0c;成功地发现了源代码中的错误。这些工具允许分析人员编写应用程序特定的规则&…

虚拟机角度下的线程

虚拟机角度下的线程 jvm与线程 一个 Java 应用程序通常只包含一个 JVM 进程&#xff08;Java Virtual Machine&#xff0c;Java虚拟机&#xff09;&#xff0c;但在某些情况下可能会有多个 JVM 进程。 一个 Java 应用通常是一个进程&#xff0c;这个进程就是jvm&#xff0c;编…

从Cookie到Session: Servlet API中的会话管理详解

文章目录 一. Cookie与Session1. Cookie与Session2. Servlet会话管理操作 二. 登录逻辑的实现 一. Cookie与Session 1. Cookie与Session 首先, 在学习过 HTTP 协议的基础上, 我们需要知道 Cookie 是 HTTP 请求报头中的一个关键字段, 本质上是浏览器在本地存储数据的一种机制,…

基本类型转换和引用类型转换

文章目录 前言基本类型转换自动数据类型转换强制数据类型转换 引用数据类型转换向上转型向下转型 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; Java 中的类型转换可以分为基本类型转换和引用类型转换两种。 基本类型转换指的是将一种基本数据类型转换…

Koa学习3:用户添加、错误处理

模型 在src目录下创建model目录&#xff0c;用来存放模型 创建用户模型 user.model.js 注意&#xff1a; UUID类型是无法自增的&#xff0c;将id设置为UUID类型时只需要为其指定默认值即可 // 数据类型 const { DataTypes } require(sequelize); // 导入已经连接了数据库…