参考资料:
https://learnopengl.com/
https://learnopengl-cn.github.io/
添加第一人称摄像机系统。
建立一个相机类,.h文件如下:
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
class Camera
{
public:
Camera(glm::vec3 position, glm::vec3 target, glm::vec3 worldup);
Camera(glm::vec3 position, float pitch, float yaw, glm::vec3 worldup);
//相机参数
glm::vec3 Position;
glm::vec3 Forward;
glm::vec3 Right;
glm::vec3 Up;
glm::vec3 WorldUp;
//欧拉角
float Pitch;
float Yaw;
float SenseX = 0.01f; //移动系数
float SenseY = 0.01f; //移动系数
float SpeedZ = 0; //前后移动速度
glm::mat4 GetViewMatrix(); //计算观察矩阵
void ProcessMouseMovement(float deltaX, float deltaY); //鼠标控制的移动
void UpdateCameraPos(); //更新相机位置,WS控制
private:
void UpdateCameraVecors(); //更新参数
};
重载构造函数,使构造时可以以初始位置、目标(与位置相减可获得相机朝向)、世界空间向上的向量来确定一个相机的状态,也可以将初始目标替换为pitch、yaw两个欧拉角来构造。接着要设定可能使用的参数,分别为相机的位置、前向向量、右向量
、上向量和世界坐标系中的上方向向量,要用到的两个欧拉角,控制相机移动快慢的移动系数,以及声明了几个方法,分别为计算观察矩阵、鼠标控制的移动、更新相机位置、更新参数。顺带一提,有如下几个信息就可以定义一个摄像机:
.cpp文件如下:
#include "Camera.h"
//构造,根据位置、初始目标、世界空间上方向向量
Camera::Camera(glm::vec3 position, glm::vec3 target, glm::vec3 worldup)
{
Position = position;
WorldUp = worldup;
Forward = glm::normalize(target - position); //目标位置减相机位置得到前向向量
Right = glm::normalize(glm::cross(WorldUp, Forward)); //世界坐标系中上向量和自身前向量叉乘得到自身右向量
Up = glm::normalize(glm::cross(Right,Forward)); //自身右向量和自身前向量叉乘得到自身前向量
}
//构造,根据位置、pitch和yaw两个欧拉角、世界空间上方向向量
Camera::Camera(glm::vec3 position, float pitch, float yaw, glm::vec3 worldup)
{
Position = position;
WorldUp = worldup;
Pitch = pitch;
Yaw = yaw;
//根据pitch和yaw构造前向向量
Forward.x = glm::cos(Pitch) * glm::sin(Yaw);
Forward.y = glm::sin(Pitch);
Forward.z = glm::cos(Pitch) * glm::cos(Yaw);
Right = glm::normalize(glm::cross(WorldUp, Forward));
Up = glm::normalize(glm::cross(Right, Forward));
}
glm::mat4 Camera::GetViewMatrix()
{
//LookAt函数需要一个位置、目标和上向量,创建观察矩阵
return glm::lookAt(Position, Position + Forward, WorldUp);
}
void Camera::ProcessMouseMovement(float deltaX, float deltaY)
{
//传入x、y的位移量,对相机自身Pitch、Yaw修改,并更新参数
Pitch -= deltaY * SenseX;
Yaw -= deltaX * SenseY;
UpdateCameraVecors();
}
void Camera::UpdateCameraPos()
{
//使Forward向量根据SpeedZ变化,达到前进后退的效果
Position += Forward * SpeedZ * 0.1f;
}
void Camera::UpdateCameraVecors()
{
//更新参数
Forward.x = glm::cos(Pitch) * glm::sin(Yaw);
Forward.y = glm::sin(Pitch);
Forward.z = glm::cos(Pitch) * glm::cos(Yaw);
Right = glm::normalize(glm::cross(WorldUp, Forward));
Up = glm::normalize(glm::cross(Right, Forward));
}
其中,构造函数接收了根据位置、初始目标、世界空间上方向向量,Position和WorldUp可以直接赋值,前向向量则由目标位置减相机位置得到。关于自身右向量的计算,世界坐标系中上向量和自身前向量叉乘得到一个与这两个向量组成的平面垂直的右向量,它不会受自身上方向向量所影响,因为自身上方向向量不管怎么变也都在组成的这个平面里,得到的叉乘向量是一样的。把这个得到的向量作为自身右向量。然后根据自身右向量和自身前向量叉乘得到自身前向量。另一个重载的构造函数中,除了根据pitch和yaw构造前向向量外,其余步骤一致。GetViewMatrix()方法中,通过GLM提供的lookAt函数,传入一个摄像机位置,一个目标位置和一个表示世界空间中的上向量的向量,接着GLM就会创建一个LookAt矩阵,我们可以把它当作我们的观察矩阵。ProcessMouseMovement方法接收两个方向的变动量,使pitch和yaw减去这个变动量达到更改相机朝向的目的,中间通过一个系数来控制转向幅度,之后更新一波参数。UpdateCameraPos方法同理使Forward向量根据SpeedZ变化,达到前进后退的效果。UpdateCameraVecors方法用于更新参数,由于Pitch和Yaw可能发生的改动,因此重新计算各个参数的值。
相机类写完后,在主文件中检查输入函数需添加相应的按键响应,改变SpeedZ的值:
//检查输入函数
void processInput(GLFWwindow* window)
{
//按下ESC键时,将WindowShouldClose设为true,循环绘制将停止
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
//按下W键时,向前移动相机
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
{
camera.SpeedZ = 1.0f;
}
//按下S键时,向后移动相机
else if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
{
camera.SpeedZ = -1.0f;
}
else
{
camera.SpeedZ = 0;
}
}
我们还需要告诉GLFW,它应该隐藏光标,并捕捉(Capture)它,可使用如下语句:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
在调用这个函数之后,无论我们怎么去移动鼠标,光标都不会显示了,它也不会离开窗口。
为了计算俯仰角和偏航角,我们需要让GLFW监听鼠标移动事件。GLFW为我们提供了这样一个回调函数,需要按它指定的参数进行输入。在此之前,先声明好变量lastX和lastY用于保存上一时刻的鼠标输入,以及一个bool类型变量判断是否为第一次输入。函数体要做的事情是,首先判断是否是第一次输入,是的话设置上一时刻X、Y值为当前鼠标值,这是因为在刚打开窗口时鼠标的xy输入不确定,而上一时刻的鼠标输入变量初始值是0,这样做避免下一时刻因为两个值差距过大导致相机瞬移。接下里定义两个float变量deltaX、deltaY,用当前鼠标的值减上一次保留的鼠标位置值获得这一时刻鼠标在xy两个方向上的移动量,用这两个移动量设置camera中的ProcessMouseMovement方法参数,进而修改相机自身参数达到相机转向的效果。
//上一时刻的鼠标输入
float lastX;
float lastY;
bool firstMouse = true; //是否为第一次输入
//GLFW监听鼠标移动事件回调函数
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse == true)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float deltaX, deltaY;
deltaX = xpos - lastX;
deltaY = ypos - lastY;
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(deltaX, deltaY);
}
剩下就是创建一个相机对象和在循环绘制阶段的末尾调用相机UpdateCameraPos的方法对相机中的Forward向量进行改变,实现相机前后移动的功能,影响这个移动的变量在按键检测阶段已经进行了设置。创建的camera对象的使用方法,主要是用来设置观察矩阵,通过viewMat = camera.GetViewMatrix()来建立观察矩阵,进而影响物体在画面中的显示,因此这个方法也要在循环绘制阶段调用。
目前的主文件代码如下:
#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>
#include "Camera.h"
//顶点数据
//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 // 左上
//};
float vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f
};
//顶点索引
unsigned int indices[] = {
0, 1, 2, //第一个三角形使用的顶点
2, 3, 0 //第二个三角形使用的顶点
};
//立方体位置
glm::vec3 cubePositions[] = {
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3(2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3(1.3f, -2.0f, -2.5f),
glm::vec3(1.5f, 2.0f, -2.5f),
glm::vec3(1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)
};
//上一时刻的鼠标输入
float lastX;
float lastY;
bool firstMouse = true; //是否为第一次输入
//相机类
//Camera camera(glm::vec3(0, 0, 3.0f), glm::vec3(0, 0, 0), glm::vec3(0, 1.0f, 0));
Camera camera(glm::vec3(0, 0, 3.0f), glm::radians(-15.0f), glm::radians(180.0f), glm::vec3(0, 1.0, 0));
//检查输入函数
void processInput(GLFWwindow* window)
{
//按下ESC键时,将WindowShouldClose设为true,循环绘制将停止
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
//按下W键时,向前移动相机
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
{
camera.SpeedZ = 1.0f;
}
//按下S键时,向后移动相机
else if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
{
camera.SpeedZ = -1.0f;
}
else
{
camera.SpeedZ = 0;
}
}
//视口改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height); //OpenGL渲染窗口的尺寸大小
}
//GLFW监听鼠标移动事件回调函数
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse == true)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float deltaX, deltaY;
deltaX = xpos - lastX;
deltaY = ypos - lastY;
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(deltaX, deltaY);
}
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); //用户改变窗口大小的时候,视口调用回调函数
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); //隐藏光标
glfwSetCursorPosCallback(window, mouse_callback);
//初始化GLEW
glewExperimental = true;
if (glewInit() != GLEW_OK) //若GLEW初始化失败,打印错误信息并终止GLFW窗口
{
printf("Init GLEW failed.");
glfwTerminate();
return -1;
}
glEnable(GL_DEPTH_TEST);
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, 5 * 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, 5 * sizeof(float), (void*)(3 * 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)); //缩放
//Model矩阵
glm::mat4 modelMat;
modelMat = glm::rotate(modelMat, glm::radians(-55.0f), glm::vec3(1.0f, 0, 0));
//View矩阵
glm::mat4 viewMat;
//viewMat = glm::translate(viewMat, glm::vec3(0, 0, -3.0f));
//Projection矩阵
glm::mat4 projMat;
projMat = glm::perspective(glm::radians(45.0f), 1600.0f / 1200.0f, 0.1f, 100.0f);
//让程序在手动关闭之前不断绘制图像
while (!glfwWindowShouldClose(window))
{
//检测输入
processInput(window);
//渲染指令
glClearColor(0.f, 0.5f, 0.5f, 1.0f); //设置清空屏幕所用的颜色
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_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();
viewMat = camera.GetViewMatrix();
//循环绘制10个物体
for (int i = 0; i < 10; i++)
{
//每个物体的模型矩阵
glm::mat4 modelMat2;
modelMat2 = glm::translate(modelMat2, cubePositions[i]);
float angle = 20.0f * i;
modelMat2 = glm::rotate(modelMat2, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
//告诉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)); //把矩阵数据发送给着色器
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "modelMat"), 1, GL_FALSE, glm::value_ptr(modelMat2)); //把矩阵数据发送给着色器
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "viewMat"), 1, GL_FALSE, glm::value_ptr(viewMat));
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "projMat"), 1, GL_FALSE, glm::value_ptr(projMat));
glDrawArrays(GL_TRIANGLES, 0, 36);
//glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //绘制,参数1:绘制模式(三角形),参数2:绘制顶点数(两个三角形6个顶点),参数3:索引的类型,参数4:指定EBO中的偏移量。
}
//检查并调用事件,交换缓冲区
glfwSwapBuffers(window); //交换颜色缓冲区,前缓冲区保存最终输出的图像,后缓冲区负责绘制渲染指令,当渲染指令执行完毕后,交换前后缓冲区,使完整图像呈现出来,避免逐像素绘制图案时的割裂感
glfwPollEvents(); //检查触发事件,如键盘输入、鼠标移动等
camera.UpdateCameraPos();
}
glfwTerminate(); //关闭GLFW并退出
return 0;
}