OpenGL学习记录(五)

参考资料:
https://learnopengl.com/
https://learnopengl-cn.github.io/

这次要实现的是Transform变换功能。OpenGL没有自带任何的矩阵和向量知识,可以使用已经做好了的数学库,如GLM。从 这里 可以下载GLM库,使用时需导入以下头文件:

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

要对图像进行位移、旋转、缩放等操作,离不开矩阵,因此在顶点着色器编码中,定义一个四维矩阵变量,与 gl_Position 相乘实现变换功能。

//顶点着色器编码
#version 330 core										
layout(location = 0) in vec3 aPos;						
layout(location = 1) in vec3 aColor;		
layout(location = 2) in vec2 aTexCoord;	

uniform mat4 transform;

out vec4 vertexColor;						  
out vec2 TexCoord;

void main() {											
	gl_Position = transform * vec4(aPos.x, aPos.y, aPos.z, 1.0);    
	vertexColor = vec4(aColor, 1.0f);	
	TexCoord = aTexCoord;
}

在main函数中的循环绘制阶段,可以使用glUniformMatrix4fv函数把矩阵数据发送给着色器。第一个参数是uniform的位置值,使用glGetUniformLocation获取对应着色器中所需的uniform变量位置值,我们定义了一个uniform mat4 transform,因此获取transform变量的位置值。第二个参数告诉OpenGL我们将要发送多少个矩阵,这里是1。第三个参数询问我们是否希望对我们的矩阵进行转置(Transpose),也就是说交换我们矩阵的行和列。OpenGL开发者通常使用一种内部矩阵布局,叫做列主序(Column-major Ordering)布局。GLM的默认布局就是列主序,所以并不需要转置矩阵,因此填GL_FALSE。最后一个参数是真正的矩阵数据,但是GLM并不是把它们的矩阵储存为OpenGL所希望接受的那种,因此我们要先用GLM的自带的函数value_ptr来变换这些数据。

glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "transform"), 1, GL_FALSE, glm::value_ptr(trans)); //把矩阵数据发送给着色器

接下来,只需要定义这个trans矩阵,也就是真正的变换矩阵了,它能够决定对图像的变换方式。使用glm::mat4声明一个四维矩阵。glm::translate可以将其转换为位移变换矩阵,也就是第四列中前三行对应元素的值,它的参数要求填入要变换的矩阵,以及三维的位移变换向量。位移矩阵如下:

在这里插入图片描述

glm::rotate可以将其转换为旋转变换矩阵,填入要变换的矩阵,要变换的角度(实际需要的是弧度值,使用glm::radians将角度转为弧度),和决定绕哪个轴旋转的三维旋转变量。绕x、y、z三轴旋转的变换矩阵如下:

在这里插入图片描述

glm::scale转换为缩放矩阵,传入要变换的矩阵,和三维缩放向量(即矩阵前三行前三列对角线上的元素值)。缩放矩阵如下:

在这里插入图片描述

值得注意的是,矩阵乘法是不遵守交换律的,这意味着它们的顺序很重要。举例来说,如果先旋转后位移,那么因为旋转之后模型的正面朝向改变了,模型就会向新的方向位移,这不符合原本的预期。为了让变换更符合直观感受,应当先进行缩放操作,然后是旋转,最后位移。而矩阵是以左乘的形式与向量进行运算的,所以几个矩阵相乘时,“最右边的”矩阵会优先对向量造成影响,因此应当以 Mt * Mr * Ms * v 这种由位移变换矩阵、旋转变换矩阵、缩放操作矩阵依次相乘的形式作为最终的变换矩阵。因此变换矩阵的代码如下:

//变换矩阵
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(0.3f, 0.3f, 0.2f)); //位移
trans = glm::rotate(trans, glm::radians(45.0f), glm::vec3(0, 0, 1.0f)); //旋转
trans = glm::scale(trans, glm::vec3(0.5f, 0.5f, 0.5f)); //缩放

这样trans矩阵将会对图像造成的影响为:先将xyz三个维度均缩放为原来的0.5倍,然后绕Z轴进行45度的旋转,再将图像在x方向移动0.3,在y方向移动0.3,z方向移动0.2。主程序文件的代码和运行后的结果如下所示:

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "Shader.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

//顶点数据
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    // 左上
};

//顶点索引
unsigned int indices[] = {
	0, 1, 2,   //第一个三角形使用的顶点
	2, 3, 0    //第二个三角形使用的顶点
};


//检查输入函数
void processInput(GLFWwindow* window)
{
	//按下ESC键时,将WindowShouldClose设为true,循环绘制将停止
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
	{
		glfwSetWindowShouldClose(window, true);
	}
}

//视口改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height); //OpenGL渲染窗口的尺寸大小
}

int main()
{
	glfwInit(); //初始化GLFW
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //告诉GLFW要使用OpenGL的版本号
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //主版本号、次版本号都为3,即3.3版本
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //告诉GLFW使用核心模式(Core-profile)

	//打开 GLFW Window
	GLFWwindow* window = glfwCreateWindow(1600, 1200, "My OpenGL Game", nullptr, nullptr);
	if (window == nullptr) //若窗口创建失败,打印错误信息,终止GLFW并return -1
	{
		printf("Open window failed.");
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window); //创建OpenGL上下文
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //用户改变窗口大小的时候,视口调用回调函数

	//初始化GLEW
	glewExperimental = true;
	if (glewInit() != GLEW_OK) //若GLEW初始化失败,打印错误信息并终止GLFW窗口
	{
		printf("Init GLEW failed.");
		glfwTerminate();
		return -1;
	}


	Shader* myShader = new Shader("vertexSource.txt", "fragmentSource.txt");

	//创建VAO(顶点数组对象)
	unsigned int VAO;
	glGenVertexArrays(1, &VAO); //生成一个顶点数组对象
	glBindVertexArray(VAO); //绑定VAO

	//创建VBO(顶点缓冲对象)
	unsigned int VBO;
	glGenBuffers(1, &VBO); //生成缓冲区对象。第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的数组,由于只需创建一个VBO,因此不需要用数组形式
	glBindBuffer(GL_ARRAY_BUFFER, VBO); //把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上,GL_ARRAY_BUFFER是一种顶点缓冲对象的缓冲类型。OpenGL允许同时绑定多个缓冲,只要它们是不同的缓冲类型。
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //把定义的数据复制到当前绑定缓冲的函数。参数1:目标缓冲类型,参数2:指定传输数据的大小(以字节为单位),参数3:我们希望发送的实际数据,参数4:指定显卡如何管理给定的数据,GL_STATIC_DRAW表示数据不会或几乎不会改变。

	//创建EBO(元素缓冲对象/索引缓冲对象)
	unsigned int EBO;
	glGenBuffers(1, &EBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);


	//位置属性
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); //告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)。参数1:要配置的顶点属性(layout(location = 0),故0),参数2:指定顶点属性的大小(vec3,故3),参数3:指定数据的类型,参数4:是否希望数据被标准化,参数5:在连续的顶点属性组之间的间隔,参数6:表示位置数据在缓冲中起始位置的偏移量(Offset)。
	glEnableVertexAttribArray(0); //以顶点属性位置值作为参数,启用顶点属性,由于前面声明了layout(location = 0),故为0

	//颜色属性
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);

	//UV属性
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
	glEnableVertexAttribArray(2);

	//使用纹理单元0绑定TexBufferA
	unsigned int TexBufferA;
	glGenTextures(1, &TexBufferA);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, TexBufferA);

	int width, height, nrChannel;
	stbi_set_flip_vertically_on_load(true); //翻转图像y轴

	//加载并生成第一张纹理
	unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannel, 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 << "图片加载失败" << std::endl;
	}
	stbi_image_free(data); //释放

	//使用纹理单元3绑定TexBufferB
	unsigned int TexBufferB;
	glGenTextures(1, &TexBufferB);
	glActiveTexture(GL_TEXTURE3);
	glBindTexture(GL_TEXTURE_2D, TexBufferB);

	//加载并生成第二张纹理
	unsigned char* data2 = stbi_load("awesomeface1.png", &width, &height, &nrChannel, 0);
	if (data2)
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data2);
		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
		std::cout << "图片加载失败" << std::endl;
	}
	stbi_image_free(data2);

	//变换矩阵
	glm::mat4 trans;
	trans = glm::translate(trans, glm::vec3(0.3f, 0.3f, 0.2f)); //位移
	trans = glm::rotate(trans, glm::radians(45.0f), glm::vec3(0, 0, 1.0f)); //旋转
	trans = glm::scale(trans, glm::vec3(0.5f, 0.5f, 0.5f)); //缩放

	//让程序在手动关闭之前不断绘制图像
	while (!glfwWindowShouldClose(window))
	{
		//检测输入
		processInput(window);

		//渲染指令
		glClearColor(0.f, 0.5f, 0.5f, 1.0f); //设置清空屏幕所用的颜色
		glClear(GL_COLOR_BUFFER_BIT); //清空屏幕的颜色缓冲区

		//绑定纹理到对应的纹理单元
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, TexBufferA);
		glActiveTexture(GL_TEXTURE3);
		glBindTexture(GL_TEXTURE_2D, TexBufferB);

		glBindVertexArray(VAO); //绘制物体的时候就拿出相应的VAO,绑定它
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //绑定EBO
		

		myShader->use();

		//告诉OpenGL每个着色器采样器属于哪个纹理单元
		glUniform1i(glGetUniformLocation(myShader->ID, "ourTexture"), 0); 
		glUniform1i(glGetUniformLocation(myShader->ID, "ourFace"), 3);

		glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "transform"), 1, GL_FALSE, glm::value_ptr(trans)); //把矩阵数据发送给着色器

		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //绘制,参数1:绘制模式(三角形),参数2:绘制顶点数(两个三角形6个顶点),参数3:索引的类型,参数4:指定EBO中的偏移量。


		//检查并调用事件,交换缓冲区
		glfwSwapBuffers(window); //交换颜色缓冲区,前缓冲区保存最终输出的图像,后缓冲区负责绘制渲染指令,当渲染指令执行完毕后,交换前后缓冲区,使完整图像呈现出来,避免逐像素绘制图案时的割裂感
		glfwPollEvents(); //检查触发事件,如键盘输入、鼠标移动等
	}


	glfwTerminate(); //关闭GLFW并退出
	return 0;
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值